跳到主要内容

django默认和自定义的用户身份验证

· 阅读需 3 分钟
Jason Lee
The owner of this blog site

前言

在做项目时,身份验证是目前疑虑最多的地方:为什么这里要新建一个 User 模型?为什么要重写管理器?这篇文章通过查阅文档,尝试从最佳实践的角度给出答案。

默认验证方式

模型 视图函数 表单

注册 登录 登出

默认 User 模型

默认 User 模型主要包含以下字段:username、password、email、first_name、last_name。也就是说,如果有别的字段的需求(比如用户头像),就要自定义用户模型。目前暂不涉及权限,等到后续项目中接触到了,再添加。

视图函数

注册

没有内置的注册视图,有内置的注册表单 UserCreationForm。UserCreationForm 的要求字段为用户名、邮箱和两次密码输入。

# views.py
class SignUpView(FormView):
template_name = 'core/signup.html'
form_class = forms.SignUpForm
success_url = '/login/'

def form_valid(self, form):
form.save()
return super().form_valid(form)

# forms.py
class SignUpForm(UserCreationForm):
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']

username = forms.CharField(widget=forms.TextInput(attrs={
'placeholder': 'Your username',
'class': 'w-full py-4 px-6 rounded-xl'
}))
email = forms.CharField(widget=forms.EmailInput(attrs={
'placeholder': 'Your email address',
'class': 'w-full py-4 px-6 rounded-xl'
}))
password1 = forms.CharField(widget=forms.PasswordInput(attrs={
'placeholder': 'Your password',
'class': 'w-full py-4 px-6 rounded-xl'
}))
password2 = forms.CharField(widget=forms.PasswordInput(attrs={
'placeholder': 'Repeat password',
'class': 'w-full py-4 px-6 rounded-xl'
}))

登录

有内置视图 LoginView,有内置的表单 AuthenticationForm

# forms.py
class LogInForm(AuthenticationForm):
username = forms.CharField(widget=forms.TextInput(attrs={
'placeholder': 'Your username',
'class': 'w-full py-4 px-6 rounded-xl'
}))
password = forms.CharField(widget=forms.PasswordInput(attrs={
'placeholder': 'Your password',
'class': 'w-full py-4 px-6 rounded-xl'
}))

# urls.py
path('login/', auth_views.LoginView.as_view(template_name='core/login.html', authentication_form=LogInForm), name='login'),

# settings.py
LOGIN_REDIRECT_URL = '/'

登出

有内置视图 LogoutView,无需视图

# urls.py
path('logout/', auth_views.LogoutView.as_view(), name='logout')

# settings.py
LOGOUT_REDIRECT_URL = '/'

自定义验证方式

有两个可以自定义的地方:认证后端和 User 模型

认证后端

前面提到的 authenticate 和 login 方法都是 django 默认认证后端提供的。

User 模型

一般有两个需要扩展:模型本身和管理器(manager)

# models.py

class CustomUserManager(UserManager):
def _create_user(self, name, email, password, **extra_fields):
if not email:
raise ValueError("You have not provided a valid e-mail address")

email = self.normalize_email(email)
user = self.model(email=email, name=name, **extra_fields)
user.set_password(password)
user.save(using=self._db)

return user

def create_user(self, name=None, email=None, password=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(name, email, password, **extra_fields)

def create_superuser(self, name=None, email=None, password=None, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
return self._create_user(name, email, password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
email = models.EmailField(unique=True)
name = models.CharField(max_length=255, blank=True, default='')
avatar = models.ImageField(upload_to='avatars', blank=True, null=True)

is_active = models.BooleanField(default=True)
is_superuser = models.BooleanField(default=False)
is_staff = models.BooleanField(default=False)

date_joined = models.DateTimeField(default=timezone.now)
last_login = models.DateTimeField(blank=True, null=True)

objects = CustomUserManager()

USERNAME_FIELD = 'email'
EMAIL_FIELD = 'email'
REQUIRED_FIELDS = []

JWT认证

· 阅读需 5 分钟
Jason Lee
The owner of this blog site

前言

各种认证方式一直是我头痛的点。奇怪的是,几乎所有的 web 开发教程都默认你已经会了,而不会专门教学,只是一笔带过。你只能自己去找博客看。参考视频教程

jwt 归为一种身份验证(authentication)的方式。

令牌(token)

这个词挺神奇的。transformer 结构里有 token 这个概念,这里也有。不过他们的含义应该完全不同。在 web 开发中,token 指的是令牌,或者凭证。

最开始,人们使用 session+cookie 的认证方式。这种情景下,服务器存储用户的相关信息,仅返回给浏览器一个 session-id。浏览器下次请求,将在请求中的 cookie 中携带 session-id,服务器接收到这个 id,并且能查找到相关用户的信息,则认证成功。

但是这种模式越来越不适合当代多服务器的 web 模式。因为这相当于要求每个服务器都要存储一个用户信息表。所以,需要一种新的认证方式。

针对上述问题,一种解决方案是,不在服务器单独建用户信息表,而是将用户信息(token)存起来发给浏览器,浏览器下次来直接带完整的用户信息。然后,因为服务器这时候已经不存储任何用户信息,且所有信息完全来自用户,为了防止用户携带假信息进行欺诈,在发给浏览器的信息中加入签名进行防伪。

至于 JWT 的存储方式,可以是 cookie,也可以是 localstorage 等。

实现方式

上图是 jwt 的结构。三段式,分别是头部、负荷和签名。头部有两个字段:生成签名用到的加密算法和 token 类型。负荷中的 iat 指的是 issue at,签发时间。签名框里的意思是,头部加负荷,再加上密钥,一同经过 sha256 算法,生成了签名。

chatgpt 的解释:

JWT 的验证签名原理如下:

服务端在接收到 JWT 后,会对头部和载荷进行验证,验证包括以下几个步骤:

a. 解析出 JWT 中的头部和载荷,并检查头部中的算法是否为可信的算法。

b. 对头部和载荷使用相同的算法和密钥进行签名,得到签名结果。

c. 将服务端得到的签名结果和 JWT 中附带的签名结果进行比较,如果两个签名结果相同,则认为 JWT 是有效的,否则认为 JWT 是无效的。

通过以上验证过程,JWT 的签名可以保证数据的完整性和真实性,从而确保 JWT 的安全性。值得注意的是,JWT 并不加密,只是通过签名来验证数据的真实性和完整性,因此在使用 JWT 时,应该注意保护 JWT 的传输过程,避免 JWT 被篡改或窃取。

其他

  • 在软件售卖场景中,有把 jwt 作为许可证来使用的。即软件使用过程中不断去读取 jwt token 文件,如果验证出过期,则软件无法使用。与一般的登录场景类似的是,许可证也是由服务端(软件卖方)签发,每次使用都要检查 token。

我在2023年回顾python3基础

· 阅读需 5 分钟
Jason Lee
The owner of this blog site

到底什么是数字、元组、字符串的不可变性?

不可变性,指的是不能改变原有位置上的元素;不包括在空位置上创建元素。所以,元组都可以连接其他同类元素使得自己变得更长,而不能改变原来的元素。

数字、字符串的不可变性:变量赋值 a=5 后再赋值 a=10,这里实际是新生成一个 int 值对象 10,再让 a 指向它,而 5 被丢弃,不是改变 a 的值,相当于新生成了 a。字符串类似。

生成器和迭代器

生成器和迭代器都可以提高性能和内存效率,因为它们可以一次生成一个元素,而不必在内存中存储整个数据集。通过理解生成器和迭代器的工作原理,我们可以更有效地处理大型数据集。

假如有一个场景,我们深度学习训练时读取数据,由于训练图片很多,不可能一次性读进内存,所以采用生成器或迭代器的方式,一次读取几张图像。

生成器和迭代器实现的事情都是类似的,即一次只输出少量的数据,只是他们的创建方式有差异。

迭代器

迭代器是一种对象,它可以用于迭代序列中的元素。迭代器具有一个next()方法,该方法返回序列中的下一个元素,并在没有更多元素时引发 StopIteration 异常。

创建迭代器的方法:一个从现有序列中直接创造:iter(seq),另一种需要创建一个类(由类实例化出对象),类带有__iter____next__方法:

class FibonacciIterator:
def __init__(self):
self.a = 0
self.b = 1

def __iter__(self):
return self

def __next__(self):
result = self.b
self.a, self.b = self.b, self.a + self.b
return result

fib_iter = FibonacciIterator()
for i in range(10):
print(next(fib_iter))
## 另外一种调用方式
for value in fib_iter:
print(value)

生成器

生成器是一种特殊的函数,它使用 yield 关键字来产生一个值,并暂停函数的执行。每次调用生成器函数时,它都会从上一次停止的位置继续执行,直到遇到 yield 语句。生成器是一种简单而强大的工具,可以在处理大型数据集时提高性能和内存效率。

def fibonacci():
a, b = 0, 1
while True:
yield b
a, b = b, a + b

f = fibonacci()
for i in range(10):
print(next(f))
## 也可以:
for value in f:
print(value)

可以看出,生成器在这个例子中会简短一些。同时可以看出,调用的时候,生成器和迭代器的两种方式都一样,分别是固定次数(可能会超出边界而引发 StopIteration 异常,以及 forin 循环。创建时的不同是:生成器是一个函数,而迭代器是一个对象。

不定长参数

不定长参数就是那种在定义函数或者方法时,括号里的参数带一个或者两个星星的。一个星星被放入元组,实参不带参数关键字;两个星星被放入字典,实参带关键字,关键字被当作字典的键名。

def printinfo( arg1, *vartuple, **vardict ):
"打印任何传入的参数"
print ("输出: ")
print (arg1)
print(vartuple)
print(vardict)

printinfo( 70, 6, a=60, b=50)

out:

输出:
70
(6,)
{'a': 60, 'b': 50}

异常处理

raise 抛出异常,程序把异常打印出来(抛给用户),并停止后续代码的运行。如果没有 raise,程序将继续运行。

flex/grid布局在tailwindcss中的使用

· 阅读需 4 分钟
Jason Lee
The owner of this blog site

前言

tailwindcss 使得不用离开 html 界面就可以完成 css coding。常见的样式多写几次自然就记住了,如果不会的,还可以查文档,但是遇到布局相关的 flex 和 grid,需要一定的知识。

flex

菜鸟教程的基础上,做一点总结。

基本组成,flex container 和 flex item。他们可以分别设置属性。

容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。主轴的开始位置(与边框的交叉点)叫做 main start,结束位置叫做 main end;交叉轴的开始位置叫做 cross start,结束位置叫做 cross end。项目默认沿主轴排列。单个项目占据的主轴空间叫做 main size,占据的交叉轴空间叫做 cross size。

所以想到 flex 布局,应该在脑海中想起的画面是:一个容器内有两条轴,一条横轴从左到右,一条竖轴从上到下。项目也是按上述顺序排列。

flex 容器的属性

  • flex-direction:决定主轴是哪一条及其方向
  • flex-wrap:主轴空间不够时是否换行
  • flex-flow:flex-direction 和 flex-flow 一起设置
  • justify-content:主轴的对齐方式
  • align-items:交叉轴的对齐方式
  • align-content:align-content 属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用

这里对 align-content 做出一点说明。所谓的多根轴线,是指在元素较多,形成换行后,每一行元素所在的轴,而不是指主轴和交叉轴:

如图,每一根红线都是一个轴。也就是说,align-content 定义了多行元素的整体对齐方式。作为对比,align-items 的对象是当行元素:

换句话说,多行元素用 align-content,单行元素用 align-items。

flex 项目属性

去除掉不常用的 order,有五个属性:

  • flex-grow:当有剩余空间时,项目放大比例
  • flex-shrink:空间不足时,缩小比例
  • flex-basis:定义了在分配多余空间之前,项目占据的主轴空间(main size)
  • flex:上述三个属性的快捷设置
  • align-self:对单个项目脱离 align-items 的对齐方式

关键是理解剩余空间。首先空间都是针对主轴来说的。

grid

grid 适合设置多行多列的,目前我遇到的情况是,设置好几个列,还有列之间的间隔,然后确定实际的一个元素占几列,就可以完成绝大部分任务了。

flex 和 grid 的 tailwindcss 写法

flex

<div class="flex 主轴对齐 交叉轴对齐 元素间距">
...
</div>

grid

<div class="max-w-7xl mx-auto(居中) grid grid-cols-n gap-n">
<div class="col-span-x">
...
</div>
<div class="col-span-n-x">
...
</div>
</div>

django-rest-framework使用入门

· 阅读需 2 分钟
Jason Lee
The owner of this blog site

浏览器行为

当我用 drf 提供的浏览器可视化界面,发现了一个奇怪的问题。同样是访问http://127.0.0.1:8000/snippets/,如果我之前填了一个表单并提交,那么按刷新按钮,会再提交一次,也就是会在数据库中添加一条和上一次一样的数据。只有在地址栏回车一下,才会真正回到列表中。这说明,如果页面中有表单,刷新按钮可能会执行 post 请求,而浏览器回车才能保证发出的 get 请求。

到底什么是序列化

python 的数据格式(字典、列表等),转化为可以传输的字符串(JSON、HTML 等),就是序列化。序列化类的实例并不是直接生成 json 数据,而是将模型实例转为一种 python 字典,然后由视图函数的JSONRenderer().render()函数转化为 json 字符串。

反过来,如果接受到了 json,按照下列方式:

import io

stream = io.BytesIO(content)
data = JSONParser().parse(stream)

serializer = SnippetSerializer(data=data)
serializer.is_valid()
# True
serializer.validated_data
# OrderedDict([('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
serializer.save()
# <Snippet: Snippet object>

和表单的处理很相似。

类视图的使用

· 阅读需 4 分钟
Jason Lee
The owner of this blog site

前言

对于类视图和函数视图,在我初学 django 的时,有过这样的迷思:自己定义的类视图似乎和函数视图没有太大的区别,无非就是判断请求方式的时候,CBV 用函数,FBV 用if...else...。而如果上了框架提供的通用类视图,代码风格似乎过于抽象,且不灵活,无法完全替代函数视图。今天,就以刚刚用函数视图完成的项目为例,尝试将所有函数视图转为类视图,看看过程中有什么新的感悟。

通用类视图里的方法

添加额外的上下文:

def get_context_data(self, **kwargs):
# Call the base implementation first to get a context
context = super().get_context_data(**kwargs)
# Add in a QuerySet of all the books
context["book_list"] = Book.objects.all()
return context

根据 url 内传来的 query 参数来动态查询

这项工作的关键部分是当基于类的视图被调用的时候,各种常用的东西被存储在 self 上,而且请求 (self.request) 根据 URLconf 抓取位置(self.args) 和基于名称 (self.kwargs) 的参数。

def get_queryset(self):
self.publisher = get_object_or_404(Publisher, name=self.kwargs["publisher"])
return Book.objects.filter(publisher=self.publisher)

执行额外的任务

class AuthorDetailView(DetailView):
queryset = Author.objects.all()

def get_object(self):
obj = super().get_object()
# Record the last accessed date
obj.last_accessed = timezone.now()
obj.save()
return obj

通用类视图到底为开发者默认做了哪些事

django 框架内置的通用类视图之所以能少些很多代码,很明显是它有一些默认行为。那么这里就记录一下各种常见内置视图的默认行为。

FormView

class ContactFormView(FormView):
template_name = "contact.html"
form_class = ContactForm
success_url = "/thanks/"

def form_valid(self, form):
# This method is called when valid form data has been POSTed.
# It should return an HttpResponse.
form.send_email()
return super().form_valid(form)

表单视图的一般行为是判断请求方式,如果是 post,就检验字段有效性,且把错误返回给浏览器,成功则重定向;如果是 get 就返回待填写的表单。

所以,FormView 就是通过暴露一些简单的配置给开发者,比如 template_name,其他行为会自动完成。如果有特殊需求,重写 get 或 post 方法。form_valid 是执行表单验证之后的程序。

form_valid 函数是一定要重写的,这也说明 django 文档里一般都是最小实现形式,不能减少。

总结

  • 类视图完全可以替代函数视图。
  • 优先用内置的通用类视图,首先确认任务类型,如果是展示类,就考虑 ListView、DetailVIew;如果是修改类,就是 CreateView、UpdateView、DeleteView。
  • 修改类的通用视图,注意表单相关项。运用上面提到的三个函数,可以实现较复杂的需求。
  • 如果不方便,也可以不用内置视图,而采用根据请求方式划分方法,比如 get 请求。
  • 类视图在采用了内置类的写法后,代码会变少;但更重要的意义是,增加了代码重用的可能性。比如,另外一个 views.py 可以继承过来。

ModelForm详解

· 阅读需 3 分钟
Jason Lee
The owner of this blog site

前言

web 开发中,我目前遇到最复杂、又是最需要的需求,就是用户验证系统。除了一般的注册登录,还有权限、分组等。而从注册开始,需要理解 django 为什么设计了 modelForm 来处理用户的表单输入。

从 form 讲起

在注册的例子中,文档给出的方法是调用如下函数:

from django.contrib.auth.models import User
User.objects.create_user(username='', email='', password='')

但是正常的网站注册方式都是通过表单,怎么建立表单和 create_user 函数的联系呢?如果搜索文档里的表单,得到的答案是:

from django.http import HttpResponseRedirect
from django.shortcuts import render

from .forms import NameForm


def get_name(request):
# if this is a POST request we need to process the form data
if request.method == "POST":
# create a form instance and populate it with data from the request:
form = NameForm(request.POST)
# check whether it's valid:
if form.is_valid():
# process the data in form.cleaned_data as required
# ...
# redirect to a new URL:
return HttpResponseRedirect("/thanks/")

# if a GET (or any other method) we'll create a blank form
else:
form = NameForm()

return render(request, "name.html", {"form": form})

我们知道了,对于提交上来的表单,先验证数据是否都有效,然后进一步的处理并重定向。可是什么是进一步的处理?

所以这大概就是文档有时候不能解决实际场景的问题,要结合别人写的代码,再回到文档中理解。

ModelForm

表单,一般都是和模型(也就是表中的字段)绑定的。因此,如果从头由forms.ModelForm继承来的表单类开始写,无疑是一种重复代码。因为与之绑定的模型字段在之前的 models.py 中定义过了。因此,django 创建了 ModelForm,直接在表单创建时和模型绑定,这样,提交上来的表单在验证通过后,保存表单就会同时保存提交上来的数据。

关于用户注册,需要知道的是,django 自带的 User 对象已经可以满足大部分场景的使用了,一般直接将其作为模型使用,而不用在 models.py 中创建。

通过这个例子,我的体会是:django 的写法太灵活了,往往一个问题有多种解决方案。可能最好的办法是摸索出一套自己习惯的写法。

tailwindcss配置

· 阅读需 3 分钟
Jason Lee
The owner of this blog site

前言

一个准备实现的 django 项目中用到了 tailwindcss。年初我曾接触过 tcss,但是由于文档对我来说有点看不懂,配置了很久还是失败了,所以搁置了下来。今天,经过良久的尝试,将成功的经验记录下来。

cdn

cdn 是最快尝试 tcss 的方法。它是通过在线引入 tcss 的 stylesheet 来实现的。

<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">

通过 CDN 引入的显著缺点是,每一次更改样式都要请求网络,如果网络不好或者断网,就会带来不好的开发体验。官网文档还给出了一些缺点:

Tailwind CLI

利用脚手架工具,可以生成一个定制的 css 文件,里面预定义了一大堆样式,比如 mt-5 等,正是我们在 html 文件中用到的 tcss 类名。不仅可以离线开发,而且可以添加自定义样式。操作方式如下:

首先,创建一个/src/tcss.css

@tailwind base;
@tailwind components;
@tailwind utilities;

<!-- your style -->
@layer components {
.btn-blue {
@apply py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75;
}
}

可以看出,tcss 有三个层次,base 层起到清除浏览器的初始默认样式,比如一些 margin 和 padding。components 负责将一些重复使用的样式组提取出来,单独取一个名字,比如上面代码的 btn-blue。最后是 utilities,就是最熟悉的 tcss 自带类,也可以自定义。

创建好上面的源 tcss 文件后,进行解析:

npx tailwindcss-cli build src/tailwind.css -o tailwind.css

上述命令竟然是我从别的教学视频里得到的,官方文档给出的命令只能得到 500 多行的解析文件,只包含了 base 层。不得不说,tcss 文档写的很烂。正常来说,上面命令运行后生成的文件含有 18 多万行代码。

在 index.html 中,header 部分引入生成的tailwind.css:

<link href="/tailwind.css" rel="stylesheet">

可以开始愉快地使用 tcss 了。

django关系字段

· 阅读需 4 分钟
Jason Lee
The owner of this blog site

前言

mysql 断断续续地学,一直对诸如外键的概念不甚清晰,今天做一个备忘。在这篇博文的基础上,加上了一些自己的理解。

建表

#_*_coding:utf-8_*_
from django.db import models

# Create your models here.

class Colors(models.Model):
colors=models.CharField(max_length=10) #蓝色
def __str__(self):
return self.colors

class Ball(models.Model):
color=models.OneToOneField("Colors") #与颜色表为一对一,颜色表为母表
description=models.CharField(max_length=10) #描述
def __str__(self):
return self.description

class Clothes(models.Model):
color=models.ForeignKey("Colors") #与颜色表为外键,颜色表为母表
description=models.CharField(max_length=10) #描述
def __str__(self):
return self.description

class Child(models.Model):
name=models.CharField(max_length=10) #姓名
favor=models.ManyToManyField('Colors') #与颜色表为多对多

一对一(models.OneToOneField)

子表从母表中选出一条数据一一对应,母表中选出来一条就少一条,子表不可以再选择母表中已被选择的那条数据。一般用于某张表的补充,比如用户基本信息是一张表,但并非每一个用户都需要有登录的权限,不需要记录用户名和密码,此时,合理的做法就是新建一张记录登录信息的表,与用户信息进行一对一的关联,可以方便的从子表查询母表信息或反向查询。

## 增加

color_obj=models.Colors.objects.create(colors="黑") #先在母表中创建颜色,并实例化给颜色表对象
models.Ball.objects.create(color=color_obj,description="黑球") #更新Ball表,color字段为颜色表对象,添加description字段

## 删除
models.Ball.objects.get(description="灰球").delete()

## 修改
color_obj=models.Colors.objects.get(colors="黑") #.get()等同于.filter().first()
color_obj.colors="灰"
color_obj.save()
models.Ball.objects.filter(description="黑球").update(color=color_obj,description="灰球")

## 查询
### 子表查询母表
models.Ball.objects.get(description="红球").color.colors

### 母表查询子表
models.Colors.objects.get(colors="红").ball.description

一对多(models.ForeignKey)

子表从母表中选出一条数据一一对应,但母表的这条数据还可以被其他子表数据选择。这里的一和多指的是自身的表的数据在对方表里出现的次数,一次为一,多次为多。比如每个员工归属于一个部门,那么就可以让员工表的部门字段与部门表进行一对多关联,可以查询到一个员工归属于哪个部门,也可反向查出某一部门有哪些员工。

## 查

color_obj=models.Colors.objects.get(colors="红")
color_obj.clothes_set.all()

models.Clothes.objects.filter(color__colors="红")

多对多(models.ManyToManyField)

比如有多个孩子,和多种颜色。每个孩子可以喜欢多种颜色,一种颜色可以被多个孩子喜欢,对于双向均是可以有多个选择。

## 查

#写法1:
child_obj=models.Child.objects.get(name="小明") #写法:子表对象.子表多对多字段.过滤条件(all()/filter())
print(child_obj.favor.all())
#写法2,反向从母表入手:
print(models.Colors.objects.filter(child__name="小明")) #母表对象.filter(子表表名小写__子表字段名="过滤条件")

matplotlib最佳实践

· 阅读需 5 分钟
Jason Lee
The owner of this blog site

前言

跟着油管上的教程学的,作者介绍了一些最佳实践,融合一些自己的思考,把觉得有用的东西记录一下。

part1 折线图

# 导包区
from matplotlib import pyplot as plt
# plt.style.use('seaborn') # 切换图片风格
plt.xkcd() # 手绘风格

# 数据区
ages_x = [25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]
dev_y = [38496, 42000, 46752, 49320, 53200,
56000, 62316, 64928, 67317, 68748, 73752]
py_dev_y = [45372, 48876, 53850, 57287, 63016,
65998, 70003, 70000, 71496, 75370, 83640]

# 绘图区
plt.plot(ages_x, dev_y, color='k', linestyle='--', label='All Devs')
plt.plot(ages_x, py_dev_y, color='b',marker='o', label='Python Devs')

plt.xlabel('Ages')
plt.ylabel('Median Salary')
plt.title('Median Salary By Age')
plt.legend()

plt.grid(True)

plt.show()

plot.plot()函数,一个对应一条曲线,而且,color, linestyle, marker 单独作为参数写,而不是混在一起;还有,把图例放在 plot 函数里而不是 legend(),都是为了增加可读性。

part2 柱状图

画柱状图的函数是 plt.bar()。但是如果将上述的 plt.plot()改成 plt.bar(),会出现重叠的情况。为了避免重叠,需要添加偏移。

ages_x = [25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]
dev_y = [38496, 42000, 46752, 49320, 53200,
56000, 62316, 64928, 67317, 68748, 73752]
py_dev_y = [45372, 48876, 53850, 57287, 63016,
65998, 70003, 70000, 71496, 75370, 83640]
js_dev_y = [37810, 43515, 46823, 49293, 53437,
56373, 62375, 66674, 68745, 68746, 74583]

x_indexes = np.arange(len(ages_x))
width=0.25

width 是柱的宽度,柱的数量*width 应该小等于 1。

plt.bar(x_indexes-width, dev_y, color='k', width=width, label='All Devs')
plt.bar(x_indexes, py_dev_y, color='b', width=width, label='Python Devs')
plt.bar(x_indexes+width, js_dev_y, color='g',width=width, label='JavaScript Devs')

紧接着,作者给出了一个实例,从 csv 文件读取数据,并统计各个编程语言的使用人数。用的是 python 标准 csv 库进行读取,后续可以用 pandas。

with open('data.csv') as csv_file:
csv_reader = csv.DictReader(csv_file)
counter = Counter()

for row in csv_reader:
counter.update(row['LanguagesWorkedWith'].split(';'))

print(counter)
with open('data.csv') as csv_file:
csv_reader = csv.DictReader(csv_file)
counter = Counter()

for row in csv_reader:
counter.update(row['LanguagesWorkedWith'].split(';'))

print(counter)

Counter({'JavaScript': 59219, 'HTML/CSS': 55466, 'SQL': 47544, 'Python': 36443, 'Java': 35917, 'Bash/Shell/PowerShell': 31991, 'C#': 27097, 'PHP': 23030, 'C++': 20524, 'TypeScript': 18523, 'C': 18017, 'Other(s):': 7920, 'Ruby': 7331, 'Go': 7201, 'Assembly': 5833, 'Swift': 5744, 'Kotlin': 5620, 'R': 5048, 'VBA': 4781, 'Objective-C': 4191, 'Scala': 3309, 'Rust': 2794, 'Dart': 1683, 'Elixir': 1260, 'Clojure': 1254, 'WebAssembly': 1015, 'F#': 973, 'Erlang': 777})

画图用 bar 函数的话,x 轴就会很拥挤。所以采用横柱状图 plt.barh(),将 x,y 颠倒。

part3 饼状图

饼状图一般适合五种以下的数据展示。

slices = [59219, 55466, 47544, 36443, 35917]
labels = ['JavaScript', 'HTML/CSS', 'SQL', 'Python', 'Java']
explode = [0, 0, 0, 0.1, 0]

plt.pie(slices, labels=labels, explode=explode,startangle=90, autopct='%1.1f%%', wedgeprops={'edgecolor': 'black'})

## wedgeprops: 楔子属性?反正就是设置图表的一些性质

plt.title('My Awesome Pie Chart')
plt.show()

part4 堆积面积图

堆积面积是 stackplot 的直译,但似乎不是很妥当?api 是 plt.stackplot()

part5 折现填充图

在折现的上方或下方进行填充,直观地表示差距。

plt.plot(ages, py_salaries, label='Python')

part6 直方图

bins 是每根柱代表的数据间距, log 表示将数据取对数

plt.hist(ages, bins=bins, edgecolor='black', log=True)

part7 散点图

plt.scatter(views, likes, c=ratio, cmap='summer', edgecolor='black', linewidth=1, alpha=0.75)
cbar = plt.colorbar()
cbar.set_label('Like/Dislike Ratio')

plt.xscale('log')
plt.yscale('log')

plt.title('Median Salary By Age')
plt.xlabel('View Count')
plt.ylabel('Total Likes')
plt.show()

c 是指颜色的深浅度,cmap 设置颜色的系列,plt.colorbar()在图中添加彩色条,plt.xscale('log')将刻度改为对数,防止过大的数远离图表中心。

part8 时序图

data['Date'] = pd.to_datetime(data['Date']) ##把字符串解析为日期格式
data.sort_values('Date', inplace=True)

price_date = data['Date']
price_close = data['Close']

plt.plot_date(price_date, price_close, linestyle='solid')

plt.gcf().autofmt_xdate() ## 自动格式化日期

part9 动画

这是和深度学习相关的一个场景,比如损失函数的显示就是动态的。


import random
from itertools import count
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

plt.style.use('fivethirtyeight')

x_vals = []
y_vals = []

# plt.plot(x_vals, y_vals)


index = count()

def animate(i):
x_vals.append(next(index))
y_vals.append(random.randint(0, 5))

plt.cla()
plt.plot(x_vals, y_vals)

ani = FuncAnimation(plt.gcf(), animate, interval=1000)
plt.tight_layout()
plt.show()

但是上述代码在 jupyter 中无法正常运行,只能用 pycharm 来输出。

part10 子图

plt.style.use('seaborn')

data = pd.read_csv('data.csv')
ages = data['Age']
dev_salaries = data['All_Devs']
py_salaries = data['Python']
js_salaries = data['JavaScript']

fig1, ax1 = plt.subplots()
fig2, ax2 = plt.subplots()

ax1.plot(ages, dev_salaries, color='#444444',
linestyle='--', label='All Devs')

ax2.plot(ages, py_salaries, label='Python')
ax2.plot(ages, js_salaries, label='JavaScript')

ax1.legend()
ax1.set_title('Median Salary (USD) by Age')
ax1.set_ylabel('Median Salary (USD)')

ax2.legend()
ax2.set_xlabel('Ages')
ax2.set_ylabel('Median Salary (USD)')

plt.tight_layout()

plt.show()

fig1.savefig('fig1.png')
fig2.savefig('fig2.png')

首先,子图中最重要的概念是 figure 和 axis。之前的代码都是针对 figure(默认),figure 可以认为是一个容器,表现为窗口;axis 就是一张具体的图。