8. 函数中的类型提示
还应该强调的是 , "Python仍是一门动态类型语言, 作者并不意图强制使用类型提示, 这只是一种约定."
--Guido van Rossum、Jukka Lehtosalo 和 Lukasz Langa
'PEP 484--Type Hints' ①
( 注 1 : 'Rationale and Goals' , 保留原文中的加粗强调 . )
自 2001 年发布的Python 2.2 统一类型和类之后 , 类型提示是Python历史发展过程中最大的变化 .
但是 , 不是所有Python用户都能从类型提示中受益 .
因此 , 必须把这作为一种可选的功能 .
'PEP 484—Type Hints' 为函数参数 , 返回值和变量的显式类型声明规定了句法和语义 ,
目标是协助开发者工具通过静态分析 ( 例如 , 不通过测试真正运行代码 ) 发现Python基准代码中的bug .
类型提示的主要受益者是使用IDE ( Integrated Development Environment , 集成开发环境 ) 和
CI ( Continuous Integration , 持续集成 ) 的专业软件工程师 .
这类人群看重的是类型提示的成本效益分析 , 不是所有Python用户都关注这一点 .
Python的用户群体分布广泛 , 包括科学家 , 交易员 , 记者 , 艺术家 , 创客 , 分析师和许多学科的学生等 .
对他们中的大多数人来说 , 学习类型提示的成本可能较高 , 除非以前用过具有静态类型 , 子类型和泛型的语言 .
考虑到他们使用Python的方式 , 以及基准代码和团队 ( 通常是 '一人团队' ) 的规模较小 , 这些用户的收益较低 .
Python默认的动态类型更简单 , 也更具表现力 ,
特别适合在数据科学 , 创造性计算和学习中编写探索数据和想法的代码 .
本章重点讲解Python函数签名中的类型提示 .
第 15 章将探讨类中的类型提示以及typing模块的其他功能 .
本章主要涵盖以下内容 :
• 通过Mypy , 实践的方式介绍渐进式类型 ;
• 鸭子类型和名义类型的互补作用 ;
• 概述可以出现在注解中的主要类型--约占本章 60 % 的篇幅 ;
• 为变长参数 ( * args和 * * kwargs ) 添加类型提示 ;
• 类型提示和静态类型的局限及缺点 .
8.1 本章新增内容
这是全新的一章 .
我写完本书第 1 版后发布的Python3 . 5 中才出现类型提示 .
考虑到静态类型系统的局限性 , PEP484只能引入一种渐进式类型系统 ( gradual typesystem )
首先为这个概念下一个定义 .
8.2 关于渐进式类型
PEP484为Python引入了一种 '渐进式类型系统' .
渐进类型是Jeremy Siek在 2006 年与Walid Taha开发的一种类型系统 ,
它允许对程序的某些部分进行动态类型化 , 而对其他部分进行静态类型化 .
程序员通过省略类型注释或添加类型注释来控制哪些部分是哪些部分 .
其他语言也有使用渐进式类型系统的 , 例如Microsoft的TypeScript ,
Dart ( Flutter SDK使用的语言 , 由谷歌创建 ) 和Hack ( PHP的一种方言 , 由Facebook的HHVM虚拟机支持 ) .
类型检查工具Mypy最初也是一门语言 , 是Python的一种方言 , 有自己的解释器 , 支持渐进式类型 .
后经Guido van Rossum的劝说 , Mypy的创建者Jukka Lehtosalo把它改造成了一个检查Python代码注解的工具 .
渐进式类型系统具有以下性质 .
是可选的
默认情况下 , 类型检查工具不应对没有类型提示的代码发出警告 .
当类型检查工具无法确定对象的类型时 , 会假定其为Any类型 . Any类型与其他所有类型兼容 .
不在运行时捕获类型错误
类型提示相关的问题由静态类型检查工具 , lint程序和IDE捕获 .
在运行时不能阻止把不一致的值传给函数或分配给变量 .
不能改善性能
类型注解提供的数据在理论上可以优化生成的字节码 ,
但是据我所知 , 截至 2021 年 7 月 , 任何Python运行时都没有实现这种优化 . ②
( 注 2 : PyPy的即时编译器提供的数据比类型提示丰富很多 ,
能在Python程序运行过程中对其进行监控 , 检测具体使用的类型 , 优化生成的机器码 . )
对渐进式类型来说 , 注解始终是可选的 , 这个性质最能体现可用性 .
在静态类型系统中 , 大多数类型约束很容易表达 , 但是也有许多类型约束很难表达 ,
有些是不易表达 , 有些则根本表达不出来 .
③你写的Python代码可能质量很高 , 也有较好的测试覆盖率 , 能顺利通过测试 ,
但就是无法添加类型提示 , 让类型检测工具满意 .
这没什么关系 , 类型提示有瑕疵就随它去吧 , 不影响产品发布 .
( 注 3 : 例如 , 截至 2021 年 7 月 , 不支持递归类型 ,
详见typing模块的 182 号工单 'Define a JSON type' 以及Mypy的 731 号工单 'Support recursive types' . )
类型提示在所有层面上均是可选的 , 一整个包都可以没有类型提示 ,
即便有类型提示 , 导入模块时也可以让类型检查工具保持静默 ,
另外还可以通过特殊的注释让类型检查工具忽略代码中指定的行 .
* --------------------------------------------------------------------------------------------- *
100 % 的类型提示覆盖率太过激进 , 只是一味追求指标 , 不现实 ,
也有碍团队充分利用Python的强大功能和灵活性 .
应该坦然接受没有类型提示的代码 , 防止注解扰乱API , 增加实现难度 .
* --------------------------------------------------------------------------------------------- *
8.3 渐进式类型实践
接下来 , 我们逐步为一个简单的函数添加类型提示 , 使用Mypy检查 , 实际体验一下渐进式类型系统 .
* * * ---------------------------------------------------------------------------------------- * * *
兼容PEP484的Python类型检查工具很多 ,
比如谷歌的pytype , 微软的Pyright , Facebook的Pyre , 以及PyCharm等IDE内置的类型检查器 .
本节的示例选择Mypy是因为它最出名 . 可以根据具体的项目或团队的喜好从其他几个工具中做出选择 .
Pytype就是不错的选择 , 对于没有类型提示的基准代码 , Pytype 仍能提供有用的建议 ,
而且容错能力比Mypy强 , 还能为代码生成注解 .
* * * ---------------------------------------------------------------------------------------- * * *
我们要注解的是show_count函数 . 这个函数会返回一个字符串 , 根据数量多少做单复数变形 .
>> > show_count( 99 , 'bird' )
'99 birds'
>> > show_count( 1 , 'bird' )
'1 bird'
>> > show_count( 0 , 'bird' )
'no birds'
示例 8 - 1 是show_count函数不带注解的源码 .
def show_count ( count, word) :
if count == 1 :
return f'1 { word} '
count_str = str ( count) if count else 'no'
return f' { count_str} { word} s'
8.3.1 Mypy初体验
对messages . py模块运行mypy命令 , 开始做类型检查 .
. . . / no_hints/ $ pip install mypy
[ lots of messages omitted. . . ]
. . . / no_hints/ $ mypy messages. py
Success: no issues found in 1 source file
使用默认设置的Mypy没有发现示例 8 - 1 存在问题 .
* * * ---------------------------------------------------------------------------------------- * * *
我使用的是Mypy 0.910 , 这是我在 2021 年 7 月审稿时Mypy的最新版 .
按照Mypy文档所说 , Mypy '严格来说是测试版软件, 偶有破坏向后兼容性的改动.'
Mypy回显的报告中至少有一条与我在 2020 年 4 月撰写本章时不一样 .
你阅读本章时看到的结果或许也有差异 .
* * * ---------------------------------------------------------------------------------------- * * *
对于没有注解的函数签名 , Mypy默认忽略 , 除非另有配置 .
示例 8 - 2 仍然使用pytest做单元测试 , 这部分代码被放在messages_test . py模块中 .
from pytest import mark
from messages import show_count
"""
test_show_count函数使用mark.parametrize装饰器,
对show_count函数进行了两组参数化测试, 每组参数都由一个数量和一个名词构成.
这些参数会依次传递给show_count函数, 并对返回结果进行检查, 检查结果与预期结果是否一致.
"""
@mark. parametrize ( 'qty, expected' , [
( 1 , '1 part' ) ,
( 2 , '2 parts' ) ,
] )
def test_show_count ( qty, expected) :
got = show_count( qty, 'part' )
assert got == expected
"""
test_show_count_zero函数用于测试show_count函数在数量为0时的返回结果是否正确.
"""
def test_show_count_zero ( ) :
got = show_count( 0 , 'part' )
assert got == 'no parts'
下面在Mypy的指引下添加类型提示 .
8.3.2 让Mypy严格要求
指定命令行选项--disallow-untyped-defs , Mypy报告没有为参数和返回值添加类型提示的函数定义 .
使用这个选项检查测试文件 , 报告 3 个错误和 1 个提示 .
.. ./no_hints/ $ mypy --disallow-untyped-defs messages_test.py
messages.py:14: error: Function is missing a type annotation
messages_test.py:10: error: Function is missing a type annotation
messages_test.py:15: error: Function is missing a return type annotation
messages_test.py:15: note: Use "-> None" if function does not return a value
Found 3 errors in 2 files ( checked 1 source file )
----------------------------------------------------------------------------
messages.py:14: 错误: 函数缺少类型批注
messages_test.py:10: 错误: 函数缺少类型批注
messages_test.py:15: 错误: 函数缺少返回类型注释
messages_test.py:15: 错误: 如果函数未返回值, 请使用'->None'
在2个文件中发现3个错误( 已检查1个源文件)
messages . py使用这个↓ : ( 使用上面的messages . py , 测试结果是一样的 , 就行号有一点不同 . )
"""
# tag::SHOW_COUNT_DOCTEST[]
>>> show_count(99, 'bird')
'99 birds'
>>> show_count(1, 'bird')
'1 bird'
>>> show_count(0, 'bird')
'no birds'
# end::SHOW_COUNT_DOCTEST[]
"""
def show_count ( count, word) :
if count == 1 :
return f'1 { word} '
count_str = str ( count) if count else 'no'
return f' { count_str} { word} s'
一开始使用渐进式类型时 , 我喜欢指定另一个选项:--disallow-incomplete-defs .
这时Mypy报告没有问题 .
.. ./no_hints/ $ mypy --disallow-incomplete-defs messages_test.py
Success: no issues found in 1 source file
-----------------------------------------
成功: 在1个源文件中未发现问题
现在 , 只在messages . py中添加返回类型 .
def show_count ( count , word ) - > str :
这就为Mypy提供了一定的信息 .
再次使用前面的命令行检查messages_test . py , Mypy将同时检查messages . py。
.. /no_hints/ $ mypy --disallow-incomplete-defs messages_test.py
messages.py:14: error: Function is missing a type annotation
for one or more arguments
Found 1 error in 1 file ( checked 1 source file )
------------------------------------------------------------
messages.py:14:错误:函数缺少类型批注
用于一个或多个参数
在1个文件中发现1个错误( 已检查1个源文件)
现在 , 可以依次为函数添加类型提示 , 不让Mypy再报告关于函数没有注解的错误 .
下面是带完整注解的签名 , 能让Mypy满意 .
def show_count ( count : int , word : str ) - > str :
* * * ---------------------------------------------------------------------------------------- * * *
如果不想每次都输入 --disallow-incomplete-defs等命令行选项 ,
可以把需要的选项保存到Mypy配置文件 ( 详见文档 ) 中 .
设置既可以针对全局 , 也可以针对单个模块 .
下面是一个简单的mypy . ini配置 , 仅供参考 .
[ mypy ]
python_version = 3.9
warn_unused_configs = True
disallow_incomplete_defs = True
* * * ---------------------------------------------------------------------------------------- * * *
8.3.3 参数的默认值
示例 8 - 1 中的show_count函数只对规则的名词有效 ,
如果复数形式不是直接在后面添加 's' , 则应该让用户提供复数形式 , 如下所示 .
> > > show_count ( 3 , 'mouse' , 'mice' )
'3 mice'
这里 , 我们采用 '测试驱动开发' .
首先 , 添加针对第三个参数的测试 .
别忘了为测试函数添加返回值类型提示 , 否则Mypy不做检查 .
def test_irregular ( ) - > None :
got = show_count( 2 , 'child' , 'children' )
assert got == '2 children'
Mypy检测到以下错误 .
.. ./hints_2/ $ mypy messages_test.py
messages_test.py:22: error: Too many arguments for "show_count"
Found 1 error in 1 file ( checked 1 source file )
----------------------------------------------------------------
messages_test:22:错误: 'show_count' 的参数太多
在1个文件中发现1个错误( 已检查1个源文件)
. . . \pythonProject> mypy messages_test. py
messages_test. py: 21 : error: Too many arguments for "show_count" [ call- arg]
Found 1 error in 1 file ( checked 1 source file )
然后 , 编辑show_count函数 , 添加可选的参数plural , 如示例 8 - 3 所示 .
def show_count ( count: int , singular: str , plural: str = '' ) - > str :
if count == 1 :
return f'1 { singular} '
count_str = str ( count) if count else 'no'
if not plural:
plural = singular + 's'
return f' { count_str} { plural} '
现在 , Mypy报告 'Success' ( 成功 ) .
* * * ---------------------------------------------------------------------------------------- * * *
以下代码有一个Mypy没有捕获的类型错误 . 你能发现吗?
def hex2rgb ( color = str ) - > tuple [ int, int, int ] :
( color被声明为str类型 , 但应该是一个字符串参数 , 而不是字符串类型本身 . )
Mypy报告的错误不是特别有用 .
colors . py : 24 : error : Function is missing a type annotation for one or more arguments
colors . py : 24 : 错误 : 函数缺少一个或多个参数的类型批注
color参数的类型提示应该是color : str .
我写的color = str不是注解 , 而是把color的默认值设为str .
根据我的经验 , 这是一种常见的错误 , 容易被忽视 , 尤其是在复杂的类型提示中 .
* * * ---------------------------------------------------------------------------------------- * * *
对于这段代码 , Mypy报告的错误信息并不是非常有用 ,
因为它只是指出函数缺少了一个或多个参数的类型注解 , 而没有具体指明是哪个参数缺少了注解 .
一个有用的Mypy报告应该能够提供更具体的错误信息 , 例如指出缺少类型注解的参数名称或位置 .
这样开发者就可以直接定位问题并进行修复 .
下面是一个更有用的错误报告示例 :
colors . py : 24 : error : Missing type annotation for parameter 'color' in function 'hex2rgb'
这个报告指出了参数color缺少了类型注解 , 并且明确指出了具体的参数名称和函数名称 .
这样的报告更加清晰明了 , 开发者可以更方便地找到问题并进行修复 .
因此 , 一个有用的Mypy报告应该提供准确的错误信息 ,
明确指出问题所在 , 包括参数名称 , 函数名称和缺少的类型注解 .
这样开发者就能够更快地定位和解决类型错误 .
* * * ---------------------------------------------------------------------------------------- * * *
编写类型提示时建议遵守以下代码风格 .
• 参数名称和 : 之间不留空格 , : 后加一个空格 . 例如 : color : str .
• 参数默认值前面的 = 两侧加空格 . 例如 : color : str = "red" .
根据PEP 8 , 除参数默认值前面的 = 之外 , 其他 = 两侧不加空格 . ( 两侧应该添加空格 , 书上错了 ! )
* * * ---------------------------------使用flake8和blue检查代码风格----------------------------- * * *
不要天真 , 你记不住这些规则的 .
请使用fake8和blue等工具 .
fake8会报告代码风格等问题 , blue则会根据代码格式化工具black内置的 ( 大多数 ) 规则重写源码。
在统一代码风格方面 , blue比black好 , 因为blue遵守Python自身的风格 , 默认使用单引号 , 将双引号作为备选 .
> > > "I prefer single quotes"
' I prefer single quotes
从CPython源码中的repr ( ) 等处可以看出Python对单引号的偏爱 .
依赖repr ( ) 的doctest 模块默认使用单引号 .
blue的作者之一Barry Warsaw 也是PEP8的共同起草人 , 自 1994 年起一直是Python核心开发者 ,
从 2019 年至今 ( 2021 年 7 月 ) 还是Python 指导委员会 ( Steering Council ) 的一员 .
默认使用单引号是有坚强后盾的 .
如果必须使用black , 那么请使用black-S选项 , 保持引号原封不动 .
* * * ---------------------------------------------------------------------------------------- * * *
8.3.4 使用None表示默认值
在示例 8 - 3 中 , 为参数plural注解的类型是str , 默认值是 '' , 没有类型冲突 .
这是最好的情况 , 不过有时使用None表示默认值则更好 .
如果可选的参数是可变类型 , 那么None是唯一合理的默认值 ( 详见 6.5 .1 节 ) .
如果想把plural参数的默认值设为None , 则函数签名要改成下面这样 .
from typing import Optional
def show_count ( count : int , singular : str , plural : Optional [ str ] = None ) - > str :
下面来分析一下 .
• Optional [ str ] 表示plural的值可以是一个str或None .
• 必须显式地提供默认值 , 即 = None .
如果不为plural分配默认值 , 则Python运行时将把它视作必需的参数 .
记住 , 类型提示在运行时会被忽略 .
注意 , 需要从typing模块中导入 0 ptional .
导入类型时 , 建议使用from typing import ×句法 , 缩短函数签名的长度。
* * * ---------------------------------------------------------------------------------------- * * *
Optional并不是一个好名称 , 因为注解不能让参数变成可选的 .
分配默认值的参数才是可选的 . Optional [ str ] 的意思很简单 , 表明参数的类型可以是str或NoneType .
在Haskell语言和EIm语言中 , 相似的类型名为Maybe .
* * * ---------------------------------------------------------------------------------------- * * *
至此 , 我们对渐进式类型有了一定的认识 , 接下来讲一下 '类型' 概念的具体含义。
8.4 类型由受支持的操作定义
各种文献对类型概念的定义不一 . 这里 , 假定类型是一系列值和一系列可操作这些值的函数 .
-- "PEP 483-—The Theory of Type Hints"
实践中 , 最好把受支持的操作当作类型的关键特征 . ④
( 4 : 除了Enum类型 , Python未提供控制类型可取值的句法 .
例如 , 使用类型提示不能把Quantity定义为 1 和 1000 之间的整数 , 也不能把AirportCode定义为 3 个字母的组合 .
NumPy提供了uint8 . nt16和其他面向机器的数值类型 , 但是在Python标准库中 ,
我们只有取值范围非常小的类型 ( NoneType 和bool ) 和特别大的类型 ( float , int , str , 各种元组等 ) .
例如 , 从操作的可行性来看 , 下述函数中的×应当是什么类型?
def double ( x) :
return x * 2
参数可以是数值 ( int , complex , Fraction , numpy . uint32等 ) ,
可以是序列 ( str , tuple , list和array ) , 可以是N维numpy . array ,
也可以是实现或继承参数为整数的__mul__方法的其他类型 .
再来看一下下面这个带注解的double函数 .
这里没有返回类型 , 请暂时忽略这一点 , 把注意力放在参数类型上 .
from collections import abc
def double ( x: abc. Sequence) :
return x * 2
"""
使用Mypy测试:
test.py:4: error: Function is missing a return type annotation [no-untyped-def]
test.py:5: error: Unsupported operand types for * ("Sequence[Any]" and "int") [operator]
test.py:4: 错误:函数缺少返回类型注释 [无-未标记类型的-函数]
test.py:5: 错误:*("Sequence[Any]"和"int") [运算符]的操作数类型不受支持
"""
类型检查工具将拒绝接受这段代码 ( 它会报告错误并拒绝接受这段代码 ) .
如果告诉Mypy , x是abc . Sequence类型 , 那么Mypy在遇到 x * 2 时将报错 ,
因为抽象基类Sequence没有实现或继承__mul__方法 .
在运行时 , 这段代码既能成功处理str , tuple , list , array等具体的序列 , 也能处理数值 ,
因为类型提示在运行时会被忽略 .
但是 , 类型检查工具只关注显式声明的类型 , 而abc . Sequence没有_mul_方法 .
这就是本节标题想要表达的内容 .
前文给出的两版double函数 , Python运行时都接受 , ×参数的值可以是任何对象 .
x * 2 操作有可能成功 , 如果×不支持乘法 , 则会抛出 1 ypeError .
然而 , 在分析带注解的double 源码时 , Mypy将报告× * 2 是错的 ,
因为声明的类型x : abc . Sequence不支持此项操作 .
在渐进式类型系统中 , 以下两种对类型的解读相互影响着彼此 .
鸭子类型
该类型是Smalltalk ( 面向对象语言的先驱 ) 以及Python , JavaScript和Ruby采用的解读视角 .
对象有类型 , 但是变量 ( 包括参数 ) 没有类型 .
在实践中 , 为对象声明的类型无关紧要 , 重要的是对象具体支持什么操作 .
如果能调用birdie . quack ( ) , 那么在当前上下文中birdie就是鸭子 .
根据定义 , 只有在运行时尝试操作对象时 , 才会施行鸭子类型相关的检查 .
这比 '名义类型' ( nominal typing ) 更灵活 , 但代价是运行时潜在的错误更多 . ⑤
( 注 5 : 鸭子类型是结构类型 ( structural typing ) 的一种内隐形式 .
引入typing . Protocol之后 , Python 3.8 及以上版本也支持结构类型 .
8.5 .10 节将对结构类型进行简单介绍 , 第 13 章将深入讲解 . )
名义类型
该类型是C + + , Java和C # 采用的解读视角 , 带注解的Python支持这种类型 . 对象和变量都有类型 .
但是 , 对象只存在于运行时 , 类型检查工具只关心使用类型提示注解变量 ( 包括参数 ) 的源码 .
如果Duck是Bird的子类 , 那么就可以把Duck实例赋值给注解为birdie : Bird的参数 .
可是在函数主体中 , 类型检查工具认为birdie . quack ( ) 调用是非法的 , 因为birdie名义上是Bird对象 ,
而该类没有提供 . quack ( ) 方法 .
在运行时 , 实参是不是Duck实例并不重要 , 因为名义类型会在静态检查阶段检查 .
类型检查工具不运行程序的任何部分 , 只读取源码 .
名义类型比鸭子类型更严格 , 优点是能在构建流水线中 , 甚至是在IDE中输入代码的过程中更早地捕获一些bug .
( 名义类型 ( Nominal Typing ) : 名义类型是一种基于类型名称或声明的类型判断方式 .
在名义类型系统中 , 类型的兼容性是根据类型之间的显式声明或继承关系来确定的 .
只有在类型声明或继承关系明确匹配的情况下 , 类型才被认为是兼容的 .
例如 , 如果存在一个名为Animal的类和一个名为Cat的类 , 并且Cat显式声明它继承自Animal ,
那么在名义类型系统中 , Cat类型将只与Animal类型兼容 , 不能将Cat类型赋值给一个期望Dog类型的变量 .
鸭子类型 ( Duck Typing ) :
鸭子类型是一种基于对象行为而非类型声明的类型判断方式 .
根据鸭子类型的原则 , 只要一个对象具有特定的方法或属性 , 而不管其实际类型是什么 ,
我们可以将其视为拥有该方法或属性的类型 .
例如 , 如果有一个函数接受一个参数 , 并期望该参数具有quack ( ) 方法 ,
那么无论传递给函数的对象是什么类型 , 只要它有quack ( ) 方法 , 就可以成功调用该函数 .
在鸭子类型中 , 对象的兼容性是根据对象的行为 ( 方法和属性 ) 来确定的 , 而不是基于明确的类型声明 . )
示例 8 - 4 是一个没有实用价值的示例 , 仅用于比较鸭子类型和名义类型 , 以及静态类型检查和运行时行为 . ⑥
( 注 6 : 继承往往会被过度使用 , 很难通过务实的简单示例说清个中缘由 ,
所以请把这个动物示例看作对子类型的一种简单说明 . )
class Bird :
pass
class Duck ( Bird) :
def quack ( self) :
print ( 'Quack!' )
def alert ( birdie) :
birdie. quack( )
def alert_duck ( birdie: Duck) - > None :
birdie. quack( )
def alert_bird ( birdie: Bird) - > None :
birdie. quack( )
使用Mypy对birds . py进行类型检查 , 报告一个问题 .
. . . / birds/ $ mypy birds. py
birds. py: 16 : error: "Bird" has no attribute "quack"
Found 1 error in 1 file ( checked 1 source file )
仅仅分析源码 , Mypy就发现alert_bird有问题 :
类型提示声明的birdie参数是Bird类型 , 但是函数主体中调用了birdie . quack ( ) , 而Bird类没有该方法 .
在daffy . py中使用birds模块试试 , 如示例 8 - 5 所示。
from birds import *
daffy = Duck( )
alert( daffy)
alert_duck( daffy)
alert_bird( daffy)
运行Mypy检查dafiy . py , 报告的错误与在birds . py中定义的alert_bird函数内调用quck一样 .
.. ./birds/ $ mypy daffy.py
birds.py:16: error: "Bird" has no attribute "quack"
Found 1 error in 1 file ( checked 1 source file )
但是 , Mypy没有报告daffy . py本身存在问题 , 3 个函数调用都有效 .
运行dafis . py , 结果如下所示 .
. . . / birds/ $ python3 daffy. py
Quack!
Quack!
Quack!
一切正常 ! 鸭子类型的优点体现得淋漓尽致 .
在运行时 , Python不关注声明的类型 , 仅使用鸭子类型 .
虽然Mypy报告alert_bird有一个错误 , 但是在运行时使用daffy调用完全没问题 .
一开始 , 这可能会让很多Python程序员感到惊讶 : 静态类型检查工具有时会在将要执行的程序中发现错误 .
然而 , 几个月之后 , 如果你接到任务 , 扩展这个示例 , 那么你或许会感谢Mypy .
以示例 8 - 6 中同样使用birds的woody . py模块为例 .
from birds import *
woody = Bird( )
alert( woody)
alert_duck( woody)
alert_bird( woody)
使用Mypy检查woody . py , 发现两个错误 .
. . . / birds/ $ mypy woody. py
birds. py: 16 : error: "Bird" has no attribute "quack"
woody. py: 5 : error: Argument 1 to "alert_duck" has incompatible type "Bird" ;
expected "Duck"
Found 2 errors in 2 files ( checked 1 source file )
第一个错误在birds . py中 , 前面已经见过 , 即在alert_bird中调用birdie . quack ( ) .
第二个错误在woody . py中 , woody是Bird实例 , 调用alert_duck ( woody ) 是无效的 ,
因为alert_duck函数的参数类型应为Duck . 所有Duck都是Bird , 但不是所有Bird都是Duck .
在运行时 , woody . py中的调用都不成功 , 如示例 8 - 7 中的控制台会话所示 .
相关说明见各个标号 .
>> > from birds import *
>> > woody = Bird( )
>> > alert( woody)
Traceback ( most recent call last) :
. . .
AttributeError: 'Bird' object has no attribute 'quack
>> > alert_duck( woody)
Traceback ( most recent call last) :
. . .
AttributeError: 'Bird' object has no attribute 'quack"
>> > alert_bird( woody)
Traceback ( most recent call last) :
. . .
AttributeError: 'Bird' object has no attribute 'quack
这个小实验表明 , 鸭子类型更容易上手 , 也更灵活 , 但是无法阻止不受支持的操作在运行时导致错误 .
名义类型在运行代码之前检测错 , 但有时会拒绝实际能运行的代码 , 比如示例 8 - 5 中的alert_bird ( daffy ) 调用 .
即使alert_bird函数有时可以正常执行 , 它的名称也不恰当 :
函数主体中的对象需要支持 . quack ( ) 方法 , 但是Bird没有这个方法 .
( 名称不恰 : 因为函数的主体中对birdie参数执行了birdie . quack ( ) 操作 .
这意味着函数期望传递一个具有quack方法的对象作为参数 .
然而 , 由于Bird类型没有定义quack方法 , 所以在调用alert_bird ( daffy ) 时会引发错误 .
所以当说名称不恰当 , alert_bird可能会给人一种误导 , 让人误以为它可以接受任何Bird类型的对象作为参数 ,
而不考虑quack方法的存在与否 . 实际上 , 该函数需要传递一个具有quack方法的对象 , 这在示例中是不满足的 .
这种情况下 , 可能更恰当的函数名称可以是alert_quacking_bird或类似的名称 ,
以更好地描述函数的要求和行为 , 即要求传递一个会 'quack' 的鸟类对象 . )
这个示例没什么实际意义 , 函数主体只有一行 .
现实中的函数更长 , 可能会把birdie参数传给更多的函数 , 而且birdie参数的原始位置可能相距较远 ,
导致难以查明运行时错误的根源 . 类型检查工具可以防止许多此类错误在运行时发生 .
* * ------------------------------------------------------------------------------------------ * *
类型提示的价值很难通过这种小示例体现出来 .
基准代码体量越大 , 好处体现得就越明显 .
鉴于此 , 拥有数百万行Python代码的公司 ( 例如Dropbox , 谷歌和Facebook ) 才愿意投资团队和工具 ,
让整个公司都接纳类型提示 , 并在CI流水线中检查日益增长的Python基准代码 .
* * ------------------------------------------------------------------------------------------ * *
本节从简单的double ( ) 函数开始 , 探索了鸭子类型和名义类型中类型与操作之间的关系 .
我们还没有为double ( ) 函数添加完整的类型提示 .
8.5 节将介绍可用于注解函数的重要类型 .
8.5 .10 节将说明为double ( ) 函数添加类型提示的一种好方式 .
不过 , 在此之前 , 还有一些基础类型需要学习 .
8.5 注解中可用的类型
大部分Python类型可以在类型提示中使用 , 不过有一些限制和建议 .
另外 , typing模块引入的特殊结构 , 在语义上或许会让你惊讶 .
本节涵盖了可用于注解的所有主要类型 :
• typing . Any ;
• 简单的类型和类 ;
• typing . Optional和typing . Union ;
• 泛化容器 , 包括元组和映射 ;
• 抽象基类 ;
• 泛化可迭代对象 ;
• 参数化泛型和TypeVar ;
• typing . Protocols--静态鸭子类型的关键 ;
• typing . callable ;
• typing . NoReturn--就此打住比较好 .
下面我们来依次介绍 .
先从一个看起来奇怪 , 好像没什么用 , 但是非常重要的类型开始 .
8.5.1 Any类型
Any类型是渐进式类型系统的基础 , 是人们熟知的 '动态类型' .
下面是一个没有类型信息的函数 :
def double ( x) :
return x * 2
在类型检查工具看来 , 假定其具有以下类型信息 :
def double ( x: Any) - > Any:
return x * 2
也就是说 , ×参数和返回值可以是任何类型 , 二者甚至可以不同 .
Any类型支持所有可能的操作 .
以下述签名为例 , 对比一下Any和object .
def double ( x : object ) - > object :
这个函数也接受每一种类型的参数 , 因为任何类型都是object的子类型 .
然而 , 类型检查工具拒绝以下函数 .
def double ( x: object ) - > object :
return x * 2
这是因为object不支持__mul__操作 .
Mypy报告的错误如下所示 .
. . . / birds/ $ mypy double_object. py
double_object. py: 2 : error: Unsupported operand types for * ( "object" and "int" )
Found 1 error in 1 file ( checked 1 source file )
越一般的类型 , 接口越狭窄 , 即支持的操作越少 .
object 类实现的操作比abc . Sequence少 , abc . Sequence实现的操作比abc . MutableSequence少 , abc . MutableSequence实现的操作比list少 .
但是 , Any是一种魔法类型 , 位于类型层次结构的顶部和底部 .
Any既是最一般的类型 ( 使用n : Any注解的参数可接受任何类型的值 ) , 也是最特定的类型 ( 支持所有可能的操作 ) .
至少 , 在类型检查工具看来是这样 .
当然 , 没有任何一种类型可以支持所有可能的操作 , 因此使用Any不利于类型检查工具完成核心任务 ,
即检测潜在的非法操作 , 防止运行时异常导致程序崩溃 .
8.5.1.1 子类型与相容
传统的面向对象名义类型系统依靠的是子类型关系 .
对T1类及其子类T2来说 , T2是T1的子类型 . 以下述代码为例 .
class T1 :
. . .
class T2 ( T1) :
. . .
def f1 ( p: T1) - > None :
. . .
o2 = T2( )
f1( o2)
f1 ( o2 ) 调用运用了里氏替换原则 ( Liskov Substitution Principle , LSP ) .
其实 , Barbarg Liskov⑦是从受支持的操作角度定义子类型的 :
用T2类型的对象替换T1类型的对象 , 如果程序的行为仍然正确 , 那么T2就是T1的子类型 .
接着上一段代码 , 像下面这样做则违背了LSP .
( 注 7 : MIT教授 , 编程语言设计者以及图灵奖得主 . )
def f2 ( p: T2) - > None :
. . .
o1 = T1( )
f2( o1)
从受支持的操作角度来看 , 这完全合理 : 作为子类 , T2继承了T1支持的所有操作 .
因此 , 在任何预期T1实例的地方都可以使用T2实例 .
然而 , 反过来就不一定成立了 , T2可能实现了其他方法 , 因此在预期T2实例的地方不一定都能使用T1实例 .
行为子类型 ( behavioral subtyping ) 这种说法更能体现关注的要点是受支持的操作 .
'行为子类型' 也用于指代LSP .
在渐进式类型系统中还有一种关系 : 相容 ( consistent-with ) .
满足子类型关系必定是相容的 , 不过对Any还有特殊的规定 .
相容规则如下 .
1. 对T1及其子类型T2 , T2与T1相容 ( 里氏替换 ) .
2. 任何类型都与Any相容 : 声明为Any类型的参数接受任何类型的对象 .
3. Any与任何类型都相容 : 始终可以把Any类型的对象传给预期其他类型的参数 .
下面使用前面定义的对象o1和o2来说明规则 2 和规则 3.
这段代码中的所有调用都是有效的 .
def f3 ( p: Any) - > None :
. . .
o0 = object ( )
o1 = T1( )
o2 = T2( )
f3( o0)
f3( o1)
f3( o2)
def f4 ( ) :
. . .
o4 = f4( )
f1( o4)
f2( o4)
f3( o4)
from typing import Any
class T1 :
. . .
class T2 ( T1) :
. . .
def f1 ( p: T1) - > None :
. . .
def f2 ( p: T2) - > None :
. . .
def f3 ( p: Any) - > None :
. . .
o0 = object ( )
o1 = T1( )
o2 = T2( )
f3( o0)
f3( o1)
f3( o2)
def f4 ( ) :
. . .
o4 = f4( )
f1( o4)
f2( o4)
f3( o4)
任何渐进式类型系统都需要像Any这样的通配类型 .
* --------------------------------------------------------------------------------------------- *
说白了 , 类型分析中所说的 '推导' 就是 '推测' .
Python和其他语言的现代化类型检查工具不强求类型注解完整无缺 , 很多表达式的类型是可以推导出来的 .
例如 , 对 x = len ( s ) * 10 来说 , 类型检查工具不需要显式类型注解就知道×是int类型 ,
因为内置函数len的类型提示是已知的。
* --------------------------------------------------------------------------------------------- *
接下来探讨可在注解中使用的其他类型 .
8.5.2 简单的类型和类
像int , float , str和bytes这样的简单的类型可以直接在类型提示中使用 .
标准库 , 外部包中的具体类 , 以及用户定义的具体类 ( 例如FrenchDeck , Vector2d和Duck ) ,
也可以在类型提示中使用 .
抽象基类在类型提示中也能用到 . 8.5 .7 节讲容器类型时会讨论抽象基类 .
对类来 , 相容的定义与子类型相似 : 子类与所有超类相容 .
然而 , '实用胜过纯粹' , 凡事总有例外 , 详见下面的提示栏 .
* --------------------------------------------------------------------------------------------- *
int与complex相容
内置类型int , float和complex之间没有名义上的子类型关系 , 它们都是object的直接子类 .
但PEP484声称 , int与float相容 , float与complex相容 . ( bool < int < float < complex )
从实用角度来看 , 这是合理的 :
int实现了float的所有操作 , 而且int还额外实现了 & , | < < 等按位运算操作 . 因此 , int也与complex相容 .
对于i = 3 , i . real是 3 , i . imag是 0. ( 意思是 : i被赋值为 3 , i . real返回 3 , i . imag返回 0. )
* --------------------------------------------------------------------------------------------- *
8.5.3 Optional类型和Union类型
8.3 .4 节提到过特殊类型Optional , 其中有一个示例使用它解决了默认值为None的问题 .
from typing import Optional
def show_count ( count: int , singular: str , plural: Optional[ str ] = None ) - > str :
. . .
Optional [ str ] 结构其实是Union [ str, None ] 的简写形式 , 表示plural的类型可以是str或None .
* --------------------------------------------------------------------------------------------- *
Python 3.10 为Optional和Union提供的句法更好 .
从Python 3.10 开始 , Union [ str, bytes ] 可以写成str | bytes .
这种写法输入的内容更少 , 也不用从typing中导入Optional或Union .
下面是以新旧两种句法编写的show_count函数的plural参数的类型提示 , 可以比较一下 .
plural : Optional [ str ] = None # 旧日句法
plural : str | None = None # 新句法
| 运算符还可用于构建isinstance和issubclass的第二个参数 , 例如isinstance ( x , int | str ) .
详见 'PEP 604--Complementary syntax for Union[]' .
* --------------------------------------------------------------------------------------------- *
内置函数ord的签名就用到了Union , 其接受str或bytes类型 , 并返回一个int . ⑧
( 注 8 : 严格来说 , ord接受的str或bytes类型必须满足len ( s ) = = 1.
接受一个长度为 1 的字符串或字节对象作为参数 . 但是 , 类型系统目前还不能表达这种约束 . )
def ord ( c: Union[ str , bytes ] ) - > int :
. . .
下面示例中的函数接受一个str , 但是可以返回一个str或float .
from typing import Union
def parse_token ( token: str ) - > Union[ str , float ] :
try :
return float ( token)
except ValueError:
return token
尽量避免创建返回Union类型值的函数 , 因为这会给用户带来额外的负担 ,
迫使他们必须在运行时检查返回值的类型 , 判断该如何处理 .
但是 , 在简单的表达式计算器中可以像上一段代码中的parse_token那样做 .
* --------------------------------------------------------------------------------------------- *
4.10 节介绍过接受str或bytes参数的函数 , 当参数为str时 , 返回str , 当参数为bytes时 , 返回bytes .
在那种情况下 , 返回值类型由输入值类型决定 , 因此不适合使用Union .
为了正确注解这样的函数 , 需要使用类型变量 ( 参见 8.5 .9 节 ) 或重载 ( 参见 15.2 节 ) .
* --------------------------------------------------------------------------------------------- *
Union [ ] 至少需要两种类型 .
嵌套的Union类型与扁平的Union类型效果相同
因此 , 下面的类型提示 :
Union [ A, B, Union [ C, D, E ] ]
与下面的类型提示作用一样 :
Union [ A, B, C, D, E ]
Union所含的类型之间不应相容 .
例如 , Union [ int, float ] 就 '画蛇添足' 了 , 因为int与float相容 .
仅使用float注解的参数也接受int值 .
8.5.4 泛化容器
泛化容器 : 容器中能够存储任意类型数据 .
大多数Python容器是异构的 .
例如 , 在一个 list中可以混合存放不同的类型 .
然而 , 实际使用中这么做没有什么意义 .
存入容器的对象往往需要进一步处理 , 因此至少要有一个通用的方法 . ⑨
( 注 9 : 对Python的原初设计有重大影响的ABC语言 , 限制列表中只能有一种类型的值 , 与第一项保持一致 . )
泛型可以用类型参数来声明 , 以指定可以处理的项的类型 .
例如 , 可以像示例 8 - 8 那样参数化一个list , 约束元素的类型 .
def tokenize ( text: str ) - > list [ str ] :
return text. upper( ) . split( )
在Python 3.9 及以上版本中 , 类型提示的意思是tokenize函数返回一个list , 而且各项均为str类型 .
stuff : list 和 stuff : list [ Any ] 这两个注解的意思相同 , 都表示stuff是一个列表 ,
而且列表中的项可以是任何类型的对象 . ( 省略即表示Any类型 . )
* --------------------------------------------------------------------------------------------- *
对于Python 3.8 或之前的版本 , 道理是一样的 ,
只是需要编写的代码更多详见 '早期支持和弃用的容器类型' 附注栏
* --------------------------------------------------------------------------------------------- *
'PEP 585Type Hinting Generics In Standard Collections' 列出了标准库中接受泛化类型提示的容器 .
( "泛化类型提示" 的核心概念是使用通用类型而不是具体类型来标注代码中的变量 , 函数参数和返回值的类型 .
它允许在代码中使用类型变量或泛型来表示不特定的类型或参数化类型 . )
下面只列出了可使用最简单的泛化类型提示形式的容器 , 例如container [ item ] .
list
set
frozenset
collections . deque
abc . Container
abc . Sequence
abc . MutableSequence
abc . Collection
abc . Set
abc . MutableSet
tuple和映射类型支持更复杂的类型提示 , 后文有两节会分别说明 .
截至Python 3.10 ,
没有什么好方法来注解带typecode构造函数参数 ( 决定数组中存放整数还是浮点数 ) 的array . array .
更棘手的问题是 , 如何检查整数区间 , 防止在运行时向数组中添加元素而导致OverflowError .
例如 , 使用typecode = 'B' 创建的数组 , 只能存放 0 和 255 之间的int值 .
目前 , Python的静态类型系统还未解决这个问题 .
* -----------------------------------早期支持和弃用的容器类型------------------------------------- *
( 如果只使用Python3 . 9 或以上版本 , 则可以跳过这个附注栏 . )
对于Python 3.7 和Python 3.8 , 需要从__future__中导入相关内容 ,
才能在内置容器 ( 例如list ) 后面使用 [ ] 表示法 , 如示例 8 - 9 所示 .
from __future__ import annotations
def tokenize ( text: str ) - > list [ str ] :
return text. upper( ) . split( )
这种方法不适用于Python3 . 6 或之前的版本 .
在Python3 . 5 及以上版本中 , 要像示例 8 - 10 那样注解tokenize函数 .
from typing import List
def tokenize ( text: str ) - > List[ str ] :
return text. upper( ) . split( )
最初 , 为了支持泛化类型提示 , PEP 484 的作者在typing模块中创建了几十种泛型 .
表 8 - 1 列出了其中一部分 . 完整的列表参阅typing模块文档 .
表 8 - 1 : 部分容器类型及对应的类型提示
容 器 对应的类型提示
list typing . List
set typing . Set
frozenset typing . FrozenSet
collections . deque typing . Deque
collections . abc . MutableSequence typing . MutableSequence
collections . abc . Sequence typing . Sequence
collections . abc . Set typing . AbstractSet
collections . abc . MutableSet typing . MutableSet
'PEP 585-Type Hinting Generics In Standard Collections' 发起过一项历时多年的行动 .
以改进泛化类型提示的可用性 . 整个过程分为 4 步 :
* 1. 在Python 3.7 中引入from __future__ import annotations ,
以list [ str ] 表示法支持在泛型中使用标准库中的类 .
* 2. Python 3.9 把这种表示法定为标准行为 , 无须从future导入即可使用list [ str ] .
* 3. 弃用typing模块中所有冗余的泛型 . ⑩ Python解释器不对弃用的类型发出警告 ,
因为类型检查工具在检查使用Python3 . 9 或以上版本编写的程序时会发出警告 .
* 4. Python 3.9 发布 5 年后发布的第一个Python版本会移除那些冗余的泛型 .
按照目前的节奏来看 , 应该是Python3 . 14 , 即Python Pi .
注 10 : 看到 'Module Contents' 下方的说明后 , 我为typing模块文档做了一点儿贡献 , 为几个小节添加了弃用警告 .
当然 , 这一切都在Guido van Rossum的监督之下 .
* --------------------------------------------------------------------------------------------- *
接下来看看如何注解泛化元组 .
8.5.5 元组类型
元组类型的注解分 3 种形式说明 :
• 用作记录的元组 ;
• 带有具名字段 , 用作记录的元组 ;
• 用作不可变序列的元组 .
1. 用作记录的元组
元组用作记录时 , 使用内置类型tuple注解 , 字段的类型在 [ ] 内声明 .
举个例子 : 一个内容为城市名 , 人口数和所属国家的元组 ,
例如 ( 'Shanghai' , 24.28 , 'China' ) , 类型提示为tuple [ str, float, str ] .
假如有一个函数 , 其接受的参数是一对地理坐标 , 返回值是一个Geohash .
该函数的用法如下所示 .
>> > shanghai = 31.2304 , 121.4737
>> > geohash( shanghai)
'wtw3sjq6q'
geohash函数使用PyPI中的geolib包定义 , 如示例 8 - 11 所示 .
from geolib import geohash as gh
PRECISION = 9
def geohash ( lat_lon: tuple [ float , float ] ) - > str :
return gh. encode( * lat_lon, PRECISION)
* --------------------------------------------------------------------------------------------- *
对于Python 3.9 以下的版本 , 要在类型提示中使用typing . Tuple .
这个类型已经弃用 , 不过 2024 年之前仍会被保留在标准库中 .
from typing import Tuple
def geohash ( lat_lon : Tuple [ float, float ] ) - > str :
. . .
* --------------------------------------------------------------------------------------------- *
2. 带有具名字段, 用作记录的元组
如果想注解带有多个字段的元组 , 或者代码中多次用到的特定类型的元组 ,
强烈建议使用typing . NamedTuple ( 参见第 5 章 ) .
示例 8 - 12 使用NamedTuple注解了示例 8 - 11 中的geohash函数 .
from typing import NamedTuple
from geolib import geohash as gh
PRECISION = 9
class Coordinate ( NamedTuple) :
lat: float
lon: float
def geohash ( lat_lon: Coordinate) - > str :
return gh. encode( * lat_lon, PRECISION)
5.2 节讲过 , typing . NamedTuple是tuple子类的制造工厂 ,
因此Coordinate与tuple [ float, float ] 相容 , 但是反过来不成立 ,
毕竟NamedTuple为Coordinate额外添加了方法 ( 例如 . _asdict ( ) ) , 另外用户也可以定义方法 .
实践中 , 可以放心地把Coordinate实例传给下面定义的display函数 .
def display ( lat_lon: tuple [ float , float ] ) - > str :
lat, lon = lat_lon
ns = 'N' if lat >= 0 else 'S'
ew = 'E' if lon >= 0 else 'W'
return f'{ abs ( lat) : 0 . 1f} °{ ns} , { abs ( lon) : 0 . 1f} °{ ew}
3. 用作不可变序列的元组
如果想注解长度不定 , 用作不可变列表的元组 , 则只能指定一个类型 , 后跟逗号和 . . .
( Python中的省略号 , 3 个点 , 不是Unicode字符 U + 2026 , 即HORIZONTAL ELLIPSIS ) .
例如 , tuple [ int, ... ] 表示项为int类型的元组 .
省略号表示元素的数量≥ 1. 可变长度的元组不能为字段指定不同的类型 .
stuff : tuple [ Any, ... ] 和stuff : tuple这两个注解的意思相同 ,
都表示stuff是一个元组 , 长度不定 , 可包含任意类型的对象 .
下面的代码使用columnize函数把一个序列转换成了元组列表 ( 类似于表格中的行和单元格 ) ,
列表中的元组长度不定 . 最后 , 按列显示各项 .
>> > animals = 'drake fawn heron ibex koala lynx tahr xerus yak zapus' . split( )
>> > table = columnize( animals)
>> > table
[ ( 'drake' , 'koala' , 'yak' ) , ( 'fawn' , 'lynx' , 'zapus' ) , ( 'heron' , 'tahr' ) ,
( 'ibex' , 'xerus' ) ]
>> > for row in table:
. . . print ( '' . join( f' { word: 10 } ' for word in row) )
. . .
drake koala yak
fawn lynx zapus
heron tahr
ibex xerus
columnize函数的实现如示例 8 - 13 所示 . 注意返回值类型 .
list [ tuple [ str, ... ] ]
from collections. abc import Sequence
def columnize (
sequence: Sequence[ str ] , num_columns: int = 0 )- > list [ tuple [ str , . . . ] ] :
if num_columns == 0 :
num_columns = round ( len ( sequence) ** 0.5 )
num_rows, reminder = divmod ( len ( sequence) , num_columns)
num_rows += bool ( reminder)
return [ tuple ( sequence[ i: : num_rows] ) for i in range ( num_rows) ]
from collections. abc import Sequence
def columnize ( sequence: Sequence[ str ] , num_columns: int = 0 ) - > list [ tuple [ str , . . . ] ] :
print ( len ( sequence) , sequence)
if num_columns == 0 :
num_columns = round ( len ( sequence) ** 0.5 )
num_rows, reminder = divmod ( len ( sequence) , num_columns)
num_rows += bool ( reminder)
return [ tuple ( sequence[ i: : num_rows] ) for i in range ( num_rows) ]
animals = 'drake fawn heron ibex koala lynx tahr xerus yak zapus' . split( )
table = columnize( animals)
print ( table)
"""
[('drake', 'koala', 'yak'), ('fawn', 'lynx', 'zapus'),
('heron', 'tahr'), ('ibex', 'xerus')]
"""
for row in table:
print ( '' . join( f' { word: 10 } ' for word in row) )
8.5.6 泛化映射
泛化映射类型使用MappingType [ KeyType, ValueType ] 形式注解 .
在Python 3.9 及以上版本中 , 内置类型dict及collections和collections . abc中的映射类型都可以这样注解 .
更早的版本必须使用typing . Dict和typing模块中的其他映射类型 ,
详见 8.5 .4 节中的 '早期支持和弃用的容器类型' 附注栏 .
示例 8 - 14 定义了一个实用的函数 , 返回反向索引 , 按名称搜索Unicode字符 .
这是对示例 4 - 21 的改造 , 更适合在服务器端使用 ( 详见第 21 章 ) .
nane_index函数的参数是起点和终点两个Unicode字符编码 ,
会返回一个名为dict [ str, set [ str ] ] 的反向索引 , 把各个单词映射到名称中含有该词的字符集合上 .
例如 , 对于 32 和 64 之间的ASCII字符 , 索引之后 , 'SIGN' 和 'DIGIT' 两个词映射的字符集合如下所示 .
这里还展示了如何搜索名为 'DIGIT EIGHT' 的字符 .
>> > index = name_index( 32 , 65 )
>> > index[ 'SIGN' ]
{ 's' , '>' , '=' , '<' , '%' , '#' }
>> > index[ 'DIGIT' ]
{ '8' , '5' , '6' , '2' , '3' , '0' , '1' , '4' , '7' , '9' }
>> > index[ 'DIGIT' ] & index[ 'EIGHT' ]
{ '8' }
示例 8 - 14 是name_index函数所在的charindex . py模块的源码 .
除了类型提示dict [ ] , 这个示例还有 3 个功能在本书中是首次出现 .
import sys
import re
import unicodedata
from collections. abc import Iterator
RE_WORD = re. compile ( r'\w+' )
STOP_CODE = sys. maxunicode + 1
def tokenize ( text: str ) - > Iterator[ str ] :
"""返回全大写的单词构成的可迭代对象"""
for match in RE_WORD. finditer( text) :
yield match . group( ) . upper( )
def name_index ( start: int = 32 , end: int = STOP_CODE) - > dict [ str , set [ str ] ] :
index: dict [ str , set [ str ] ] = { }
for char in ( chr ( i) for i in range ( start, end) ) :
if name := unicodedata. name( char, '' ) :
for word in tokenize( name) :
index. setdefault( word, set ( ) ) . add( char)
return index
import sys
import re
import unicodedata
from collections. abc import Iterator
RE_WORD = re. compile ( r'\w+' )
STOP_CODE = sys. maxunicode + 1
"""
在Python中, sys.maxunicode是一个整数, 表示当前Python解释器中可能存在的Unicode字符的最大码位(code point)
在Python 3中, 这个值通常为1114111(即0x10FFFF), 因为Python 3默认使用UTF-32编码来表示Unicode字符.
sys.maxunicode + 1则是这个值加一, 用于指示“停止码位”(stop code)的位置.
停止码位是一个特殊的码位, 不属于Unicode字符集, 但在处理Unicode字符串时可以用来标记字符串的结尾.
在Python中, 通常使用stopiteration异常来表示迭代器已经到达'停止码位'的位置, 即序列的末尾.
当一个迭代器到达这个位置时, 它会抛出StopIteration异常, 告知调用方已经没有更多的元素可供迭代.
"""
def tokenize ( text: str ) - > Iterator[ str ] :
"""返回全大写的单词构成的可迭代对象"""
for match in RE_WORD. finditer( text) :
yield match . group( ) . upper( )
def name_index ( start: int = 32 , end: int = STOP_CODE) - > dict [ str , set [ str ] ] :
index: dict [ str , set [ str ] ] = { }
for char in ( chr ( i) for i in range ( start, end) ) :
"""
if 条件中使用了海象运算符:=.
这样做是为了把unicodedata.name()调用的结果赋值给name, 并把该结果作为整个表达式的求解结果.
如果结果是表示假值的, 则不更新index. ⑪
注11: 有几个示例适合使用:=, 但是本书并未介绍这个运算符.
详细说明见'PEP572-Assignment Expressions'.
"""
if name := unicodedata. name( char, '' ) :
for word in tokenize( name) :
index. setdefault( word, set ( ) ) . add( char)
return index
index = name_index( 32 , 65 )
print ( index[ 'SIGN' ] )
print ( index[ 'DIGIT' ] )
print ( index[ 'DIGIT' ] & index[ 'EIGHT' ] )
* --------------------------------------------------------------------------------------------- *
当把dict用作记录时 , 一般来说 , 所有键都使用str类型 , 对应的值是什么类型则取决于键的含义 . 详见 15.3 节 .
* --------------------------------------------------------------------------------------------- *
8.5.7 抽象基类
发送时要保守 , 接收时要大方 .
--伯斯塔尔定律 , 又称稳健性法则 .
表 8 - 1 列出了collections . abc中的几个抽象类 .
理想情况下 , 函数的参数应接受那些抽象类型
( 或Python 3.9 之前的版本中typing模块中对应的类型 ) , 而不是具体类型 .
这样对调用方来说更加灵活 . 以下述函数签名为例 .
from collections. abc import Mapping
def name2hex ( name: str , color_map: Mapping[ str , int ] ) - > str :
. . .
由于注解的类型是abc . Mapping ,
因此调用方可以提供dict , defaultdict和ChainMap的实例 , UserDict子类的实例 , 或者Mapping的任何子类型 .
相比之下 , 再看下面的签名 .
def name2hex ( name: str , color_map: dict [ str , int ] ) - > str :
. . .
这里 , color_map必须是dict或其子类型 , 例如defaultDict或OrderedDict .
特别注意 , 使用collections . UserDict的子类无法通过类型检查 ,
尽管 3.6 .5 节讲过 , 建议扩展collections . UserDict自定义映射 .
Mypy会拒绝UserDict或其衍生类的实例 , 因为UserDict不是dict的子类 ,
二者是同级关系 , 都是abc . MutableMapping子类 . ⑫
( 注 12 : 其实 , dict是abc . MutableMapping的虚拟子类 ( 第 13 章将介绍虚拟子类的概念 ) .
现在 , 只需知道 , issubclass ( dict,abc . MutableMapping ) 的结果为True ,
尽管dict是用C语言实现的 , 而且未从abc . MutableMapping继承任何东西 . dict只继承object . )
因此 , 一般来说在参数的类型提示中最好使用abc . Mapping或abc . MutableMapping ,
不要使用dict ( 也不要在遗留代码中使用typing . Dict ) .
如果name2hex函数无须改动传入的color_map , 则最准确的类型提示是abc . Mapping .
如此一来 , 调用方就不用提供实现了setdefault , pop和update等方法的对象了 ,
因为这些方法属于MutableMapping接口 , 而不是Mapping接口 .
这样做体现了伯斯塔尔定律的后半部分 : 接收时要大方 .
伯斯塔尔定律还指出 , 发送时要保守 .
因此 , 函数的返回值始终应该是一个具体对象 , 即返回值的类型提示应当是具体类型 .
8.5 .4 节中的示例就是如此 , 返回值类型为list [ str ] .
def tokenize ( text : str ) - > list [ str ] :
return text . upper ( ) . split ( )
在typing . List的文档中有这样一段话 .
泛化版list . 可用于注解返回值类型 .
如果想注解参数 , 推荐使用抽象容器类型 , 例如Sequence或Iterable .
typing . Dict和typing . Set的文档也有类似的说明 .
记住 , 从Python 3.9 开始 , collections . abc中的大多数抽象基类和collections中的具体类 ,
以及内置的容器 , 全都支持泛化类型提示 , 例如collections . deque [ str ] .
使用Python 3.8 或之前的版本编写代码时才需要使用typing模块中对应的容器类型 .
已经泛化的类很多 , 完整列表见 'PEP 585--Type Hinting Generics In Standard Collections'
中的 'Implementation' 一节 .
在结束讨论类型提示中的抽象基类之前 , 还要讲一下numbers包中的抽象基类 .
1. 论数字塔的倒下
nunbers包定义了 'PEP3141--AType Hierarchy for Numbers' 提出的所谓的数字塔 ( numeric tower ) .
这座塔是一种抽象基类构成的线性层次结构 , Nunber位于最顶层 .
• Number
• Complex
• Real
• Rational
• Integral
这些抽象基类完全适应运行时类型检查 , 不过静态类型检查不支持它们 .
PEP 484 中的 'Numeric Tower' 一节拒绝使用numbers包中的抽象基类 ,
规定应把内置类型complex , float和int当作特例 ( 详见 8.5 .2 节的提示栏 'int与complex相容' ) .
( 在Python中 , 抽象基类是用来定义接口和约束的类 , 它们不能直接被实例化 .
抽象基类提供了一种方式来描述类应该具有的方法和属性 , 从而实现多态性和类型检查 .
对于抽象基类而言 , 运行时类型检查是完全支持的 .
也就是说 , 可以使用抽象基类来检查对象是否满足特定的接口要求 .
如果一个类继承了抽象基类 , 并实现了该基类定义的接口 , 那么该类的实例就可以通过类型检查 .
然而 , 对于静态类型检查 , 即通过类型提示和类型检查工具 ( 如mypy ) 在编译时进行类型检查 ,
抽象基类的支持是有限的 .
PEP 484 中的 'Numeric Tower' 一节指出 , 不建议直接使用 'numbers' 包中的抽象基类来进行静态类型检查 .
在PEP 484 中 , 提到应将内置类型 'complex' , 'float' 和 'int' 视为特例 .
也就是说 , 在静态类型检查中 , 可以将这些内置类型直接作为类型提示 , 而无需使用 'numbers' 包中的抽象基类 . )
13.6 .8 节在比较协议和抽象基类时将再次探讨这个问题 .
实践中 , 针对静态类型检查 , 注解数字参数有以下几种选择 .
* 1. 按照PEP488的建议 , 使用int、float或complex中的某个具体类型。
* 2. 声明一种联合类型 , 例如Union [ float, Decimal, Fraction ] .
* 3. 如果不想硬编码具体类型 , 可以使用SupportsFloat等数字协议 ( 详见 13.6 .2 节 ) .
8.5 .10 节将介绍一些理解数字协议的预备知识 .
接下来介绍类型提示中经常用到的一种抽象基类 , 即Iterable .
8.5.8 Iterable
前文引用的typing . List文档推荐使用Sequence和Iterable注解函数的参数 .
标准库中的math . fsum函数 , 其参数的类型提示用的就是Iterable .
def fsum ( _seq: Iterable[ float ] ) - > float :
. . .
* --------------------------------------------------------------------------------------------- *
存根文件和Typeshed项目
截至Python 3.10 , 标准库不含注解 , 但是Mypy , PyCharm等可在Typeshed项目中找到所需的类型提示 .
这些类型提示位于一种存根文件 ( stub file ) 中 , 这是一种特殊的源文件 , 扩展名为 . pyi ,
文件中保存带注解的函数和方法签名 , 没有实现 , 有点儿类似于C语言的头文件 .
math . fsum 函数的签名位于 / stdlib / 2 and3 / math . pyi文件中 .
_seq开头的下划线是PEP484为仅限位置参数所做的约定 , 详见 8.6 节 .
* --------------------------------------------------------------------------------------------- *
示例 8 - 15 是另一个使用Iterable参数的例子 . 产生的项是tuple [ str, str ] 类型 .
函数的用法如下所示 .
>> > l33t = [ ( 'a' , '4' ) , ( 'e' , '3' ) , ( 'i' , '1' ) , ( 'o' , '0' ) ]
>> > text = 'mad skilled noob powned leet'
>> > from replacer import zip_replace
>> > zip_replace( text, l33t)
'm4d sk1ll3d n00b p0wn3d l33t'
示例 8 - 15 是具体实现 .
from collections. abc import Iterable
FromTo = tuple [ str , str ]
def zip_replace ( text: str , changes: Iterable[ FromTo] ) - > str :
for from_, to in changes:
text = text. replace( from_, to)
return text
from collections. abc import Iterable
FromTo = tuple [ str , str ]
def zip_replace ( text: str , changes: Iterable[ FromTo] ) - > str :
for from_, to in changes:
text = text. replace( from_, to)
return text
l33t = [ ( 'a' , '4' ) , ( 'e' , '3' ) , ( 'i' , '1' ) , ( 'o' , '0' ) ]
text = 'mad skilled noob powned leet'
print ( zip_replace( text, l33t) )
* --------------------------------------------------------------------------------------------- *
在Python 3.10 中显式使用TypeAlias
'PEP613-—Explicit Type Aliases' 引入了一种特殊类型 , 即TypeAlias ,
以让创建类型别名的赋值操作更显眼 , 也让类型检查更容易 .
从Python 3.10 开始 , 创建类型别名的首选方式如下所示 .
from typing import TypeAlias
FromTo : TypeAlias = tuple [ str, str ]
* --------------------------------------------------------------------------------------------- *
abc . Iterable与abc . Sequence
nath . fsum和replacer . zip_replace必须迭代整个Iterable参数才能返回结果 .
如果输入值是像itertools . cycle生成器这样的无穷可迭代对象 ,
则这两个函数将耗尽内存 , 导致Python进程崩溃 .
尽管存在一定危险 , 但是对于现代的Python , 经常需要提供接受Iterable输入值的函数 ,
而且必须完整处理才能返回结果 .
调用方可以根据需要 , 选择通过生成器提供输入数据 ,
而不是预先构建好序列 , 这样在项数较多时可以节省不少内存 .
( 意思是 : 不提供构建好序列 , 而是提供一个生成器节省内存 . )
另外 , 示例 8 - 13 中的columnize函数需要一个Sequence参数 , 而不是Iterable ,
因为该函数必须事先获得输入的长度 ( len ( ) ) , 算出行数 .
( Sequence和Iterable都是Python中的抽象基类 , 用来表示不同类型的数据集合 .
Iterable表示可以被迭代的数据集合 , 也就是可以使用for循环依次读取其中的元素 .
所有实现了__iter__方法的类都是Iterable的子类 .
例如 , list , tuple , set , dict , str , file等类型都是Iterable的子类 .
Sequence是Iterable的子类 , 表示一个长度固定 , 元素可通过整数索引访问的序列类型 .
所有实现了__getitem__和__len__方法的类都是Sequence的子类 .
例如 , list , tuple , str , bytes , bytearray等类型都是Sequence的子类 .
因此 , 与Iterable不同 , Sequence要求其元素是有序的 , 并且可以通过索引进行随机访问 .
如果只需要迭代其中的元素而不需要使用随机访问 , 那么就只需要保证数据集是Iterable即可 .
由于columnize函数需要得到输入序列的长度 , 以便计算行数和列数 ,
因此要求该序列是一个Sequence类型而不是仅仅是一个可迭代对象 ( Iterable ) .
)
与Sequence一样 , Iterable最适合注解参数的类型 .
用来注解返回值类型的话则太过含糊 . 函数的返回值类型应该具体 , 明确 .
示例 8 - 14 中注解返回值类型的Iterator与Iterable紧密相关 .
( Iterator是Iterable的子类 , 因为迭代器能够进行迭代 , 所以它们也是可迭代的 .
但是 , 一个Iterable对象不一定是Iterator对象 , 因为可迭代对象只需实现__iter__ ( ) 方法即可 ,
而不要求实现__next__ ( ) 方法 . 但是 , 所有的Iterator对象都必须实现__next__ ( ) 方法 . )
第 17 章在讲解生成器和经典迭代器时将再探讨Iterator类型 .
8.5.9 参数化泛型和TypeVar
参数化泛型是一种泛型 , 写作list [ T ] , 其中T是类型变量 , 每次使用时会绑定具体的类型 .
这样可在结果的类型中使用参数的类型 .
示例 8 - 16 定义的sample函数接受两个参数 , 一个是元素类型为T的Sequence , 另一个是int .
该函数会返回一个list , 元素的类型也是T , 具体类型由第一个参数决定 .
示例 8 - 16 是具体实现。
from collections. abc import Sequence
from random import shuffle
from typing import TypeVar
T = TypeVar( 'T' )
def sample ( population: Sequence[ T] , size: int ) - > list [ T] :
if size < 1 :
raise ValueError( 'size must be >= 1' )
result = list ( population)
shuffle( result)
return result[ : size]
在sample函数中使用类型变量达到的效果通过下面两种情况可以体现 .
• 调用时如果传入tuple [ int, ... ] 类型 ( 与Sequence [ int ] 相容 ) 的元组 , 类型参数为int ,
那么返回值类型为list [ int ] .
• 调用时如果传入一个str ( 与Sequence [ str ] 相容 ) , 类型参数为str , 那么返回值类型为list [ str ] .
* * ------------------------------------------------------------------------------------------- * *
为什么需要TypeVar?
PEP 484 的作者希望借助typing模块引入类型提示 , 不改动语言的其他部分 .
通过精巧的元编程技术 , 让类支持 [ ] 运算符 ( 例如 Sequence [ T ] ) 不成问题 .
但是 , 方括号内的T变量必须在某处定义 , 否则要大范围改动Python解释器才能让泛型支持特殊的 [ ] 表示法 .
鉴于此 , 我们增加了typing . TypeVar构造函数 , 把变量名称引入当前命名空间 .
由于Java , C # 和TypeScript等语言不要求事先声明类型变量的名称 , 因此没有与Python的TypeVar类对应的结构 .
* * ------------------------------------------------------------------------------------------- * *
标准库中的statistics . mode函数也是一例 .
该函数会返回一系列值中出现次数最多的数据点 .
下面是statistics . mode函数文档给出的一个使用示例 .
> > > mode ( [ 1 , 1 , 2 , 3 , 3 , 3 , 3 , 4 ] )
3
不使用TypeVar , mode函数的签名可能会像示例 8 - 17 那样 .
from collections import Counter
from collections. abc import Iterable
def mode ( data: Iterable[ float ] ) - > float :
pairs = Counter( data) . most_common( 1 )
if len ( pairs) == 0 :
raise ValueError( 'no mode for empty data' )
return pairs[ 0 ] [ 0 ]
mode 经常用于处理int或float值 , 但是Python还有其他数值类型 ,
因此返回值类型最好与Iterable中的元素类型保持一致 .
使用TypeVar可以改进签名 , 先来看一个看似简单 , 但是不正确的参数化签名 .
from collections. abc import Iterable
from typing import Typevar
T = TypeVar( 'T' )
def mode ( data: Iterable[ T] ) - > T:
. . .
第一次出现在签名中时 , 类型参数T可以是任何类型 , 第二次出现时 , 与第一次的类型相同 .
因此 , 任何可迭代对象都与Iterable [ T ] 相容 ,
包括collections . Counter无法处理的不可哈希的可迭代类型 .
需要限制可以赋予T的类型 . 下面两节介绍了两种方式 .
1. 受限的TypeVar
TypeVar还接受一些位置参数 , 以对类型参数施加限制 .
可以改进mode函数的签名 , 以接受指定的几种数值类型 , 如下所示 .
from collections. abc import Iterable
from decimal import Decimal
from fractions import Fraction
from typing import Typevar
NumberT = TypeVar( 'NumberT' , float , Decimal, Fraction)
def mode ( data: Iterable[ NumberT] ) - > NumberT:
. . .
这样好多了 . 这也是 2020 年 5 月 25 日typeshed项目中存根文件statistics . pyi为mode函数提供的签名 .
然而 , statistics . mode文档中还有以下示例 .
>> > mode( [ "red" , "blue" , "blue" , "red" , "green" , "red" , "red" ] )
'red'
你的第一反应可能是在NumberT定义中添加str .
NumberT = TypeVar( 'NumberT' , float , Decimal, Fraction, str )
这样做当然可以,但是NumberT名称就文不对题了 .
而且 , 不能发现一种mode可以处理的类型就添加一个 .
更好的方法是使用接下来介绍的TypeVar的另一项功能 .
2. 有界的TypeVar
如前所述 , 在示例 8 - 17 中 , mode函数主体内的Counter类用于排名 .
Counter基于dict , 因此可迭代对象data中元素的类型必须是可哈希的 ( 元素做为键 , 键必须是可哈希的 ! ) .
乍一看 , 下面的签名行之有效 .
from collections. abc import Iterable, Hashable
def mode ( data: Iterable[ Hashable] ) - > Hashable:
. . .
现在的问题是 , 返回的项是Hashable类型 .
Hashable是一个抽象基类 , 只实现了_hash方法 .
因此 , 除了调用hash ( ) , 类型检查工具不会允许对返回值做其他任何操作 .
所以 , 这么做没什么实际意义 .
( 返回Hashable对象 , 那么这个对象在进行操作的时候 , 除了实现的_hash方法 ,
其它操作都提示错误 , 实际上可以操作的 ,
上例中 , 返回的是一个字符串 , 被标注为Hashable后 , 字符串使用字符串方法都给出错误提示 , 但它是可以运行的 . )
解决方法是使用TypeVar的另一个可选参数 , 即关键字参数bound .
这个参数会为可接受的类型设定一个上边界 .
示例 8 - 18 使用bound = Hashable指明 , 类型参数可以是Hashable或它的任何子类型 . ⑭
( 注 14 : 我为typeshed项目贡献了这种方法 . 从 2020 年 5 月 26 日起 , statistics . pyi就是这样注解mode函数的 . )
from collections import Counter
from collections. abc import Iterable, Hashable
from typing import TypeVar
HashableT = TypeVar( 'HashableT' , bound= Hashable)
def mode ( data: Iterable[ HashableT] ) - > HashableT:
pairs = Counter( data) . most_common( 1 )
if len ( pairs) == 0 :
raise ValueError( 'no mode for empty data' )
return pairs[ 0 ] [ 0 ]
总结一下 :
• 受限的类型变量会把类型设为TypeVar声明中列出的某个类型 ;
• 有界的类型变量会把类型设为根据表达式推导出的类型 ,
但前提是推导的类型与Typevar的bound = 关键字参数声明的边界相容 .
* * ------------------------------------------------------------------------------------------- * *
为TypeVar声明边界的关键字参数名为bound = ,
因为 '绑定' 一般指为变量设定值在Python的引用语义下表示为值绑定名称 .
把那个关键字参数命名为boundary = 可能要好一些 , 因为不容易让人误解 .
* * ------------------------------------------------------------------------------------------- * *
typing . TypeVar构造函数还有两个可选参数 , 即covariant和contravariant , 15.7 节将介绍 .
下面让我们通过AnyStr来结束对TypeVar的介绍 .
3. 预定义的类型变量AnyStr
typing 模块提供了一个预定义的类型变量 , 名为AnyStr . 这个类型变量的定义如下所示 .
AnyStr = TypeVar( 'AnyStr' , bytes , str )
很多接受bytes或str的函数会使用AnyStr , 返回值也是二者之一 .
接下来换个话题 , 讲一下typing . Protocol .
这是Python3 . 8 新增的功能 , 旨在以更符合Python风格的方式编写类型提示 .
8.5.10 静态协议
* * ------------------------------------------------------------------------------------------- * *
在面向对象编程中 , 把 '协议' 当作非正式的接口历史久远 , 可以追溯到Smalltalk .
自Python诞生伊始 , 这个概念就存在 .
然而 , 对类型提示来说 , 协议指的是typing . Protocol的子类 , 定义接口供类型检查工具核查 .
这两种协议将在第 13 章详述 . 本节只是借着函数注解做个简单介绍 .
* * ------------------------------------------------------------------------------------------- * *
'PEP 544-Protocols: Structural subtyping (static duck typing)'
提出的Protocol类型类似于Go语言中的接口 : 定义协议类型时指定一个或多个方法 ,
在需要使用协议类型的地方 , 类型检查工具会核查有没有实现指定的方法 .
在Python中 , 协议通过typing . Protocol的子类定义 .
然而 , 实现协议的类不会与定义协议的类建立任何关系 , 不继承 , 也不用注册 .
类型检查工具负责查找可用的协议类型 , 施行用法检查 .
以下问题可以通过Protocol和TypeVar解决 .
假如你想创建top ( it , n ) 函数 , 返回可迭代对象it中排位靠前的n个元素 .
>> > top( [ 4 , 1 , 5 , 2 , 6 , 7 , 3 ] , 3 )
[ 7 , 6 , 5 ]
>> > l = 'mango pear apple kiwi banana' . split( )
>> > top( l, 3 )
[ 'pear' , 'mango' , 'kiwi' ]
>> > 12 = [ ( len ( s) , s) for s in l]
>> > 12
[ ( 5 , 'mango' ) , ( 4 , 'pear' ) , ( 5 , 'apple' ) , ( 4 , 'kiwi' ) , ( 6 , 'banana' ) ]
>> > top( l2, 3 )
[ ( 6 , 'banana' ) ,( 5 , 'mango' ) , ( 5 , 'apple' ) ]
示例 8 - 19 使用参数化泛型定义top函数 .
def top ( series: Iterable[ T] , length: int ) - > list [ T] :
ordered = sorted ( series, reverse= True )
return ordered[ : length]
那么如何约束T呢?
T不能是Any或object , 因为series必须支持使用sorted函数排序 .
其实 , 内置函数sorted接受任何Iterable [ Any ] ,
但前提是可选参数key传入的函数能根据各个元素计算任意的排序键 .
如果把一个普通对象列表传给sorted函数 , 而不提供key参数 , 那么情况如何呢? 下面来试试 .
>> > l = [ object ( ) for _ in range ( 4 ) ]
[ object object at Ox10fc2fca0> , < object object at 0x10fc2fbb0 > ,
< object object at Ox10fc2fbc0> , < object object at Ox10fc2fbd0> ]
>> > sorted ( l)
Traceback ( most recent call last) :
File "stdin>" , line 1 , in < module>
TypeError: '<' not supported between instances of 'object' and 'object'
错误消息表明 , sorted函数会在可迭代对象的元素上使用 < 运算符 .
支持 < 运算符就可以了吗? 再试一下 . ⑮
( 注 15 : 打开一个交互式控制台就能通过鸭子类型探索语言功能 , 这是一件多么美好的事情 .
在不支持鸭子类型的语言中 , 我十分怀念这种探索方式 . )
>> > class Spam :
. . . def __init__ ( self, n) : self. n = n
. . . def __lt__ ( self, other) : return self. n < other. n
. . . def __repr__ ( self) : return f'Spam( { self. n} )'
. . .
>> > l = [ Spam( n) for n in range ( 5 , 0 , - 1 ) ]
[ Spam( 5 ) , Spam( 4 ) , Spam( 3 ) , Spam( 2 ) , Spam( 1 ) ]
>> > sorted ( l)
[ Spam( 1 ) , Spam( 2 ) , Spam( 3 ) , Spam( 4 ) , Spam( 5 ) ]
确实如此 , Spam对象构成的列表可以排序 , 因为Spam实现了支持 < 运算符的特殊方法__lt__ .
所以 , 示例 8 - 19 中的类型参数T应该被限定为实现了__lt__的类型 .
示例 8 - 18 需要实现了__hash__的类型参数 , 因此可以把类型参数的上边界设为typing . Hashable .
但是 , 对目前遇到的问题 , typing或abc中没有合适的类型 , 需要自己创建 .
示例 8 - 20 通过Protocol定义了一个新类型SupportsLessThan .
from typing import Protocol, Any
class SupportsLessThan ( Protocol) :
def __lt__ ( self, other: Any) - > bool : . . .
如果类型T实现了协议P定义的所有方法且类型签名匹配 , 那么T就与P相容 .
现在 , 可以使用SupportsLessThan定义top函数 , 如示例 8 - 21 所示 , 这一版正常可用了 .
from collections. abc import Iterable
from typing import TypeVar
from comparable import SupportsLessThan
LT = TypeVar( 'LT' , bound= SupportsLessThan)
def top ( series: Iterable[ LT] , length: int ) - > list [ LT] :
ordered = sorted ( series, reverse= True )
return ordered[ : length]
下面使用pytest测试一下top函数 .
示例 8 - 22 是测试套件的一部分 , 先使用top处理通过生成器表达式产出的tuple [ int, str ] 值 ,
再处理一个object列表 .
对于object列表 , 我们预期抛出TypeError异常 .
from collections. abc import Iterator
from typing import TYPE_CHECKING
import pytest
from top import top
def test_top_tuples ( ) - > None :
fruit = 'mango pear apple kiwi banana' . split( )
series: Iterator[ tuple [ int , str ] ] = (
( len ( s) , s) for s in fruit)
length = 3
expected = [ ( 6 , 'banana' ) , ( 5 , 'mango' ) , ( 5 , 'apple' ) ]
result = top( series, length)
if TYPE_CHECKING:
reveal_type( series)
reveal_type( expected)
reveal_type( result)
assert result == expected
def test_top_objects_error ( ) - > None :
series = [ object ( ) for _ in range ( 4 ) ]
if TYPE_CHECKING:
reveal_type( series)
with pytest. raises( TypeError) as excinfo:
top( series, 3 )
assert "'<' not supported" in str ( excinfo. value)
上述测试能通过 .
不过 , 即使没有top . py中的类型提示 , 测试也能通过 .
其实 , 我们的目的是使用Mypy检查测试文件 , 确认TypeVar 的声明是正确的 .
mypy命令的输出如示例 8 - 23 所示 .
. . . / comparable/ $ mypy top_test. py
top_test. py: 32 : note:
Revealed type is "typing.Iterator[Tuple[builtins.int, builtins.str]]"
top_test. py: 33 : note:
Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]"
top_test. py: 34 : note:
Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]"
top_test. py: 41 : note:
Revealed type is "builtins.list[builtins.object*]"
top_test. py: 43 : error:
Value of type variable "LT" of "top" cannot be "object"
Found 1 error in 1 file ( checked 1 source file )
* * ------------------------------------------------------------------------------------------- * *
截至Mypy 0.910 ( 2021 年 7 月 ) , reveal_type的输出有时不显示我声明的类型 , 而是显示相容的类型 .
鉴于此 , 我没有使用typing . Iterator , 而是用了abc . Iterator . 请忽略这个细节 .
Mypy的输出还是有用的 . 讨论输出时 , 我会假装Mypy已经修正了这个问题 .
( 在Mypy 0.910 版本中 , 使用reveal_type时可能会出现类型显示不正确的问题 ,
Mypy会显示相容的类型 , 而不是用户声明的类型 .
因此 , 选择了使用abc . Iterator而不是typing . Iterator , 这是一个很好的解决方案 ,
因为abc . Iterator是一个抽象基类 , 可以表示任何可迭代的对象 .
这样 , 即使在reveal_type输出不正确的情况下 , 您的代码也可以正常运行 . )
* * ------------------------------------------------------------------------------------------- * *
与抽象基类相比 , 协议类型的关键优势在于 , 类型无须做任何特殊的声明就可以与协议类型相容 .
如此一来 , 协议可以利用现有的类型或者不受我们控制的代码实现的类型创建 .
在需要SupportsLessThan参数的地方可以直接使用SupportsLessThan协议 ,
无须衍生或注册str , tuple , float , set等类型 , 只要实现了__lt__方法即可 .
而且 , 类型检查工具仍能发挥作用 , 因为SupportsLessThan是显式定义的协议 ,
这与鸭子类型正好相反 , 鸭子类型隐含的协议对类型检查工具是不可见的 .
特殊的类Protocol由PEP 544 --Protocols : Structural subtyping ( static duck typing ) ' 引 | 入 .
示例 8 - 21 揭示了这个功能为什么属于静态鸭子类型范畴 :
top函数的series参数采用的注解方式想表达的意思是 :
'series的名义类型无关紧要, 只要实现_lt_方法即可.'
Python的鸭子类型并不阻止我们隐含这层意思 , 只是静态类型检查工具无从知晓 .
类型检查工具不能读取使用C语言编写的CPython源码 , 也无法在控制台中试验 ,
以确定sorted函数只能处理支持 < 运算符的元素 .
现在 , 可以让静态类型检查工具明确知晓鸭子类型的深意 .
所以我们说,typing . Protocol实现的是 '静态鸭子类型' . ⑰
( 注 17 : 不知道静态鸭子类型这个术语是谁发明的 , 不过随着Go语言的出现 , 这种说法越来越常见 .
Go语言中的接口语义更像Python的协议 , 与Jαva的名义接口不太一样 . )
关于typing . Protocol , 要讲的内容还有很多 .
第 13 章将比较结构类型 , 鸭子类型和抽象基类 ( 形式化协议的另一种方式 ) , 届时再做讨论 .
另外 , 15.2 节将解释如何使用 @ typing . overload声明重载的函数签名 ,
其中包括一个大量使用typing . Protocol和有界的TypeVar的示例 .
* * ------------------------------------------------------------------------------------------- * *
使用typing . Protocol可以注解 8.4 节定义的double函数 , 功能没有一点儿损失 .
关键是要定义一个含有__mul__方法的协议类 .
建议你尝试一下 . 实现方式见 13.6 .1 节 .
* * ------------------------------------------------------------------------------------------- * *
8.5.11 Callable
collections . abc模块提供的Callable类型
( 尚未使用Python 3.9 的用户在typing模块中寻找 ) 用于注解回调参数或高阶函数返回的可调用对象 .
Callable类型可像下面这样参数化 .
Callable[ [ ParamType1, ParamType2] , ReturnType]
参数列表 , 即这里的 [ ParamType1, ParamType2 ] , 可以包含零或多个类型 .
下面以 18.3 节实现的简单交互式解释器中的repl函数为例 . ⑲
( 注 18 : REPL是Read-Eval-Print-Loop的简称 , 这是交互式解释器的基本行为。 )
def repl ( input_fn: Callable[ [ Any] , str ] = input ] ) - > None :
常规使用过程中 , repl函数使用Python内置函数input读取用户输入的表达式 .
然而 , 如果是做自动化测试或与其他输入源集成 , 则repl函数会接受一个可选的参数input_fn .
这是一个Callable , 参数类型和返回值类型都与input相同 .
在typeshed项目中 , 内置函数input的签名如下所示 .
def input ( _prompt: Any = . . . ) - > s tr: . . .
input函数的签名与下面的Callable类型提示相容 .
Callable[ [ Any] , str ]
可选或关键字参数类型没有专门的注解句法 .
正如typing . Callable文档所述 : '这种函数类型很少用作回调类型.'
如果想让类型提示匹配的签名灵活一些 , 可以把整个参数列表替换成 . . . , 如下所示 .
Callable[ . . . , ReturnType]
泛化类型参数与类型层次结构的交互引入了一个新类型概念 : 型变 ( variance ) .
1. Callable 类型的型变
假设一个温度控制系统中有示例 8 - 24 所示的update函数 .
这个函数很简单 , 调用probe函数获取当前温度 , 再调用display函数向用户显示当前温度 .
出于教学目的 , probe和display都会作为参数传给update .
这个示例的目的是比较两种Callable注解 : 一种有返回值类型 , 另一种有参数类型 .
from collections. abc import Callable
def update (
probe: Callable[ [ ] , float ] ,
display: Callable[ [ float ] , None ]
) - > None :
temperature = probe( )
display( temperature)
def probe_ok ( ) - > int :
return 42
def display_wrong ( temperature: int ) - > None :
print ( hex ( temperature) )
update( probe_ok, display_wrong)
def display_ok ( temperature: complex ) - > None :
print ( temperature)
update( probe_ok, display_ok)
综上所述 , 如果预期接受返回float值的回调 , 则提供返回int值的回调是可以的 ,
因为在预期float值的地方都能使用int值 .
正式地说 , Callable [ [ ] , int ] 是Callable [ [ ] , float ] 的子类型 , 因为int是float的子类型 .
这意味着 , 那个Callable的返回值类型经历了协变 ( covariant ) , 因为int和float之间具有子类型关系 ,
而且变化方向与Callable类型中返回值的类型变化方向相同 .
反过来 , 如果回调预期处理float值 , 却提供接受int参数的回调 , 则会导致类型错误 .
正式地说 , Callable [ [ int ] , None ] 不是Callable [ [ float ] , None ] 的子类型 .
虽然int是float的子类型 , 但是在参数化Callable类型中 , 关系是相反的 ,
即Callable [ [ float ] , None ] 是Callable [ [ int ] , None ] 的子类型 .
因此我们说 , 那个Callable声明的参数类型经历了逆变 ( contravariant ) .
15.7 节将进一步说明型变 , 给出一些不变 ( invariant ) 类型 , 协变类型和逆变类型的示例 .
目前 , 可以认为大多数参数化泛型是 '不变' 的 , 这样更容易理解 .
如果声明scores : list [ float ] , 就只能把list [ float ] 值赋给scores ,
不能使用声明为list [ int ] 或list [ complex ] 的对象 .
• 不接受list [ int ] 对象的原因是 , scores中不能存放float值 , 而代码可能需要这么做 .
( 意思应该是 , 如果使用list [ int ] , scores中不能存放float值了 . )
• 不接受 list [ complex ] 对象的原因是 , 代码可能需要排序scores , 找出中位数 ,
而complex未提供__lt__ , 所以list [ complex ] 不可排序 .
下面是本章要讲的最后一个特殊类型 .
8.5.12 NoReturn
这个特殊类型仅用于注解绝不返回的函数的返回值类型 . 这类函数通常会抛出异常 .
标准库中有很多这样的函数 .
例如 , sys . exit ( ) 会抛出SystemExit , 终止Python进程 .
在typeshed项目中 , 该函数的签名如下所示 .
def exit ( _status: object = . . . ) - > NoReturn: . . .
_status是仅限位置参数 , 而且有默认值 .
存根文件未明确给出默认值 , 而是使用 . . . 代替 .
_status的类型是object , 因此也可以是None , 所以没必要注解为Optional [ object ] .
示例 24 - 6 将使用NoReturn注解__flag_unknown_attrs方法 .
该方法会构建一个对用户友好而且全面的错误消息 , 然后抛出AttributeError .
本章内容很多 , 就快结束了 . 8.6 节介绍位置参数和变长参数的注解 .
8.6 注解仅限位置参数和变长参数
回到示例 7 - 9 中的tag函数 .
上一次见到该函数的签名是在 '仅限位置参数' 一节 .
def tag ( name, / , * content, class_= None , ** attrs) :
. . .
下面是带完整注解的tag函数签名 . 由于签名较长 , 因此按照格式化工具blue的方式分成了几行 .
from typing import Optional
def tag (
name: str ,
* content: str ,
class_: Optional[ str ] = None ,
** attrs: str ,
) - > str :
注意任意个位置参数的类型提示 * content : str , 这表明这些参数必须是str类型 .
在函数主体中 , 局部变量content的类型为tuple [ str, ... ] .
在这个示例中 , 任意个关键字参数的类型提示是 * * attrs : str , ( 关键字就是字符串 , 这个的str限制值是str . )
因此在函数主体中 , attrs的类型为dict [ str, str ] . ( 第一个str是键 , 第二个str是值 . )
如果类型提示是 * * attrs : float , 那么在函数主体中 , attrs的类型为dict [ str, float ] .
如果attrs参数接受不同类型的值 , 则需要使用Union [ ] 或Any , 注解为 * * attrs : Any .
针对仅限位置参数的 / 表示法只可在Python 3.8 及以上版本中使用 .
在Python 3.7 或以下版本中 , 这会导致句法错误 .
PEP 484 约定 , 在仅限位置参数的名称前加两个下划线 .
下面使用PEP 484 约定的方式注解tag函数的签名 , 这一次分成两行 .
from typing import Optional
def tag ( __name: str , * content: str , class_: Optional[ str ] = None ,
** attrs: str ) - > str :
以上两种声明仅限位置参数的方式Mypy均可识别并检验 .
最后 , 简单讲一下类型提示及其支持的静态类型系统的局限性 .
8.7 类型不完美, 测试须全面
大型企业基准代码的维护人员反映 , 静态类型检查工具能发现很多bug ,
而且这个阶段发现的bug比上线运行之后发现的bug修复成本更低 .
然而 , 有必要指出的是 , 早在引入静态类型之前 , 自动化测试就已经是行业标准做法 , 我熟知的公司均已广泛采用 .
虽然静态类型优势诸多 , 但是也不能保证绝对正确 . 静态类型很难发现以下问题 .
误报
代码中正确的类型被检查工具报告有错误 .
漏报
代码中不正确的类型没有被检查工具报告有错误 .
此外 , 如果对所有代码都做类型检查 , 那么我们将失去Python的一些表现力 .
• 一些便利的功能无法做静态检查 , 比如像config ( * * settings ) 这种参数拆包 .
• 一般来说 , 类型检查工具对特性 ( property ) , 描述符 , 元类和元编程等高级功能的支持很差 ,
或者根本无法理解 .
• 类型检查工具跟不上Python版本的变化 ( 有时落后不止一年 ) , 可能拒绝使用语言新功能的代码 , 甚至崩溃 .
常见的数据约束在类型系统中无法表达 , 即使是简单的约束 .
例如 , 类型提示不能确保 '数量必须是大于0的整数' 或 '标签必须是6~12个ASCII字母的字符串' .
通常 , 类型提示对捕获业务逻辑中的错误没有帮助 .
考虑到这些缺点 , 类型提示不能作为软件质量的保障支柱 , 而且盲目使用只会放大缺点 .
建议把静态类型检查工具纳入现代CI流水线 , 与测试运行程序 , lint程序等结合在一起使用 .
CI流水线的目的是减少软件故障 , 自动化测试可以捕获许多超出类型提示能力范围的bug .
Python写出的代码都能使用Python测试 , 有没有类型提示无关紧要 .
* * ------------------------------------------------------------------------------------------- * *
本节的标题和结论受Bruce Eckel的一篇文章启发 .
⑲那篇文章题为 'Strong Typing vs. Strong Testing' ,
收录在由Joel Spolsky 所著的The Best Software Writing I一书中 .
Bruce很喜欢Python , 写了几本讲C , Java , Scala和Kotlin的书 .
在那篇文章中 , 他说他一直推崇静态类型 , 直到接触Python才恍然大悟 : ' 如果一个Python程序有足够的单元测试 ,
那么它就可以像C , Java或C # 程序一样稳健 ( 不过Python测试编写起来更快 ) . '
( 注 19 : 本节原文标题为 'Imperfect Typing and Strong Testing' . ——译者注 )
* * ------------------------------------------------------------------------------------------- * *
至此 , 我们对Python类型提示的介绍将暂告一段落 .
第 15 章会继续这个话题 , 涉及泛化类 , 型变 , 签名重载 , 类型校正等 .
另外 , 本书的多个示例中会出现类型提示的身影 .
8.8 本章小结
本章首先简要介绍了渐进式类型概念 , 然后开始实践 .
不借助读取类型提示的工具很难理解渐进式类型 , 因此我们在Mypy错误报告的指引之下 , 为一个函数添加了注解 .
接着 , 我们又回到渐进式类型概念上 , 指出这其实是一种混合概念 ,
综合了Python传统的鸭子类型和Java , C + + 等静态类型语言的名义类型 .
本章大部分篇幅分门别类介绍了注解可用的主要类型 .
本章讲到的很多类型与我们熟悉的Python对象类型 ( 例如容器 , 元组和可调用对象 ) 有关 ,
不过也延伸到了泛型表示法 ( 例如Sequence [ float ] ) .
这些类型中有很多是在typing模块中临时实现的 , 因为直到Python 3.9 改造标准类型之后才支持泛化 .
有些类型是特殊的实体 .
Any , Optional , Union和NoReturn不关联内存中的实际对象 , 只存在于类型系统的抽象层面上 .
我们研究了参数化泛型和类型变量 , 为类型提示提供了更大的灵活性 , 而且不失类型安全性 .
引人Protocol后 , 参数化泛型更具表现力 .
Protocol在Python 3.8 中才出现 , 还未大范围使用 , 但是重要性不容忽视 .
Protocol使得静态鸭子类型成为可能 .
静态鸭子类型是Python内在的鸭子类型和名义类型之间的重要桥梁 , 令静态类型检查工具能捕获更多的bug .
介绍一些类型时 , 我们使用Mypy做试验 ,
利用Mypy提供的魔法函数reveal_type ( ) 观察类型检查错误和推导的类型 .
最后又介绍了如何注解仅限位置参数和变长参数 .
类型提示是一个复杂的话题 , 还在不断发展中 .
幸运的是 , 这是可选功能 , 因此Python广泛的用户群体不受影响 .
请不要听信类型布道者的话 , 认为所有Python代码都需要类型提示 .
Python类型提示由荣誉的仁慈 '独裁者' ⑳全力推动 , 为表感激 , 本章开头和结尾都引用了他的话 .
( 注 20 : Benevolent Dictator For Life ( BDFL ) .
参见Guido van Rossum对这个称呼的考证文章 'Origin of BDFL' . )
我不希望在道德上有义务为一个Python版本一直添加类型提示 .
我坚信 , 类型提示有存在的必要 , 然而很多时候得不偿失 . 用与不用由你自己选择 , 这多好 . ⑳
-- Guido van Rossum
( 注 21 : 出自YouTube视频 'Type Hints by Guido van Rossum(March2015)' .
引用的内容从 13 分 40 秒开始 . 为了表达清楚 , 我稍微做了修改 . )
8.9 延伸阅读
Bernát Gábor在他的一篇质量优秀的博文 'The state of type hints in Python' 中写道 :
需要编写单元测试的地方都应添加类型提示 .
我十分推崇测试 , 不过我经常做验证性编程 .
当验证想法时 , 测试和类型提示没有多大作用 , 只会拖慢进程 .
在我能找到的资料中 , Gabor的文章对Python类型提示的介绍最好 ,
Geir Arne Hjelle的 'Python Type Checking (Guide)' 一文也不错 .
Claudio Jolowicz的 'Hypermodern Python Chapter 4: Typing' 一文则更简短 , 也讲到了运行时类型检查验证 .
如果想深入了解 , 那么Mypy文档是最好的资料 .
不管使用哪个类型检查工具 , Mypy文档都有参考价值 , 因为Mypy文档不光解读Mypy工具自身 , 也有教程 ,
以及涉及Python类型一般性话题的参考页面 .
Mypy文档还提供了一份便利的速查表 , 并对常见问题给出了解决方案 .
typing模块文档适合快速浏览 , 不触及深层细节 .
'PEP 483-—The Theory of Type Hints' 对型变做了深入解读 , 还使用Callable说明了逆变 .
最根本的参考资料必然是与类型有关的 20 多个PEP文档 .
PEP的目标受众是Python核心开发人员和Python指导委员会 , 如果没有大量预备知识 , 则读起来肯定费力 .
前面说过 , 第 15 章将继续探讨类型话题 , 15.10 节还会提供更多参考资料 .
表 15 - 1 列出了截至 2021 年年底已经通过或正在讨论的与类型有关的PEP .
'Awesome Python Typing' 仓库收集了相关工具和资料的链接 , 具有一定的参考价值 .
* ---------------------------------------------杂谈--------------------------------------------- *
'行动起来' :
忘掉可望不可及的超轻单车 , 忘掉华丽的运动衫 , 忘掉夹在小踏板上厚实的鞋子 , 忘掉无尽里程的磨炼 .
像小时候那样骑车吧 , 只有跨上单车你才能感受骑行的真正乐趣 .
——Grant Petersen < < 单车手册:放在口袋里的单车实用指南 > >
如果编程不是你的本职工作 , 而是协助你工作的工具 , 或者用来学习 , 捣鼓小项目 , 义或者只是个人兴趣 ,
那么可能不需要类型提示 , 就像大多数骑自行车的人不需要硬鞋底和金属防滑钉一样 .
动手编程吧 .
* --------------------------------------------------------------------------------------------- *
'类型的认知效应' :
我担心类型提示对Python编程风格会产生影响 .
我同意大多数API用户能从类型提示中受益 .
但是 , Python吸引我的原因之一是 , Python提供的函数足够强大 , 完全可以取代整个API ,
而且我们自己也可以编写同样强大的函数 .
以内置函数max ( ) 为例 . 这个函数功能强大 , 但不难理解 .
然而 , 读到 15.2 .1 节你会发现 , 为了正确注解该函数 , 要编写 14 行类型提示 ,
这还不包括为类型提示提供支持的一个typing . Protocol定义和几个TypeVar定义 .
我担心的是 , 倘若代码库对类型提示提出严格要求 , 则程序员会打消编写这种函数的念头 .
根据维基百科 , '语言相对论' ( 也叫萨丕尔-沃夫假说 ) 是一个
'主张语言结构对使用者的世界观或认知有影响的理论' .
维基百科进一步说道 :
• '强版本' 认为 , 语言 '决定' 思维 , 语系限制并决定认知范畴 ;
• '弱版本' 认为 , 语系和用法只 '影响' 思维和决策 .
语言学家普遍认为强版本是错误的 , 但有经验证据支持弱版本 .
我不知道编程语言方面有没有相关研究 , 但是根据我的经验 , 编程语言对我处理问题的方式确实产生了很大的影响 .
我从事编程工作使用的第一门编程语言是 8 位计算机时代的Applesoft BASIC .
BASIC不直接支持递归 , 必须自己动手实现调用栈 .
所以 , 我从不考虑使用递归算法或数据结构 .
我知道这些东西在某种概念层面上是存在的 , 但是我绝不使用它们解决问题 .
几十年后 , 我接触到了Elixir .
起初 , 我喜欢用递归解决问题 , 不加甄别 .
后来发现 , 我的很多方案 , 使用Elixir Enum模块和Stream模块中现有的函数更简单 .
我了解到 , 地道的Elixir应用程序级代码很少显式递归调用 , 而是使用底层已经实现递归的枚举和流 .
语言相对论可以解释一个普遍的观点 ( 也是未经证实的 ) ,
即学习不同的编程语言 , 尤其是支持不同编程范式的语言 , 能让你成为更好的程序员 .
有了使用Elixir的经验 , 编写Python或Go代码时 , 我更有可能使用函数式编程模式 .
现在来看具体的例子 .
如果Kenneth Reitz当初决定 ( 或者受命于领导 ) 注解所有函数 , 那么requests包的API势必截然不同 .
他的初衷是写出易于使用 , 灵活且强大的API .
他成功了 , requests包非常受欢迎 .
2020 年 5 月 , 根据PyPI Stats , requests包排名第 4 , 一天的下载量达到 260 万次 .
第 1 名是requests的一个依赖 , 即urllib3 .
2017 年 , requests包的维护人员决定 , 不投入时间编写类型提示 .
维护人员之一Cory Benfield在一封电子邮件中说道 :
我觉得符合Python风格的API最不需要使用类型系统 , 因为其得到的回报最少 .
在那封邮件中 , Benfield给出了一个极端示例 : 倘若为requests . request ( )
函数的files关键字参数添加类型定义 , 将是下面这样 .
Optional[
Union[
Mapping[
basestring ,
Union[
Tuple[ basestring , Optional[ Union[ basestring , file ] ] ] ,
Tuple[ basestring , Optional[ Union[ basestring , file ] ] ,
Optional[ basestring ] ] ,
Tuple[ basestring , Optional[ Union[ basestring , file ] ] ,
Optional[ basestring ] , Optional[ Headers] ]
]
] ,
Iterable[
Tuple[
basestring ,
Union[
Tuple[ basestring , Optional[ Union[ basestring , file ] ] ] ,
Tuple[ basestring , Optional[ Union[ basestring , file ] ] ,
Optional[ basestring ] ] ,
Tuple[ basestring , Optional[ Union[ basestring , file ] ] ,
Optional[ basestring ] , Optional[ Headers] ]
]
]
]
]
]
而且 , 还要先定义以下类型 .
Headers = Union[
Mapping[ basestring , basestring ] ,
Iterable[ Tuple[ basestring , basestring ] ] ,
]
假如维护人员当初下决心实现 100 % 的类型提示覆盖率 , 那么requests包还能这么受欢迎吗?
另一个重量级包SQLAlchemy也没有对类型提示太过上心 .
这些包之所以优秀 , 就是因为坚守了Python的动态本性 .
虽然类型提示有好处 , 但也要付出代价 .
首先 , 要投入大量时间理解类型系统的机制 . 这只是一次性成本 .
除此之外 , 还有经常性成本 , 永无终期 .
如果无论什么都要做类型检查 , 则必将损失部分Python表现力 .
参数拆包 ( 例如config ( * * settings ) ) 这种好用的功能则超出了类型检查工具的能力范围 .
如果想对config ( * * settings ) 这样的调用做类型检查 , 则必须把每个参数拆解开来 .
这让我回想起了 35 年前编写的Turbo Pascal代码 .
对使用元编程的库来说 , 想添加注解很难 , 有时根本不可能 .
是的 , 元编程有点儿泛滥 , 不过很多Python包这么好用还是得益于元编程 .
在大公司中 , 如果自上而下严格规定 , 必须添加类型提示 , 没有例外 ,
那么我敢打赌 , 我们很快就会看到有人使用代码生成工具 ,
减少Python源码中的样板代码--这是动态程度较低的语言的常见做法 .
对于某些项目和情形 , 类型提示是没有意义的 .
即便能起到一定作用 , 作用也不大 .
任何合理的类型提示策略都要包含例外规定 .
开创了面向对象编程的图灵奖获得者Alan Kay 曾经说过 :
有些人对类型系统非常虔诚 , 作为一名数学家 , 我喜欢类型系统的想法 ,
但从来没有人造出一个范围合理的类型系统 . ㉒
( 注 22 : 来源是 'AConversation with Alan Kay' . )
感谢Guido把类型定为可选功能 .
请根据实际需求使用类型 , 不要一处不落 , 全部注解 .
严格遵守Java1 . 5 那种编程风格是不可取的 .
'鸭子类型优势显著' :
鸭子类型符合我的思维方式 .
静态鸭子类型是很好的折中方案 , 使得静态类型检查成为可能 ,
而且不失一些灵活性 , 还不囿于某些名义类型系统的复杂性 .
在PEP544之前 , 类型提示给我的感觉是不太符合Python风格 .
Python引入typing . Protocol之后 , 让我欣喜若狂 .
'黑暗势力' 终于被压制住了 .
'泛化还是特化?' :
从Python的角度来看 , 在类型上下文中使用术语 '泛化' ( generic ) 落后了 .
'generic' 一般有两个意思 : '通用的' 和 '无商标的' .
以list和list [ str ] 为例 . 前者属于泛化 , 可接受任何对象 ; 后者属于特化 , 只接受str .
然而 , '泛化' 在Java中说得通 . 在Java 1.5 之前 , 所有容器 ( 除了神奇的array ) 都是 '特化' 的 ,
只能存放Object引用 , 从容器中取出的项都要校正类型才能使用 .
从Java 1.5 开始 , 容器接受类型参数 , 已经 '泛化' 了 .