前言
我已经使用Python超过12年,在国内可能是应用Python最广泛的前公司工作超过6年,接触无数Python项目,其中少的几千行,多的几百万行Python代码。除此之外我非常关注类型系统的发展,可以说我见证了类型系统从无到有,从有到优,发展到今天的整个过程,借着Python 3.11发布,就想到了这么个主题。
我从反对到支持
最有意思的是,刚有类型注解时我是坚决的反对者,而现在我是坚定的支持者。为什么呢?
有很多静态语言开发者吐槽Python经常引用的一句话是:
动态类型一时爽,代码重构火葬场
一直到现在对这句话我还是嗤之以鼻,我认为【代码重构火葬场】的根源还是开发者的能力和编程规范的问题,静态语言只是相对于动态语言,提供了门槛不让你犯错。而使用Python语言的开发者的上限和下限区别就太大了,这也是Python在国内发展缓慢的原因之一: 优秀的Python工程师实在太少了。
从前公司离职前我印象里没有一个项目的代码是有类型注解的,尤其是那些上百万行的大型项目可以说完全没有类型注解,其中很多逻辑极为复杂,代码逻辑诡异,我甚至觉得以当时的Python类型系统并不能完美的支持前公司把代码都加上类型注解。在早些年,这些项目都是相对稳定迭代的,我认为如果团队的开发者对Python熟悉,有好的编程习惯和工作态度,在加上有一些工作流保证代码质量,没有类型注解不是什么问题。
以我阅读过很多优秀开源项目和认识一些非常优秀的Python工程师的经历来说,代码大面积重构一般难点在两个地方:
-
很多高效率、聪明的代码非常简短难懂,如果能力不够是很难理解和维护它的。 -
很多能力一般的开发者写的代码设计有问题、细节考虑不周,这样的代码在野蛮生长的过程中满足了产品开发进度要求,但是在不断地留坑。这些代码几经迭代,参数、逻辑非常混乱复杂,能力稍差的维护者不敢动它的逻辑。
所以长久的、从根本的解决代码的质量问题其实要编写可维护的代码,而不要滥用或者错误使用语言特性,尤其是不要炫技。
好,回到正题。我一开始反对Python引入类型系统,是因为我用的就是你Python这个动态语言的不受约束,写代码爽(没写过Python你是真不知道有多爽),你别管我怎么传值,反正我能高效完成开发,也能利用例如标准库、元类、描述符、自省、IPython等等语言特性和工具快速迭代,正常下班。结果你现在告诉我,你推荐我对参数、变量、返回值标注类型?你Python不想着提高你的运行效率还要求我使用静态语言的类型系统,那我为什么不直接用静态语言?隔壁Golang它不香吗?
我逐渐地「被动」接受和支持,是因为现在Python开发者能力的下限真的是一年不如一年,如果没有类型注解的约束,很多Python开发者写的代码真的一言难尽。我是从大概17年开始认为类型注解应该是一个好的商业应用的必选,在前公司我就深刻的感受到新来的很多工程师对Python的熟悉程度、写代码的能力等等越来越差,如果你关注微博和前公司的话,一定见过#XX崩了#这个热搜。其实有大部分都是人为的问题,事实上,如果有一个好的类型注解支持其中大部分是可以避免的。
为什么需要类型系统
在这里先解释一下,文章会多次的使用【类型系统】、【类型注解】等词,你可能感觉很混乱。我觉得它们是不一样的,我认为【类型系统】是实现检查对特定类型的使用是否符合该类型的规范的系统,【类型注解】是Python语言特性,类型系统除此之外还有执行类型检查的工具(做类型检查器,Type Checker,本文提到的是mypy,一会还会介绍还有其他的工具)。
Python是一种动态类型语言,Python解释器仅在代码运行时进行类型检查,并且变量的类型在其生命周期内是进行更改的:
In : a = 1
In : type(a) # `type()`返回对象的类型
Out: int
In : a = 'string' # 改变了变量的类型
In : type(a)
Out: str
In : def test(a):
...: return a + 1
...:
In : test(1) # OK
Out: 2
In : test('s') # Error 因为字符串和数字不能相加
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Input In , in <cell line: 1>()
----> 1 test('s')
Input In , in test(a)
1 def test(a):
----> 2 return a + 1
TypeError: can only concatenate str (not "int") to str
相比于静态语言(像Java、C/C++和Go等)在编译期间就能发现并改进代码问题,动态语言直到运行时才会发现这类类型问题,所以就会出现低级错误把整站搞挂了这种直觉上让开发者不能理解和接受的事件。
现在Python社区的推荐的实践是「给Python代码标注类型,再配合静态检查工具,那么也会向静态语言那样在代码提交前就发现问题」,这样的方式已经在其他语言里面获得了成功,如Javascript到TypeScript、PHP到Hack。
Python这种动态语言在阅读代码时很考验编程经验,即便是再资深的Python工程师也需要通过代码了解变量的类型,而加了类型注解后,对于开发者理解和维护会容易很多。
另外一个需要类型注解的理由是它给IDE(如Pycharm、VS Code等)提供了尽量多甚至是准确的信息,这对于形参的类型提示、检查、批量处理等操作来说如有神助。
接下来,按着时间线,来了解下Python类型系统的发展,看看类型注解都能帮助Python开发者做什么。
PEP 3107 – Function Annotations
从Python 3.0开始就加入了PEP 3107里面设计的新的语法「可以给函数参数和返回值注释」:
In : def test(a: 'this is str', b: 'this is int'): # 形参冒号后面的就是注释
...: ...
...:
In : test.__annotations__ # 注释信息存在__annotations__里面
Out: {'a': 'this is str', 'b': 'this is int'}
In : def test2(a: str, b: int):
...: ...
...:
...:
In : test2.__annotations__
Out: {'a': str, 'b': int}
In : test2(1, '2') # OK 即便不符合注释内容也无所谓
In : def test3(a: str, b: int) -> int: # 使用-> 后面对返回值注释
...: return b * 2
...:
...:
In : test3('a', 3)
Out: 6
In : def test4(a: str, b: int) -> int: # 返回值只是注释
...: return a * 2
...:
...:
In : test4('a', 3)
Out: 'aa'
In : test4.__annotations__
Out: {'a': str, 'b': int, 'return': str}
这个语法里面的注释的值是表达式,所以可以是字符串、类名、类型名、变量等等,但这些注释并不附加没有任何语义,也不会做检查。
mypy
mypy是作者Jukka Lehtosalo 2012年为了完成博士学位论文而做的,当时Jukka认为Python效率低下,且应该有健全的静态类型,所以它实现了这个Python的变种。注意此时mypy的定位并不是静态检查工具。
在PyCon 2013时,Jukka做了<Mypy: Optional Static Typing for Python>这个分享,之后和Python之父Guido van Rossum(以下都简称Guido)对于它的课题以及mypy进行了交流,发现Guido也在思考类似的问题(但是没有行动)。最终他接受了Guido的建议:
-
让mypy的语法和CPython兼容。 -
可以使用普通的Python解释器直接运行mypy程序。 -
加强mypy的类型检查器部分的实现。
对,到这里mypy就开始走向了静态检查工具的方向,如果早期你使用它就会发现它其实叫做mypy-lang
,现在已经不在提lang
,而是static analyzer
或者 lint tool
。
接着Guido邀请他访问Dropbox(Guido13-19年在Dropbox)并最终给了Jukka工作机会。由于类型系统需要解决和讨论的问题还很多,入职后Jukka并没有专门从事mypy的工作,但是对mypy研究一直在进行。
Guido的类型注解提案
在Europython 2014上simplejson作者Bob Ippolito做了<What can python learn from Haskell?>的演讲,提到了一些类型方面的建议,之后Guido、Jukka和Bob进行了深入交流。其中「用mypy语法给函数注解」这个方案获得了Guido认可。
不久,Guido在Python邮件组提交了提案: <Proposal: Use mypy syntax for function annotations>(延伸阅读链接2)。
在这个最初的草案中,明确了把mypy作为一个类型检查的linter,而不是作为编译器或解释器,并确定了类型注解的定位:
-
在运行时不能进行数据类型推断。 -
类型注解会被解释器当作注释丢弃掉。类型注解功能是为了提高开发者的体验而生的,所以不应该影响原有程序。
类型标注风格在一开始就已经确定了:
from typing import List, Dict
def word_count(input: List[str]) -> Dict[str, int]:
result = {} #type: Dict[str, int]
for line in input:
for word in line.split():
result[word] = result.get(word, 0) + 1
return result
形参input: <type>
是语言支持时的语法(Python 3), 而#type: type
这种注释是当语言不支持语法的兼容用法(Python 2)。
这个提案中还提到了很多内容,例如把mypy的typing.py文件拷贝到标准库、调整PEP 3107的注释方式等,就不挨个介绍了。在之后的PEP里面还有具体介绍最终的实现。
类型注解的PEP提案(Python 3.5)
接着,Guido和Ivan Levkivskyi的<PEP 483 – The Theory of Type Hints>(延伸阅读链接3,也就是类型注解理论)和Guido、Jukka和Łukasz Langa的<PEP 484 – Type Hints>(延伸阅读链接4,类型注解最主要的PEP)提交了。这些PEP提案在Python 3.5实现了,所以从Python 3.5开始正式支持类型注解了。
先统一一下对
虽然PEP的id更小,但是在我的理解PEP 483是PEP 484的理论补充,对于开发者来说,类型注解主要看PEP 484。这个PEP的内容很多,我把它总结成如下几部分内容。
类型注解不会强制推行
PEP明确提出,Python将永远保留动态类型语言的特性,而类型注解将来也不会作为默认策略强制推行。走到现在,可以看到它的进化非常平缓,非侵入性的,如果你不关注可以完全忽略这部分内容。
确定了注解语法
如之前Guido在邮件组的草案一样,我就不重复了。另外一个重要语法是用中括号把类型括起来表现容器/泛型结构的元素类型:
In : from typing import *
In : List[int]
Out: typing.List[int]
In : Tuple[int]
Out: typing.Tuple[int]
In : Dict[str, int] # 因为有键和值2个类型
Out: typing.Dict[str, int]
In : T = TypeVar('T')
In : Generic[T]
Out: typing.Generic[~T]
In : class Item:
...: ...
...:
In : Sequence[Item]
Out: typing.Sequence[__main__.Item]
In : Set[Any]
Out: typing.Set[typing.Any]
泛型和TypeVar
在之前的文章Python 3.11新加入的和类型系统相关的新特性: PEP 646 – Variadic Generics已经介绍过了泛型和TypeVar,这里不重复了。
用Union组合多个类型
当单个参数可以是多个类型时可以使用Union组合。不过在Python 3.10的PEP 604中提供了新的|
语法,更Pythonic,具体的可以看我之前的文章Python 3.10新加入的四个和类型系统相关的新特性: PEP 604: New Type Union Operator
Stub文件
Stub(存根)文件是包含类型注解的文件,这些注解仅供类型检查器使用,而不是在运行时使用。
Stub文件通常后缀是pyi
,你可以理解为在Stub文件中重新定义了一遍相关的函数/类/变量等内容的类型,而原来的.py
源文件不受影响。
这个Stub机制之后还会专门说。
后记
Python 3.5发布后,Guido、Jukka等人组建了一个专门的专门研究mypy,并且对它做了很多性能改进,在Python 3.6发布时(也就是16年底),Dropbox完成了超过400万行代码的类型注解,mypy在各个团队里面迅速普及。当时他们还专门写了一篇文章介绍这个事情,这个是Python类型系统发展的里程碑事件了(延伸阅读16)。
Python3.6
Python 3的大的feature除了类型系统就是asyncio,Python 3.6里面除了引入异步生成器、异步推导式以外还加入了非常好用的f-string。这个版本是我心目中第一个可以用生产环境的Python 3版本。
不过这个版本里面对于类型注解相关的新特性只有<PEP 526 – Syntax for Variable Annotations>(延伸阅读链接6)。在Python 3.5引入的类型注解主要是针对函数/方法的,而PEP 526是针对于变量的:
my_var: int # 不带默认值
my_var: int = 10 # 带默认值
my_var = 5 # OK
other_var: int = 'a' # Rejected
some_list: List[int] = []
body: Optional[List[str]]
class BasicStarship:
captain: str = 'Picard' # 带默认值的实例变量
damage: int # 不带不认知
stats: ClassVar[Dict[str, int]] = {} # 使用ClassVar就是类变量
Python 3.7
这个版本中主要有2个类型系统的修改。
PEP 560 – Core support for typing module and generic types
最初PEP 484中的设计是不会对核心CPython解释器进行任何和类型标注相关更改,完全由标准库typing和外部的mypy等静态检查工具来完成。但是可以想象这会存在潜在的限制,在很多特殊场景里面需要做很多hack,存在一些不好解决的bug,还有性能问题(具体的可以看PEP内容,延伸阅读链接7)。但是此时已经有大量的开发者在使用类型注解,所以官方决定解除这个限制,可以通过添加两个特殊方法__class_getitem__
和__mro_entries__
以便更好地支持泛型类型。
这2个新方法日常基本接触不到,就不介绍了。
PEP 563 – Postponed Evaluation of Annotations
当时的类型注解是在函数/变量定义时进行评估的,这就有了2个问题,第一个是类型注解是在模块导入时执行的,它的执行是需要有开销的,而第二个是向前引用(Forward References)的问题,我举个例子:
In : class Item:
...: def __init__(self, id):
...: self.id = id
...:
...: @classmethod
...: def get(cls: Item, id) -> Item: # 期待返回Item类型的结果
...: return cls(id)
...:
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In [1], line 1
----> 1 class Item:
2 def __init__(self, id):
3 self.id = id
Cell In [1], line 6, in Item()
2 def __init__(self, id):
3 self.id = id
5 @classmethod
----> 6 def get(cls: Item, id) -> Item:
7 return cls(id)
NameError: name 'Item' is not defined
这样用会报错,因为如果按照方法定义时计算的话,那个时候Item类还没有创建成功呢。
所以这个PEP是建议更改函数/变量注释的评估(Evaluate)时机,以便在函数/变量定义时不再对它们进行评估: 会先在__annotations__
以字符串形式保存在之后评估:
In : from __future__ import annotations
In : class Item:
...: def __init__(self, id):
...: self.id = id
...:
...: @classmethod
...: def get(cls: Item, id) -> Item:
...: return cls(id)
...:
In : Item.get(1).id
Out: 1
In : Item.get.__annotations__
Out: {'cls': 'Item', 'return': 'Item'} # 返回值被自动保存成了字符串类型
In : from typing import get_type_hints
In : get_type_hints(Item.get)
Out: {'cls': __main__.Item, 'return': __main__.Item} # 别担心,typing提供方法获得正确的类型
在Python 3.11之前,解决这个问题的其中一个方案就是使用from __future__ import annotations
(当然,还可以直接让返回值的标注为字符串)。
当时说这个PEP的评估设想会在Python 3.10作为默认的方案,但是在Python 3.11引入了新的Self类型更好的解决了这个问题,具体的可以看我之前写的: Python 3.11新加入的和类型系统相关的新特性: PEP 673 – Self Type,这个方案也就被抛弃了,Python 3.11的会更新日志里明确说了「PEP 563 may not be the future」,这个计划已经被无限期的搁置。
Python 3.8
这个版本是类型系统的一次重大的更新,它主要包含如下几个新功能。
PEP 589 – TypedDict: Type Hints for Dictionaries with a Fixed Set of Keys
新增的TypedDict
是具有一组固定键的字典的类型提示,非常有价值。具体的在 Python 3.11新加入的和类型系统相关的新特性: PEP 655 – Marking individual TypedDict items as required or potentially-missing
PEP 586 – Literal Types
之前定义参数或者返回使用的都是抽象的类型,而这个字面值类型可以直接定义一(多)个具体的可选值:
from typing import Literal, Union
def accepts_only_four(x: Literal[4]) -> None:
pass
accepts_only_four(4) # OK
accepts_only_four(19) # Rejected
accepts_only_four(2 + 2) # Rejected 这就是字面量哈,需要直接写值,不能通过计算
def open(path: Union[str, bytes, int],
mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"],
):
...
open('1.py', 'r') # OK
open('1.py', 'xx') # Rejected
open('1.py', 'b') # Rejected
union_var: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None] # 其实这就是一个字面量值的组合罢了
union_var = 5 # OK
union_var = 'foo' # OK
union_var = 1 # OK
union_var = [1, 2] # Rejected
最后那个union_var
其实就是个演示,实际工作中用处不大。
PEP 591 – Adding a final qualifier to typing
这个PEP定义了Final限定符,可以通过final装饰器或者Final作为类型注解。它用在:
-
被声明的方法不能被重载 -
被声明的类不能被继承(子类化) -
被声明的属性或者变量不能被重新设值。
这个PEP比较好理解,直接粘贴PEP里面的例子一看就懂了:
from typing import final, Final
@final
class Base:
...
class Derived(Base): # Error: Cannot inherit from final class "Base"
...
RATE: Final = 3000
class Base:
DEFAULT_ID: Final = 0
RATE = 300 # Error: can't assign to final attribute
Base.DEFAULT_ID = 1 # Error: can't override a final attribute
这个我没实际用过,总之如果你不希望某个方法/函数/类/属性/变量等内容的类型在运行过程中被修改就可以使用它。
PEP 544 – Protocols: Structural subtyping (static duck typing)
鸭子类型(duck typing)在程序设计中是动态类型的一种风格。 在这种风格中,「当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子」。
所以在Python中编写接收特定输入的函数时,我们只需要关心该函数输入的行为、属性,而不是该函数输入的显式类型:
class Duck:
def quack(self) -> str:
return "Quack."
def sonorize(duck: Duck) -> None:
print(duck.quack())
sonorize(Duck())
在上述例子里,sonorize
函数并不关心形参duck的类型,只要它有quack
方法就可以让函数正常执行。
在过去,类型注解只支持上述直接方案(可以传入当前类Duck或者其子类),这个PEP提供了Protocol
来对鸭子类型进行支持。那么只需要判断是否有同样的结构就可以了,这种类型叫做Structural subtyping
。看个例子:
from typing import Protocol
class Quacker(Protocol):
def quack(self) -> str:
...
class OtherDuck:
def quack(self) -> str:
return "QUACK!"
def sonorize2(duck: Quacker) -> None:
print(duck.quack())
sonorize2(Duck())
sonorize2(OtherDuck())
定义类时只需要继承Protocol就可以声明一个接口类型,当遇到接口类型的注解时,只要接收到的对象实现了接口类型的所有方法,即可通过类型注解的检查。所以Duck
和OtherDuck
都可以作为参数传给sonorize2
。
如果你学过Golang的接口,会更好理解这个PEP的内容。mypy官网列出了很多使用protocol的例子,链接是延伸阅读9。
Python 3.9
这个版本中只有一个主要的类型系统的新特性,就是<PEP 585 – Type Hinting Generics In Standard Collections>。
原来注解使用的Collection类型(列表、字典、集合、元组、collections模块内的结构等等)需要从typing模块显示的import,举个例子:
from collections import Counter
from typing import List, Dict, Counter as CounterType
l : List[int] = [1, 2]
dct: Dict[str, int] = {'key': 10}
c: CounterType[str] = Counter('abbddx')
而现在这些类型已经原生支持泛型了,可以直接当做类型用了:
l2: list[int] = [1, 2]
dct2: dict[str, int] = {'key': 10}
c2: Counter[str] = Counter('abbddx')
这样非常方便。
Python 3.10
这个版本有四个新的特性,我之前专门写过: Python 3.10新加入的四个和类型系统相关的新特性,具体的可以看原文。简单说一下:
-
PEP 604: New Type Union Operator。可以使用 |
组合不同的类型。 -
PEP 613: TypeAlias。「类型别名」这个类型可以帮助分辨是TypeAlias,还是普通的赋值。 -
PEP 647: User-Defined Type Guards。当某个参数类型本来可以符合多个类型,但是在特定的条件里可以让类型范围缩小。 -
PEP 612 – Parameter Specification Variables。新增的 typing.ParamSpec
帮助我们方便【引用】位置和关键字参数,而这个PEP另外一个新增的typing.Concatenate
是提供一种添加、删除或转换另一个可调用对象的参数的能力。
Python 3.11
刚刚发布的版本,这个版本有五个新的特性,我之前专门写过: Python 3.11新加入的和类型系统相关的新特性,具体的可以看原文。这个就简单说一下:
-
PEP 646 – Variadic Generics。可变数量的泛型类型,之前介绍的TypeVar是单个泛型,而这次引入了数量不确定的泛型类型TypeVarTuple。 -
PEP 673 – Self Type。解决前面提到的向前引用的问题,替代PEP 563成为解决这个问题的新方案。 -
PEP 675 – Arbitrary Literal String Type。 LiteralString
可以表示任意的字符串字面值,不像前面的typing.Literal,只能规定几个对应的确定的值,灵活性太差。 -
PEP 681 – Data Class Transforms。实现了一种把普通类的一些和标准库dataclasses相似的行为的类型检查自动转换的方案。 -
PEP 655 – Marking individual TypedDict items as required or potentially-missing。可以明确 TypedDict
内各个键值的类型是可选还是必选。
Python的发展史目前就到这里了,相信未来很有很多路要走,我们继续期待吧。接着说一点和类型系统相关的主题。
类型注解代码存放分发方案
类型注解通常是直接在源码上加,但是也有相当多的项目使用Stub文件把代码和注解分开,这里继续展开Stub文件的存放分发方案。
受「懒」、「不喜欢」、「兼容性考虑」或者「不认可类型」注解等等原因影响,我们日常使用的大部分库是没有注解的的,知名项目的情况越来越好,但是一些相对受众少不太知名的项目类型注解没有或者极少。
为此缓解这个问题,社区提供了Library stub机制,也就是PEP 561(延伸阅读链接11)。Stub文件为库的公共接口定义类型注解,使静态检查器可以覆盖到对应库的使用。这样可以在第三方引入库这个角度缓解开发者的负担,也间接提供了很多范例帮助开发者快速熟悉和理解类型注解,甚至可以作为借鉴。
这部分通过不同的知名项目来了解一下这类文件的存放方案,从而了解这个机制。
1. 将它们与代码放在同一目录中
这个方式是最简单的,开发者和静态检查工具可以容易的发现,当然在开源项目中见得不多,主要场景是私有的代码库。日志库loguru就是这样的:
➜ ll loguru/__init__*
-rw-r--r-- 1 weiming.dong staff 626B Oct 27 23:20 loguru/__init__.py
-rw-r--r-- 1 weiming.dong staff 14K Oct 27 23:20 loguru/__init__.pyi
loguru库使用__init__.py
暴露接口,所以它把想要注解的都放在了loguru/__init__.pyi
里。
2. 上传到PYPI
另外一个方式就是把Stub文件独立作为一个包上传到PYPI,可以在需要时安装或者更新它。把类型注解和原始代码完全分离,这样的好处是不会影响原来的代码逻辑,这样既不影响开发的进度和效率,也能尽量的覆盖类型标注。不过这主要是一个管理的问题,类型注解理论上永远都会滞后于代码迭代。
我觉得这个方案比较好的场景是完成注解的开发者不是源项目的开发者,更适合社区行为。例如Django就没有官方的类型标注支持,如果你需要的话可以使用django-stubs,它的目录结构和Django的一样,但只有.pyi
文件标注类型。
3. 官方的typeshed
社区提供了一个独立的项目typeshed(https://github.com/python/typeshed),包含了Python标准库(stdlib目录)及一些第三方库(stubs目录)的stub文件。
如Flask-SQLAlchemy、redis、requests等知名项目。看一个例子了解下这套流程吧:
import requests
r = requests.post('https://httpbin.org/post', data={'key': 'value'})
print(r.json())
在一个新的项目中,准备用requests这个库,运行mypy会报如下错误:
➜ python -m pip install requests
➜ mypy reuquests_type.py
reuquests_type.py:1: error: Library stubs not installed for "requests" (or incompatible with Python 3.11)
reuquests_type.py:1: note: Hint: "python3 -m pip install types-requests"
reuquests_type.py:1: note: (or run "mypy --install-types" to install all missing stub packages)
reuquests_type.py:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
Found 1 error in 1 file (checked 1 source file)
它会非常明确的告诉你有这样的Stub文件,只是还没有安装,安装了再运行mypy就好了:
➜ python3 -m pip install types-requests
➜ mypy reuquests_type.py
Success: no issues found in 1 source file
这个types-requests
库其实就是上面这个typepushed里面的stubs/requests,这是项目自己实现的一个上传PYPI功能,具体的可以看README页面的说明。
typing_extensions
Python标准库的typing更新受限于Python版本发布,如果你想提前使用一些在未来新的版本才会有的特性,可以安装最新版的typing_extensions
。
➜ python3 -m pip install -U typing_extensions
例如现在Python 3.11才发布,Python 3.12需要等到明年10月份。但是通过typing_extensions
,你可以现在就体验:
-
<PEP 698 – Override Decorator for Static Typing>里面的override。 -
<PEP 696 – Type defaults for TypeVarLikes>里面的 TypeVar
、ParamSpec
、TypeVarTuple
的默认值。 -
<PEP 695 – Type Parameter Syntax>里面的 infer_variance
。
当一个版本发布后,下一个版本的那些PEP就或慢慢实现和完善,目前typing_extensions
对PEP 688、PEP 692的功能还没实现,需要再等等。当然,受CPython解释器限制,也不是下一个版本的每个PEP都可以提前体验,可以关注社区对应讨论。
其他静态类型检查工具
前面我只用了mypy这一种官方提供的静态类型检查工具,它是最主流的,但是依然有另外几个工具也值得提一下。
注: 下面这几个我只是列出来,生产环境使用mypy永远是第一选择,其他的如果有必要可以作为额外的检查工具。其他的如Pydantic这种在运行时强制执行类型检查的我并不赞同所以本文就不涉及了。
pyright
pyright是微软开源的静态类型检查工具,它是用TypeScript编写的,它的特点主要是和VS Code(毕竟也是微软家的)的集成(通过部分功能开源的Pylance)。
pytype
pytype是谷歌开源的静态类型检查工具,它没有mypy对类型的那么严格的要求,更宽松一些,另外是可以通过代码对没有注解的逻辑进行类型推测。在PyCon 2019时,开发者做一个演讲介绍和mypy的区别,可以看延伸阅读链接14,其中举了2个例子:
# Case 1
def f():
return "PyCon"
def g():
return f() + 2019 # str和int相加其实会报错,pytype会报错,但是mypy不会
# Case 2
from typing import List
def get_list() -> List[str]:
lst = ["PyCon"]
lst.append(2019)
return [str(x) for x in lst] # mypy会报错,但是pytype会报错
另外我的感受是它的开发迭代更慢,对于社区的响应和支持差mypy很多。
pyre
pre是Facebook开源的静态类型检查工具,它的存在感比较低。它的价值按官网说主要是比mypy快,因为在大型项目中mypy会非常慢,这会让本地的检查非常耗时,不过我暂时没有大型项目的经验这部分不了解。
pyre另外一个功能是通过pyre infer
对代码做自动的类型推断,可以直接修改源代码,不过我试了一下效率很差。目前没有可以自动做类型注解的能在生产环境中使用工具,还是人工更靠谱。
怎么让自己成为Type Hints专家?
在我的理解里面:
-
typing模块和mypy的官方文档都非常完善,也有对应的例子,熟读和理解它。 -
熟悉和类型注解的那些PEP提案。 -
在自己的项目或者公司小型的1-2个项目中实战一下。
我认为这样就可以足够了。
另外我特别推荐Adam Johnson的博客,里面有很多非常细的类型注解知识点的理解和案例,非常值得去阅读。
后记
这些年随着越来越多的库开始使用类型注解,类型注解越来越受到开发者的关注,未来也势必会变得流行。在最后,从一个Python开发的角度尝试说服大家成为类型注解的支持者。
从我的角度,看看当时我不喜欢类型标注的理由吧:
-
开发成本。必须承认给项目引入静态检查增加了学习和完成工作的成本,但是如果从长远得看,它能带来的收益是远大于开发者的付出。Python不加注解写起来真的很爽,但是出现bug的几率高的太多了,即便是现在的我也会时常编写一些会有低级的、类型有关的错误的代码跑到服务器上,等报错了才发现,哎呀,这里没考虑到,然后紧急改一下,而使用类型注解可以在运行前就发现绝大部分这类问题。 -
降低了可读性。本来简洁的Python代码加了注解就变得很混乱,让你有一种写的不是Python的感觉。是的,使用类型注解是一种心理的转变,这个是需要过程适应的,其实如果你写过短短几天,你就会习惯在读代码时忽略对应的类型注解。当然如果你正在关注它的类型,那么类型注解反而直接给你答案而不会自己读逻辑去总结,这个角度反而是提高了代码可读性。
我以前写的时候偶尔会遇到对于一些复杂逻辑注解非常难表达的,即便表达出来也会有注解的内容非常冗长、不易理解、类型不准确、不灵活等问题,这个时候特别容易怀疑这个类型注解到底行不行,很劝退。不过后来都解决了,其实是自己对类型注解了解的不够深刻,所以要善用TypeVar+bound
、TypeDict
、Protocol
、Generics
、@overload
等等特性。
代码目录
本文代码可以在 mp 项目找到
延伸阅读
-
http://mypy-lang.blogspot.com/2012/12/why-mypy-can-be-more-efficient-than.html -
https://mail.python.org/pipermail/python-ideas/2014-August/028618.html -
https://peps.python.org/pep-0483/ -
https://peps.python.org/pep-0484/ -
https://docs.python.org/zh-cn/3/glossary.html -
https://peps.python.org/pep-0526/ -
https://peps.python.org/pep-0560/ -
https://peps.python.org/pep-0563/ -
https://mypy.readthedocs.io/en/stable/protocols.html -
https://peps.python.org/pep-0585/ -
https://peps.python.org/pep-0561/ -
https://github.com/microsoft/pyright -
https://github.com/google/pytype -
https://www.youtube.com/watch?v=yFcCuinRVnU&t=2300s -
https://github.com/facebook/pyre-check -
https://dropbox.tech/application/our-journey-to-type-checking-4-million-lines-of-python -
https://adamj.eu/tech/
原文始发于微信公众号(Python之美):Python类型系统发展史
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论