本文是《提升你的 Python 项目代码健壮性和性能》系列的第一篇文章。
当我刚知道 Python 要添加类型的时候,我的内心是拒绝的。
Why, Why, Why? 就是因为不喜欢类型,也不喜欢特别动态的语言。
但是,尝试了俩个疗程之后,腰也不疼了,腿也不疼了,走起路来都有劲了
嗯,真香。
人们常说
动态类型一时爽,代码重构火葬场。
在刚写 Python 的前两年里并没有感受很深。
直到,开始和别人协作的时候,才发现各种莫名其妙的问题。
动态类型给人极大的灵活性,写的时候很爽,但如果解放了双手,撸起袖子一通写,自己写起来爽了,自己重构的时候或者其他人来看代码的时候,头发就会加速掉落。
聪明的你很容易反问,只要我们团队不犯这些错误,不就好了么?
是的,当我们讨论 Python Annotation 的时候,往往陷入类型之争。
我并不想讨论静态类型和动态类型孰好孰坏。
我想讨论的是加了 Typing 极大的提升代码的健壮性。
先从 Gradual Typing 说起吧。
在你刚入门一门编程语言的时候,我们常常说,Java 是强类型静态语言,Python 是强类型动态语言
从这两位诞生开始,静态类型和动态类型就一直进行旷日持久的圣战。
然而,而现在的发展趋势是:
什么是 Gradual Typing?
Gradual typing 允许开发者仅在程序的部分地区使用 Annotate/Type. 即,既不是黑猫(静态), 也不是白猫(动态),从而诞生了熊猫(动静结合)。
话说回来,要知道为什么这么搞,首先要知道动态类型和静态类型会给程序带来什么。
静态类型的语言,比如在写 Java 的时候,如果你把一个 int 赋值给了 string 的变量,IDE 会通过类型检查器立即报错并告诉你,你这个值赋值错啦。这个就是 Java 程序的检查阶段。 动态类型的语言,比如在写
Python 的时候,如果不用一些额外的手段,这种低级的错误,并不会在检查时爆出来,只会在运行时爆出来。如果线上还是出这个问题,就蛋疼了。
为了进行友好的讨论,本人将精分成 Javaer 和 Pythonist, 通过两人对话的方式,来讨论类型。
Javaer: 我先喝杯咖啡
Pythonist: 生命苦短,我用 Python。
Javaer: P 哥,请(为什么叫 P 哥?Python 1989 年出生,Java 1995 年)
Pythonist: J 弟,请
Javaer: 静态类型可以较低成本的提早捕获 BUG, 比如:
Python: 等等,你小子还广征博引了还,首先,提早捕获 Bug, 我这里也有呀,比如我这里可以通过 flake8 来检查出有些没有定义的变量,仅仅是类型没有检查而已。其次,IDE
给我的补全又不是完全无法补全。弱一点罢了。你说的类型检查的问题:
Java: 关于你说的第三点,我完全可以提升测试代码的覆盖率。哎?似乎我这个开发测试成本也上来了。看来类型检查也不能解决这个问题
Javaer: 来 P 哥
Python: 你每次修改,都要加类型,加类型,改类型,直到类型检查器完全接受。不麻烦嘛?面向类型检查器编程?
Javaer: 来,
Python: 来,你说的没错,
Python: 再来,
再比如说,
LeetCode 上面有一道题目,叫做最长连续 1
Input 是 [1,1,0,1,1,1] Output 是 3
我们尝试用 Python 来看下
def find_max_consecutive_ones(num):
return max(map(lambda x: len(x), ''.join([str(num) for num in nums]).split('0')))
我们尝试用 Java 来看下
public class Solution {
public int findMaxConsecutiveOnes(int[] nums) {
int result = 0;
int tmp = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] == 0)
tmp = 0;
else {
tmp += 1;
result = Math.max(tmp, result);
}
}
return result;
}
}
『贴图』
Javaer: 我不是那个意思,浓缩就是精华嘛,表达能力弱又怎么样,我 Javaer 可以直接封装好这个功能当成工具类用,从外部使用上用起来差不多好吧,从项目角度表达力并不是决定性因素,静态类型检查可以提早在编译阶段做字节码优化。你的
GIL…
Pythonist: 好了,咱就不要提 GIL 了
Pythonist: 动态类型不需要花时间写 type annotation, 写起来速度杠杠的。
Javaer: 静态语言一时爽,动态类型火葬场好伐?举个例子,太动态的东西,就是不好做类型推断,比如贵圈的著名的 sqlalchemy 做的那么动态,query.get() 结合 flask
来用,YouModel.query.get() 出来的 YouModel 你还要点进去查看一下具体属性,你要用 title 还是 name, 拼错了,怎么办?都不报错的。
Javaer: 静态类型迫使你思考程序的时候更加严谨认真,这将会提升你的代码质量。
Pythonist: 这点我是不服的,你只是花费了大量的时间在类型检查上,写的认不认真不完全取决于你编程的水平和态度好伐?假如你的观点成立,语言只是武器,峨眉师太拿一把倚天剑,不还是被张三丰空手取来?
Javaer: 但你不能否认,峨眉师太拿着倚天剑确实可以秒杀很多人。
旁白君:有道是,梅须逊雪三分白,雪却输梅一段香。
Gradual Typing 就是在动态语言的基础上,增加了可选的类型声明 (Type Annotation)
这对于我这种人是福音,
对于我个人而言,我是希望 Python 是有类型的
但我又不希望这个声明不是强制性的
mypy 是一个可选的静态分析器,官网介绍上说,mypy 将使你的程序更加易懂,调试和维护。
这个程序
Dropbox 的团队开发,Guido van Rossum 领导开发
本小节部分摘录 Type hints cheat sheet
建议读者收藏原网址 https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html
# 内置类型
x: int = 1
x: float = 1.0
x: bool = True
x: str = "test"
x: bytes = b"test"
child: bool
if age < 18:
child = True
else:
child = False
# 普通函数
def stringify(num: int) -> str:
return str(num)
# 生成器
def f(n: int) -> Iterable[int]:
i = 0
while i < n:
yield i
i += 1
直接看起来似乎,加不加 typing 对现在的代码改善并不是很明显嘛。
我们可以给复杂类型起别名:
比如:
def f() -> Union[List[Dict[Tuple[int, str], Set[int]]], Tuple[str, List[str]]]:
def b() -> Union[List[Dict[Tuple[int, str], Set[int]]], Tuple[str, List[str]]]:
AliasType = Union[List[Dict[Tuple[int, str], Set[int]]], Tuple[str, List[str]]]
def f() -> AliasType:
...
def b() -> AliasType:
...
看起来还行,但还是没有感觉到很明显的代码质量改善。
好,再看一例,使用 ClassVar 禁止属性无法在实例上设置
from typing import ClassVar
class A:
x: ClassVar[int] = 0 # Class variable only
A.x += 1 # OK
a = A()
a.x = 1 # Error: Cannot assign to class variable "x" via instance
print(a.x) # OK -- can be read through an instance
举个例子,flask-sqlalchemy, 可以通过 YouModel.query.get(id) 来拿到 YouModel 的实例,但 IDE 不能推断出这个实例是什么。
# 方法一,Cast
you_model_ins: YouModel = YouModel.query.get(id)
# 方法二,包装一下 get 方法
class YouModel(base):
def get(id) -> "YouModel": # 注意这里的字符串
pass
you_model_ins = YouModel.get(id)
细心的读者可能看到这里的 YouModel 的返回值类型居然使用了 YouModel 的字符串,如果是 Java 的话,是可以直接写 YouModel 的。
# 加上类型延迟求值
from __future__ import annotations
class YouModel(base):
def get(id) -> YouModel:
pass
you_model_ins = YouModel.get(id)
还有其他的用法,请参考 MyPY 的官方文档
有的地方的代码不进行检查的话会方便很多。
与 flake8 类似,在注释后面写上标志就可以忽略了。
youcode # type: igonre
我现在有两个文件,一个是 user.py 另一个是 order.py
在 user 里面有个方法需要返回 order 里面的 Order 列表,order 里面有个 order.owner 需要返回 User 实例。
如果不用类型声明的话,在 user 需要 order 的时候 import 进来即可规避循环导入。
在使用类型声明之后,建议在 user 里面这么写
if TYPE_CHECKING:
from project.models.order import Order # noqa
通过本文了解了基本的 Typing Anotation 的用法,其实效果还不够,本着对爱学习的读者老爷的负责的态度。
所谓『纸上得来终觉浅,绝知此事要宫刑』, 哦不『躬行』
推荐一个超级牛的大项目来让大家了解一下 typing annotation 的最佳实践。
https://github.com/zulip/zulip/
当然,从这个项目里面不仅仅能学到 typing annotation, 还能学到大项目下,牛 X 的公司的做法
有机会的话,我会挑其中的一小部分讲解一下。
ChangeLog: