IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Python知识库 -> 《流畅的Python第二版》读书笔记——函数中的类型注解 -> 正文阅读

[Python知识库]《流畅的Python第二版》读书笔记——函数中的类型注解

引言

这是《流畅的Python第二版》抢先版的读书笔记。Python版本暂时用的是python3.10。为了使开发更简单、快捷,本文使用了JupyterLab。

本章关注于Python在函数签名中的类型注解(Type Hints)。在Python中引入了显式的类型注解,可以为函数参数、返回值、变量等添加类型提示。主要目的在于帮助开发工具通过静态检查发现代码中的Bug。

新内容简介

全新内容。

由于静态类型系统的限制,PEP 484 引入了渐进类型系统(gradual type system)。

什么是渐进类型

PEP 484 引入了渐进类型系统。一个渐进类型系统:

  • 是可选的 默认情况下,类型检查器不应该为没有类型注解的代码发出告警。当类型检查器不能决定对象的类型时,会假设它为Any类型。Any类型被认为与所有其他类型兼容。
  • 不在运行时捕获类型错误 类型注解由静态类型检查器使用,IDE会发出告警。但它们不会防止不一致的值传递到函数或在运行时赋值给变量。
  • 不增强性能 类型注解提供的数据理论上允许在生成的字节码上优化,但这种优化未在任何Python运行时中实现。

渐进类型最有用的特征在于注解总是可选的。

在静态类型系统中,大多数类型约束很容易表达,但还有一些是笨重的、一些是难的,还有一小部分是不可能的。你很可能会编写一段优秀的Python代码,具有良好的测试覆盖率和测试通过率,但仍无法增加使类型检查器满意的类型注解。

类型注解在所有级别上都是可选的:你的整个包可以没有类型注解,当你引入该包到使用类型注解的模块时可以静默类型检查器,也可以增加特殊的评论来类型检查器忽略代码中特定的行。

渐进类型实战

由一个简单的函数开始,然后逐渐增加类型注解。
messages.py

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'
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'


show_count(99, 'bird')
'99 birds'
show_count(1, 'bird')
'1 bird'
show_count(0, 'bird')
'no birds'

由Mypy开始

messages.py上运行mypy命令来开始类型检查:

! pip install mypy
Requirement already satisfied: mypy in d:\program_tool\py39\lib\site-packages (0.982)
Requirement already satisfied: typing-extensions>=3.10 in d:\program_tool\py39\lib\site-packages (from mypy) (3.10.0.2)
Requirement already satisfied: tomli>=1.1.0 in d:\program_tool\py39\lib\site-packages (from mypy) (2.0.1)
Requirement already satisfied: mypy-extensions>=0.4.3 in d:\program_tool\py39\lib\site-packages (from mypy) (0.4.3)
! mypy ./messages/no_hints/messages.py
Success: no issues found in 1 source file

此时Mypy没有发现任何问题。

如果一个函数签名没有任何注解,Mypy默认会忽略它。下面,增加pytest单元测试,该代码在messages_test.py中:

from pytest import mark

from messages import 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

def test_show_count_zero():
    got = show_count(0, 'part')
    assert got == 'no parts'

下面开始增加类型注解。

让Mypy更严格

命令行可选项--disallow-untyped-defs使Mypy标记任何对其所有参数和返回值没有类型注解的函数定义。

使用--disallow-untyped-defs在测试文件上产生三个error和一个note:

! mypy  --disallow-untyped-defs ./messages/no_hints/messages_test.py
messages\no_hints\messages.py:1: error: Function is missing a type annotation
messages\no_hints\messages_test.py:9: error: Function is missing a type annotation
messages\no_hints\messages_test.py:13: error: Function is missing a return type annotation
messages\no_hints\messages_test.py:13: note: Use "-> None" if function does not return a value
Found 3 errors in 2 files (checked 1 source file)

对于渐进类型的第一步,我倾向于使用另一个可选项:--disallow-incomplete-defs。初始时,它不会抱怨任何问题:

! mypy  --disallow-incomplete-defs ./messages/no_hints/messages_test.py
Success: no issues found in 1 source file

接着,我可以仅为message.py中的show_count增加返回类型:

def show_count(count, word) -> str:

这足以让Mypy查看它了。使用前面同样的命令行去检查messages_test.py会让Mypy再次查看messages.py

! mypy  --disallow-incomplete-defs ./messages/no_hints/messages_test.py
messages\no_hints\messages.py:1: error: Function is missing a type annotation for one or more arguments
Found 1 error in 1 file (checked 1 source file)

现在我们可以逐渐为函数增加类型注解,而不会得到我们没有标注的告警。下面是能满足Mypy的完整带注解签名:

def show_count(count: int, word: str) -> str:
! mypy  --disallow-incomplete-defs ./messages/no_hints/messages_test.py
Success: no issues found in 1 source file

除了在命令行中使用可选项之外,还可以把你想要的可选项写到Mypy配置文件中。可以有全局设定和模块设定。
下面是一个简单的mypy.ini例子:

[mypy]
python_version = 3.9
warn_unused_configs = True
disallow_incomplete_defs = True

默认的参数值

示例中的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  ./messages/hints_2/messages_test.py
messages\hints_2\messages_test.py:21: error: Too many arguments for "show_count"
Found 1 error in 1 file (checked 1 source file)

现在我们编辑show_count,增加可选的plural参数:

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  ./messages/hints_2/messages_test.py
Success: no issues found in 1 source file

现在Mypy就不会报错了。

使用None作为默认值

在上面,参数plural被注解为str,它的默认值为'',所以没有类型冲突。

这种方法不错,但在其他情况下,None是更好的默认值。如果可选的参数需要一个可变类型,那么None是唯一合理的默认值。

为了让None作为plural参数的默认值,新的签名变成:

from typing import Optional

def show_count(count: int, singular: str, plural: Optional[str] = None) -> str:

我们拆开来看:

  • Optional[str] 意味着plural可以为strNone
  • 你必须显示地提供默认值 = None

如果没有为plural赋予默认值,Python运行时会将它看成必传参数。记住:在运行时,忽略类型注解。

注意我们需要从typing引入Optional。在导入类型时,最好使用from typing import X的语法来减少函数签名的长度。

Optional并不是一个很好的名称,因为它不会使参数成为可选的。使参数成为可选的原因是为该参数分配一个默认值。Optional[str]仅表示:此参数的类型可以是strNoneT类型。

类型被定义来支持操作

实际上,将受支持的操作集合看成一种类型的定义特征更有用。

比如,从可用的操作角度来看,在下面的函数中,x的有效类型是什么?

def double(x):
    return x * 2

x参数类型可能是数值(intcomplexFractionnumpy.uint32等),但也可能是一个序列(strtuplelistarray),一个N维的numpy.array,或其他实现或继承了__mul__方法能接受int参数的类型。

然而,考虑下面注解的double方法。先忽略返回类型,我们关注于参数类型:

from collections import abc
def double(x: abc.Sequence):
    return x * 2

类型检查器会拒绝该代码。如果你告诉Mypy x是类型abc.Sequence,它会标记x * 2为错误因为abc.Sequence没有实现或继承__mul__方法。在运行时,该代码可以在具体的序列,比如strtuplelistarray等,以及数值上正常运行,因为运行时忽略类型注解。但类型检查器只关心显示声明的,而abc.Sequence没有__mul__

这就是本小节的标题是“类型被定义来支持操作”的原因。Python运行时接收任意类型作为x参数。x * 2可能正常执行,或抛出TypeError(如果x不支持该操作)。相比之下,Mypy会认为注解版本下doulbe中的x * 2是错误的,因为x的类型abc.Sequence不支持该操作。

在渐进类型系统中,我们有两种不同的观点相互作用:

  • 鸭子类型(Duck typing) 这种观点被Smalltalk——首创的面向对象语言——以及Pyhton,JS,Ruby采纳。对象必须指定类型,但变量(包括参数)是无类型的。实际上,对象定义的类型不重要,重要的是它实际支持的操作。如果我们能调用birdie.quack(),那么birdie在该上下文中是鸭子。根据定义,只有在运行时尝试对对象进行操作时才强制鸭子类型。这比名义类型更灵活,但代价是在运行时允许更多的错误。
  • 名义类型(Nominal typing) 该观点被C++、Java和C#采纳,也被注解的Python支持。对象和变量都有类型。但对象只在运行时存在,然后类型检查器只关心由类型注解标注的变量(和参数)的源码。如果DuckBird的子类,你可以分配一个Duck实例到标记为Bird的参数birdie。但在函数体中,类型检查器认为调用birdie.quack()是非法的,因为birdie名义上是Bird,而该类没有提供.quack()方法。在运行时的实际参数是否为Duck并不重要,因为名义类型化是静态强制的。类型检查器不会运行任何程序,只会读取源代码。这比鸭子类型更严格,鸭子类型可以早在构建pipeline时就可以捕获一些bug,甚至在IDE中输入代码时。

下面的示例故意同时对比鸭子类型和名义类型,以及静态类型检查和运行时行为。
birds.py:

class Bird:
    pass

class Duck(Bird):  # Duck是Bird子类
    def quack(self):
        print('Quack!')

def alert(birdie):  # alert没有类型注解,所以类型检查忽略它
    birdie.quack()

def alert_duck(birdie: Duck) -> None:  # alert_duck接收一个类型为Duck的参数
    birdie.quack()

def alert_bird(birdie: Bird) -> None:  # alert_bird接收一个类型为Bird的参数
    birdie.quack()

用Mypy进行类型检查,我们可以看到一个错误:

! mypy  ./birds/birds.py
birds\birds.py:15: 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模块:
daffy.py:

from birds import *

daffy = Duck()
alert(daffy)       # 有效调用,alert没有类型注解
alert_duck(daffy)  # 有效调用,alert_duck接收Duck参数,daffy是Duck
alert_bird(daffy)  # 有效调用,alert_bird接收Bird参数,daffy也是Bird——Duck的超类

daffy.py上运行Mypy抛出同样的错误:

! mypy  ./birds/daffy.py
birds\birds.py:15: error: "Bird" has no attribute "quack"
Found 1 error in 1 file (checked 1 source file)

但是Mypy认为daffy.py本身并没有什么问题:这三个函数调用没问题。
现在,如果你运行daffy.py,你会得到:

! python ./birds/daffy.py
Quack!
Quack!
Quack!

所有方法都没问题。
在运行时,Python不会关心声明类型。它只用鸭子类型。Mypy在alert_bird上标记了一个错误,但通过daffy在运行时调用它却没问题。
刚开始让很多Pythonistas惊讶:一个静态类型检查器有时会在我们知道可以执行的程序中发现错误。

然而,如果几个月后你的任务是扩展这个bird的例子,你可能会感激Mypy。考虑一下这个woody.py模块,它也使用了birds
woody.py:

from birds import *

woody = Bird()
alert(woody)
alert_duck(woody)
alert_bird(woody)
! mypy  ./birds/woody.py
birds\birds.py:15: error: "Bird" has no attribute "quack"
birds\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)

Mypy发现了两个错误,第一个是在birds.py中:alert_bird内的birdie.quack(),我们之前见过。
第二个是在woody.py中:woodyBird的实例,所以调用alert_duck(woody)是无效的,因为该函数需要的是Duck。每个Duck都是Bird,但并不是每个Bird都是Duck

在运行时,woody.py中没有一个成功执行的。

from birds.birds import *
woody = Bird()
alert(woody) # Mypy无法检测到此错误,因为alert中没有类型提示。
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

Cell In [11], line 3
      1 from birds.birds import *
      2 woody = Bird()
----> 3 alert(woody)


File D:\workspace\fluent-python-2nd\08-def-type-hints\birds\birds.py:9, in alert(birdie)
      8 def alert(birdie):  
----> 9     birdie.quack()


AttributeError: 'Bird' object has no attribute 'quack'
alert_duck(woody) # Mypy报告了该问题:第一个参数的类型不兼容,期望Duck
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

Cell In [13], line 1
----> 1 alert_duck(woody)


File D:\workspace\fluent-python-2nd\08-def-type-hints\birds\birds.py:12, in alert_duck(birdie)
     11 def alert_duck(birdie: Duck) -> None:  
---> 12     birdie.quack()


AttributeError: 'Bird' object has no attribute 'quack'
alert_bird(woody) # Mypy告诉我们alert_bird的函数体是错误的:Bird没有属性quack
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

Cell In [14], line 1
----> 1 alert_bird(woody)


File D:\workspace\fluent-python-2nd\08-def-type-hints\birds\birds.py:15, in alert_bird(birdie)
     14 def alert_bird(birdie: Bird) -> None:  
---> 15     birdie.quack()


AttributeError: 'Bird' object has no attribute 'quack'

这个小实验表明,鸭子类型更容易理解,也更灵活,但允许不受支持的操作在运行时导致错误。标称输入在运行前检测错误,但有时会拒绝实际运行的代码——例如上面示例中的调用allalert_bird(daffy)。即使它有时可以工作,alert_bird函数的命名也是错误的:它的主体确实需要一个支持.quack()方法的对象,而Bird没有这个方法。

在这个例子中,函数是单行的。但在实际代码中,它们可以更长;他们可以将birdie参数传递给更多的函数,而birdie参数的起源可能是许多函数调用,这使得很难查明运行时错误的原因。类型检查器可以防止在运行时发生许多这样的错误。

注解可用的类型

有很多Python类型可用在类型注解上,但这里有一些约束和建议。除此之外,typing模块引入了令人惊讶的特殊的具有语义的结构。

Any类型

任何渐进类型系统的基石是Any类型,也被称为动态类型。当类型检查器看到未注解的函数:

def double(x):
    return x * 2

它会假设为:

def double(x: Any) -> Any:
    return x * 2

这意味着x和返回值可以是任何类型,包括不同的类型。Any被认为可以支持任何操作。

Anyobject对比,考虑下面的签名:

def double(x: object) -> object:

该函数也支持任何类型的参数,因为所有类型都是object子类。

然而,类型检查器会拒绝该函数。因为object不支持__mul__操作:

! mypy  ./double/double_object.py
double\double_object.py:2: error: Unsupported operand types for * ("object" and "int")
Found 1 error in 1 file (checked 1 source file)

更通用的类型具有更窄(narrower)的接口,比如,它们支持更少的操作。objectabc.Sequence实现的操作更少,而后者比abc.MutableSequence实现的更少。

Any是一个魔法类型,它位于类型层次结构的顶部和底部。它同时也是最通用的类型——所以一个参数n: Any支持任何类型的值——同时最具体的类型,支持任何可能的操作。至少,这是类型检查器理解的Any

当然,没有类型可以支持所有可能的操作,所以使用Any可以阻止类型检查器完成其核心任务:在程序崩溃和运行时异常之前检测潜在的非法操作。

子类型vs一致

传统的面向对象名义类型系统依赖子类型关系。给定一个类T1和一个子类T2,那么T2T1的子类型。
考虑下面的代码:

class T1:
    pass

class T2(T1):
     pass
    
def f1(p: T1) -> None:
    pass

o2 = T2()

f1(o2) # OK

f1(o2)是里氏替代原则的应用。Barbara Liskov实际上定义的是关于支持的操作的子类型:如果T2类型的对象替换了T1类型的对象,并且该程序仍然正确地行为,则T2T1的子类型。

继续前面的代码,下面显示违背了里氏替代原则:

def f2(p: T2) -> None:
    pass

o1 = T1()

f2(o1) # 类型错误

从支持操作的角度来看,这是完全有意义的:作为子类,T2继承且必须支持T1的所有操作。所以一个T2的实例能用在任何期望为T1实例的地方。但反过来不一定成立:T2可能实现了额外的方法,所以T1的实例就可能无法应用到T2能用的地方。

在渐进类型系统中,有另外一种关系:一致(consistent-with),适用于子类型的适用范围,并对Any类型有特殊规定。

一致的规则是:

  1. 给定类型T1和子类型T2,那么T2T1是一致的。
  2. 所有的类型与Any都是一致的:你可以传递任何类型的对象到声明为Any类型的参数。
  3. Any与所有类型一致:你总可以传递Any类型到期望其他类型的参数。

下面是解释规则2,3的代码:

def f3(p: Any) -> None:
    ...
    
o0 = object()
o1 = T1()
o2 = T2()
f3(o0) #
f3(o1) # all OK: rule #2
f3(o2) #

def f4(): # 隐式返回类型 : `Any`
    ...
o4 = f4() # 引用的类型: `Any`
f1(o4) #
f2(o4) # all OK: rule #3
f3(o4) #

所有的渐进类型系统都需要一个像Any这样的通配符类型。

简单类型和类

简单类型像int,float,strbytes可以直接用于类型注解。标准库中的具体类、外部包或用户定义的类也能用于类型注解。

抽象类可用在类型注解。

在类中,一致(consistent-with)像子类型那样定义:一个子类与所有它的父类一致。

然而,实用优于纯粹,所以有一个重要的例外,如下所述。

** intcomplex一致

内置类型intfloatcomplex类型之间不存在名义的子类型关系:它们是object的直接子类。但是PEP 484声明intfloat是一致的,floatcomplex是一致的。它在实践中是有意义的:int实现了所有float执行的操作,且int实现其他操作——位操作,如&|<<等。最终的结果是: intcomplex是一致的。对于i = 3i.real是3i.imag0

Optional和Union类型

Optional解决了None作为默认值的问题:

from typing import Optional
def show_count(count: int, singular: str, plural: Optional[str] = None) -> str:

Optional[str]实际上是Union[str, None]的简写,后者一位置plural的类型可以是strNone

在Python3.10中比OptionalUnion更好的语法

在Python3.10中,我们可以写str | bytes而不是Union[str, bytes]。它更简介,且不需要从typing中引入OptionalUnion
对比旧语法和新语法:

plural: Optional[str] = None # 旧
plural: str | None = None # 新

|操作符也能与isintanceissubclass一起用来构建第二个参数:isinstance(x, int | str)

内置函数ord的签名是Union的简单例子——它接收strbytes,然后返回int:

def ord(c: Union[str, bytes]) -> int: ...

还可以以str为参数,接收strfloat:

from typing import Union
def parse_token(token: str) -> Union[str, float]:
    try:
        return float(token)
    except ValueError:
        return token

可以的话,不要创建返回Union类型的函数,因为它们给用户带来额外的负担——强制用户在运行时检查返回值类型。但上面的parse_token是一个合理的用例。

Union[]至少需要两个类型。嵌套的Union类型与扁平的一样:

Union[A, B, Union[C, D, E]]

Union[A, B, C, D, E]

效果相同。

Union对于它们之间不一致的类型更有用。比如:Union[int, float]是多余的,因为intfloat是一致的。如果你只使用float来注释参数,那么它也会接受int值。

泛型

大多数Python集合都是异构的。例如,你可以在一个列表中混合不同类型的。但是,在实际中,这并不是很有用:如果你将对象放在集合中,你可能希望稍后对它们进行操作,通常这意味着它们必须至少共享一个常见的方法。

泛型可以用类型参数来声明,以指定它们可以处理的元素的类型。

比如,list能约束其中元素的类型,比如:

def tokenize(text: str) -> list[str]:
    return text.upper().split()

在Python≥3.9中,这意味着tokenize返回一个列表,其中的元素是str类型。
注解stuff: liststuff: list[Any]是一样的:stuff是任意类型的列表。

PEP 585列出了标准库中的接收泛型注解的集合。下面的列表仅显示使用最简单的泛型类型注解,container[item]目录:

  • list
  • collections.deque
  • abc.Sequence
  • abc.MutableSequence
  • set
  • abc.Container
  • abc.Set
  • abc.MutableSet
  • frozenset
  • abc.Collection

元组和映射支持更复杂的类型注解,我们后面再探讨。

从Python3.10开始,没有好的方式去标注array.array,考虑typecode构造函数参数,它决定数组中存储的是整数还是浮点数。一个更难的问题是当为数组添加元素时如何检查整数的范围以防止溢出。比如,typecode='B'的数组只能存储从0到255的int值。因此,Python的静态类型系统并不能满足这个挑战。

元组类型

有三种方法来注解元组类型:

  • 元组作为记录(record)
  • 元组作为带有命名字段的记录
  • 元组作为不可变序列

元组作为记录

如果你把元组当成记录使用,使用元组内置并通过[]定义字段类型。

比如,类型注解为tuple[str, float, str]来接收具有城市名、人口和国家的元组: ('Shanghai', 24.28, 'China')

考虑一个函数,它接受一对地理坐标并返回一个地理散列,使用如下:

>>> shanghai = 31.2304, 121.4737
>>> geohash(shanghai)
'wtw3sjq6q'

下面显示geohash是如何定义的:

from geolib import geohash as gh # type: ignore  # 该注释阻止Mypy报告geolib包没有类型注解

PRECISION = 9

def geohash(lat_lon: tuple[float, float]) -> str:  # lat_lon参数注解为有两个float字段的元组
    return gh.encode(*lat_lon, PRECISION)

元组作为带有命名字段的记录

为了注解一个具有很多字段,或指定类型的元组,强烈建议使用typing.NamedTuple

from typing import NamedTuple
from geolib import geohash as gh # type: ignore
PRECISION = 9

class Coordinate(NamedTuple):
    lat: float
    lon: float
    
def geohash(lat_lon: Coordinate) -> str:
    return gh.encode(*lat_lon, PRECISION)

typing.NamedTuple是用于元组子类的工厂,所以Coordinatetuple[float, float]一致,但反过来不成立。毕竟,CoordinateNamedTuple添加的额外方法,像._asdict()

实际上,这意味着传递一个Coordiante实例到下面定义的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}'

元组作为不可变序列

为了注解未指定长度作为不可变列表的元组,你必须指定单个类型,然后是,...

比如,tuple[int, ...]是一个包含int元素的元组。

...(省略)表示任何数量的(>=1)元素是可接受的。但没有办法为任意长度的元组指定不同类型的字段。

注解stuff: tuple[Any, ...]stuff: tuple是一样的:stuff是一个未定长度包含Any类型的元组。

下面是一个columnize函数,将一个序列转换成未定长度的元组列表,作为表格中的rows和cells:

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)]

这非常适合于展示列中的项,比如:

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')]

泛型映射

泛型映射类型被标注为MappingType[KeyType, ValueType]。在Python≥3.9中,内置的dictcollections以及collections.abc中的映射类型接受该注解。对于早期版本,你必须使用typing.Dict和其他typing模块中的映射类型。

下面显示了一个返回反向索引的函数来按名称搜索Unicode字符的实际用法。

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]:  # 这是一个生成器函数
    """return iterable of uppercased words"""
    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被标注了,否则,Mypy会报告: Need type annotation for 'index' (hint: "index: dict[<type>, <type>] = ...")
    index: dict[str, set[str]] = {}  
    for char in (chr(i) for i in range(start, end)):
        # 使用海象运算符保存if表达式中的unicodedata.name(char, '')的结果到name,并根据这个name来进行判断,如果为'',则条件为False
        # unicodedata.name(char, '')返回char对应的名称,比如unicodedata.name('$')='DOLLAR SIGN'
        if name := unicodedata.name(char, ''): 
            # 为名称进行分词
            for word in tokenize(name):
                index.setdefault(word, set()).add(char)
    return index

给定开始和结束Unicode字符码,name_index返回一个dict[str, set[str]],这是一个反向索引映射每个单词到一组名称中有该单词的字符。
比如在索引ASCII字符从32到64后,下面是映射到单词’SIGN’和’DIGIT’的字符集,以及如何找到名为’DIGIT EIGHT’的字符。

index = name_index(32, 65)
index['SIGN']
{'#', '$', '%', '+', '<', '=', '>'}
index['DIGIT']
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
index['DIGIT'] & index['EIGHT']
{'8'}

抽象基类

Collection相应类型注解
listtyping.List
settyping.Set
frozensettyping.FrozenSet
collections.dequetyping.Deque
collections.abc.MutableSequencetyping.MutableSequence
collections.abc.Sequencetyping.Sequence
collections.abc.Settyping.AbstractSet
collections.abc.MutableSettyping.MutableSet

上表是一些集合类型和它们对应的类型注解。
理论上,一个函数应该接收这些抽象类型——或它们相等的typing类型注解,这会给调用者更多的灵活性。

考虑下面的签名:

from collections.abc import Mapping

def name2hex(name: str, color_map: Mapping[str, int]) -> str:

使用abc.Mapping允许调用者使用dictdefaultdictChainMap、一个UserDict的子类,或任何其他继承了Mapping的子类。

相反地,看下面这个签名:

def name2hex(name: str, color_map: dict[str, int]) -> str:

现在color_map必须为dict或它的子类,比如defaultDictOrderedDict。实际上,collections.UserDict子类不会通过color_map的类型检查。因为UserDict不是dict的子类。它们是兄弟关系,都继承自abc.MutableMapping

所以,通常来说在类型注解中使用abc.Mappingabc.MutableMapping,而不是dicttyping.Dict更好。如果name2hex函数不需要修改给定的color_map,那么最准确的注解类型是abc.Mapping。这样,调用者不需要提供实现了像setdefaultpopupdate方法的对象,它们仅来自MutableMapping接口。

一个函数的返回值通常是具体对象,所以返回类型注解应该是具体类型:

def tokenize(text: str) -> list[str]:
    return text.upper().split()

Python文档关于typing.List描述的是:

list的泛型版本。用于注解返回类型。为了注解参数,最好使用抽象集合类型,如SequenceIterable

同样的描述出现在typing.Dicttyping.Set中。

请记住,从Python 3.9开始,大多数来自collections.abc的抽象类和collections中的具体类,以及内置集合,都支持泛型类型注解标记,比如collections.deque[str]

我们下面讨论数字抽象类。

数字塔的倒塌

numbers包定义了所谓的数字塔(numeric tower),该塔是抽象类的线性层次结构,Number位于顶层:

  • Number
  • Complex
  • Real
  • Rational
  • Integral

这些抽象类非常适用于运行时类型检查,但它们不支持静态类型检查。PEP 484的"Numeric Tower"小节拒绝数字抽象类,并指出内置类型complextfloatint应该看成特例,如我们上面讨论的。

实际上,如果你想标注数字参数用于静态类型检查,这里有一些选择:

  1. 使用具体类intfloatcomplex——如PEP 488建议的
  2. 定义一个联合类型,像Union[float, Decimal, Fraction]
  3. 如果你不想硬编码具体类,使用数字协议如SupportsFloat

Iterable

上面引用的typing.List文档推荐SequenceIterable作为函数参数类型注解。

一个Iteralbe参数的例子出现在math.fsum函数:

def fsum(__seq: Iterable[float]) -> float:

下面是另一个使用Iterable参数的例子,产生是tuple[str, str]的项:

from collections.abc import Iterable

FromTo = tuple[str, str] # FromTo是类型别名,为了可读性

def zip_replace(text: str, changes: Iterable[FromTo]) -> str:  # changes需要是Iterable[FromTo],即Iterable[tuple[str, 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'
zip_replace(text, l33t)
'm4d sk1ll3d n00b p0wn3d l33t'

Python3.10中显示的类型别名(TypeAlias)
PEP613引入了一个特殊类型,TypeAlias使创建类型别名的分配更可读,更容易类型检查。

abc.Iterable vs abc.Sequence

math.fsumzip_replace必须迭代整个可迭代参数来返回一个结果。给定一个无限的可迭代对象,比如itertools.cycle生成器作为输入,这些函数会消耗所有的内存并导致Python进程崩溃。尽管存在这种潜在的危险,但现在在Python中,提供接受可迭代输入的函数是相当常见的,即使它们必须完全处理它才能返回结果。这使调用者可以选择提供输入数据作为生成器而不是一个预建的序列,如果输入项的数量很大,那么可能会节省大量的内存。

另一方面,上面例子中的columnize函数需要一个Sequence参数而不是Iterable,因为它必须调用输入的len()

SequenceIterable是最适合用于参数类型的。但作为返回类型太模糊了。一个函数应该更精确地描述它所返回的具体类型。

Iterable紧密关联的是Iterator类型,用于上面例子中的返回类型。

参数化泛型和TypeVar

一个参数化泛型(parameterized generic)是一种泛型类,写作list[T],其中T是类型变量,会绑定到具体类型。这允许在结果类型上反映一个参数类型。

下面定义了sample,一个函数接收两个参数:类型TSequence,和int。它返回同样T类型的元素列表。

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函数,返回序列中最常见的数据。

from statistics import mode
mode([1, 1, 2, 3, 3, 3, 3, 4])
3

如果没使用TypeVarmode可能会有下面这样的签名:

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的值都是intfloat,但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的可能类型。我们将在接下来的两个小节中看到两种这样做的方法。

受限的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:

这比之前那个好多了,然而statistics.mode的文档包含这样的例子:

>>> mode(["red", "blue", "blue", "red", "green", "red", "red"])
'red'

很快,我们就可以将str添加到NumberT的定义中来适配:

NumberT = TypeVar('NumberT', float, Decimal, Fraction, str)

这当然有效,但如果NumberT接受str,它的名字就严重错了。更重要的是,我们不能不断列出认识到的mode可以处理的新类型。我们可以利用TypeVar的另一个特性做得更好。

绑定的TypeVar

查看前面mode示例中的方法体,我们可以看到用Counter类进行排序的。Counter是基于dict的,因此可迭代的data的元素类型必须是可哈希的。

首先,这样的签名看起来可以工作:

from collections.abc import Iterable, Hashable

def mode(data: Iterable[Hashable]) -> Hashable:

但现在的问题是返回项的类型是Hashable:一个只实现了__hash__方法的抽象类。所以类型检查器除了允许让我们对返回值调用hash()方法外,不能调用其他任何方法。这非常不好用。

解决方法是使用另一个TypeVar的可选参数:bound关键字参数。它设定了可接受类型的上界。在下面的例子中,我们有bound=Hashable,这意味着类型参数可以是Hashable或它的任何子类。

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]

AnyStr预定义的类型变量

typing模块包括一个名为AnyStr的预定义TypeVar。它的定义是这样的:AnyStr=TypeVar("AnyStr",bytes,str)AnyStr用于许多接受字节或str的函数,并返回给定类型的值。

静态协议

Protocol类型,如PEP 544所描述,类似于Go中的接口:协议类型通过指定一个或多个方法来定义,并且类型检查器验证是否实现了协议类型需要的方法。

在Python中,协议定义被写成一个typing.Protocol的子类。但是,实现协议的类不需要继承、注册或声明与定义该协议的类的任何关系。由类型检查器来查找可用的协议类型并强制它们的使用。

这里有一个问题,可以通过ProtocolTypeVar来解决。假设你想要创建一个函数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']
>>>
>>> l2 = [(len(s), s) for s in l]
>>> l2
[(5, 'mango'), (4, 'pear'), (5, 'apple'), (4, 'kiwi'), (6, 'banana')]
>>> top(l2, 3)
[(6, 'banana'), (5, 'mango'), (5, 'apple')]

一个参数化的泛型top实现可能如下所示:

def top(series: Iterable[T], length: int) -> list[T]:
    ordered = sorted(series, reverse=True)
    return ordered[:length]

问题在于如何限制T,它不能是Anyobject,因为series必须能在sorted中工作。sorted内置项实际上接受Iterable[Any],但这是因为可选参数key接受一个函数,能从每个元素计算任意排序键。
如果你给sorted一个普通对象列表,但不提供key参数,会发生什么?让我们尝试一下:

l = [object() for _ in range(4)]
l
[<object at 0x2932d1e84b0>,
 <object at 0x2932d1e83e0>,
 <object at 0x2932d1e8410>,
 <object at 0x2932d1e83a0>]
sorted(l)
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

Cell In [13], line 1
----> 1 sorted(l)


TypeError: '<' not supported between instances of 'object' and 'object'

错误消息显示sorted在可迭代的元素上使用<操作符进行排序。这就够了吗?让我们来做另一个的实验:

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)]
l
[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__方法。

因此,实现中的T类型参数应该限制于实现__lt__的类型。在前面,我们需要一个实现__hash__的类型参数,所以我们能够使用类型Hashable为类型参数的上界。但是现在在typingabc中没有合适的类型来使用,所以我们需要创建它。先忙显示了新的SupportsLessThan,一个Protocol

from typing import Protocol, Any

class SupportsLessThan(Protocol):  # 协议是typing.Protocol的子类
    # 包含一个或多个方法定义,方法体中为 ...
    def __lt__(self, other: Any) -> bool: 
        ...

一个类型T与一个协议P一致,如果T实现了P中定义的所有方法,以及匹配的类型签名。

给定SupportsLessThan,我们就可以定义可工作版的top

from collections.abc import Iterable
from typing import TypeVar

LT = TypeVar('LT', bound=SupportsLessThan)

def top(series: Iterable[LT], length: int) -> list[LT]:
    ordered = sorted(series, reverse=True)
    return ordered[:length]

让我们测试驱动top,下面实现了测试代码。首先基于生成器表达式调用top,然后用object列表。基于object列表,期望一个TypeError异常。

from collections.abc import Iterator
from typing import TYPE_CHECKING  # TYPE_CHECKING在运行时为False,但类型检查器在类型检查时假装它为True

import pytest

from top import top

@pytest.mark.parametrize('series, length, expected', [
    ((1, 2, 3), 2, [3, 2]),
    ((1, 2, 3), 3, [3, 2, 1]),
    ((3, 3, 3), 1, [3]),
])
def test_top(
    series: tuple[float, ...],
    length: int,
    expected: list[float],
) -> None:
    result = top(series, length)
    assert expected == result

def test_top_tuples() -> None:
    fruit = 'mango pear apple kiwi banana'.split()
    series: Iterator[tuple[int, str]] = (  # series变量的显示类型定义
        (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不能在运行时调用,因为它不是一个常规函数,而是一个Mypy调试工具,不需要import
        reveal_type(expected)
        reveal_type(result)
    assert result == expected

# intentional type error
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)  # 该行会由Mppy标记为错误
    assert "'<' not supported" in str(excinfo.value)

前面的测试通过了——但是无论top.py中有没有类型提示都会通过。更重要的是,如果我用Mypy检查该测试文件,我将看到TypeVar正在按照预期进行工作。请参见mypy命令输出:

! mypy ./comparable/top_test.py
comparable\top_test.py:29: note: Revealed type is "typing.Iterator[Tuple[builtins.int, builtins.str]]"
comparable\top_test.py:30: note: Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]"
comparable\top_test.py:31: note: Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]"
comparable\top_test.py:38: note: Revealed type is "builtins.list[builtins.object]"
comparable\top_test.py:40: error: Value of type variable "LT" of "top" cannot be "object"
Found 1 error in 1 file (checked 1 source file)
comparable\top_test.py:29: note:
    Revealed type is "typing.Iterator[Tuple[builtins.int, builtins.str]]" # 在test_top_tuples中,reveal_type(series)返回Iterator[Tuple[int, str]]
comparable\top_test.py:30: note: 
    Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]"
comparable\top_test.py:31: note: 
    Revealed type is "builtins.list[Tuple[builtins.int, builtins.str]]" # reveal_type(result)确定top返回的类型是 list[tuple[int, str]]
comparable\top_test.py:38: note: 
    Revealed type is "builtins.list[builtins.object]" 
comparable\top_test.py:40: error: Value of type variable "LT" of "top" cannot be "object" # 检测到了故意触发的错误,series的元素类型必须是SupportLessThan
Found 1 error in 1 file (checked 1 source file)

在抽象类上的协议类型的关键优势是类型不需要任何特殊声明而可以与一个协议类型一致。这允许我们可以创建一个利用预先存在类型的协议。在需要使用SupportsLessThan时,我们不需要继承或注册strtuplefloatset等,只需要实现__lt__。而且类型检查器仍然能够完成它的工作,因为支持SupportsLessThan被显式地定义为一个协议——与鸭子类型中常见的隐式协议相反,后者对类型检查器是不可见的。

可调用

为了标注可回调参数或由高阶函数返回的可调用(callable)对象,collections.abc提供了Callable类型:

Callable[[ParamType1, ParamType2], ReturnType]

参数列表[ParamType1, ParamType2]可以有0个或多个类型。下面是repl函数的例子:

def repl(input_fn: Callable[[Any], str] = input]) -> None:

在使用时,repl函数使用Python的内建input读取用户的表达式。然而,但是,对于自动化测试或与其他输入源的集成,repl接收一个可选参数input_fn:一个具有同样参数和返回类型作为输入的可调用对象。

def input(__prompt: Any = ...) -> str: ...

input签名与Callable类型注解一致:

Callable[[Any], str]

没有可以用来注释可选参数类型或关键字参数类型的语法。typing.Callable文档说这些函数类型几乎捕获作为可回调类型。如果你需要一个类型注解来匹配具有灵活签名的函数,那么可以将整个参数列表用...替代:

Callable[..., ReturnType]

泛型类型参数与类型层次结构的交互引入了一个新的类型概念:variance。

可调用类型中的Variance

有一个温度控制系统,具有一个简单的update函数。它通过调用probe函数来获取当前的温度,然后调用display显示温度。probedisplay都作为参数传递。本示例的目的是对比两个Callable注解:一个有返回类型,另一个有参数类型。

from collections.abc import Callable

def update(  # update函数有两个可调用参数
        probe: Callable[[], float],  # probe必须是无参且返回float的可调用对象
        display: Callable[[float], None]  # display有一个float参数且返回None
    ) -> None:
    temperature = probe()
    # imagine lots of control code here
    display(temperature)

def probe_ok() -> int:  # probe_ok与Callable[[], float]一致,因为返回int并没有违反期望的float
    return 42

def display_wrong(temperature: int) -> None:  # display_wrong与Callable[[float], None]不一致,因为无法保证期望int的函数能处理float,比如hex函数期望int但拒绝float
    print(hex(temperature))

update(probe_ok, display_wrong)  # type error  # Mypy标记该行,因为上面的原因

def display_ok(temperature: complex) -> None:  # display_ok与Callable[[float], None]一致,因为接收complex的函数也能处理float
    print(temperature)

update(probe_ok, display_ok)  # OK  # Mypy不会报告错误

NoReturn

这是一个用于标记函数不会有返回的特殊类型。通常,它们的存在是为了抛出异常。在标准库中有几十个这样的函数。

比如sys.exit()抛出SystemExit异常来终止Python进程。

它在typeshed的签名如下:

def exit(__status: object = ...) -> NoReturn: ...

__status是一个仅位置参数,并且有默认值。Stub文件不会写出默认值,而使用...替代。__status的类型是object,这意味着可能为None

注解仅位置参数和可变参数

我们之前看到最后版本的tag如下:

def tag(name, /, *content, class_=None, **attrs):

下面完全注释的,用几行编写——这是长签名的常见约定,用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,因此函数中attrs的类型是dict[str, str]。而对于类型注解**attrs: float,函数中的attrs的类型会是dict[str, float]

  Python知识库 最新文章
Python中String模块
【Python】 14-CVS文件操作
python的panda库读写文件
使用Nordic的nrf52840实现蓝牙DFU过程
【Python学习记录】numpy数组用法整理
Python学习笔记
python字符串和列表
python如何从txt文件中解析出有效的数据
Python编程从入门到实践自学/3.1-3.2
python变量
上一篇文章      下一篇文章      查看所有文章
加:2022-11-05 00:24:53  更:2022-11-05 00:25:24 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 6:53:39-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码