Python v3.10


Python 基础教程

1. Python 解释器

调用解释器

Python 解释器在可用的机器上通常安装于 /usr/local/bin/python3.10 路径下;将 /usr/local/bin 加入你的 Unix 终端的搜索路径就可以通过键入以下命令来启动它:

python3.10

这样,就可以在 shell 中运行 Python 了。因为可以选择安装目录,解释器也有可能安装在别的位置;如果还不明白,就去问问身边的 Python 大神或系统管理员。(例如,常见备选路径还有 /usr/local/python。)

在 Windows 机器上当你从 Microsoft Store 安装 Python 之后,python3.10 命令将可使用。 如果你安装了 py.exe 启动器,你将可以使用 py 命令。

在主提示符中,输入文件结束符(Unix 里是 Control-D,Windows 里是 Control-Z),就会退出解释器,退出状态码为 0。如果不能退出,还可以输入这个命令:quit()

在支持 GNU Readline 库的系统中,解释器的行编辑功能包括交互式编辑、历史替换、代码补全等。检测是否支持命令行编辑最快速的方式是,在首次出现 Python 提示符时,输入 Control-P。听到“哔”提示音,说明支持行编辑;如果没有反应,或回显了 ^P,则说明不支持行编辑;只能用退格键删除当前行的字符。

解释器的操作方式类似 Unix Shell:用与 tty 设备关联的标准输入调用时,可以交互式地读取和执行命令;以文件名参数,或标准输入文件调用时,则读取并执行文件中的 脚本

启动解释器的另一种方式是 python -c command [arg] ...,这与 shell 的 -c 选项类似,其中,command 需换成要执行的语句。由于 Python 语句经常包含空格等被 shell 特殊对待的字符,一般情况下,建议用单引号标注整个 command

Python 模块也可以当作脚本使用。输入:python -m module [arg] ...,会执行 module 的源文件,这跟在命令行把路径写全了一样。

在交互模式下运行脚本文件,只要在脚本名称参数前,加上选项 -i 就可以了。

传入参数

解释器读取命令行参数,把脚本名与其他参数转化为字符串列表存到 sys 模块的 argv 变量里。执行 import sys,可以导入这个模块,并访问该列表。该列表最少有一个元素;未给定输入参数时,sys.argv[0] 是空字符串。给定脚本名是 '-' (标准输入)时,sys.argv[0]'-'。使用 -c command 时,sys.argv[0]'-c'。如果使用选项 -m module*,sys.argv[0] 就是包含目录的模块全名。解释器不处理 -c *command-m module 之后的选项,而是直接留在 sys.argv 中由命令或模块来处理。

交互模式

在终端(tty)输入并执行指令时,解释器在 交互模式(interactive mode) 中运行。在这种模式中,会显示 主提示符,提示输入下一条指令,主提示符通常用三个大于号(>>>)表示;输入连续行时,显示 次要提示符,默认是三个点(...)。进入解释器时,首先显示欢迎信息、版本信息、版权声明,然后才是提示符:

$ python3.10
Python 3.10 (default, June 4 2019, 09:25:04)
[GCC 4.8.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

输入多行架构的语句时,要用连续行。以 if 为例:

>>> the_world_is_flat = True
>>> if the_world_is_flat:
...     print("Be careful not to fall off!")
...
Be careful not to fall off!

解释器的运行环境

源文件的字符编码

默认情况下,Python 源码文件的编码是 UTF-8。这种编码支持世界上大多数语言的字符,可以用于字符串字面值、变量、函数名及注释 —— 尽管标准库只用常规的 ASCII 字符作为变量名或函数名,可移植代码都应遵守此约定。要正确显示这些字符,编辑器必须能识别 UTF-8 编码,而且必须使用支持文件中所有字符的字体。

如果不使用默认编码,则要声明文件的编码,文件的 第一 行要写成特殊注释。句法如下:

# -*- coding: encoding -*-

其中,encoding 可以是 Python 支持的任意一种 codecs

比如,声明使用 Windows-1252 编码,源码文件要写成:

# -*- coding: cp1252 -*-

第一行 的规则也有一种例外情况,源码以 UNIX “shebang” 行 开头。此时,编码声明要写在文件的第二行。例如:

#!/usr/bin/env python3
# -*- coding: cp1252 -*-

Unix 系统中,为了不与同时安装的 Python 2.x 冲突,Python 3.x 解释器默认安装的执行文件名不是 python

2. Python 速览

下面的例子以是否显示提示符(>>> 与 …)区分输入与输出:输入例子中的代码时,要键入以提示符开头的行中提示符后的所有内容;未以提示符开头的行是解释器的输出。注意,例子中的某行出现的第二个提示符是用来结束多行命令的,此时,要键入一个空白行。

本手册中的许多例子,甚至交互式命令都包含注释。Python 注释以 # 开头,直到该物理行结束。注释可以在行开头,或空白符与代码之后,但不能在字符串里面。字符串中的 # 号就是 # 号。注释用于阐明代码,Python 不解释注释,键入例子时,可以不输入注释。

示例如下:

# this is the first comment
spam = 1  # and this is the second comment
          # ... and now a third!
text = "# This is not a comment because it's inside quotes."

Python 用作计算器

现在,尝试一些简单的 Python 命令。启动解释器,等待主提示符(>>> )出现。

数字

解释器像一个简单的计算器:输入表达式,就会给出答案。表达式的语法很直接:运算符 +-*/ 的用法和其他大部分语言一样(比如,Pascal 或 C);括号 (()) 用来分组。例如:

>>> 2 + 2
4
>>> 50 - 5*6
20
>>> (50 - 5*6) / 4
5.0
>>> 8 / 5  # division always returns a floating point number
1.6

整数(如,2420 )的类型是 int,带小数(如,5.01.6 )的类型是 float。本教程后半部分将介绍更多数字类型。

除法运算(/)返回浮点数。用 // 运算符执行 floor division 的结果是整数(忽略小数);计算余数用 %

>>> 17 / 3  # classic division returns a float
5.666666666666667
>>>
>>> 17 // 3  # floor division discards the fractional part
5
>>> 17 % 3  # the % operator returns the remainder of the division
2
>>> 5 * 3 + 2  # floored quotient * divisor + remainder
17

Python 用 ** 运算符计算乘方:

>>> 5 ** 2  # 5 squared
25
>>> 2 ** 7  # 2 to the power of 7
128

等号(=)用于给变量赋值。赋值后,下一个交互提示符的位置不显示任何结果:

>>> width = 20
>>> height = 5 * 9
>>> width * height
900

如果变量未定义(即,未赋值),使用该变量会提示错误:

>>> n  # try to access an undefined variable
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'n' is not defined

Python 全面支持浮点数;混合类型运算数的运算会把整数转换为浮点数:

>>> 4 * 3.75 - 1
14.0

交互模式下,上次输出的表达式会赋给变量 _。把 Python 当作计算器时,用该变量实现下一步计算更简单,例如:

>>> tax = 12.5 / 100
>>> price = 100.50
>>> price * tax
12.5625
>>> price + _
113.0625
>>> round(_, 2)
113.06

最好把该变量当作只读类型。不要为它显式赋值,否则会创建一个同名独立局部变量,该变量会用它的魔法行为屏蔽内置变量。

除了 intfloat,Python 还支持其他数字类型,例如 DecimalFraction。Python 还内置支持 复数,后缀 jJ 用于表示虚数(例如 3+5j )。

字符串

除了数字,Python 还可以操作字符串。字符串有多种表现形式,用单引号('……')或双引号("……")标注的结果相同 2。反斜杠 \ 用于转义:

>>> 'spam eggs'  # single quotes
'spam eggs'
>>> 'doesn\'t'  # use \' to escape the single quote...
"doesn't"
>>> "doesn't"  # ...or use double quotes instead
"doesn't"
>>> '"Yes," they said.'
'"Yes," they said.'
>>> "\"Yes,\" they said."
'"Yes," they said.'
>>> '"Isn\'t," they said.'
'"Isn\'t," they said.'

交互式解释器会为输出的字符串加注引号,特殊字符使用反斜杠转义。虽然,有时输出的字符串看起来与输入的字符串不一样(外加的引号可能会改变),但两个字符串是相同的。如果字符串中有单引号而没有双引号,该字符串外将加注双引号,反之,则加注单引号。print() 函数输出的内容更简洁易读,它会省略两边的引号,并输出转义后的特殊字符:

>>> '"Isn\'t," they said.'
'"Isn\'t," they said.'
>>> print('"Isn\'t," they said.')
"Isn't," they said.
>>> s = 'First line.\nSecond line.'  # \n means newline
>>> s  # without print(), \n is included in the output
'First line.\nSecond line.'
>>> print(s)  # with print(), \n produces a new line
First line.
Second line.

如果不希望前置 \ 的字符转义成特殊字符,可以使用 原始字符串,在引号前添加 r 即可:

>>> print('C:\some\name')  # here \n means newline!
C:\some
ame
>>> print(r'C:\some\name')  # note the r before the quote
C:\some\name

字符串字面值可以实现跨行连续输入。实现方式是用三引号:"""..."""'''...''',字符串行尾会自动加上回车换行,如果不需要回车换行,在行尾添加 \ 即可。示例如下:

print("""\
Usage: thingy [OPTIONS]
     -h                        Display this usage message
     -H hostname               Hostname to connect to
""")

输出如下(注意,第一行没有换行):

Usage: thingy [OPTIONS]
     -h                        Display this usage message
     -H hostname               Hostname to connect to

字符串可以用 + 合并(粘到一起),也可以用 * 重复:

>>> # 3 times 'un', followed by 'ium'
>>> 3 * 'un' + 'ium'
'unununium'

相邻的两个或多个 字符串字面值 (引号标注的字符)会自动合并:

>>> 'Py' 'thon'
'Python'

拆分长字符串时,这个功能特别实用:

>>> text = ('Put several strings within parentheses '
...         'to have them joined together.')
>>> text
'Put several strings within parentheses to have them joined together.'

这项功能只能用于两个字面值,不能用于变量或表达式:

>>> prefix = 'Py'
>>> prefix 'thon'  # can't concatenate a variable and a string literal
  File "<stdin>", line 1
    prefix 'thon'
                ^
SyntaxError: invalid syntax
>>> ('un' * 3) 'ium'
  File "<stdin>", line 1
    ('un' * 3) 'ium'
                   ^
SyntaxError: invalid syntax

合并多个变量,或合并变量与字面值,要用 +

>>> prefix + 'thon'
'Python'

字符串支持 索引 (下标访问),第一个字符的索引是 0。单字符没有专用的类型,就是长度为一的字符串:

>>> word = 'Python'
>>> word[0]  # character in position 0
'P'
>>> word[5]  # character in position 5
'n'

索引还支持负数,用负数索引时,从右边开始计数:

>>> word[-1]  # last character
'n'
>>> word[-2]  # second-last character
'o'
>>> word[-6]
'P'

注意,-0 和 0 一样,因此,负数索引从 -1 开始。

除了索引,字符串还支持 切片。索引可以提取单个字符,切片 则提取子字符串:

>>> word[0:2]  # characters from position 0 (included) to 2 (excluded)
'Py'
>>> word[2:5]  # characters from position 2 (included) to 5 (excluded)
'tho'

切片索引的默认值很有用;省略开始索引时,默认值为 0,省略结束索引时,默认为到字符串的结尾:

>>> word[:2]   # character from the beginning to position 2 (excluded)
'Py'
>>> word[4:]   # characters from position 4 (included) to the end
'on'
>>> word[-2:]  # characters from the second-last (included) to the end
'on'

注意,输出结果包含切片开始,但不包含切片结束。因此,s[:i] + s[i:] 总是等于 s

>>> word[:2] + word[2:]
'Python'
>>> word[:4] + word[4:]
'Python'

还可以这样理解切片,索引指向的是字符 之间 ,第一个字符的左侧标为 0,最后一个字符的右侧标为 nn 是字符串长度。例如:

 +---+---+---+---+---+---+
 | P | y | t | h | o | n |
 +---+---+---+---+---+---+
 0   1   2   3   4   5   6
-6  -5  -4  -3  -2  -1

第一行数字是字符串中索引 0…6 的位置,第二行数字是对应的负数索引位置。ij 的切片由 ij 之间所有对应的字符组成。

对于使用非负索引的切片,如果两个索引都不越界,切片长度就是起止索引之差。例如, word[1:3] 的长度是 2。

索引越界会报错:

>>> word[42]  # the word only has 6 characters
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: string index out of range

但是,切片会自动处理越界索引:

>>> word[4:42]
'on'
>>> word[42:]
''

Python 字符串不能修改,是 immutable 的。因此,为字符串中某个索引位置赋值会报错:

>>> word[0] = 'J'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment
>>> word[2:] = 'py'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

要生成不同的字符串,应新建一个字符串:

>>> 'J' + word[1:]
'Jython'
>>> word[:2] + 'py'
'Pypy'

内置函数 len() 返回字符串的长度:

>>> s = 'supercalifragilisticexpialidocious'
>>> len(s)
34

列表

Python 支持多种 复合 数据类型,可将不同值组合在一起。最常用的 列表 ,是用方括号标注,逗号分隔的一组值。列表 可以包含不同类型的元素,但一般情况下,各个元素的类型相同:

>>> squares = [1, 4, 9, 16, 25]
>>> squares
[1, 4, 9, 16, 25]

和字符串(及其他内置 sequence 类型)一样,列表也支持索引和切片:

>>> squares[0]  # indexing returns the item
1
>>> squares[-1]
25
>>> squares[-3:]  # slicing returns a new list
[9, 16, 25]

切片操作返回包含请求元素的新列表。以下切片操作会返回列表的 浅拷贝:

>>> squares[:]
[1, 4, 9, 16, 25]

列表还支持合并操作:

>>> squares + [36, 49, 64, 81, 100]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

与 immutable 字符串不同, 列表是 mutable 类型,其内容可以改变:

>>> cubes = [1, 8, 27, 65, 125]  # something's wrong here
>>> 4 ** 3  # the cube of 4 is 64, not 65!
64
>>> cubes[3] = 64  # replace the wrong value
>>> cubes
[1, 8, 27, 64, 125]

append() 方法 可以在列表结尾添加新元素(详见后文):

>>> cubes.append(216)  # add the cube of 6
>>> cubes.append(7 ** 3)  # and the cube of 7
>>> cubes
[1, 8, 27, 64, 125, 216, 343]

为切片赋值可以改变列表大小,甚至清空整个列表:

>>> letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
>>> letters
['a', 'b', 'c', 'd', 'e', 'f', 'g']
>>> # replace some values
>>> letters[2:5] = ['C', 'D', 'E']
>>> letters
['a', 'b', 'C', 'D', 'E', 'f', 'g']
>>> # now remove them
>>> letters[2:5] = []
>>> letters
['a', 'b', 'f', 'g']
>>> # clear the list by replacing all the elements with an empty list
>>> letters[:] = []
>>> letters
[]

内置函数 len() 也支持列表:

>>> letters = ['a', 'b', 'c', 'd']
>>> len(letters)
4

还可以嵌套列表(创建包含其他列表的列表),例如:

>>> a = ['a', 'b', 'c']
>>> n = [1, 2, 3]
>>> x = [a, n]
>>> x
[['a', 'b', 'c'], [1, 2, 3]]
>>> x[0]
['a', 'b', 'c']
>>> x[0][1]
'b'

走向编程的第一步

当然,Python 还可以完成比二加二更复杂的任务。 例如,可以编写 斐波那契数列 的初始子序列,如下所示:

>>> # Fibonacci series:
... # the sum of two elements defines the next
... a, b = 0, 1
>>> while a < 10:
...     print(a)
...     a, b = b, a+b
...
0
1
1
2
3
5
8

本例引入了几个新功能。

  • 第一行中的 多重赋值:变量 ab 同时获得新值 0 和 1。最后一行又用了一次多重赋值,这体现在右表达式在赋值前就已经求值了。右表达式求值顺序为从左到右。

  • while 循环只要条件(这里指:a < 10)保持为真就会一直执行。Python 和 C 一样,任何非零整数都为真,零为假。这个条件也可以是字符串或列表的值,事实上,任何序列都可以;长度非零就为真,空序列则为假。示例中的判断只是最简单的比较。比较操作符的标准写法和 C 语言一样: < (小于)、 > (大于)、 == (等于)、 <= (小于等于)、 >= (大于等于)及 != (不等于)。

  • 循环体缩进的 :缩进是 Python 组织语句的方式。在交互式命令行里,得为每个缩输入制表符或空格。使用文本编辑器可以实现更复杂的输入方式;所有像样的文本编辑器都支持自动缩进。交互式输入复合语句时, 要在最后输入空白行表示结束(因为解析器不知道哪一行代码是最后一行)。注意,同一块语句的每一行的缩进相同。

  • print() 函数输出给定参数的值。与表达式不同(比如,之前计算器的例子),它能处理多个参数,包括浮点数与字符串。它输出的字符串不带引号,且各参数项之间会插入一个空格,这样可以实现更好的格式化操作:

    >>> i = 256*256
    >>> print('The value of i is', i)
    The value of i is 65536

    关键字参数 end 可以取消输出后面的换行, 或用另一个字符串结尾:

    >>> a, b = 0, 1
    >>> while a < 1000:
    ...     print(a, end=',')
    ...     a, b = b, a+b
    ...
    0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,

注:

  • **- 的优先级更高, 所以 -3**2 会被解释成 -(3**2) ,因此,结果是 -9。要避免这个问题,并且得到 9, 可以用 (-3)**2
  • 和其他语言不一样,特殊字符如 \n 在单引号('...')和双引号("...")里的意义一样。这两种引号唯一的区别是,不需要在单引号里转义双引号 ",但必须把单引号转义成 \',反之亦然。

3. 流程控制工具

if 语句

最让人耳熟能详的应该是 if 语句。例如:

>>> x = int(input("Please enter an integer: "))
Please enter an integer: 42
>>> if x < 0:
...     x = 0
...     print('Negative changed to zero')
... elif x == 0:
...     print('Zero')
... elif x == 1:
...     print('Single')
... else:
...     print('More')
...
More

if 语句包含零个或多个 elif 子句,及可选的 else 子句。关键字 ‘elif‘ 是 ‘else if’ 的缩写,适用于避免过多的缩进。可以把 ifelifelif … 序列看作是其他语言中 switchcase 语句的替代品。

如果你要将同一个值与多个常量进行比较,或是要检查特定类型或属性,你可能会发现 match 语句是很有用的。

for 语句

Python 的 for 语句与 C 或 Pascal 中的不同。Python 的 for 语句不迭代算术递增数值(如 Pascal),或是给予用户定义迭代步骤和暂停条件的能力(如 C),而是迭代列表或字符串等任意序列,元素的迭代顺序与在序列中出现的顺序一致。 例如:

>>> # Measure some strings:
... words = ['cat', 'window', 'defenestrate']
>>> for w in words:
...     print(w, len(w))
...
cat 3
window 6
defenestrate 12

遍历某个集合的同时修改该集合的内容,很难获取想要的结果。要在遍历时修改集合的内容,应该遍历该集合的副本或创建新的集合:

# Create a sample collection
users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}
# Strategy:  Iterate over a copy
for user, status in users.copy().items():
    if status == 'inactive':
        del users[user]
# Strategy:  Create a new collection
active_users = {}
for user, status in users.items():
    if status == 'active':
        active_users[user] = status

range() 函数

内置函数 range() 常用于遍历数字序列,该函数可以生成算术级数:

>>> for i in range(5):
...     print(i)
...
0
1
2
3
4

生成的序列不包含给定的终止数值;range(10) 生成 10 个值,这是一个长度为 10 的序列,其中的元素索引都是合法的。range 可以不从 0 开始,还可以按指定幅度递增(递增幅度称为 ‘步进’,支持负数):

>>> list(range(5, 10))
[5, 6, 7, 8, 9]
>>> list(range(0, 10, 3))
[0, 3, 6, 9]
>>> list(range(-10, -100, -30))
[-10, -40, -70]

range()len() 组合在一起,可以按索引迭代序列:

>>> a = ['Mary', 'had', 'a', 'little', 'lamb']
>>> for i in range(len(a)):
...     print(i, a[i])
...
0 Mary
1 had
2 a
3 little
4 lamb

不过,大多数情况下,enumerate() 函数更便捷 。

如果只输出 range,会出现意想不到的结果:

>>> range(10)
range(0, 10)

range() 返回对象的操作和列表很像,但其实这两种对象不是一回事。迭代时,该对象基于所需序列返回连续项,并没有生成真正的列表,从而节省了空间。

这种对象称为可迭代对象 iterable,函数或程序结构可通过该对象获取连续项,直到所有元素全部迭代完毕。for 语句就是这样的架构,sum() 是一种把可迭代对象作为参数的函数:

>>> sum(range(4))  # 0 + 1 + 2 + 3
6

下文将介绍更多返回可迭代对象或把可迭代对象当作参数的函数。

循环中的 breakcontinue 语句及 else 子句

break 语句和 C 中的类似,用于跳出最近的 forwhile 循环。

循环语句支持 else 子句;for 循环中,可迭代对象中的元素全部循环完毕时,或 while 循环的条件为假时,执行该子句;break 语句终止循环时,不执行该子句。 请看下面这个查找素数的循环示例:

>>> for n in range(2, 10):
...     for x in range(2, n):
...         if n % x == 0:
...             print(n, 'equals', x, '*', n//x)
...             break
...     else:
...         # loop fell through without finding a factor
...         print(n, 'is a prime number')
...
2 is a prime number
3 is a prime number
4 equals 2 * 2
5 is a prime number
6 equals 2 * 3
7 is a prime number
8 equals 2 * 4
9 equals 3 * 3

(没错,这段代码就是这么写。仔细看:else 子句属于 for 循环,不属于 if 语句。)

if 语句相比,循环的 else 子句更像 tryelse 子句: tryelse 子句在未触发异常时执行,循环的 else 子句则在未运行 break 时执行。

continue 语句也借鉴自 C 语言,表示继续执行循环的下一次迭代:

>>> for num in range(2, 10):
...     if num % 2 == 0:
...         print("Found an even number", num)
...         continue
...     print("Found an odd number", num)
...
Found an even number 2
Found an odd number 3
Found an even number 4
Found an odd number 5
Found an even number 6
Found an odd number 7
Found an even number 8
Found an odd number 9

pass 语句

pass 语句不执行任何操作。语法上需要一个语句,但程序不实际执行任何动作时,可以使用该语句。例如:

>>> while True:
...     pass  # Busy-wait for keyboard interrupt (Ctrl+C)
...

下面这段代码创建了一个最小的类:

>>> class MyEmptyClass:
...     pass
...

pass 还可以用作函数或条件子句的占位符,让开发者聚焦更抽象的层次。此时,程序直接忽略 pass

>>> def initlog(*args):
...     pass   # Remember to implement this!
...

match 语句

match 语句接受一个表达式并将它的值与以一个或多个 case 语句块形式给出的一系列模式进行比较。 这在表面上很类似 C, Java 或 JavaScript (以及许多其他语言) 中的 switch 语句,但它还能够从值中提取子部分 (序列元素或对象属性) 并赋值给变量。

最简单的形式是将一个目标值与一个或多个字面值进行比较:

def http_error(status):
    match status:
        case 400:
            return "Bad request"
        case 404:
            return "Not found"
        case 418:
            return "I'm a teapot"
        case _:
            return "Something's wrong with the internet"

请注意最后一个代码块: “变量名” _ 被作为 通配符 并必定会匹配成功。 如果没有任何 case 语句匹配成功,则任何分支都不会被执行。

你可以使用 | (“ or ”)在一个模式中组合几个字面值:

case 401 | 403 | 404:
    return "Not allowed"

模式的形式可以类似于解包赋值,并可被用于绑定变量:

# point is an (x, y) tuple
match point:
    case (0, 0):
        print("Origin")
    case (0, y):
        print(f"Y={y}")
    case (x, 0):
        print(f"X={x}")
    case (x, y):
        print(f"X={x}, Y={y}")
    case _:
        raise ValueError("Not a point")

请仔细研究此代码! 第一个模式有两个字面值,可以看作是上面所示字面值模式的扩展。 但接下来的两个模式结合了一个字面值和一个变量,而变量 绑定 了一个来自目标的值 (point)。 第四个模式捕获了两个值,这使得它在概念上类似于解包赋值 (x, y) = point

如果你使用类来结构化你的数据,你可以使用类名之后跟一个类似于构造器的参数列表,这样能够捕获属性放入到变量中:

class Point:
    x: int
    y: int
def where_is(point):
    match point:
        case Point(x=0, y=0):
            print("Origin")
        case Point(x=0, y=y):
            print(f"Y={y}")
        case Point(x=x, y=0):
            print(f"X={x}")
        case Point():
            print("Somewhere else")
        case _:
            print("Not a point")

你可以在某些为其属性提供了排序的内置类(例如 dataclass)中使用位置参数。 你也可以通过在你的类中设置 __match_args__ 特殊属性来为模式中的属性定义一个专门的位置。 如果它被设为 (“x”, “y”),则以下模式均为等价的(并且都是将 y 属性绑定到 var 变量):

Point(1, var)
Point(1, y=var)
Point(x=1, y=var)
Point(y=var, x=1)

读取模式的推荐方式是将它们看做是你会在赋值操作左侧放置的内容的扩展形式,以便理解各个变量将会被设置的值。 只有单独的名称 (例如上面的 var) 会被 match 语句所赋值。 带点号的名称 (例如 foo.bar)、属性名称 (例如上面的 x=y=) 或类名称 (通过其后的 “(…)” 来识别,例如上面的 Point) 都绝不会被赋值。

模式可以任意地嵌套。 例如,如果我们有一个由点组成的短列表,则可以这样匹配它:

match points:
    case []:
        print("No points")
    case [Point(0, 0)]:
        print("The origin")
    case [Point(x, y)]:
        print(f"Single point {x}, {y}")
    case [Point(0, y1), Point(0, y2)]:
        print(f"Two on the Y axis at {y1}, {y2}")
    case _:
        print("Something else")

我们可以向一个模式添加 if 子句,称为“守护项”。 如果守护项为假值,则 match 将继续尝试下一个 case 语句块。 请注意值的捕获发生在守护项被求值之前。:

match point:
    case Point(x, y) if x == y:
        print(f"Y=X at {x}")
    case Point(x, y):
        print(f"Not on the diagonal")

此语句的一些其他关键特性:

  • 类似于解包赋值,元组和列表模式具有完全相同的含义并且实际上能匹配任意序列。 一个重要的例外是它们不能匹配迭代器或字符串。

  • 序列模式支持扩展解包操作: [x, y, *rest](x, y, *rest) 的作用类似于解包赋值。 在 * 之后的名称也可以为 _,因此 (x, y, *_) 可以匹配包含至少两个条目的序列而不必绑定其余的条目。

  • 映射模式: {"bandwidth": b, "latency": l} 会从一个字典中捕获 "bandwidth""latency" 的值。 与序列模式不同,额外的键会被忽略。 解包操作例如 **rest 也受到支持。 (但 **_ 是冗余的,因而不被允许。)

  • 子模式可使用 as 关键字来捕获:

    case (Point(x1, y1), Point(x2, y2) as p2): ...

    将把输入的第二个元素捕获为 p2 (只要输入是包含两个点的序列)

  • 大多数字面值是按相等性比较的,但是单例对象 True, FalseNone 则是按标识号比较的。

  • 模式可以使用命名常量。 这些命名常量必须为带点号的名称以防止它们被解读为捕获变量:

    from enum import Enum
    class Color(Enum):
        RED = 0
        GREEN = 1
        BLUE = 2
    match color:
        case Color.RED:
            print("I see red!")
        case Color.GREEN:
            print("Grass is green")
        case Color.BLUE:
            print("I'm feeling the blues :(")

定义函数

下列代码创建一个可以输出限定数值内的斐波那契数列函数:

>>> def fib(n):    # write Fibonacci series up to n
...     """Print a Fibonacci series up to n."""
...     a, b = 0, 1
...     while a < n:
...         print(a, end=' ')
...         a, b = b, a+b
...     print()
...
>>> # Now call the function we just defined:
... fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

定义 函数使用关键字 def,后跟函数名与括号内的形参列表。函数语句从下一行开始,并且必须缩进。

函数内的第一条语句是字符串时,该字符串就是文档字符串,也称为 docstring。利用文档字符串可以自动生成在线文档或打印版文档,还可以让开发者在浏览代码时直接查阅文档;Python 开发者最好养成在代码中加入文档字符串的好习惯。

函数在 执行 时使用函数局部变量符号表,所有函数变量赋值都存在局部符号表中;引用变量时,首先,在局部符号表里查找变量,然后,是外层函数局部符号表,再是全局符号表,最后是内置名称符号表。因此,尽管可以引用全局变量和外层函数的变量,但最好不要在函数内直接赋值(除非是 global 语句定义的全局变量,或 nonlocal 语句定义的外层函数变量)。

在调用函数时会将实际参数(实参)引入到被调用函数的局部符号表中;因此,实参是使用 按值调用 来传递的(其中的 始终是对象的 引用 而不是对象的值)。 1 当一个函数调用另外一个函数时,会为该调用创建一个新的局部符号表。

函数定义在当前符号表中把函数名与函数对象关联在一起。解释器把函数名指向的对象作为用户自定义函数。还可以使用其他名称指向同一个函数对象,并访问访该函数:

>>> fib
<function fib at 10042ed0>
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89

fib 不返回值,因此,其他语言不把它当作函数,而是当作过程。事实上,没有 return 语句的函数也返回值,只不过这个值比较是 None (是一个内置名称)。一般来说,解释器不会输出单独的返回值 None ,如需查看该值,可以使用 print()

>>> fib(0)
>>> print(fib(0))
None

编写不直接输出斐波那契数列运算结果,而是返回运算结果列表的函数也非常简单:

>>> def fib2(n):  # return Fibonacci series up to n
...     """Return a list containing the Fibonacci series up to n."""
...     result = []
...     a, b = 0, 1
...     while a < n:
...         result.append(a)    # see below
...         a, b = b, a+b
...     return result
...
>>> f100 = fib2(100)    # call it
>>> f100                # write the result
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

本例也新引入了一些 Python 功能:

  • return 语句返回函数的值。return 语句不带表达式参数时,返回 None。函数执行完毕退出也返回 None
  • result.append(a) 语句调用了列表对象 result方法 。方法是“从属于”对象的函数,命名为 obj.methodnameobj 是对象(也可以是表达式),methodname 是对象类型定义的方法名。不同类型定义不同的方法,不同类型的方法名可以相同,且不会引起歧义。示例中的方法 append() 是为列表对象定义的,用于在列表末尾添加新元素。本例中,该方法相当于 result = result + [a] ,但更有效。

函数定义详解

函数定义支持可变数量的参数。这里列出三种可以组合使用的形式。

默认值参数

为参数指定默认值是非常有用的方式。调用函数时,可以使用比定义时更少的参数,例如:

def ask_ok(prompt, retries=4, reminder='Please try again!'):
    while True:
        ok = input(prompt)
        if ok in ('y', 'ye', 'yes'):
            return True
        if ok in ('n', 'no', 'nop', 'nope'):
            return False
        retries = retries - 1
        if retries < 0:
            raise ValueError('invalid user response')
        print(reminder)

该函数可以用以下方式调用:

  • 只给出必选实参:ask_ok('Do you really want to quit?')
  • 给出一个可选实参:ask_ok('OK to overwrite the file?', 2)
  • 给出所有实参:ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')

本例还使用了关键字 in ,用于确认序列中是否包含某个值。

默认值在 定义 作用域里的函数定义中求值,所以:

i = 5
def f(arg=i):
    print(arg)
i = 6
f()

上例输出的是 5

重要警告: 默认值只计算一次。默认值为列表、字典或类实例等可变对象时,会产生与该规则不同的结果。例如,下面的函数会累积后续调用时传递的参数:

def f(a, L=[]):
    L.append(a)
    return L
print(f(1))
print(f(2))
print(f(3))

输出结果如下:

[1]
[1, 2]
[1, 2, 3]

不想在后续调用之间共享默认值时,应以如下方式编写函数:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

关键字参数

kwarg=value 形式的 关键字参数 也可以用于调用函数。函数示例如下:

def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
    print("-- This parrot wouldn't", action, end=' ')
    print("if you put", voltage, "volts through it.")
    print("-- Lovely plumage, the", type)
    print("-- It's", state, "!")

该函数接受一个必选参数(voltage)和三个可选参数(state, actiontype)。该函数可用下列方式调用:

parrot(1000)                                          # 1 positional argument
parrot(voltage=1000)                                  # 1 keyword argument
parrot(voltage=1000000, action='VOOOOOM')             # 2 keyword arguments
parrot(action='VOOOOOM', voltage=1000000)             # 2 keyword arguments
parrot('a million', 'bereft of life', 'jump')         # 3 positional arguments
parrot('a thousand', state='pushing up the daisies')  # 1 positional, 1 keyword

以下调用函数的方式都无效:

parrot()                     # required argument missing
parrot(voltage=5.0, 'dead')  # non-keyword argument after a keyword argument
parrot(110, voltage=220)     # duplicate value for the same argument
parrot(actor='John Cleese')  # unknown keyword argument

函数调用时,关键字参数必须跟在位置参数后面。所有传递的关键字参数都必须匹配一个函数接受的参数(比如,actor 不是函数 parrot 的有效参数),关键字参数的顺序并不重要。这也包括必选参数,(比如,parrot(voltage=1000) 也有效)。不能对同一个参数多次赋值,下面就是一个因此限制而失败的例子:

>>> def function(a):
...     pass
...
>>> function(0, a=0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: function() got multiple values for argument 'a'

最后一个形参为 **name 形式时,接收一个字典,该字典包含与函数中已定义形参对应之外的所有关键字参数。**name 形参可以与 *name 形参组合使用(*name 必须在 **name 前面), *name 形参接收一个 元组,该元组包含形参列表之外的位置参数。例如,可以定义下面这样的函数:

def cheeseshop(kind, *arguments, **keywords):
    print("-- Do you have any", kind, "?")
    print("-- I'm sorry, we're all out of", kind)
    for arg in arguments:
        print(arg)
    print("-" * 40)
    for kw in keywords:
        print(kw, ":", keywords[kw])

该函数可以用如下方式调用:

cheeseshop("Limburger", "It's very runny, sir.",
           "It's really very, VERY runny, sir.",
           shopkeeper="Michael Palin",
           client="John Cleese",
           sketch="Cheese Shop Sketch")

输出结果如下:

-- Do you have any Limburger ?
-- I'm sorry, we're all out of Limburger
It's very runny, sir.
It's really very, VERY runny, sir.
----------------------------------------
shopkeeper : Michael Palin
client : John Cleese
sketch : Cheese Shop Sketch

注意,关键字参数在输出结果中的顺序与调用函数时的顺序一致。

特殊参数

默认情况下,参数可以按位置或显式关键字传递给 Python 函数。为了让代码易读、高效,最好限制参数的传递方式,这样,开发者只需查看函数定义,即可确定参数项是仅按位置、按位置或关键字,还是仅按关键字传递。

函数定义如下:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        Positional or keyword   |
        |                                - Keyword only
         -- Positional only

/* 是可选的。这些符号表明形参如何把参数值传递给函数:位置、位置或关键字、关键字。关键字形参也叫作命名形参。

位置或关键字参数

函数定义中未使用 /* 时,参数可以按位置或关键字传递给函数。

仅位置参数

此处再介绍一些细节,特定形参可以标记为 仅限位置仅限位置 时,形参的顺序很重要,且这些形参不能用关键字传递。仅限位置形参应放在 / (正斜杠)前。/ 用于在逻辑上分割仅限位置形参与其它形参。如果函数定义中没有 /,则表示没有仅限位置形参。

/ 后可以是 位置或关键字仅限关键字 形参。

仅限关键字参数

把形参标记为 仅限关键字*,表明必须以关键字参数形式传递该形参,应在参数列表中第一个 *仅限关键字 形参前添加 *

函数示例

请看下面的函数定义示例,注意 /* 标记:

>>> def standard_arg(arg):
...     print(arg)
...
>>> def pos_only_arg(arg, /):
...     print(arg)
...
>>> def kwd_only_arg(*, arg):
...     print(arg)
...
>>> def combined_example(pos_only, /, standard, *, kwd_only):
...     print(pos_only, standard, kwd_only)

第一个函数定义 standard_arg 是最常见的形式,对调用方式没有任何限制,可以按位置也可以按关键字传递参数:

>>> standard_arg(2)
2
>>> standard_arg(arg=2)
2

第二个函数 pos_only_arg 的函数定义中有 /,仅限使用位置形参:

>>> pos_only_arg(1)
1
>>> pos_only_arg(arg=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: pos_only_arg() got some positional-only arguments passed as keyword arguments: 'arg'

第三个函数 kwd_only_args 的函数定义通过 * 表明仅限关键字参数:

>>> kwd_only_arg(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given
>>> kwd_only_arg(arg=3)
3

最后一个函数在同一个函数定义中,使用了全部三种调用惯例:

>>> combined_example(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() takes 2 positional arguments but 3 were given
>>> combined_example(1, 2, kwd_only=3)
1 2 3
>>> combined_example(1, standard=2, kwd_only=3)
1 2 3
>>> combined_example(pos_only=1, standard=2, kwd_only=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() got some positional-only arguments passed as keyword arguments: 'pos_only'

下面的函数定义中,kwdsname 当作键,因此,可能与位置参数 name 产生潜在冲突:

def foo(name, **kwds):
    return 'name' in kwds

调用该函数不可能返回 True,因为关键字 'name' 总与第一个形参绑定。例如:

>>> foo(1, **{'name': 2})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'name'
>>>

加上 / (仅限位置参数)后,就可以了。此时,函数定义把 name 当作位置参数,'name' 也可以作为关键字参数的键:

def foo(name, /, **kwds):
    return 'name' in kwds
>>> foo(1, **{'name': 2})
True

换句话说,仅限位置形参的名称可以在 **kwds 中使用,而不产生歧义。

小结

以下用例决定哪些形参可以用于函数定义:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):

说明:

  • 使用仅限位置形参,可以让用户无法使用形参名。形参名没有实际意义时,强制调用函数的实参顺序时,或同时接收位置形参和关键字时,这种方式很有用。
  • 当形参名有实际意义,且显式名称可以让函数定义更易理解时,阻止用户依赖传递实参的位置时,才使用关键字。
  • 对于 API,使用仅限位置形参,可以防止未来修改形参名时造成破坏性的 API 变动。

任意实参列表

调用函数时,使用任意数量的实参是最少见的选项。这些实参包含在元组中。在可变数量的实参之前,可能有若干个普通参数:

def write_multiple_items(file, separator, *args):
    file.write(separator.join(args))

variadic 实参用于采集传递给函数的所有剩余实参,因此,它们通常在形参列表的末尾。*args 形参后的任何形式参数只能是仅限关键字参数,即只能用作关键字参数,不能用作位置参数:

>>> def concat(*args, sep="/"):
...     return sep.join(args)
...
>>> concat("earth", "mars", "venus")
'earth/mars/venus'
>>> concat("earth", "mars", "venus", sep=".")
'earth.mars.venus'

解包实参列表

函数调用要求独立的位置参数,但实参在列表或元组里时,要执行相反的操作。例如,内置的 range() 函数要求独立的 startstop 实参。如果这些参数不是独立的,则要在调用函数时,用 * 操作符把实参从列表或元组解包出来:

>>> list(range(3, 6))            # normal call with separate arguments
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args))            # call with arguments unpacked from a list
[3, 4, 5]

同样,字典可以用 ** 操作符传递关键字参数:

>>> def parrot(voltage, state='a stiff', action='voom'):
...     print("-- This parrot wouldn't", action, end=' ')
...     print("if you put", voltage, "volts through it.", end=' ')
...     print("E's", state, "!")
...
>>> d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
>>> parrot(**d)
-- This parrot wouldn't VOOM if you put four million volts through it. E's bleedin' demised !

Lambda 表达式

lambda 关键字用于创建小巧的匿名函数。lambda a, b: a+b 函数返回两个参数的和。Lambda 函数可用于任何需要函数对象的地方。在语法上,匿名函数只能是单个表达式。在语义上,它只是常规函数定义的语法糖。与嵌套函数定义一样,lambda 函数可以引用包含作用域中的变量:

>>> def make_incrementor(n):
...     return lambda x: x + n
...
>>> f = make_incrementor(42)
>>> f(0)
42
>>> f(1)
43

上例用 lambda 表达式返回函数。还可以把匿名函数用作传递的实参:

>>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort(key=lambda pair: pair[1])
>>> pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

文档字符串

以下是文档字符串内容和格式的约定。

第一行应为对象用途的简短摘要。为保持简洁,不要在这里显式说明对象名或类型,因为可通过其他方式获取这些信息(除非该名称碰巧是描述函数操作的动词)。这一行应以大写字母开头,以句点结尾。

文档字符串为多行时,第二行应为空白行,在视觉上将摘要与其余描述分开。后面的行可包含若干段落,描述对象的调用约定、副作用等。

Python 解析器不会删除 Python 中多行字符串字面值的缩进,因此,文档处理工具应在必要时删除缩进。这项操作遵循以下约定:文档字符串第一行 之后 的第一个非空行决定了整个文档字符串的缩进量(第一行通常与字符串开头的引号相邻,其缩进在字符串中并不明显,因此,不能用第一行的缩进),然后,删除字符串中所有行开头处与此缩进“等价”的空白符。不能有比此缩进更少的行,但如果出现了缩进更少的行,应删除这些行的所有前导空白符。转化制表符后(通常为 8 个空格),应测试空白符的等效性。

下面是多行文档字符串的一个例子:

>>> def my_function():
...     """Do nothing, but document it.
...
...     No, really, it doesn't do anything.
...     """
...     pass
...
>>> print(my_function.__doc__)
Do nothing, but document it.
    No, really, it doesn't do anything.

函数注解

函数注解 是可选的用户自定义函数类型的元数据完整信息。

标注 以字典的形式存放在函数的 __annotations__ 属性中,并且不会影响函数的任何其他部分。 形参标注的定义方式是在形参名后加冒号,后面跟一个表达式,该表达式会被求值为标注的值。 返回值标注的定义方式是加组合符号 ->,后面跟一个表达式,该标注位于形参列表和表示 def 语句结束的冒号之间。 下面的示例有一个必须的参数,一个可选的关键字参数以及返回值都带有相应的标注:

>>> def f(ham: str, eggs: str = 'eggs') -> str:
...     print("Annotations:", f.__annotations__)
...     print("Arguments:", ham, eggs)
...     return ham + ' and ' + eggs
...
>>> f('spam')
Annotations: {'ham': <class 'str'>, 'return': <class 'str'>, 'eggs': <class 'str'>}
Arguments: spam eggs
'spam and eggs'

编码风格

现在你将要写更长,更复杂的 Python 代码,是时候讨论一下 代码风格 了。 大多数语言都能以不同的风格被编写(或更准确地说,被格式化);有些比其他的更具有可读性。 能让其他人轻松阅读你的代码总是一个好主意,采用一种好的编码风格对此有很大帮助。

Python 项目大多都遵循 PEP 8 的风格指南;它推行的编码风格易于阅读、赏心悦目。Python 开发者均应抽时间悉心研读;以下是该提案中的核心要点:

  • 缩进,用 4 个空格,不要用制表符。

    4 个空格是小缩进(更深嵌套)和大缩进(更易阅读)之间的折中方案。制表符会引起混乱,最好别用。

  • 换行,一行不超过 79 个字符。

    这样换行的小屏阅读体验更好,还便于在大屏显示器上并排阅读多个代码文件。

  • 用空行分隔函数和类,及函数内较大的代码块。

  • 最好把注释放到单独一行。

  • 使用文档字符串。

  • 运算符前后、逗号后要用空格,但不要直接在括号内使用: a = f(1, 2) + g(3, 4)

  • 类和函数的命名要一致;按惯例,命名类用 UpperCamelCase,命名函数与方法用 lowercase_with_underscores。命名方法中第一个参数总是用 self

  • 编写用于国际多语环境的代码时,不要用生僻的编码。Python 默认的 UTF-8 或纯 ASCII 可以胜任各种情况。

  • 同理,就算多语阅读、维护代码的可能再小,也不要在标识符中使用非 ASCII 字符。

4. 数据结构

列表详解

列表数据类型支持很多方法,列表对象的所有方法所示如下:

list.append(x)

在列表末尾添加一个元素,相当于 a[len(a):] = [x]

list.extend(iterable)

用可迭代对象的元素扩展列表。相当于 a[len(a):] = iterable

list.insert(i, x)

在指定位置插入元素。第一个参数是插入元素的索引,因此,a.insert(0, x) 在列表开头插入元素, a.insert(len(a), x) 等同于 a.append(x)

list.remove(x)

从列表中删除第一个值为 x 的元素。未找到指定元素时,触发 ValueError 异常。

list.pop([i])

删除列表中指定位置的元素,并返回被删除的元素。未指定位置时,a.pop() 删除并返回列表的最后一个元素。(方法签名中 i 两边的方括号表示该参数是可选的,不是要求输入方括号。这种表示法常见于 Python 参考库)。

list.clear()

删除列表里的所有元素,相当于 del a[:]

list.index(x[, start[, end]])

返回列表中第一个值为 x 的元素的零基索引。未找到指定元素时,触发 ValueError 异常。

可选参数 startend 是切片符号,用于将搜索限制为列表的特定子序列。返回的索引是相对于整个序列的开始计算的,而不是 start 参数。

list.count(x)

返回列表中元素 x 出现的次数。

list.sort(**, key=None, reverse=False*)

就地排序列表中的元素(要了解自定义排序参数,详见 sorted())。

list.reverse()

翻转列表中的元素。

list.copy()

返回列表的浅拷贝。相当于 a[:]

多数列表方法示例:

>>> fruits = ['orange', 'apple', 'pear', 'banana', 'kiwi', 'apple', 'banana']
>>> fruits.count('apple')
2
>>> fruits.count('tangerine')
0
>>> fruits.index('banana')
3
>>> fruits.index('banana', 4)  # Find next banana starting a position 4
6
>>> fruits.reverse()
>>> fruits
['banana', 'apple', 'kiwi', 'banana', 'pear', 'apple', 'orange']
>>> fruits.append('grape')
>>> fruits
['banana', 'apple', 'kiwi', 'banana', 'pear', 'apple', 'orange', 'grape']
>>> fruits.sort()
>>> fruits
['apple', 'apple', 'banana', 'banana', 'grape', 'kiwi', 'orange', 'pear']
>>> fruits.pop()
'pear'

insertremovesort 等方法只修改列表,不输出返回值——返回的默认值为 None 。这是所有 Python 可变数据结构的设计原则。

还有,不是所有数据都可以排序或比较。例如,[None, 'hello', 10] 就不可排序,因为整数不能与字符串对比,而 None 不能与其他类型对比。有些类型根本就没有定义顺序关系,例如,3+4j < 5+7j 这种对比操作就是无效的。

用列表实现堆栈

使用列表方法实现堆栈非常容易,最后插入的最先取出(“后进先出”)。把元素添加到堆栈的顶端,使用 append() 。从堆栈顶部取出元素,使用 pop() ,不用指定索引。例如:

>>> stack = [3, 4, 5]
>>> stack.append(6)
>>> stack.append(7)
>>> stack
[3, 4, 5, 6, 7]
>>> stack.pop()
7
>>> stack
[3, 4, 5, 6]
>>> stack.pop()
6
>>> stack.pop()
5
>>> stack
[3, 4]

用列表实现队列

列表也可以用作队列,最先加入的元素,最先取出(“先进先出”);然而,列表作为队列的效率很低。因为,在列表末尾添加和删除元素非常快,但在列表开头插入或移除元素却很慢(因为所有其他元素都必须移动一位)。

实现队列最好用 collections.deque,可以快速从两端添加或删除元素。例如:

>>> from collections import deque
>>> queue = deque(["Eric", "John", "Michael"])
>>> queue.append("Terry")           # Terry arrives
>>> queue.append("Graham")          # Graham arrives
>>> queue.popleft()                 # The first to arrive now leaves
'Eric'
>>> queue.popleft()                 # The second to arrive now leaves
'John'
>>> queue                           # Remaining queue in order of arrival
deque(['Michael', 'Terry', 'Graham'])

列表推导式

列表推导式创建列表的方式更简洁。常见的用法为,对序列或可迭代对象中的每个元素应用某种操作,用生成的结果创建新的列表;或用满足特定条件的元素创建子序列。

例如,创建平方值的列表:

>>> squares = []
>>> for x in range(10):
...     squares.append(x**2)
...
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

注意,这段代码创建(或覆盖)变量 x,该变量在循环结束后仍然存在。下述方法可以无副作用地计算平方列表:

squares = list(map(lambda x: x**2, range(10)))

或等价于:

squares = [x**2 for x in range(10)]

上面这种写法更简洁、易读。

列表推导式的方括号内包含以下内容:一个表达式,后面为一个 for 子句,然后,是零个或多个 forif 子句。结果是由表达式依据 forif 子句求值计算而得出一个新列表。 举例来说,以下列表推导式将两个列表中不相等的元素组合起来:

>>> [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

等价于:

>>> combs = []
>>> for x in [1,2,3]:
...     for y in [3,1,4]:
...         if x != y:
...             combs.append((x, y))
...
>>> combs
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

注意,上面两段代码中,forif 的顺序相同。

表达式是元组(例如上例的 (x, y))时,必须加上括号:

>>> vec = [-4, -2, 0, 2, 4]
>>> # create a new list with the values doubled
>>> [x*2 for x in vec]
[-8, -4, 0, 4, 8]
>>> # filter the list to exclude negative numbers
>>> [x for x in vec if x >= 0]
[0, 2, 4]
>>> # apply a function to all the elements
>>> [abs(x) for x in vec]
[4, 2, 0, 2, 4]
>>> # call a method on each element
>>> freshfruit = ['  banana', '  loganberry ', 'passion fruit  ']
>>> [weapon.strip() for weapon in freshfruit]
['banana', 'loganberry', 'passion fruit']
>>> # create a list of 2-tuples like (number, square)
>>> [(x, x**2) for x in range(6)]
[(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (5, 25)]
>>> # the tuple must be parenthesized, otherwise an error is raised
>>> [x, x**2 for x in range(6)]
  File "<stdin>", line 1, in <module>
    [x, x**2 for x in range(6)]
               ^
SyntaxError: invalid syntax
>>> # flatten a list using a listcomp with two 'for'
>>> vec = [[1,2,3], [4,5,6], [7,8,9]]
>>> [num for elem in vec for num in elem]
[1, 2, 3, 4, 5, 6, 7, 8, 9]

列表推导式可以使用复杂的表达式和嵌套函数:

>>> from math import pi
>>> [str(round(pi, i)) for i in range(1, 6)]
['3.1', '3.14', '3.142', '3.1416', '3.14159']

嵌套的列表推导式

列表推导式中的初始表达式可以是任何表达式,甚至可以是另一个列表推导式。

下面这个 3x4 矩阵,由 3 个长度为 4 的列表组成:

>>> matrix = [
...     [1, 2, 3, 4],
...     [5, 6, 7, 8],
...     [9, 10, 11, 12],
... ]

下面的列表推导式可以转置行列:

>>> [[row[i] for row in matrix] for i in range(4)]
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

如上节所示,嵌套的列表推导式基于其后的 for 求值,所以这个例子等价于:

>>> transposed = []
>>> for i in range(4):
...     transposed.append([row[i] for row in matrix])
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

反过来说,也等价于:

>>> transposed = []
>>> for i in range(4):
...     # the following 3 lines implement the nested listcomp
...     transposed_row = []
...     for row in matrix:
...         transposed_row.append(row[i])
...     transposed.append(transposed_row)
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

实际应用中,最好用内置函数替代复杂的流程语句。此时,zip() 函数更好用:

>>> list(zip(*matrix))
[(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]

del 语句

del 语句按索引,而不是值从列表中移除元素。与返回值的 pop() 方法不同, del 语句也可以从列表中移除切片,或清空整个列表(之前是将空列表赋值给切片)。 例如:

>>> a = [-1, 1, 66.25, 333, 333, 1234.5]
>>> del a[0]
>>> a
[1, 66.25, 333, 333, 1234.5]
>>> del a[2:4]
>>> a
[1, 66.25, 1234.5]
>>> del a[:]
>>> a
[]

del 也可以用来删除整个变量:

>>> del a

此后,再引用 a 就会报错(直到为它赋与另一个值)。后文会介绍 del 的其他用法。

元组和序列

列表和字符串有很多共性,例如,索引和切片操作。这两种数据类型是 序列 (参见 序列类型 —- list, tuple, range)。随着 Python 语言的发展,其他的序列类型也被加入其中。本节介绍另一种标准序列类型:元组

元组由多个用逗号隔开的值组成,例如:

>>> t = 12345, 54321, 'hello!'
>>> t[0]
12345
>>> t
(12345, 54321, 'hello!')
>>> # Tuples may be nested:
... u = t, (1, 2, 3, 4, 5)
>>> u
((12345, 54321, 'hello!'), (1, 2, 3, 4, 5))
>>> # Tuples are immutable:
... t[0] = 88888
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> # but they can contain mutable objects:
... v = ([1, 2, 3], [3, 2, 1])
>>> v
([1, 2, 3], [3, 2, 1])

输出时,元组都要由圆括号标注,这样才能正确地解释嵌套元组。输入时,圆括号可有可无,不过经常是必须的(如果元组是更大的表达式的一部分)。不允许为元组中的单个元素赋值,当然,可以创建含列表等可变对象的元组。

虽然,元组与列表很像,但使用场景不同,用途也不同。元组是 immutable (不可变的),一般可包含异质元素序列,通过解包(见本节下文)或索引访问(如果是 namedtuples,可以属性访问)。列表是 mutable (可变的),列表元素一般为同质类型,可迭代访问。

构造 0 个或 1 个元素的元组比较特殊:为了适应这种情况,对句法有一些额外的改变。用一对空圆括号就可以创建空元组;只有一个元素的元组可以通过在这个元素后添加逗号来构建(圆括号里只有一个值的话不够明确)。丑陋,但是有效。例如:

>>> empty = ()
>>> singleton = 'hello',    # <-- note trailing comma
>>> len(empty)
0
>>> len(singleton)
1
>>> singleton
('hello',)

语句 t = 12345, 54321, 'hello!'元组打包 的例子:值 12345, 54321'hello!' 一起被打包进元组。逆操作也可以:

>>> x, y, z = t

称之为 序列解包 也是妥妥的,适用于右侧的任何序列。序列解包时,左侧变量与右侧序列元素的数量应相等。注意,多重赋值其实只是元组打包和序列解包的组合。

集合

Python 还支持 集合 这种数据类型。集合是由不重复元素组成的无序容器。基本用法包括成员检测、消除重复元素。集合对象支持合集、交集、差集、对称差分等数学运算。

创建集合用花括号或 set() 函数。注意,创建空集合只能用 set(),不能用 {}{} 创建的是空字典。

以下是一些简单的示例

>>> basket = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'}
>>> print(basket)                      # show that duplicates have been removed
{'orange', 'banana', 'pear', 'apple'}
>>> 'orange' in basket                 # fast membership testing
True
>>> 'crabgrass' in basket
False
>>> # Demonstrate set operations on unique letters from two words
...
>>> a = set('abracadabra')
>>> b = set('alacazam')
>>> a                                  # unique letters in a
{'a', 'r', 'b', 'c', 'd'}
>>> a - b                              # letters in a but not in b
{'r', 'd', 'b'}
>>> a | b                              # letters in a or b or both
{'a', 'c', 'r', 'd', 'b', 'm', 'z', 'l'}
>>> a & b                              # letters in both a and b
{'a', 'c'}
>>> a ^ b                              # letters in a or b but not both
{'r', 'd', 'b', 'm', 'z', 'l'}

与 列表推导式 类似,集合也支持推导式:

>>> a = {x for x in 'abracadabra' if x not in 'abc'}
>>> a
{'r', 'd'}

字典

字典 也是一种常用的 Python 內置数据类型。其他语言可能把字典称为 联合内存联合数组*。与以连续整数为索引的序列不同,字典以 *关键字 为索引,关键字通常是字符串或数字,也可以是其他任意不可变类型。只包含字符串、数字、元组的元组,也可以用作关键字。但如果元组直接或间接地包含了可变对象,就不能用作关键字。列表不能当关键字,因为列表可以用索引、切片、append()extend() 等方法修改。

可以把字典理解为 键值对 的集合,但字典的键必须是唯一的。花括号 {} 用于创建空字典。另一种初始化字典的方式是,在花括号里输入逗号分隔的键值对,这也是字典的输出方式。

字典的主要用途是通过关键字存储、提取值。用 del 可以删除键值对。用已存在的关键字存储值,与该关键字关联的旧值会被取代。通过不存在的键提取值,则会报错。

对字典执行 list(d) 操作,返回该字典中所有键的列表,按插入次序排列(如需排序,请使用 sorted(d))。检查字典里是否存在某个键,使用关键字 in

以下是一些字典的简单示例:

>>> tel = {'jack': 4098, 'sape': 4139}
>>> tel['guido'] = 4127
>>> tel
{'jack': 4098, 'sape': 4139, 'guido': 4127}
>>> tel['jack']
4098
>>> del tel['sape']
>>> tel['irv'] = 4127
>>> tel
{'jack': 4098, 'guido': 4127, 'irv': 4127}
>>> list(tel)
['jack', 'guido', 'irv']
>>> sorted(tel)
['guido', 'irv', 'jack']
>>> 'guido' in tel
True
>>> 'jack' not in tel
False

dict() 构造函数可以直接用键值对序列创建字典:

>>> dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])
{'sape': 4139, 'guido': 4127, 'jack': 4098}

字典推导式可以用任意键值表达式创建字典:

>>> {x: x**2 for x in (2, 4, 6)}
{2: 4, 4: 16, 6: 36}

关键字是比较简单的字符串时,直接用关键字参数指定键值对更便捷:

>>> dict(sape=4139, guido=4127, jack=4098)
{'sape': 4139, 'guido': 4127, 'jack': 4098}

循环的技巧

在字典中循环时,用 items() 方法可同时取出键和对应的值:

>>> knights = {'gallahad': 'the pure', 'robin': 'the brave'}
>>> for k, v in knights.items():
...     print(k, v)
...
gallahad the pure
robin the brave

在序列中循环时,用 enumerate() 函数可以同时取出位置索引和对应的值:

>>> for i, v in enumerate(['tic', 'tac', 'toe']):
...     print(i, v)
...
0 tic
1 tac
2 toe

同时循环两个或多个序列时,用 zip() 函数可以将其内的元素一一匹配:

>>> questions = ['name', 'quest', 'favorite color']
>>> answers = ['lancelot', 'the holy grail', 'blue']
>>> for q, a in zip(questions, answers):
...     print('What is your {0}?  It is {1}.'.format(q, a))
...
What is your name?  It is lancelot.
What is your quest?  It is the holy grail.
What is your favorite color?  It is blue.

逆向循环序列时,先正向定位序列,然后调用 reversed() 函数:

>>> for i in reversed(range(1, 10, 2)):
...     print(i)
...
9
7
5
3
1

按指定顺序循环序列,可以用 sorted() 函数,在不改动原序列的基础上,返回一个重新的序列:

>>> basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana']
>>> for i in sorted(basket):
...     print(i)
...
apple
apple
banana
orange
orange
pear

使用 set() 去除序列中的重复元素。使用 sorted()set() 则按排序后的顺序,循环遍历序列中的唯一元素:

>>> basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana']
>>> for f in sorted(set(basket)):
...     print(f)
...
apple
banana
orange
pear

一般来说,在循环中修改列表的内容时,创建新列表比较简单,且安全:

>>> import math
>>> raw_data = [56.2, float('NaN'), 51.7, 55.3, 52.5, float('NaN'), 47.8]
>>> filtered_data = []
>>> for value in raw_data:
...     if not math.isnan(value):
...         filtered_data.append(value)
...
>>> filtered_data
[56.2, 51.7, 55.3, 52.5, 47.8]

深入条件控制

whileif 条件句不只可以进行比较,还可以使用任意运算符。

比较运算符 innot in 校验序列里是否存在某个值。运算符 isis not 比较两个对象是否为同一个对象。所有比较运算符的优先级都一样,且低于数值运算符。

比较操作支持链式操作。例如,a < b == c 校验 a 是否小于 b,且 b 是否等于 c

比较操作可以用布尔运算符 andor 组合,并且,比较操作(或其他布尔运算)的结果都可以用 not 取反。这些操作符的优先级低于比较操作符;not 的优先级最高, or 的优先级最低,因此,A and not B or C 等价于 (A and (not B)) or C。与其他运算符操作一样,此处也可以用圆括号表示想要的组合。

布尔运算符 andor 也称为 短路 运算符:其参数从左至右解析,一旦可以确定结果,解析就会停止。例如,如果 AC 为真,B 为假,那么 A and B and C 不会解析 C。用作普通值而不是布尔值时,短路操作符返回的值通常是最后一个变量。

还可以把比较操作或逻辑表达式的结果赋值给变量,例如:

>>> string1, string2, string3 = '', 'Trondheim', 'Hammer Dance'
>>> non_null = string1 or string2 or string3
>>> non_null
'Trondheim'

注意,Python 与 C 不同,在表达式内部赋值必须显式使用 海象运算符 :=。 这避免了 C 程序中常见的问题:要在表达式中写 == 时,却写成了 =

序列和其他类型的比较

序列对象可以与相同序列类型的其他对象比较。这种比较使用 字典式 顺序:首先,比较前两个对应元素,如果不相等,则可确定比较结果;如果相等,则比较之后的两个元素,以此类推,直到其中一个序列结束。如果要比较的两个元素本身是相同类型的序列,则递归地执行字典式顺序比较。如果两个序列中所有的对应元素都相等,则两个序列相等。如果一个序列是另一个的初始子序列,则较短的序列可被视为较小(较少)的序列。 对于字符串来说,字典式顺序使用 Unicode 码位序号排序单个字符。下面列出了一些比较相同类型序列的例子:

(1, 2, 3)              < (1, 2, 4)
[1, 2, 3]              < [1, 2, 4]
'ABC' < 'C' < 'Pascal' < 'Python'
(1, 2, 3, 4)           < (1, 2, 4)
(1, 2)                 < (1, 2, -1)
(1, 2, 3)             == (1.0, 2.0, 3.0)
(1, 2, ('aa', 'ab'))   < (1, 2, ('abc', 'a'), 4)

注意,对不同类型的对象来说,只要待比较的对象提供了合适的比较方法,就可以使用 <> 进行比较。例如,混合数值类型通过数值进行比较,所以,0 等于 0.0,等等。否则,解释器不会随便给出一个对比结果,而是触发 TypeError 异常。

注:

  • 别的语言可能会返回可变对象,允许方法连续执行,例如,d->insert("a")->remove("b")->sort();

5. 模块

退出 Python 解释器后,再次进入时,之前在 Python 解释器中定义的函数和变量就丢失了。因此,编写较长程序时,建议用文本编辑器代替解释器,执行文件中的输入内容,这就是编写 脚本 。随着程序越来越长,为了方便维护,最好把脚本拆分成多个文件。编写脚本还一个好处,不同程序调用同一个函数时,不用每次把函数复制到各个程序。

为实现这些需求,Python 把各种定义存入一个文件,在脚本或解释器的交互式实例中使用。这个文件就是 模块 ;模块中的定义可以 导入 到其他模块或 模块(在顶层和计算器模式下,执行脚本中可访问的变量集)。

模块是包含 Python 定义和语句的文件。其文件名是模块名加后缀名 .py 。在模块内部,通过全局变量 __name__ 可以获取模块名(即字符串)。例如,用文本编辑器在当前目录下创建 fibo.py 文件,输入以下内容:

# Fibonacci numbers module
def fib(n):    # write Fibonacci series up to n
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a+b
    print()
def fib2(n):   # return Fibonacci series up to n
    result = []
    a, b = 0, 1
    while a < n:
        result.append(a)
        a, b = b, a+b
    return result

现在,进入 Python 解释器,用以下命令导入该模块:

>>> import fibo

这项操作不直接把 fibo 函数定义的名称导入到当前符号表,只导入模块名 fibo 。要使用模块名访问函数:

>>> fibo.fib(1000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
>>> fibo.fib2(100)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
>>> fibo.__name__
'fibo'

如果经常使用某个函数,可以把它赋值给局部变量:

>>> fib = fibo.fib
>>> fib(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

模块详解

模块包含可执行语句及函数定义。这些语句用于初始化模块,且仅在 import 语句 第一次 遇到模块名时执行。1 (文件作为脚本运行时,也会执行这些语句。)

模块有自己的私有符号表,用作模块中所有函数的全局符号表。因此,在模块内使用全局变量时,不用担心与用户定义的全局变量发生冲突。另一方面,可以用与访问模块函数一样的标记法,访问模块的全局变量,modname.itemname

可以把其他模块导入模块。按惯例,所有 import 语句都放在模块(或脚本)开头,但这不是必须的。导入的模块名存在导入模块的全局符号表里。

import 语句有一个变体,可以直接把模块里的名称导入到另一个模块的符号表。例如:

>>> from fibo import fib, fib2
>>> fib(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

这段代码不会把模块名导入到局部符号表里(因此,本例没有定义 fibo)。

还有一种变体可以导入模块内定义的所有名称:

>>> from fibo import *
>>> fib(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

这种方式会导入所有不以下划线(_)开头的名称。大多数情况下,不要用这个功能,这种方式向解释器导入了一批未知的名称,可能会覆盖已经定义的名称。

注意,一般情况下,不建议从模块或包内导入 *, 因为,这项操作经常让代码变得难以理解。不过,为了在交互式编译器中少打几个字,这么用也没问题。

模块名后使用 as 时,直接把 as 后的名称与导入模块绑定。

>>> import fibo as fib
>>> fib.fib(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

import fibo 一样,这种方式也可以有效地导入模块,唯一的区别是,导入的名称是 fib

from 中也可以使用这种方式,效果类似:

>>> from fibo import fib as fibonacci
>>> fibonacci(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

注解

为了保证运行效率,每次解释器会话只导入一次模块。如果更改了模块内容,必须重启解释器;仅交互测试一个模块时,也可以使用 importlib.reload(),例如 import importlib; importlib.reload(modulename)

以脚本方式执行模块

可以用以下方式运行 Python 模块:

python fibo.py <arguments>

这项操作将执行模块里的代码,和导入模块一样,但会把 __name__ 赋值为 "__main__"。 也就是把下列代码添加到模块末尾:

if __name__ == "__main__":
    import sys
    fib(int(sys.argv[1]))

既可以把这个文件当脚本使用,也可以用作导入的模块, 因为,解析命令行的代码只有在模块以 “main” 文件执行时才会运行:

$ python fibo.py 500 
1 1 2 3 5 8 13 21 34

导入模块时,不运行这些代码:

>>> import fibo
>>>

这种操作常用于为模块提供便捷用户接口,或用于测试(把模块当作执行测试套件的脚本运行)。

模块搜索路径

导入 spam 模块时,解释器首先查找名为 spam 的内置模块。如果没找到,解释器再从 sys.path 变量中的目录列表里查找 spam.py 文件。sys.path 初始化时包含以下位置:

  • 输入脚本的目录(或未指定文件时的当前目录)。
  • PYTHONPATH (目录列表,与 shell 变量 PATH 的语法一样)。
  • The installation-dependent default (by convention including a site-packages directory, handled by the site module).

注解

在支持 symlink 的文件系统中,输入脚本目录是在追加 symlink 后计算出来的。换句话说,包含 symlink 的目录并 没有 添加至模块搜索路径。

初始化后,Python 程序可以更改 sys.path。运行脚本的目录在标准库路径之前,置于搜索路径的开头。即,加载的是该目录里的脚本,而不是标准库的同名模块。 除非刻意替换,否则会报错。

“已编译的” Python 文件

为了快速加载模块,Python 把模块的编译版缓存在 __pycache__ 目录中,文件名为 module.*version*.pyc,version 对编译文件格式进行编码,一般是 Python 的版本号。例如,CPython 的 3.3 发行版中,spam.py 的编译版本缓存为 __pycache__/spam.cpython-33.pyc。使用这种命名惯例,可以让不同 Python 发行版及不同版本的已编译模块共存。

Python 对比编译版本与源码的修改日期,查看它是否已过期,是否要重新编译,此过程完全自动化。此外,编译模块与平台无关,因此,可在不同架构系统之间共享相同的支持库。

Python 在两种情况下不检查缓存。其一,从命令行直接载入模块,只重新编译,不存储编译结果;其二,没有源模块,就不会检查缓存。为了支持无源文件(仅编译)发行版本, 编译模块必须在源目录下,并且绝不能有源模块。

给专业人士的一些小建议:

  • 在 Python 命令中使用 -O-OO 开关,可以减小编译模块的大小。-O 去除断言语句,-OO 去除断言语句和 doc 字符串。有些程序可能依赖于这些内容,因此,没有十足的把握,不要使用这两个选项。“优化过的”模块带有 opt- 标签,并且文件通常会一小些。将来的发行版或许会改进优化的效果。
  • .pyc 文件读取的程序不比从 .py 读取的执行速度快,.pyc 文件只是加载速度更快。
  • compileall 模块可以为一个目录下的所有模块创建 .pyc 文件。
  • 本过程的细节及决策流程图,详见 PEP 3147

标准模块

Python 自带一个标准模块的库,它在 Python 库参考(此处以下称为”库参考” )里另外描述。 一些模块是内嵌到编译器里面的, 它们给一些虽并非语言核心但却内嵌的操作提供接口,要么是为了效率,要么是给操作系统基础操作例如系统调入提供接口。 这些模块集是一个配置选项, 并且还依赖于底层的操作系统。 例如,winreg 模块只在 Windows 系统上提供。一个特别值得注意的模块 sys,它被内嵌到每一个 Python 编译器中。sys.ps1sys.ps2 变量定义了一些字符,它们可以用作主提示符和辅助提示符:

>>> import sys
>>> sys.ps1
'>>> '
>>> sys.ps2
'... '
>>> sys.ps1 = 'C> '
C> print('Yuck!')
Yuck!
C>

只有解释器用于交互模式时,才定义这两个变量。

变量 sys.path 是字符串列表,用于确定解释器的模块搜索路径。该变量以环境变量 PYTHONPATH 提取的默认路径进行初始化,如未设置 PYTHONPATH,则使用内置的默认路径。可以用标准列表操作修改该变量:

>>> import sys
>>> sys.path.append('/ufs/guido/lib/python')

dir() 函数

内置函数 dir() 用于查找模块定义的名称。返回结果是经过排序的字符串列表:

>>> import fibo, sys
>>> dir(fibo)
['__name__', 'fib', 'fib2']
>>> dir(sys)  
['__breakpointhook__', '__displayhook__', '__doc__', '__excepthook__',
 '__interactivehook__', '__loader__', '__name__', '__package__', '__spec__',
 '__stderr__', '__stdin__', '__stdout__', '__unraisablehook__',
 '_clear_type_cache', '_current_frames', '_debugmallocstats', '_framework',
 '_getframe', '_git', '_home', '_xoptions', 'abiflags', 'addaudithook',
 'api_version', 'argv', 'audit', 'base_exec_prefix', 'base_prefix',
 'breakpointhook', 'builtin_module_names', 'byteorder', 'call_tracing',
 'callstats', 'copyright', 'displayhook', 'dont_write_bytecode', 'exc_info',
 'excepthook', 'exec_prefix', 'executable', 'exit', 'flags', 'float_info',
 'float_repr_style', 'get_asyncgen_hooks', 'get_coroutine_origin_tracking_depth',
 'getallocatedblocks', 'getdefaultencoding', 'getdlopenflags',
 'getfilesystemencodeerrors', 'getfilesystemencoding', 'getprofile',
 'getrecursionlimit', 'getrefcount', 'getsizeof', 'getswitchinterval',
 'gettrace', 'hash_info', 'hexversion', 'implementation', 'int_info',
 'intern', 'is_finalizing', 'last_traceback', 'last_type', 'last_value',
 'maxsize', 'maxunicode', 'meta_path', 'modules', 'path', 'path_hooks',
 'path_importer_cache', 'platform', 'prefix', 'ps1', 'ps2', 'pycache_prefix',
 'set_asyncgen_hooks', 'set_coroutine_origin_tracking_depth', 'setdlopenflags',
 'setprofile', 'setrecursionlimit', 'setswitchinterval', 'settrace', 'stderr',
 'stdin', 'stdout', 'thread_info', 'unraisablehook', 'version', 'version_info',
 'warnoptions']

没有参数时,dir() 列出当前定义的名称:

>>> a = [1, 2, 3, 4, 5]
>>> import fibo
>>> fib = fibo.fib
>>> dir()
['__builtins__', '__name__', 'a', 'fib', 'fibo', 'sys']

注意,该函数列出所有类型的名称:变量、模块、函数等。

dir() 不会列出内置函数和变量的名称。这些内容的定义在标准模块 builtins 里:

>>> import builtins
>>> dir(builtins)  
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning',
 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError',
 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning',
 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False',
 'FileExistsError', 'FileNotFoundError', 'FloatingPointError',
 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError',
 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError',
 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError',
 'MemoryError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented',
 'NotImplementedError', 'OSError', 'OverflowError',
 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError',
 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning',
 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError',
 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError',
 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError',
 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning',
 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__',
 '__debug__', '__doc__', '__import__', '__name__', '__package__', 'abs',
 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable',
 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits',
 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit',
 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr',
 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass',
 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview',
 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property',
 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice',
 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars',
 'zip']

包是一种用“点式模块名”构造 Python 模块命名空间的方法。例如,模块名 A.B 表示包 A 中名为 B 的子模块。正如模块可以区分不同模块之间的全局变量名称一样,点式模块名可以区分 NumPy 或 Pillow 等不同多模块包之间的模块名称。

假设要为统一处理声音文件与声音数据设计一个模块集(“包”)。声音文件的格式很多(通常以扩展名来识别,例如:.wav.aiff.au),因此,为了不同文件格式之间的转换,需要创建和维护一个不断增长的模块集合。为了实现对声音数据的不同处理(例如,混声、添加回声、均衡器功能、创造人工立体声效果),还要编写无穷无尽的模块流。下面这个分级文件树展示了这个包的架构:

sound/                          Top-level package
      __init__.py               Initialize the sound package
      formats/                  Subpackage for file format conversions
              __init__.py
              wavread.py
              wavwrite.py
              aiffread.py
              aiffwrite.py
              auread.py
              auwrite.py
              ...
      effects/                  Subpackage for sound effects
              __init__.py
              echo.py
              surround.py
              reverse.py
              ...
      filters/                  Subpackage for filters
              __init__.py
              equalizer.py
              vocoder.py
              karaoke.py
              ...

导入包时,Python 搜索 sys.path 里的目录,查找包的子目录。

Python 只把含 __init__.py 文件的目录当成包。这样可以防止以 string 等通用名称命名的目录,无意中屏蔽出现在后方模块搜索路径中的有效模块。 最简情况下,__init__.py 只是一个空文件,但该文件也可以执行包的初始化代码,或设置 __all__ 变量,详见下文。

还可以从包中导入单个模块,例如:

import sound.effects.echo

这段代码加载子模块 sound.effects.echo ,但引用时必须使用子模块的全名:

sound.effects.echo.echofilter(input, output, delay=0.7, atten=4)

另一种导入子模块的方法是 :

from sound.effects import echo

这段代码还可以加载子模块 echo ,不加包前缀也可以使用。因此,可以按如下方式使用:

echo.echofilter(input, output, delay=0.7, atten=4)

Import 语句的另一种变体是直接导入所需的函数或变量:

from sound.effects.echo import echofilter

同样,这样也会加载子模块 echo,但可以直接使用函数 echofilter()

echofilter(input, output, delay=0.7, atten=4)

注意,使用 from package import item 时,item 可以是包的子模块(或子包),也可以是包中定义的函数、类或变量等其他名称。import 语句首先测试包中是否定义了 item;如果未在包中定义,则假定 item 是模块,并尝试加载。如果找不到 item,则触发 ImportError 异常。

相反,使用 import item.subitem.subsubitem 句法时,除最后一项外,每个 item 都必须是包;最后一项可以是模块或包,但不能是上一项中定义的类、函数或变量。

从包中导入 *

使用 from sound.effects import * 时会发生什么?理想情况下,该语句在文件系统查找并导入包的所有子模块。这项操作花费的时间较长,并且导入子模块可能会产生不必要的副作用,这种副作用只有在显式导入子模块时才会发生。

唯一的解决方案是提供包的显式索引。import 语句使用如下惯例:如果包的 __init__.py 代码定义了列表 __all__,运行 from package import * 时,它就是用于导入的模块名列表。发布包的新版本时,包的作者应更新此列表。如果包的作者认为没有必要在包中执行导入 * 操作,也可以不提供此列表。例如,sound/effects/__init__.py 文件包含以下代码:

__all__ = ["echo", "surround", "reverse"]

即,from sound.effects import * 将导入 sound 包中的这三个命名子模块。

如果没有定义 __all__from sound.effects import * 语句 不会 把包 sound.effects 中所有子模块都导入到当前命名空间;该语句只确保导入包 sound.effects (可能还会运行 __init__.py 中的初始化代码),然后,再导入包中定义的名称。这些名称包括 __init__.py 中定义的任何名称(以及显式加载的子模块),还包括之前 import 语句显式加载的包里的子模块。请看以下代码:

import sound.effects.echo
import sound.effects.surround
from sound.effects import *

本例中,执行 from...import 语句时,将把 echosurround 模块导入至当前命名空间,因为,它们是在 sound.effects 包里定义的。(该导入操作在定义了 __all__ 时也有效。)

虽然,可以把模块设计为用 import * 时只导出遵循指定模式的名称,但仍不提倡在生产代码中使用这种做法。

记住,使用 from package import specific_submodule 没有任何问题! 实际上,除了导入模块使用不同包的同名子模块之外,这种方式是推荐用法。

子包参考

包中含有多个子包时(与示例中的 sound 包一样),可以使用绝对导入引用兄弟包中的子模块。例如,要在模块 sound.filters.vocoder 中使用 sound.effects 包的 echo 模块时,可以用 from sound.effects import echo 导入。

还可以用 import 语句的 from module import name 形式执行相对导入。这些导入语句使用前导句点表示相对导入中的当前包和父包。例如,相对于 surround 模块,可以使用:

from . import echo
from .. import formats
from ..filters import equalizer

注意,相对导入基于当前模块名。因为主模块名是 "__main__" ,所以 Python 程序的主模块必须始终使用绝对导入。

多目录中的包

包还支持特殊属性 __path__。该属性初始化为在包的 __init__.py 文件中的代码执行前所在的目录名列表。这个变量可以修改,但这样做会影响将来搜索包中模块和子包的操作。

这个功能虽然不常用,但可用于扩展包中的模块集。

注:

  • 实际上,函数定义也是“可执行”的“语句”;执行模块级函数定义时,函数名将被导入到模块的全局符号表。

6. 输入与输出

更复杂的输出格式

至此,我们已学习了两种写入值的方法:表达式语句print() 函数。第三种方法是使用文件对象的 write() 方法;标准输出文件称为 sys.stdout。详见标准库参考。

对输出格式的控制不只是打印空格分隔的值,还需要更多方式。格式化输出包括以下几种方法。

  • 使用 格式化字符串字面值 ,要在字符串开头的引号/三引号前添加 fF 。在这种字符串中,可以在 {} 字符之间输入引用的变量,或字面值的 Python 表达式。

    >>> year = 2016
    >>> event = 'Referendum'
    >>> f'Results of the {year} {event}'
    'Results of the 2016 Referendum'
  • 字符串的 str.format() 方法需要更多手动操作。该方法也用 {} 标记替换变量的位置,虽然这种方法支持详细的格式化指令,但需要提供格式化信息。

    >>> yes_votes = 42_572_654
    >>> no_votes = 43_132_495
    >>> percentage = yes_votes / (yes_votes + no_votes)
    >>> '{:-9} YES votes  {:2.2%}'.format(yes_votes, percentage)
    ' 42572654 YES votes  49.67%'
  • 最后,还可以用字符串切片和合并操作完成字符串处理操作,创建任何排版布局。字符串类型还支持将字符串按给定列宽进行填充,这些方法也很有用。

如果不需要花哨的输出,只想快速显示变量进行调试,可以用 repr()str() 函数把值转化为字符串。

str() 函数返回供人阅读的值,repr() 则生成适于解释器读取的值(如果没有等效的语法,则强制执行 SyntaxError)。对于没有支持供人阅读展示结果的对象, str() 返回与 repr() 相同的值。一般情况下,数字、列表或字典等结构的值,使用这两个函数输出的表现形式是一样的。字符串有两种不同的表现形式。

示例如下:

>>> s = 'Hello, world.'
>>> str(s)
'Hello, world.'
>>> repr(s)
"'Hello, world.'"
>>> str(1/7)
'0.14285714285714285'
>>> x = 10 * 3.25
>>> y = 200 * 200
>>> s = 'The value of x is ' + repr(x) + ', and y is ' + repr(y) + '...'
>>> print(s)
The value of x is 32.5, and y is 40000...
>>> # The repr() of a string adds string quotes and backslashes:
... hello = 'hello, world\n'
>>> hellos = repr(hello)
>>> print(hellos)
'hello, world\n'
>>> # The argument to repr() may be any Python object:
... repr((x, y, ('spam', 'eggs')))
"(32.5, 40000, ('spam', 'eggs'))"

string 模块包含 Template 类,提供了将值替换为字符串的另一种方法。该类使用 $x 占位符,并用字典的值进行替换,但对格式控制的支持比较有限。

格式化字符串字面值

格式化字符串字面值 (简称为 f-字符串)在字符串前加前缀 fF,通过 {expression} 表达式,把 Python 表达式的值添加到字符串内。

格式说明符是可选的,写在表达式后面,可以更好地控制格式化值的方式。下例将 pi 舍入到小数点后三位:

>>> import math
>>> print(f'The value of pi is approximately {math.pi:.3f}.')
The value of pi is approximately 3.142.

':' 后传递整数,为该字段设置最小字符宽度,常用于列对齐:

>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 7678}
>>> for name, phone in table.items():
...     print(f'{name:10} ==> {phone:10d}')
...
Sjoerd     ==>       4127
Jack       ==>       4098
Dcab       ==>       7678

还有一些修饰符可以在格式化前转换值。 '!a' 应用 ascii()'!s' 应用 str()'!r' 应用 repr()

>>> animals = 'eels'
>>> print(f'My hovercraft is full of {animals}.')
My hovercraft is full of eels.
>>> print(f'My hovercraft is full of {animals!r}.')
My hovercraft is full of 'eels'.

字符串 format() 方法

str.format() 方法的基本用法如下所示:

>>> print('We are the {} who say "{}!"'.format('knights', 'Ni'))
We are the knights who say "Ni!"

花括号及之内的字符(称为格式字段)被替换为传递给 str.format() 方法的对象。花括号中的数字表示传递给 str.format() 方法的对象所在的位置。

>>> print('{0} and {1}'.format('spam', 'eggs'))
spam and eggs
>>> print('{1} and {0}'.format('spam', 'eggs'))
eggs and spam

str.format() 方法中使用关键字参数名引用值。

>>> print('This {food} is {adjective}.'.format(
...       food='spam', adjective='absolutely horrible'))
This spam is absolutely horrible.

位置参数和关键字参数可以任意组合:

>>> print('The story of {0}, {1}, and {other}.'.format('Bill', 'Manfred',
                                                       other='Georg'))
The story of Bill, Manfred, and Georg.

如果不想分拆较长的格式字符串,最好按名称引用变量进行格式化,不要按位置。这项操作可以通过传递字典,并用方括号 '[]' 访问键来完成。

>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
>>> print('Jack: {0[Jack]:d}; Sjoerd: {0[Sjoerd]:d}; '
...       'Dcab: {0[Dcab]:d}'.format(table))
Jack: 4098; Sjoerd: 4127; Dcab: 8637678

也可以用 ‘**‘ 符号,把 table 当作传递的关键字参数。

>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
>>> print('Jack: {Jack:d}; Sjoerd: {Sjoerd:d}; Dcab: {Dcab:d}'.format(**table))
Jack: 4098; Sjoerd: 4127; Dcab: 8637678

与内置函数 vars() 结合使用时,这种方式非常实用,可以返回包含所有局部变量的字典。

例如,下面的代码生成一组整齐的列,包含给定整数及其平方与立方:

>>> for x in range(1, 11):
...     print('{0:2d} {1:3d} {2:4d}'.format(x, x*x, x*x*x))
...
 1   1    1
 2   4    8
 3   9   27
 4  16   64
 5  25  125
 6  36  216
 7  49  343
 8  64  512
 9  81  729
10 100 1000

手动格式化字符串

下面是使用手动格式化方式实现的同一个平方和立方的表:

>>> for x in range(1, 11):
...     print(repr(x).rjust(2), repr(x*x).rjust(3), end=' ')
...     # Note use of 'end' on previous line
...     print(repr(x*x*x).rjust(4))
...
 1   1    1
 2   4    8
 3   9   27
 4  16   64
 5  25  125
 6  36  216
 7  49  343
 8  64  512
 9  81  729
10 100 1000

(注意,每列之间的空格是通过使用 print() 添加的:它总在其参数间添加空格。)

字符串对象的 str.rjust() 方法通过在左侧填充空格,对给定宽度字段中的字符串进行右对齐。同类方法还有 str.ljust()str.center() 。这些方法不写入任何内容,只返回一个新字符串,如果输入的字符串太长,它们不会截断字符串,而是原样返回;虽然这种方式会弄乱列布局,但也比另一种方法好,后者在显示值时可能不准确(如果真的想截断字符串,可以使用 x.ljust(n)[:n] 这样的切片操作 。)

另一种方法是 str.zfill() ,该方法在数字字符串左边填充零,且能识别正负号:

>>> '12'.zfill(5)
'00012'
>>> '-3.14'.zfill(7)
'-003.14'
>>> '3.14159265359'.zfill(5)
'3.14159265359'

旧式字符串格式化方法

% 运算符(求余符)也可用于字符串格式化。给定 'string' % values,则 string 中的 % 实例会以零个或多个 values 元素替换。此操作被称为字符串插值。例如:

>>> import math
>>> print('The value of pi is approximately %5.3f.' % math.pi)
The value of pi is approximately 3.142.

读写文件

open() 返回 file object,最常用的参数有两个: open(filename, mode)

>>> f = open('workfile', 'w')

第一个实参是文件名字符串。第二个实参是包含描述文件使用方式字符的字符串。mode 的值包括 'r' ,表示文件只能读取;'w' 表示只能写入(现有同名文件会被覆盖);'a' 表示打开文件并追加内容,任何写入的数据会自动添加到文件末尾。'r+' 表示打开文件进行读写。mode 实参是可选的,省略时的默认值为 'r'

通常,文件以 text mode 打开,即,从文件中读取或写入字符串时,都以指定编码方式进行编码。如未指定编码格式,默认值与平台相关 。在 mode 中追加的 'b' 则以 binary mode 打开文件:此时,数据以字节对象的形式进行读写。该模式用于所有不包含文本的文件。

在文本模式下读取文件时,默认把平台特定的行结束符(Unix 上为 \n, Windows 上为 \r\n)转换为 \n。在文本模式下写入数据时,默认把 \n 转换回平台特定结束符。这种操作方式在后台修改文件数据对文本文件来说没有问题,但会破坏 JPEGEXE 等二进制文件中的数据。注意,在读写此类文件时,一定要使用二进制模式。

在处理文件对象时,最好使用 with 关键字。优点是,子句体结束后,文件会正确关闭,即便触发异常也可以。而且,使用 with 相比等效的 try-finally 代码块要简短得多:

>>> with open('workfile') as f:
...     read_data = f.read()
>>> # We can check that the file has been automatically closed.
>>> f.closed
True

如果没有使用 with 关键字,则应调用 f.close() 关闭文件,即可释放文件占用的系统资源。

警告:

调用 f.write() 时,未使用 with 关键字,或未调用 f.close(),即使程序正常退出,也可能 导致 f.write() 的参数没有完全写入磁盘。

通过 with 语句,或调用 f.close() 关闭文件对象后,再次使用该文件对象将会失败。

>>> f.close()
>>> f.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.

文件对象的方法

本节下文中的例子假定已创建 f 文件对象。

f.read(size) 可用于读取文件内容,它会读取一些数据,并返回字符串(文本模式),或字节串对象(在二进制模式下)。 size 是可选的数值参数。省略 sizesize 为负数时,读取并返回整个文件的内容;文件大小是内存的两倍时,会出现问题。size 取其他值时,读取并返回最多 size 个字符(文本模式)或 size 个字节(二进制模式)。如已到达文件末尾,f.read() 返回空字符串('')。

>>> f.read()
'This is the entire file.\n'
>>> f.read()
''

f.readline() 从文件中读取单行数据;字符串末尾保留换行符(\n),只有在文件不以换行符结尾时,文件的最后一行才会省略换行符。这种方式让返回值清晰明确;只要 f.readline() 返回空字符串,就表示已经到达了文件末尾,空行使用 '\n' 表示,该字符串只包含一个换行符。

>>> f.readline()
'This is the first line of the file.\n'
>>> f.readline()
'Second line of the file\n'
>>> f.readline()
''

从文件中读取多行时,可以用循环遍历整个文件对象。这种操作能高效利用内存,快速,且代码简单:

>>> for line in f:
...     print(line, end='')
...
This is the first line of the file.
Second line of the file

如需以列表形式读取文件中的所有行,可以用 list(f)f.readlines()

f.write(string)string 的内容写入文件,并返回写入的字符数。

>>> f.write('This is a test\n')
15

写入其他类型的对象前,要先把它们转化为字符串(文本模式)或字节对象(二进制模式):

>>> value = ('the answer', 42)
>>> s = str(value)  # convert the tuple to string
>>> f.write(s)
18

f.tell() 返回整数,给出文件对象在文件中的当前位置,表示为二进制模式下时从文件开始的字节数,以及文本模式下的意义不明的数字。

f.seek(offset, whence) 可以改变文件对象的位置。通过向参考点添加 offset 计算位置;参考点由 whence 参数指定。 whence 值为 0 时,表示从文件开头计算,1 表示使用当前文件位置,2 表示使用文件末尾作为参考点。省略 whence 时,其默认值为 0,即使用文件开头作为参考点。

>>> f = open('workfile', 'rb+')
>>> f.write(b'0123456789abcdef')
16
>>> f.seek(5)      # Go to the 6th byte in the file
5
>>> f.read(1)
b'5'
>>> f.seek(-3, 2)  # Go to the 3rd byte before the end
13
>>> f.read(1)
b'd'

在文本文件(模式字符串未使用 b 时打开的文件)中,只允许相对于文件开头搜索(使用 seek(0, 2) 搜索到文件末尾是个例外),唯一有效的 offset 值是能从 f.tell() 中返回的,或 0。其他 offset 值都会产生未定义的行为。

文件对象还支持 isatty()truncate() 等方法,但不常用;文件对象的完整指南详见库参考。

使用 json 保存结构化数据

从文件写入或读取字符串很简单,数字则稍显麻烦,因为 read() 方法只返回字符串,这些字符串必须传递给 int() 这样的函数,接受 '123' 这样的字符串,并返回数字值 123。保存嵌套列表、字典等复杂数据类型时,手动解析和序列化的操作非常复杂。

Python 支持 JSON (JavaScript Object Notation) 这种流行数据交换格式,用户无需没完没了地编写、调试代码,才能把复杂的数据类型保存到文件。json 标准模块采用 Python 数据层次结构,并将之转换为字符串表示形式;这个过程称为 serializing (序列化)。从字符串表示中重建数据称为 deserializing (解序化)。在序列化和解序化之间,表示对象的字符串可能已经存储在文件或数据中,或通过网络连接发送到远方 的机器。

注解

JSON 格式通常用于现代应用程序的数据交换。程序员早已对它耳熟能详,可谓是交互操作的不二之选。

只需一行简单的代码即可查看某个对象的 JSON 字符串表现形式:

>>> import json
>>> x = [1, 'simple', 'list']
>>> json.dumps(x)
'[1, "simple", "list"]'

dumps() 函数还有一个变体, dump() ,它只将对象序列化为 text file 。因此,如果 f 是 text file 对象,可以这样做:

json.dump(x, f)

要再次解码对象,如果 f 是已打开、供读取的 text file 对象:

x = json.load(f)

这种简单的序列化技术可以处理列表和字典,但在 JSON 中序列化任意类的实例,则需要付出额外努力。json 模块的参考包含对此的解释。

注:

  • pickle - 封存模块:与 JSON 不同,pickle 是一种允许对复杂 Python 对象进行序列化的协议。因此,它为 Python 所特有,不能用于与其他语言编写的应用程序通信。默认情况下它也是不安全的:如果解序化的数据是由手段高明的攻击者精心设计的,这种不受信任来源的 pickle 数据可以执行任意代码。

7. 错误和异常

句法错误

句法错误又称解析错误,是学习 Python 时最常见的错误:

>>> while True print('Hello world')
  File "<stdin>", line 1
    while True print('Hello world')
                   ^
SyntaxError: invalid syntax

解析器会复现出现句法错误的代码行,并用小“箭头”指向行里检测到的第一个错误。错误是由箭头 上方 的 token 触发的(至少是在这里检测出的):本例中,在 print() 函数中检测到错误,因为,在它前面缺少冒号(':') 。错误信息还输出文件名与行号,在使用脚本文件时,就可以知道去哪里查错。

异常

即使语句或表达式使用了正确的语法,执行时仍可能触发错误。执行时检测到的错误称为 异常,异常不一定导致严重的后果:很快我们就能学会如何处理 Python 的异常。大多数异常不会被程序处理,而是显示下列错误信息:

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str

错误信息的最后一行说明程序遇到了什么类型的错误。异常有不同的类型,而类型名称会作为错误信息的一部分中打印出来:上述示例中的异常类型依次是:ZeroDivisionErrorNameErrorTypeError。作为异常类型打印的字符串是发生的内置异常的名称。对于所有内置异常都是如此,但对于用户定义的异常则不一定如此(虽然这种规范很有用)。标准的异常类型是内置的标识符(不是保留关键字)。

此行其余部分根据异常类型,结合出错原因,说明错误细节。

错误信息开头用堆栈回溯形式展示发生异常的语境。一般会列出源代码行的堆栈回溯;但不会显示从标准输入读取的行。

内置异常 列出了内置异常及其含义。

异常的处理

可以编写程序处理选定的异常。下例会要求用户一直输入内容,直到输入有效的整数,但允许用户中断程序(使用 Control-C 或操作系统支持的其他操作);注意,用户中断程序会触发 KeyboardInterrupt 异常。

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...

try 语句的工作原理如下:

  • 首先,执行 try 子句tryexcept 关键字之间的(多行)语句)。
  • 如果没有触发异常,则跳过 except 子句try 语句执行完毕。
  • 如果在执行 try 子句时发生了异常,则跳过该子句中剩下的部分。 如果异常的类型与 except 关键字后指定的异常相匹配,则会执行 except 子句,然后跳到 try/except 代码块之后继续执行。
  • 如果发生的异常与 except 子句 中指定的异常不匹配,则它会被传递到外部的 try 语句中;如果没有找到处理程序,则它是一个 未处理异常 且执行将终止并输出如上所示的消息。

try 语句可以有多个 except 子句 来为不同的异常指定处理程序。 但最多只有一个处理程序会被执行。 处理程序只处理对应的 try 子句 中发生的异常,而不处理同一 try 语句内其他处理程序中的异常。 except 子句 可以用带圆括号的元组来指定多个异常,例如:

... except (RuntimeError, TypeError, NameError):
...     pass

如果发生的异常与 except 子句中的类是同一个类或是它的基类时,则该类与该异常相兼容(反之则不成立 —- 列出派生类的 except 子句 与基类不兼容)。 例如,下面的代码将依次打印 B, C, D:

class B(Exception):
    pass
class C(B):
    pass
class D(C):
    pass
for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")

请注意如果颠倒 except 子句 的顺序(把 except B 放在最前),则会输出 B, B, B —- 即触发了第一个匹配的 except 子句

所有异常都继承自 BaseException,因此它可被用作通配符。 但这种用法要非常谨慎小心,因为它很容易掩盖真正的编程错误! 它还可被用于打印错误消息然后重新引发异常(允许调用者再来处理该异常):

import sys
try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error: {0}".format(err))
except ValueError:
    print("Could not convert data to an integer.")
except BaseException as err:
    print(f"Unexpected {err=}, {type(err)=}")
    raise

可以选择让最后一个 except 子句省略异常名称,但在此之后异常值必须从 sys.exc_info()[1] 获取。

tryexcept 语句具有可选的 else 子句*,该子句如果存在,它必须放在所有 *except 子句 之后。 它适用于 try 子句 没有引发异常但又必须要执行的代码。 例如:

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        f.close()

使用 else 子句比向 try 子句添加额外的代码要好,可以避免意外捕获非 tryexcept 语句保护的代码触发的异常。

发生异常时,它可能具有关联值,即异常 参数 。是否需要参数,以及参数的类型取决于异常的类型。

except 子句 可以在异常名称后面指定一个变量。 这个变量会绑定到一个异常实例并将参数存储在 instance.args 中。 为了方便起见,该异常实例定义了 __str__() 以便能直接打印参数而无需引用 .args。 也可以在引发异常之前先实例化一个异常并根据需要向其添加任何属性。:

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # the exception instance
...     print(inst.args)     # arguments stored in .args
...     print(inst)          # __str__ allows args to be printed directly,
...                          # but may be overridden in exception subclasses
...     x, y = inst.args     # unpack args
...     print('x =', x)
...     print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

如果异常有参数,则它们将作为未处理异常的消息的最后一部分(’详细信息’)打印。

异常处理程序不仅会处理在 try 子句 中发生的异常,还会处理在 try 子句 中调用(包括间接调用)的函数。 例如:

>>> def this_fails():
...     x = 1/0
...
>>> try:
...     this_fails()
... except ZeroDivisionError as err:
...     print('Handling run-time error:', err)
...
Handling run-time error: division by zero

触发异常

raise 语句支持强制触发指定的异常。例如:

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: HiThere

raise 唯一的参数就是要触发的异常。这个参数必须是异常实例或异常类(派生自 Exception 类)。如果传递的是异常类,将通过调用没有参数的构造函数来隐式实例化:

raise ValueError  # shorthand for 'raise ValueError()'

如果只想判断是否触发了异常,但并不打算处理该异常,则可以使用更简单的 raise 语句重新触发异常:

>>> try:
...     raise NameError('HiThere')
... except NameError:
...     print('An exception flew by!')
...     raise
...
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
NameError: HiThere

异常链

raise 语句支持可选的 from 子句,该子句用于启用链式异常。 例如:

# exc must be exception instance or None.
raise RuntimeError from exc

转换异常时,这种方式很有用。例如:

>>> def func():
...     raise ConnectionError
...
>>> try:
...     func()
... except ConnectionError as exc:
...     raise RuntimeError('Failed to open database') from exc
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 2, in func
ConnectionError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
RuntimeError: Failed to open database

异常链会在 exceptfinally 子句内部引发异常时自动生成。 这可以通过使用 from None 这样的写法来禁用:

>>> try:
...     open('database.sqlite')
... except OSError:
...     raise RuntimeError from None
...
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
RuntimeError

用户自定义异常

程序可以通过创建新的异常类命名自己的异常。不论是以直接还是间接的方式,异常都应从 Exception 类派生。

异常类和其他类一样,可以执行任何操作。但通常会比较简单,只提供让处理异常的程序提取错误信息的一些属性。创建能触发多个不同错误的模块时,一般只为该模块定义异常基类,然后再根据不同的错误条件,创建指定异常类的子类:

class Error(Exception):
    """Base class for exceptions in this module."""
    pass
class InputError(Error):
    """Exception raised for errors in the input.
    Attributes:
        expression -- input expression in which the error occurred
        message -- explanation of the error
    """
    def __init__(self, expression, message):
        self.expression = expression
        self.message = message
class TransitionError(Error):
    """Raised when an operation attempts a state transition that's not
    allowed.
    Attributes:
        previous -- state at beginning of transition
        next -- attempted new state
        message -- explanation of why the specific transition is not allowed
    """
    def __init__(self, previous, next, message):
        self.previous = previous
        self.next = next
        self.message = message

大多数异常命名都以 “Error” 结尾,类似标准异常的命名。

许多标准模块都需要自定义异常,以报告由其定义的函数中出现的错误。

定义清理操作

try 语句还有一个可选子句,用于定义在所有情况下都必须要执行的清理操作。例如:

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Goodbye, world!')
...
Goodbye, world!
KeyboardInterrupt
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>

如果存在 finally 子句,则 finally 子句是 try 语句结束前执行的最后一项任务。不论 try 语句是否触发异常,都会执行 finally 子句。以下内容介绍了几种比较复杂的触发异常情景:

  • 如果执行 try 子句期间触发了某个异常,则某个 except 子句应处理该异常。如果该异常没有 except 子句处理,在 finally 子句执行后会被重新触发。
  • exceptelse 子句执行期间也会触发异常。 同样,该异常会在 finally 子句执行之后被重新触发。
  • 如果 finally 子句中包含 breakcontinuereturn 等语句,异常将不会被重新引发。
  • 如果执行 try 语句时遇到 breakcontinuereturn 语句,则 finally 子句在执行 breakcontinuereturn 语句之前执行。
  • 如果 finally 子句中包含 return 语句,则返回值来自 finally 子句的某个 return 语句的返回值,而不是来自 try 子句的 return 语句的返回值。

例如:

>>> def bool_return():
...     try:
...         return True
...     finally:
...         return False
...
>>> bool_return()
False

这是一个比较复杂的例子:

>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print("division by zero!")
...     else:
...         print("result is", result)
...     finally:
...         print("executing finally clause")
...
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'

如上所示,任何情况下都会执行 finally 子句。except 子句不处理两个字符串相除触发的 TypeError,因此会在 finally 子句执行后被重新触发。

在实际应用程序中,finally 子句对于释放外部资源(例如文件或者网络连接)非常有用,无论是否成功使用资源。

预定义的清理操作

某些对象定义了不需要该对象时要执行的标准清理操作。无论使用该对象的操作是否成功,都会执行清理操作。比如,下例要打开一个文件,并输出文件内容:

for line in open("myfile.txt"):
    print(line, end="")

这个代码的问题在于,执行完代码后,文件在一段不确定的时间内处于打开状态。在简单脚本中这没有问题,但对于较大的应用程序来说可能会出问题。with 语句支持以及时、正确的清理的方式使用文件对象:

with open("myfile.txt") as f:
    for line in f:
        print(line, end="")

语句执行完毕后,即使在处理行时遇到问题,都会关闭文件 f。和文件一样,支持预定义清理操作的对象会在文档中指出这一点。

8. 类

类把数据与功能绑定在一起。创建新类就是创建新的对象 类型*,从而创建该类型的新 *实例 。类实例具有多种保持自身状态的属性。类实例还支持(由类定义的)修改自身状态的方法。

和其他编程语言相比,Python 只用了很少的新语法和语义就加入了类。Python 的类是 C++ 和 Modula-3 中类机制的结合体,而且支持所有面向对象编程(OOP)的标准特性:类继承机制支持多个基类,派生类可以覆盖基类的任何方法,类的方法可以调用基类中相同名称的方法。对象可以包含任意数量和类型的数据。和模块一样,类也拥有 Python 天然的动态特性:在运行时创建,创建后也可以修改。

在 C++ 术语中,通常类成员(包括数据成员)是 public ,所有成员函数都是 virtual。 与在 Modula-3 中一样,没有用于从对象的方法中引用对象成员的简写:方法函数在声明时,有一个显示的参数代表本对象,该参数由调用隐式提供。 与 Smalltalk 一样,类本身也是对象。 这为导入和重命名提供了语义。 与 C++ 和 Modula-3 不同,内置类型可以用作基类,供用户扩展。 此外,与 C++ 一样,大多数具有特殊语法(算术运算符,下标等)的内置运算符都可以为类实例而重新定义。

(由于缺乏关于类的公认术语,我会偶尔使用 Smalltalk、C++ 的术语,我还会使用 Modula-3 的术语,因为它的面向对象语义比 C++ 更接近 Python,但估计没几个读者听说过这门语言。)

名称和对象

对象之间相互独立,多个名称(在多个作用域内)可以绑定到同一个对象。 其他语言称之为别名。Python 初学者通常不容易理解这个概念,处理数字、字符串、元组等不可变基本类型时,可以不必理会。 但是,对涉及可变对象,如列表、字典等大多数其他类型的 Python 代码的语义,别名可能会产生意料之外的效果。这样做,通常是为了让程序受益,因为别名在某些方面就像指针。例如,传递对象的代价很小,因为实现只传递一个指针;如果函数修改了作为参数传递的对象,调用者就可以看到更改 —- 无需 Pascal 用两个不同参数的传递机制。

Python 作用域和命名空间

在介绍类之前,我首先要告诉你一些 Python 的作用域规则。类定义对命名空间有一些巧妙的技巧,你需要知道作用域和命名空间如何工作才能完全理解正在发生的事情。顺便说一下,关于这个主题的知识对任何高级 Python 程序员都很有用。

让我们从一些定义开始。

namespace (命名空间)是一个从名字到对象的映射。 当前大部分命名空间都由 Python 字典实现,但一般情况下基本不会去关注它们(除了要面对性能问题时),而且也有可能在将来更改。 下面是几个命名空间的例子:存放内置函数的集合(包含 abs() 这样的函数,和内建的异常等);模块中的全局名称;函数调用中的局部名称。 从某种意义上说,对象的属性集合也是一种命名空间的形式。 关于命名空间的重要一点是,不同命名空间中的名称之间绝对没有关系;例如,两个不同的模块都可以定义一个 maximize 函数而不会产生混淆 —- 模块的用户必须在其前面加上模块名称。

顺便说明一下,我把任何跟在一个点号之后的名称都称为 属性 —- 例如,在表达式 z.real 中,real 是对象 z 的一个属性。按严格的说法,对模块中名称的引用属于属性引用:在表达式 modname.funcname 中,modname 是一个模块对象而 funcname 是它的一个属性。在此情况下在模块的属性和模块中定义的全局名称之间正好存在一个直观的映射:它们共享相同的命名空间!

属性可以是只读或者可写的。如果为后者,那么对属性的赋值是可行的。模块属性是可写的,你可以写 modname.the_answer = 42 。可写的属性同样可以用 del 语句删除。例如, del modname.the_answer 将会从名为 modname 的对象中移除 the_answer 属性。

命名空间在不同时刻被创建,拥有不同的生存期。包含内置名称的命名空间是在 Python 解释器启动时创建的,永远不会被删除。模块的全局命名空间在模块定义被读入时创建;通常,模块命名空间也会持续到解释器退出。被解释器的顶层调用执行的语句,从一个脚本文件读取或交互式地读取,被认为是 __main__ 模块调用的一部分,因此它们拥有自己的全局命名空间。(内置名称实际上也存在于一个模块中;这个模块被称作 builtins 。)

一个函数的本地命名空间在这个函数被调用时创建,并在函数返回或抛出一个不在函数内部处理的错误时被删除。(事实上,比起描述到底发生了什么,忘掉它更好。)当然,每次递归调用都会有它自己的本地命名空间。

一个 作用域 是一个命名空间可直接访问的 Python 程序的文本区域。 这里的 “可直接访问” 意味着对名称的非限定引用会尝试在命名空间中查找名称。

虽然作用域是静态地确定的,但它们会被动态地使用。 在执行期间的任何时刻,会有 3 或 4 个命名空间可被直接访问的嵌套作用域:

  • 最先搜索的最内部作用域包含局部名称
  • 从最近的封闭作用域开始搜索的任何封闭函数的作用域包含非局部名称,也包括非全局名称
  • 倒数第二个作用域包含当前模块的全局名称
  • 最外面的作用域(最后搜索)是包含内置名称的命名空间

如果一个名称被声明为全局变量,则所有引用和赋值将直接指向包含该模块的全局名称的中间作用域。 要重新绑定在最内层作用域以外找到的变量,可以使用 nonlocal 语句声明为非本地变量。 如果没有被声明为非本地变量,这些变量将是只读的(尝试写入这样的变量只会在最内层作用域中创建一个 新的 局部变量,而同名的外部变量保持不变)。

通常,当前局部作用域将(按字面文本)引用当前函数的局部名称。 在函数以外,局部作用域将引用与全局作用域相一致的命名空间:模块的命名空间。 类定义将在局部命名空间内再放置另一个命名空间。

重要的是应该意识到作用域是按字面文本来确定的:在一个模块内定义的函数的全局作用域就是该模块的命名空间,无论该函数从什么地方或以什么别名被调用。 另一方面,实际的名称搜索是在运行时动态完成的 —- 但是,Python 正在朝着“编译时静态名称解析”的方向发展,因此不要过于依赖动态名称解析! (事实上,局部变量已经是被静态确定了。)

Python 的一个特殊规定是这样的 — 如果不存在生效的 globalnonlocal 语句 — 则对名称的赋值总是会进入最内层作用域。 赋值不会复制数据 —- 它们只是将名称绑定到对象。 删除也是如此:语句 del x 会从局部作用域所引用的命名空间中移除对 x 的绑定。 事实上,所有引入新名称的操作都是使用局部作用域:特别地,import 语句和函数定义会在局部作用域中绑定模块或函数名称。

global 语句可被用来表明特定变量生存于全局作用域并且应当在其中被重新绑定;nonlocal 语句表明特定变量生存于外层作用域中并且应当在其中被重新绑定。

作用域和命名空间示例

这个例子演示了如何引用不同作用域和名称空间,以及 globalnonlocal 会如何影响变量绑定:

def scope_test():
    def do_local():
        spam = "local spam"
    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"
    def do_global():
        global spam
        spam = "global spam"
    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)
scope_test()
print("In global scope:", spam)

示例代码的输出是:

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

请注意 局部 赋值(这是默认状态)不会改变 scope_testspam 的绑定。 nonlocal 赋值会改变 scope_testspam 的绑定,而 global 赋值会改变模块层级的绑定。

您还可以发现在 global 赋值之前没有 spam 的绑定。

初探类

类引入了一些新语法,三种新对象类型和一些新语义。

类定义语法

最简单的类定义看起来像这样:

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

类定义与函数定义 (def 语句) 一样必须被执行才会起作用。 (你可以尝试将类定义放在 if 语句的一个分支或是函数的内部。)

在实践中,类定义内的语句通常都是函数定义,但也允许有其他语句,有时还很有用 —- 我们会稍后再回来说明这个问题。 在类内部的函数定义通常具有一种特别形式的参数列表,这是方法调用的约定规范所指明的 —- 这个问题也将在稍后再说明。

当进入类定义时,将创建一个新的命名空间,并将其用作局部作用域 —- 因此,所有对局部变量的赋值都是在这个新命名空间之内。 特别的,函数定义会绑定到这里的新函数名称。

当(从结尾处)正常离开类定义时,将创建一个 类对象。 这基本上是一个包围在类定义所创建命名空间内容周围的包装器;我们将在下一节了解有关类对象的更多信息。 原始的(在进入类定义之前起作用的)局部作用域将重新生效,类对象将在这里被绑定到类定义头所给出的类名称 (在这个示例中为 ClassName)。

Class 对象

类对象支持两种操作:属性引用和实例化。

属性引用 使用 Python 中所有属性引用所使用的标准语法: obj.name。 有效的属性名称是类对象被创建时存在于类命名空间中的所有名称。 因此,如果类定义是这样的:

class MyClass:
    """A simple example class"""
    i = 12345
    def f(self):
        return 'hello world'

那么 MyClass.iMyClass.f 就是有效的属性引用,将分别返回一个整数和一个函数对象。 类属性也可以被赋值,因此可以通过赋值来更改 MyClass.i 的值。 __doc__ 也是一个有效的属性,将返回所属类的文档字符串: "A simple example class"

类的 实例化 使用函数表示法。 可以把类对象视为是返回该类的一个新实例的不带参数的函数。 举例来说(假设使用上述的类):

x = MyClass()

创建类的新 实例 并将此对象分配给局部变量 x

实例化操作(“调用”类对象)会创建一个空对象。 许多类喜欢创建带有特定初始状态的自定义实例。 为此类定义可能包含一个名为 __init__() 的特殊方法,就像这样:

def __init__(self):
    self.data = []

当一个类定义了 __init__() 方法时,类的实例化操作会自动为新创建的类实例发起调用 __init__()。 因此在这个示例中,可以通过以下语句获得一个经初始化的新实例:

x = MyClass()

当然,__init__() 方法还可以有额外参数以实现更高灵活性。 在这种情况下,提供给类实例化运算符的参数将被传递给 __init__()。 例如,:

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

实例对象

现在我们能用实例对象做什么? 实例对象所能理解的唯一操作是属性引用。 有两种有效的属性名称:数据属性和方法。

数据属性 对应于 Smalltalk 中的“实例变量”,以及 C++ 中的“数据成员”。 数据属性不需要声明;像局部变量一样,它们将在第一次被赋值时产生。 例如,如果 x 是上面创建的 MyClass 的实例,则以下代码段将打印数值 16,且不保留任何追踪信息:

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

另一类实例属性引用称为 方法。 方法是“从属于”对象的函数。 (在 Python 中,方法这个术语并不是类实例所特有的:其他对象也可以有方法。 例如,列表对象具有 append, insert, remove, sort 等方法。 然而,在以下讨论中,我们使用方法一词将专指类实例对象的方法,除非另外显式地说明。)

实例对象的有效方法名称依赖于其所属的类。 根据定义,一个类中所有是函数对象的属性都是定义了其实例的相应方法。 因此在我们的示例中,x.f 是有效的方法引用,因为 MyClass.f 是一个函数,而 x.i 不是方法,因为 MyClass.i 不是函数。 但是 x.fMyClass.f 并不是一回事 —- 它是一个 方法对象,不是函数对象。

方法对象

通常,方法在绑定后立即被调用:

x.f()

MyClass 示例中,这将返回字符串 'hello world'。 但是,立即调用一个方法并不是必须的: x.f 是一个方法对象,它可以被保存起来以后再调用。 例如:

xf = x.f
while True:
    print(xf())

将持续打印 hello world,直到结束。

当一个方法被调用时到底发生了什么? 你可能已经注意到上面调用 x.f() 时并没有带参数,虽然 f() 的函数定义指定了一个参数。 这个参数发生了什么事? 当不带参数地调用一个需要参数的函数时 Python 肯定会引发异常 —- 即使参数实际未被使用…

实际上,你可能已经猜到了答案:方法的特殊之处就在于实例对象会作为函数的第一个参数被传入。 在我们的示例中,调用 x.f() 其实就相当于 MyClass.f(x)。 总之,调用一个具有 n 个参数的方法就相当于调用再多一个参数的对应函数,这个参数值为方法所属实例对象,位置在其他参数之前。

如果你仍然无法理解方法的运作原理,那么查看实现细节可能会弄清楚问题。 当一个实例的非数据属性被引用时,将搜索实例所属的类。 如果被引用的属性名称表示一个有效的类属性中的函数对象,会通过打包(指向)查找到的实例对象和函数对象到一个抽象对象的方式来创建方法对象:这个抽象对象就是方法对象。 当附带参数列表调用方法对象时,将基于实例对象和参数列表构建一个新的参数列表,并使用这个新参数列表调用相应的函数对象。

类和实例变量

一般来说,实例变量用于每个实例的唯一数据,而类变量用于类的所有实例共享的属性和方法:

class Dog:
    kind = 'canine'         # class variable shared by all instances
    def __init__(self, name):
        self.name = name    # instance variable unique to each instance
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'

正如 名称和对象 中已讨论过的,共享数据可能在涉及 mutable 对象例如列表和字典的时候导致令人惊讶的结果。 例如以下代码中的 tricks 列表不应该被用作类变量,因为所有的 Dog 实例将只共享一个单独的列表:

class Dog:
    tricks = []             # mistaken use of a class variable
    def __init__(self, name):
        self.name = name
    def add_trick(self, trick):
        self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']

正确的类设计应该使用实例变量:

class Dog:
    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog
    def add_trick(self, trick):
        self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

补充说明

如果同样的属性名称同时出现在实例和类中,则属性查找会优先选择实例:

>>> class Warehouse:
        purpose = 'storage'
        region = 'west'
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east

数据属性可以被方法以及一个对象的普通用户(“客户端”)所引用。 换句话说,类不能用于实现纯抽象数据类型。 实际上,在 Python 中没有任何东西能强制隐藏数据 —- 它是完全基于约定的。 (而在另一方面,用 C 语言编写的 Python 实现则可以完全隐藏实现细节,并在必要时控制对象的访问;此特性可以通过用 C 编写 Python 扩展来使用。)

客户端应当谨慎地使用数据属性 —- 客户端可能通过直接操作数据属性的方式破坏由方法所维护的固定变量。 请注意客户端可以向一个实例对象添加他们自己的数据属性而不会影响方法的可用性,只要保证避免名称冲突 —- 再次提醒,在此使用命名约定可以省去许多令人头痛的麻烦。

在方法内部引用数据属性(或其他方法!)并没有简便方式。 我发现这实际上提升了方法的可读性:当浏览一个方法代码时,不会存在混淆局部变量和实例变量的机会。

方法的第一个参数常常被命名为 self。 这也不过就是一个约定: self 这一名称在 Python 中绝对没有特殊含义。 但是要注意,不遵循此约定会使得你的代码对其他 Python 程序员来说缺乏可读性,而且也可以想像一个 类浏览器 程序的编写可能会依赖于这样的约定。

任何一个作为类属性的函数都为该类的实例定义了一个相应方法。 函数定义的文本并非必须包含于类定义之内:将一个函数对象赋值给一个局部变量也是可以的。 例如:

# Function defined outside the class
def f1(self, x, y):
    return min(x, x+y)
class C:
    f = f1
    def g(self):
        return 'hello world'
    h = g

现在 f, gh 都是 C 类的引用函数对象的属性,因而它们就都是 C 的实例的方法 —- 其中 h 完全等同于 g。 但请注意,本示例的做法通常只会令程序的阅读者感到迷惑。

方法可以通过使用 self 参数的方法属性调用其他方法:

class Bag:
    def __init__(self):
        self.data = []
    def add(self, x):
        self.data.append(x)
    def addtwice(self, x):
        self.add(x)
        self.add(x)

方法可以通过与普通函数相同的方式引用全局名称。 与方法相关联的全局作用域就是包含其定义的模块。 (类永远不会被作为全局作用域。) 虽然我们很少会有充分的理由在方法中使用全局作用域,但全局作用域存在许多合理的使用场景:举个例子,导入到全局作用域的函数和模块可以被方法所使用,在其中定义的函数和类也一样。 通常,包含该方法的类本身是在全局作用域中定义的,而在下一节中我们将会发现为何方法需要引用其所属类的很好的理由。

每个值都是一个对象,因此具有 (也称为 类型),并存储为 object.__class__

继承

当然,如果不支持继承,语言特性就不值得称为“类”。派生类定义的语法如下所示:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

名称 BaseClassName 必须定义于包含派生类定义的作用域中。 也允许用其他任意表达式代替基类名称所在的位置。 这有时也可能会用得上,例如,当基类定义在另一个模块中的时候:

class DerivedClassName(modname.BaseClassName):

派生类定义的执行过程与基类相同。 当构造类对象时,基类会被记住。 此信息将被用来解析属性引用:如果请求的属性在类中找不到,搜索将转往基类中进行查找。 如果基类本身也派生自其他某个类,则此规则将被递归地应用。

派生类的实例化没有任何特殊之处: DerivedClassName() 会创建该类的一个新实例。 方法引用将按以下方式解析:搜索相应的类属性,如有必要将按基类继承链逐步向下查找,如果产生了一个函数对象则方法引用就生效。

派生类可能会重写其基类的方法。 因为方法在调用同一对象的其他方法时没有特殊权限,所以调用同一基类中定义的另一方法的基类方法最终可能会调用覆盖它的派生类的方法。 (对 C++ 程序员的提示:Python 中所有的方法实际上都是 virtual 方法。)

在派生类中的重载方法实际上可能想要扩展而非简单地替换同名的基类方法。 有一种方式可以简单地直接调用基类方法:即调用 BaseClassName.methodname(self, arguments)。 有时这对客户端来说也是有用的。 (请注意仅当此基类可在全局作用域中以 BaseClassName 的名称被访问时方可使用此方式。)

Python有两个内置函数可被用于继承机制:

  • 使用 isinstance() 来检查一个实例的类型: isinstance(obj, int) 仅会在 obj.__class__int 或某个派生自 int 的类时为 True
  • 使用 issubclass() 来检查类的继承关系: issubclass(bool, int)True,因为 boolint 的子类。 但是,issubclass(float, int)False,因为 float 不是 int 的子类。

多重继承

Python 也支持一种多重继承。 带有多个基类的类定义语句如下所示:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

对于多数应用来说,在最简单的情况下,你可以认为搜索从父类所继承属性的操作是深度优先、从左至右的,当层次结构中存在重叠时不会在同一个类中搜索两次。 因此,如果某一属性在 DerivedClassName 中未找到,则会到 Base1 中搜索它,然后(递归地)到 Base1 的基类中搜索,如果在那里未找到,再到 Base2 中搜索,依此类推。

真实情况比这个更复杂一些;方法解析顺序会动态改变以支持对 super() 的协同调用。 这种方式在某些其他多重继承型语言中被称为后续方法调用,它比单继承型语言中的 super 调用更强大。

动态改变顺序是有必要的,因为所有多重继承的情况都会显示出一个或更多的菱形关联(即至少有一个父类可通过多条路径被最底层类所访问)。 例如,所有类都是继承自 object,因此任何多重继承的情况都提供了一条以上的路径可以通向 object。 为了确保基类不会被访问一次以上,动态算法会用一种特殊方式将搜索顺序线性化, 保留每个类所指定的从左至右的顺序,只调用每个父类一次,并且保持单调(即一个类可以被子类化而不影响其父类的优先顺序)。 总而言之,这些特性使得设计具有多重继承的可靠且可扩展的类成为可能。 要了解更多细节,请参阅 https://www.python.org/download/releases/2.3/mro/。

私有变量

那种仅限从一个对象内部访问的“私有”实例变量在 Python 中并不存在。 但是,大多数 Python 代码都遵循这样一个约定:带有一个下划线的名称 (例如 _spam) 应该被当作是 API 的非公有部分 (无论它是函数、方法或是数据成员)。 这应当被视为一个实现细节,可能不经通知即加以改变。

由于存在对于类私有成员的有效使用场景(例如避免名称与子类所定义的名称相冲突),因此存在对此种机制的有限支持,称为 名称改写。 任何形式为 __spam 的标识符(至少带有两个前缀下划线,至多一个后缀下划线)的文本将被替换为 _classname__spam,其中 classname 为去除了前缀下划线的当前类名称。 这种改写不考虑标识符的句法位置,只要它出现在类定义内部就会进行。

名称改写有助于让子类重载方法而不破坏类内方法调用。例如:

class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)
    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)
    __update = update   # private copy of original update() method
class MappingSubclass(Mapping):
    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

上面的示例即使在 MappingSubclass 引入了一个 __update 标识符的情况下也不会出错,因为它会在 Mapping 类中被替换为 _Mapping__update 而在 MappingSubclass 类中被替换为 _MappingSubclass__update

请注意,改写规则的设计主要是为了避免意外冲突;访问或修改被视为私有的变量仍然是可能的。这在特殊情况下甚至会很有用,例如在调试器中。

请注意传递给 exec()eval() 的代码不会将发起调用类的类名视作当前类;这类似于 global 语句的效果,因此这种效果仅限于同时经过字节码编译的代码。 同样的限制也适用于 getattr(), setattr()delattr(),以及对于 __dict__ 的直接引用。

杂项说明

有时会需要使用类似于 Pascal 的“record”或 C 的“struct”这样的数据类型,将一些命名数据项捆绑在一起。 这种情况适合定义一个空类:

class Employee:
    pass
john = Employee()  # Create an empty employee record
# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000

一段需要特定抽象数据类型的 Python 代码往往可以被传入一个模拟了该数据类型的方法的类作为替代。 例如,如果你有一个基于文件对象来格式化某些数据的函数,你可以定义一个带有 read()readline() 方法从字符串缓存获取数据的类,并将其作为参数传入。

实例方法对象也具有属性: m.__self__ 就是带有 m() 方法的实例对象,而 m.__func__ 则是该方法所对应的函数对象。

迭代器

到目前为止,您可能已经注意到大多数容器对象都可以使用 for 语句:

for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one':1, 'two':2}:
    print(key)
for char in "123":
    print(char)
for line in open("myfile.txt"):
    print(line, end='')

这种访问风格清晰、简洁又方便。 迭代器的使用非常普遍并使得 Python 成为一个统一的整体。 在幕后,for 语句会在容器对象上调用 iter()。 该函数返回一个定义了 __next__() 方法的迭代器对象,此方法将逐一访问容器中的元素。 当元素用尽时,__next__() 将引发 StopIteration 异常来通知终止 for 循环。 你可以使用 next() 内置函数来调用 __next__() 方法;这个例子显示了它的运作方式:

>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    next(it)
StopIteration

看过迭代器协议的幕后机制,给你的类添加迭代器行为就很容易了。 定义一个 __iter__() 方法来返回一个带有 __next__() 方法的对象。 如果类已定义了 __next__(),则 __iter__() 可以简单地返回 self:

class Reverse:
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)
    def __iter__(self):
        return self
    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
m
a
p
s

生成器

生成器 是一个用于创建迭代器的简单而强大的工具。 它们的写法类似于标准的函数,但当它们要返回数据时会使用 yield 语句。 每次在生成器上调用 next() 时,它会从上次离开的位置恢复执行(它会记住上次执行语句时的所有数据值)。 一个显示如何非常容易地创建生成器的示例如下:

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g

可以用生成器来完成的操作同样可以用前一节所描述的基于类的迭代器来完成。 但生成器的写法更为紧凑,因为它会自动创建 __iter__()__next__() 方法。

另一个关键特性在于局部变量和执行状态会在每次调用之间自动保存。 这使得该函数相比使用 self.indexself.data 这种实例变量的方式更易编写且更为清晰。

除了会自动创建方法和保存程序状态,当生成器终结时,它们还会自动引发 StopIteration。 这些特性结合在一起,使得创建迭代器能与编写常规函数一样容易。

生成器表达式

某些简单的生成器可以写成简洁的表达式代码,所用语法类似列表推导式,但外层为圆括号而非方括号。 这种表达式被设计用于生成器将立即被外层函数所使用的情况。 生成器表达式相比完整的生成器更紧凑但较不灵活,相比等效的列表推导式则更为节省内存。

示例:

>>> sum(i*i for i in range(10))                 # sum of squares
285
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
260
>>> unique_words = set(word for line in page  for word in line.split())
>>> valedictorian = max((student.gpa, student.name) for student in graduates)
>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']

注:

  • 存在一个例外。 模块对象有一个秘密的只读属性 __dict__,它返回用于实现模块命名空间的字典;__dict__ 是属性但不是全局名称。 显然,使用这个将违反命名空间实现的抽象,应当仅被用于事后调试器之类的场合。

9. 标准库简介

操作系统接口

os 模块提供了许多与操作系统交互的函数:

>>> import os
>>> os.getcwd()      # Return the current working directory
'C:\\Python310'
>>> os.chdir('/server/accesslogs')   # Change current working directory
>>> os.system('mkdir today')   # Run the command mkdir in the system shell
0

一定要使用 import os 而不是 from os import * 。这将避免内建的 open() 函数被 os.open() 隐式替换掉,因为它们的使用方式大不相同。

内置的 dir()help() 函数可用作交互式辅助工具,用于处理大型模块,如 os:

>>> import os
>>> dir(os)
<returns a list of all module functions>
>>> help(os)
<returns an extensive manual page created from the module's docstrings>

对于日常文件和目录管理任务, shutil 模块提供了更易于使用的更高级别的接口:

>>> import shutil
>>> shutil.copyfile('data.db', 'archive.db')
'archive.db'
>>> shutil.move('/build/executables', 'installdir')
'installdir'

文件通配符

glob 模块提供了一个在目录中使用通配符搜索创建文件列表的函数:

>>> import glob
>>> glob.glob('*.py')
['primes.py', 'random.py', 'quote.py']

命令行参数

通用实用程序脚本通常需要处理命令行参数。这些参数作为列表存储在 sys 模块的 argv 属性中。例如,以下输出来自在命令行运行 python demo.py one two three

>>> import sys
>>> print(sys.argv)
['demo.py', 'one', 'two', 'three']

argparse 模块提供了一种更复杂的机制来处理命令行参数。 以下脚本可提取一个或多个文件名,并可选择要显示的行数:

import argparse
parser = argparse.ArgumentParser(prog = 'top',
    description = 'Show top lines from each file')
parser.add_argument('filenames', nargs='+')
parser.add_argument('-l', '--lines', type=int, default=10)
args = parser.parse_args()
print(args)

当在通过 python top.py --lines=5 alpha.txt beta.txt 在命令行运行时,该脚本会将 args.lines 设为 5 并将 args.filenames 设为 ['alpha.txt', 'beta.txt']

错误输出重定向和程序终止

sys 模块还具有 stdinstdoutstderr 的属性。后者对于发出警告和错误消息非常有用,即使在 stdout 被重定向后也可以看到它们:

>>> sys.stderr.write('Warning, log file not found starting a new one\n')
Warning, log file not found starting a new one

终止脚本的最直接方法是使用 sys.exit()

字符串模式匹配

re 模块为高级字符串处理提供正则表达式工具。对于复杂的匹配和操作,正则表达式提供简洁,优化的解决方案:

>>> import re
>>> re.findall(r'\bf[a-z]*', 'which foot or hand fell fastest')
['foot', 'fell', 'fastest']
>>> re.sub(r'(\b[a-z]+) \1', r'\1', 'cat in the the hat')
'cat in the hat'

当只需要简单的功能时,首选字符串方法因为它们更容易阅读和调试:

>>> 'tea for too'.replace('too', 'two')
'tea for two'

数学

math 模块提供对浮点数学的底层C库函数的访问:

>>> import math
>>> math.cos(math.pi / 4)
0.70710678118654757
>>> math.log(1024, 2)
10.0

random 模块提供了进行随机选择的工具:

>>> import random
>>> random.choice(['apple', 'pear', 'banana'])
'apple'
>>> random.sample(range(100), 10)   # sampling without replacement
[30, 83, 16, 4, 8, 81, 41, 50, 18, 33]
>>> random.random()    # random float
0.17970987693706186
>>> random.randrange(6)    # random integer chosen from range(6)
4

statistics 模块计算数值数据的基本统计属性(均值,中位数,方差等):

>>> import statistics
>>> data = [2.75, 1.75, 1.25, 0.25, 0.5, 1.25, 3.5]
>>> statistics.mean(data)
1.6071428571428572
>>> statistics.median(data)
1.25
>>> statistics.variance(data)
1.3720238095238095

SciPy项目 https://scipy.org有许多其他模块用于数值计算。

互联网访问

有许多模块可用于访问互联网和处理互联网协议。其中两个最简单的 urllib.request 用于从URL检索数据,以及 smtplib 用于发送邮件:

>>> from urllib.request import urlopen
>>> with urlopen('http://tycho.usno.navy.mil/cgi-bin/timer.pl') as response:
...     for line in response:
...         line = line.decode('utf-8')  # Decoding the binary data to text.
...         if 'EST' in line or 'EDT' in line:  # look for Eastern Time
...             print(line)
<BR>Nov. 25, 09:43:32 PM EST
>>> import smtplib
>>> server = smtplib.SMTP('localhost')
>>> server.sendmail('soothsayer@example.org', 'jcaesar@example.org',
... """To: jcaesar@example.org
... From: soothsayer@example.org
...
... Beware the Ides of March.
... """)
>>> server.quit()

(请注意,第二个示例需要在localhost上运行的邮件服务器。)

日期和时间

datetime 模块提供了以简单和复杂的方式操作日期和时间的类。虽然支持日期和时间算法,但实现的重点是有效的成员提取以进行输出格式化和操作。该模块还支持可感知时区的对象。

>>> # dates are easily constructed and formatted
>>> from datetime import date
>>> now = date.today()
>>> now
datetime.date(2003, 12, 2)
>>> now.strftime("%m-%d-%y. %d %b %Y is a %A on the %d day of %B.")
'12-02-03. 02 Dec 2003 is a Tuesday on the 02 day of December.'
>>> # dates support calendar arithmetic
>>> birthday = date(1964, 7, 31)
>>> age = now - birthday
>>> age.days
14368

数据压缩

常见的数据存档和压缩格式由模块直接支持,包括:zlib, gzip, bz2, lzma, zipfiletarfile。:

>>> import zlib
>>> s = b'witch which has which witches wrist watch'
>>> len(s)
41
>>> t = zlib.compress(s)
>>> len(t)
37
>>> zlib.decompress(t)
b'witch which has which witches wrist watch'
>>> zlib.crc32(s)
226805979

性能测量

一些Python用户对了解同一问题的不同方法的相对性能产生了浓厚的兴趣。 Python提供了一种可以立即回答这些问题的测量工具。

例如,元组封包和拆包功能相比传统的交换参数可能更具吸引力。timeit 模块可以快速演示在运行效率方面一定的优势:

>>> from timeit import Timer
>>> Timer('t=a; a=b; b=t', 'a=1; b=2').timeit()
0.57535828626024577
>>> Timer('a,b = b,a', 'a=1; b=2').timeit()
0.54962537085770791

timeit 的精细粒度级别相反, profilepstats 模块提供了用于在较大的代码块中识别时间关键部分的工具。

质量控制

开发高质量软件的一种方法是在开发过程中为每个函数编写测试,并在开发过程中经常运行这些测试。

doctest 模块提供了一个工具,用于扫描模块并验证程序文档字符串中嵌入的测试。测试构造就像将典型调用及其结果剪切并粘贴到文档字符串一样简单。这通过向用户提供示例来改进文档,并且它允许doctest模块确保代码保持对文档的真实:

def average(values):
    """Computes the arithmetic mean of a list of numbers.
    >>> print(average([20, 30, 70]))
    40.0
    """
    return sum(values) / len(values)
import doctest
doctest.testmod()   # automatically validate the embedded tests

unittest 模块不像 doctest 模块那样易于使用,但它允许在一个单独的文件中维护更全面的测试集:

import unittest
class TestStatisticalFunctions(unittest.TestCase):
    def test_average(self):
        self.assertEqual(average([20, 30, 70]), 40.0)
        self.assertEqual(round(average([1, 5, 7]), 1), 4.3)
        with self.assertRaises(ZeroDivisionError):
            average([])
        with self.assertRaises(TypeError):
            average(20, 30, 70)
unittest.main()  # Calling from the command line invokes all tests

自带电池

Python有“自带电池”的理念。通过其包的复杂和强大功能可以最好地看到这一点。例如:

  • xmlrpc.clientxmlrpc.server 模块使得实现远程过程调用变成了小菜一碟。 尽管存在于模块名称中,但用户不需要直接了解或处理 XML。
  • email 包是一个用于管理电子邮件的库,包括MIME和其他符合 RFC 2822 规范的邮件文档。与 smtplibpoplib 不同(它们实际上做的是发送和接收消息),电子邮件包提供完整的工具集,用于构建或解码复杂的消息结构(包括附件)以及实现互联网编码和标头协议。
  • json 包为解析这种流行的数据交换格式提供了强大的支持。 csv 模块支持以逗号分隔值格式直接读取和写入文件,这种格式通常为数据库和电子表格所支持。 XML 处理由 xml.etree.ElementTreexml.domxml.sax 包支持。这些模块和软件包共同大大简化了 Python 应用程序和其他工具之间的数据交换。
  • sqlite3 模块是 SQLite 数据库库的包装器,提供了一个可以使用稍微非标准的 SQL 语法更新和访问的持久数据库。
  • 国际化由许多模块支持,包括 gettextlocale ,以及 codecs 包。

格式化输出

reprlib 模块提供了一个定制化版本的 repr() 函数,用于缩略显示大型或深层嵌套的容器对象:

>>> import reprlib
>>> reprlib.repr(set('supercalifragilisticexpialidocious'))
"{'a', 'c', 'd', 'e', 'f', 'g', ...}"

pprint 模块提供了更加复杂的打印控制,其输出的内置对象和用户自定义对象能够被解释器直接读取。当输出结果过长而需要折行时,“美化输出机制”会添加换行符和缩进,以更清楚地展示数据结构:

>>> import pprint
>>> t = [[[['black', 'cyan'], 'white', ['green', 'red']], [['magenta',
...     'yellow'], 'blue']]]
...
>>> pprint.pprint(t, width=30)
[[[['black', 'cyan'],
   'white',
   ['green', 'red']],
  [['magenta', 'yellow'],
   'blue']]]

textwrap 模块能够格式化文本段落,以适应给定的屏幕宽度:

>>> import textwrap
>>> doc = """The wrap() method is just like fill() except that it returns
... a list of strings instead of one big string with newlines to separate
... the wrapped lines."""
...
>>> print(textwrap.fill(doc, width=40))
The wrap() method is just like fill()
except that it returns a list of strings
instead of one big string with newlines
to separate the wrapped lines.

locale 模块处理与特定地域文化相关的数据格式。locale 模块的 format 函数包含一个 grouping 属性,可直接将数字格式化为带有组分隔符的样式:

>>> import locale
>>> locale.setlocale(locale.LC_ALL, 'English_United States.1252')
'English_United States.1252'
>>> conv = locale.localeconv()          # get a mapping of conventions
>>> x = 1234567.8
>>> locale.format("%d", x, grouping=True)
'1,234,567'
>>> locale.format_string("%s%.*f", (conv['currency_symbol'],
...                      conv['frac_digits'], x), grouping=True)
'$1,234,567.80'

模板

string 模块包含一个通用的 Template 类,具有适用于最终用户的简化语法。它允许用户在不更改应用逻辑的情况下定制自己的应用。

上述格式化操作是通过占位符实现的,占位符由 $ 加上合法的 Python 标识符(只能包含字母、数字和下划线)构成。一旦使用花括号将占位符括起来,就可以在后面直接跟上更多的字母和数字而无需空格分割。$$ 将被转义成单个字符 $:

>>> from string import Template
>>> t = Template('${village}folk send $$10 to $cause.')
>>> t.substitute(village='Nottingham', cause='the ditch fund')
'Nottinghamfolk send $10 to the ditch fund.'

如果在字典或关键字参数中未提供某个占位符的值,那么 substitute() 方法将抛出 KeyError。对于邮件合并类型的应用,用户提供的数据有可能是不完整的,此时使用 safe_substitute() 方法更加合适 —— 如果数据缺失,它会直接将占位符原样保留。

>>> t = Template('Return the $item to $owner.')
>>> d = dict(item='unladen swallow')
>>> t.substitute(d)
Traceback (most recent call last):
  ...
KeyError: 'owner'
>>> t.safe_substitute(d)
'Return the unladen swallow to $owner.'

Template 的子类可以自定义分隔符。例如,以下是某个照片浏览器的批量重命名功能,采用了百分号作为日期、照片序号和照片格式的占位符:

>>> import time, os.path
>>> photofiles = ['img_1074.jpg', 'img_1076.jpg', 'img_1077.jpg']
>>> class BatchRename(Template):
...     delimiter = '%'
>>> fmt = input('Enter rename style (%d-date %n-seqnum %f-format):  ')
Enter rename style (%d-date %n-seqnum %f-format):  Ashley_%n%f
>>> t = BatchRename(fmt)
>>> date = time.strftime('%d%b%y')
>>> for i, filename in enumerate(photofiles):
...     base, ext = os.path.splitext(filename)
...     newname = t.substitute(d=date, n=i, f=ext)
...     print('{0} --> {1}'.format(filename, newname))
img_1074.jpg --> Ashley_0.jpg
img_1076.jpg --> Ashley_1.jpg
img_1077.jpg --> Ashley_2.jpg

模板的另一个应用是将程序逻辑与多样的格式化输出细节分离开来。这使得对 XML 文件、纯文本报表和 HTML 网络报表使用自定义模板成为可能。

使用二进制数据记录格式

struct 模块提供了 pack()unpack() 函数,用于处理不定长度的二进制记录格式。下面的例子展示了在不使用 zipfile 模块的情况下,如何循环遍历一个 ZIP 文件的所有头信息。Pack 代码 "H""I" 分别代表两字节和四字节无符号整数。"<" 代表它们是标准尺寸的小端字节序:

import struct
with open('myfile.zip', 'rb') as f:
    data = f.read()
start = 0
for i in range(3):                      # show the first 3 file headers
    start += 14
    fields = struct.unpack('<IIIHH', data[start:start+16])
    crc32, comp_size, uncomp_size, filenamesize, extra_size = fields
    start += 16
    filename = data[start:start+filenamesize]
    start += filenamesize
    extra = data[start:start+extra_size]
    print(filename, hex(crc32), comp_size, uncomp_size)
    start += extra_size + comp_size     # skip to the next header

多线程

线程是一种对于非顺序依赖的多个任务进行解耦的技术。多线程可以提高应用的响应效率,当接收用户输入的同时,保持其他任务在后台运行。一个有关的应用场景是,将 I/O 和计算运行在两个并行的线程中。

以下代码展示了高阶的 threading 模块如何在后台运行任务,且不影响主程序的继续运行:

import threading, zipfile
class AsyncZip(threading.Thread):
    def __init__(self, infile, outfile):
        threading.Thread.__init__(self)
        self.infile = infile
        self.outfile = outfile
    def run(self):
        f = zipfile.ZipFile(self.outfile, 'w', zipfile.ZIP_DEFLATED)
        f.write(self.infile)
        f.close()
        print('Finished background zip of:', self.infile)
background = AsyncZip('mydata.txt', 'myarchive.zip')
background.start()
print('The main program continues to run in foreground.')
background.join()    # Wait for the background task to finish
print('Main program waited until background was done.')

多线程应用面临的主要挑战是,相互协调的多个线程之间需要共享数据或其他资源。为此,threading 模块提供了多个同步操作原语,包括线程锁、事件、条件变量和信号量。

尽管这些工具非常强大,但微小的设计错误却可以导致一些难以复现的问题。因此,实现多任务协作的首选方法是将所有对资源的请求集中到一个线程中,然后使用 queue 模块向该线程供应来自其他线程的请求。 应用程序使用 Queue 对象进行线程间通信和协调,更易于设计,更易读,更可靠。

日志记录

logging 模块提供功能齐全且灵活的日志记录系统。在最简单的情况下,日志消息被发送到文件或 sys.stderr

import logging
logging.debug('Debugging information')
logging.info('Informational message')
logging.warning('Warning:config file %s not found', 'server.conf')
logging.error('Error occurred')
logging.critical('Critical error -- shutting down')

这会产生以下输出:

WARNING:root:Warning:config file server.conf not found
ERROR:root:Error occurred
CRITICAL:root:Critical error -- shutting down

默认情况下,informational 和 debugging 消息被压制,输出会发送到标准错误流。其他输出选项包括将消息转发到电子邮件,数据报,套接字或 HTTP 服务器。新的过滤器可以根据消息优先级选择不同的路由方式:DEBUGINFOWARNINGERROR,和 CRITICAL

日志系统可以直接从 Python 配置,也可以从用户配置文件加载,以便自定义日志记录而无需更改应用程序。

弱引用

Python 会自动进行内存管理(对大多数对象进行引用计数并使用 garbage collection 来清除循环引用)。 当某个对象的最后一个引用被移除后不久就会释放其所占用的内存。

此方式对大多数应用来说都适用,但偶尔也必须在对象持续被其他对象所使用时跟踪它们。 不幸的是,跟踪它们将创建一个会令其永久化的引用。 weakref 模块提供的工具可以不必创建引用就能跟踪对象。 当对象不再需要时,它将自动从一个弱引用表中被移除,并为弱引用对象触发一个回调。 典型应用包括对创建开销较大的对象进行缓存:

>>> import weakref, gc
>>> class A:
...     def __init__(self, value):
...         self.value = value
...     def __repr__(self):
...         return str(self.value)
...
>>> a = A(10)                   # create a reference
>>> d = weakref.WeakValueDictionary()
>>> d['primary'] = a            # does not create a reference
>>> d['primary']                # fetch the object if it is still alive
10
>>> del a                       # remove the one reference
>>> gc.collect()                # run garbage collection right away
0
>>> d['primary']                # entry was automatically removed
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    d['primary']                # entry was automatically removed
  File "C:/python310/lib/weakref.py", line 46, in __getitem__
    o = self.data[key]()
KeyError: 'primary'

用于操作列表的工具

许多对于数据结构的需求可以通过内置列表类型来满足。 但是,有时也会需要具有不同效费比的替代实现。

array 模块提供了一种 array() 对象,它类似于列表,但只能存储类型一致的数据且存储密集更高。 下面的例子演示了一个以两个字节为存储单元的无符号二进制数值的数组 (类型码为 "H"),而对于普通列表来说,每个条目存储为标准 Python 的 int 对象通常要占用16 个字节:

>>> from array import array
>>> a = array('H', [4000, 10, 700, 22222])
>>> sum(a)
26932
>>> a[1:3]
array('H', [10, 700])

collections 模块提供了一种 deque() 对象,它类似于列表,但从左端添加和弹出的速度较快,而在中间查找的速度较慢。 此种对象适用于实现队列和广度优先树搜索:

>>> from collections import deque
>>> d = deque(["task1", "task2", "task3"])
>>> d.append("task4")
>>> print("Handling", d.popleft())
Handling task1

unsearched = deque([starting_node])
def breadth_first_search(unsearched):
    node = unsearched.popleft()
    for m in gen_moves(node):
        if is_goal(m):
            return m
        unsearched.append(m)

在替代的列表实现以外,标准库也提供了其他工具,例如 bisect 模块具有用于操作有序列表的函数:

>>> import bisect
>>> scores = [(100, 'perl'), (200, 'tcl'), (400, 'lua'), (500, 'python')]
>>> bisect.insort(scores, (300, 'ruby'))
>>> scores
[(100, 'perl'), (200, 'tcl'), (300, 'ruby'), (400, 'lua'), (500, 'python')]

heapq 模块提供了基于常规列表来实现堆的函数。 最小值的条目总是保持在位置零。 这对于需要重复访问最小元素而不希望运行完整列表排序的应用来说非常有用:

>>> from heapq import heapify, heappop, heappush
>>> data = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0]
>>> heapify(data)                      # rearrange the list into heap order
>>> heappush(data, -5)                 # add a new entry
>>> [heappop(data) for i in range(3)]  # fetch the three smallest entries
[-5, 0, 1]

十进制浮点运算

decimal 模块提供了一种 Decimal 数据类型用于十进制浮点运算。 相比内置的 float 二进制浮点实现,该类特别适用于

  • 财务应用和其他需要精确十进制表示的用途,
  • 控制精度,
  • 控制四舍五入以满足法律或监管要求,
  • 跟踪有效小数位,或
  • 用户期望结果与手工完成的计算相匹配的应用程序。

例如,使用十进制浮点和二进制浮点数计算70美分手机和5%税的总费用,会产生的不同结果。如果结果四舍五入到最接近的分数差异会更大:

>>> from decimal import *
>>> round(Decimal('0.70') * Decimal('1.05'), 2)
Decimal('0.74')
>>> round(.70 * 1.05, 2)
0.73

Decimal 表示的结果会保留尾部的零,并根据具有两个有效位的被乘数自动推出四个有效位。 Decimal 可以模拟手工运算来避免当二进制浮点数无法精确表示十进制数时会导致的问题。

精确表示特性使得 Decimal 类能够执行对于二进制浮点数来说不适用的模运算和相等性检测:

>>> Decimal('1.00') % Decimal('.10')
Decimal('0.00')
>>> 1.00 % 0.10
0.09999999999999995
>>> sum([Decimal('0.1')]*10) == Decimal('1.0')
True
>>> sum([0.1]*10) == 1.0
False

decimal 模块提供了运算所需要的足够精度:

>>> getcontext().prec = 36
>>> Decimal(1) / Decimal(7)
Decimal('0.142857142857142857142857142857142857')

10. 虚拟环境和包

概述

Python应用程序通常会使用不在标准库内的软件包和模块。应用程序有时需要特定版本的库,因为应用程序可能需要修复特定的错误,或者可以使用库的过时版本的接口编写应用程序。

这意味着一个Python安装可能无法满足每个应用程序的要求。如果应用程序A需要特定模块的1.0版本但应用程序B需要2.0版本,则需求存在冲突,安装版本1.0或2.0将导致某一个应用程序无法运行。

这个问题的解决方案是创建一个 virtual environment,一个目录树,其中安装有特定Python版本,以及许多其他包。

然后,不同的应用将可以使用不同的虚拟环境。 要解决先前需求相冲突的例子,应用程序 A 可以拥有自己的 安装了 1.0 版本的虚拟环境,而应用程序 B 则拥有安装了 2.0 版本的另一个虚拟环境。 如果应用程序 B 要求将某个库升级到 3.0 版本,也不会影响应用程序 A 的环境。

创建虚拟环境

用于创建和管理虚拟环境的模块称为 venvvenv 通常会安装你可用的最新版本的 Python。如果您的系统上有多个版本的 Python,您可以通过运行 python3 或您想要的任何版本来选择特定的Python版本。

要创建虚拟环境,请确定要放置它的目录,并将 venv 模块作为脚本运行目录路径:

python3 -m venv tutorial-env

这将创建 tutorial-env 目录,如果它不存在的话,并在其中创建包含 Python 解释器副本和各种支持文件的目录。

虚拟环境的常用目录位置是 .venv。 这个名称通常会令该目录在你的终端中保持隐藏,从而避免需要对所在目录进行额外解释的一般名称。 它还能防止与某些工具所支持的 .env 环境变量定义文件发生冲突。

创建虚拟环境后,您可以激活它。

在Windows上,运行:

tutorial-env\Scripts\activate.bat

在Unix或MacOS上,运行:

source tutorial-env/bin/activate

(这个脚本是为bash shell编写的。如果你使用 cshfish shell,你应该改用 activate.cshactivate.fish 脚本。)

激活虚拟环境将改变你所用终端的提示符,以显示你正在使用的虚拟环境,并修改环境以使 python 命令所运行的将是已安装的特定 Python 版本。 例如:

$ source ~/envs/tutorial-env/bin/activate
(tutorial-env) $ python
Python 3.5.1 (default, May  6 2016, 10:59:36)
  ...
>>> import sys
>>> sys.path
['', '/usr/local/lib/python35.zip', ...,
'~/envs/tutorial-env/lib/python3.5/site-packages']
>>>

使用pip管理包

你可以使用一个名为 pip 的程序来安装、升级和移除软件包。 默认情况下 pip 将从 Python Package Index https://pypi.org安装软件包。 你可以在你的 web 浏览器中查看 Python Package Index。

pip 有许多子命令: “install”, “uninstall”, “freeze” 等等。

您可以通过指定包的名称来安装最新版本的包:

(tutorial-env) $ python -m pip install novas
Collecting novas
  Downloading novas-3.1.1.3.tar.gz (136kB)
Installing collected packages: novas
  Running setup.py install for novas
Successfully installed novas-3.1.1.3

您还可以通过提供包名称后跟 == 和版本号来安装特定版本的包:

(tutorial-env) $ python -m pip install requests==2.6.0
Collecting requests==2.6.0
  Using cached requests-2.6.0-py2.py3-none-any.whl
Installing collected packages: requests
Successfully installed requests-2.6.0

如果你重新运行这个命令,pip 会注意到已经安装了所请求的版本并且什么都不做。您可以提供不同的版本号来获取该版本,或者您可以运行 pip install --upgrade 将软件包升级到最新版本:

(tutorial-env) $ python -m pip install --upgrade requests
Collecting requests
Installing collected packages: requests
  Found existing installation: requests 2.6.0
    Uninstalling requests-2.6.0:
      Successfully uninstalled requests-2.6.0
Successfully installed requests-2.7.0

pip uninstall 后跟一个或多个包名称将从虚拟环境中删除包。

pip show 将显示有关特定包的信息:

(tutorial-env) $ pip show requests
---
Metadata-Version: 2.0
Name: requests
Version: 2.7.0
Summary: Python HTTP for Humans.
Home-page: http://python-requests.org
Author: Kenneth Reitz
Author-email: me@kennethreitz.com
License: Apache 2.0
Location: /Users/akuchling/envs/tutorial-env/lib/python3.4/site-packages
Requires:

pip list 将显示虚拟环境中安装的所有软件包:

(tutorial-env) $ pip list
novas (3.1.1.3)
numpy (1.9.2)
pip (7.0.3)
requests (2.7.0)
setuptools (16.0)

pip freeze 将生成一个类似的已安装包列表,但输出使用 pip install 期望的格式。一个常见的约定是将此列表放在 requirements.txt 文件中:

(tutorial-env) $ pip freeze > requirements.txt
(tutorial-env) $ cat requirements.txt
novas==3.1.1.3
numpy==1.9.2
requests==2.7.0

然后可以将 requirements.txt 提交给版本控制并作为应用程序的一部分提供。然后用户可以使用 install -r 安装所有必需的包:

(tutorial-env) $ python -m pip install -r requirements.txt
Collecting novas==3.1.1.3 (from -r requirements.txt (line 1))
  ...
Collecting numpy==1.9.2 (from -r requirements.txt (line 2))
  ...
Collecting requests==2.7.0 (from -r requirements.txt (line 3))
  ...
Installing collected packages: novas, numpy, requests
  Running setup.py install for novas
Successfully installed novas-3.1.1.3 numpy-1.9.2 requests-2.7.0

11. 交互式编辑和编辑历史

某些版本的 Python 解释器支持编辑当前输入行和编辑历史记录,类似 Korn shell 和 GNU Bash shell 的功能 。这个功能使用了 GNU Readline 来实现,一个支持多种编辑方式的库。这个库有它自己的文档,在这里我们就不重复说明了。

Tab 补全和编辑历史

在解释器启动的时候,补全变量和模块名的功能将 自动打开,以便在按下 Tab 键的时候调用补全函数。它会查看 Python 语句名称,当前局部变量和可用的模块名称。处理像 string.a 的表达式,它会求值在最后一个 '.' 之前的表达式,接着根据求值结果对象的属性给出补全建议。如果拥有 __getattr__() 方法的对象是表达式的一部分,注意这可能会执行程序定义的代码。默认配置下会把编辑历史记录保存在用户目录下名为 .python_history 的文件。在下一次 Python 解释器会话期间,编辑历史记录仍旧可用。

默认交互式解释器的替代品

Python 解释器与早期版本的相比,向前迈进了一大步;无论怎样,还有些希望的功能:如果能在编辑连续行时建议缩进(解析器知道接下来是否需要缩进符号),那将很棒。补全机制可以使用解释器的符号表。有命令去检查(甚至建议)括号,引号以及其他符号是否匹配。

一个可选的增强型交互式解释器是 IPython,它已经存在了有一段时间,它具有 tab 补全,探索对象和高级历史记录管理功能。它还可以彻底定制并嵌入到其他应用程序中。另一个相似的增强型交互式环境是 bpython

12. 浮点算术:争议和限制

浮点数在计算机硬件中表示为以 2 为基数(二进制)的小数。举例而言,十进制的小数

0.125

等于 1/10 + 2/100 + 5/1000 ,同理,二进制的小数

0.001

等于0/2 + 0/4 + 1/8。这两个小数具有相同的值,唯一真正的区别是第一个是以 10 为基数的小数表示法,第二个则是 2 为基数。

不幸的是,大多数的十进制小数都不能精确地表示为二进制小数。这导致在大多数情况下,你输入的十进制浮点数都只能近似地以二进制浮点数形式储存在计算机中。

用十进制来理解这个问题显得更加容易一些。考虑分数 1/3 。我们可以得到它在十进制下的一个近似值

0.3

或者,更近似的,:

0.33

或者,更近似的,:

0.333

以此类推。结果是无论你写下多少的数字,它都永远不会等于 1/3 ,只是更加更加地接近 1/3 。

同样的道理,无论你使用多少位以 2 为基数的数码,十进制的 0.1 都无法精确地表示为一个以 2 为基数的小数。 在以 2 为基数的情况下, 1/10 是一个无限循环小数

0.0001100110011001100110011001100110011001100110011...

在任何一个位置停下,你都只能得到一个近似值。因此,在今天的大部分架构上,浮点数都只能近似地使用二进制小数表示,对应分数的分子使用每 8 字节的前 53 位表示,分母则表示为 2 的幂次。在 1/10 这个例子中,相应的二进制分数是 3602879701896397 / 2 ** 55 ,它很接近 1/10 ,但并不是 1/10 。

大部分用户都不会意识到这个差异的存在,因为 Python 只会打印计算机中存储的二进制值的十进制近似值。在大部分计算机中,如果 Python 想把 0.1 的二进制对应的精确十进制打印出来,将会变成这样

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

这比大多数人认为有用的数字更多,因此Python通过显示舍入值来保持可管理的位数

>>> 1 / 10
0.1

牢记,即使输出的结果看起来好像就是 1/10 的精确值,实际储存的值只是最接近 1/10 的计算机可表示的二进制分数。

有趣的是,有许多不同的十进制数共享相同的最接近的近似二进制小数。例如, 0.10.100000000000000010.1000000000000000055511151231257827021181583404541015625 全都近似于 3602879701896397 / 2 ** 55 。由于所有这些十进制值都具有相同的近似值,因此可以显示其中任何一个,同时仍然保留不变的 eval(repr(x)) == x

在历史上,Python 提示符和内置的 repr() 函数会选择具有 17 位有效数字的来显示,即 0.10000000000000001。 从 Python 3.1 开始,Python(在大多数系统上)现在能够选择这些表示中最短的并简单地显示 0.1

请注意这种情况是二进制浮点数的本质特性:它不是 Python 的错误,也不是你代码中的错误。 你会在所有支持你的硬件中的浮点运算的语言中发现同样的情况(虽然某些语言在默认状态或所有输出模块下都不会 显示 这种差异)。

想要更美观的输出,你可能会希望使用字符串格式化来产生限定长度的有效位数:

>>> format(math.pi, '.12g')  # give 12 significant digits
'3.14159265359'
>>> format(math.pi, '.2f')   # give 2 digits after the point
'3.14'
>>> repr(math.pi)
'3.141592653589793'

必须重点了解的是,这在实际上只是一个假象:你只是将真正的机器码值进行了舍入操作再 显示 而已。

一个假象还可能导致另一个假象。 例如,由于这个 0.1 并非真正的 1/10,将三个 0.1 的值相加也不一定能恰好得到 0.3:

>>> .1 + .1 + .1 == .3
False

而且,由于这个 0.1 无法精确表示 1/10 的值而这个 0.3 也无法精确表示 3/10 的值,使用 round() 函数进行预先舍入也是没用的:

>>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)
False

虽然这些小数无法精确表示其所要代表的实际值,round() 函数还是可以用来“事后舍入”,使得实际的结果值可以做相互比较:

>>> round(.1 + .1 + .1, 10) == round(.3, 10)
True

二进制浮点运算会造成许多这样的“意外”。

正如那篇文章的结尾所言,“对此问题并无简单的答案。” 但是也不必过于担心浮点数的问题! Python 浮点运算中的错误是从浮点运算硬件继承而来,而在大多数机器上每次浮点运算得到的 2**53 数码位都会被作为 1 个整体来处理。 这对大多数任务来说都已足够,但你确实需要记住它并非十进制算术,且每次浮点运算都可能会导致新的舍入错误。

虽然病态的情况确实存在,但对于大多数正常的浮点运算使用来说,你只需简单地将最终显示的结果舍入为你期望的十进制数值即可得到你期望的结果。 str() 通常已足够。

对于需要精确十进制表示的使用场景,请尝试使用 decimal 模块,该模块实现了适合会计应用和高精度应用的十进制运算。

另一种形式的精确运算由 fractions 模块提供支持,该模块实现了基于有理数的算术运算(因此可以精确表示像 1/3 这样的数值)。

如果你是浮点运算的重度用户则你应当了解一下 NumPy 包以及由 SciPy 项目所提供的许多其他数字和统计运算包。 参见 https://scipy.org

Python 也提供了一些工具,可以在你真的 想要 知道一个浮点数精确值的少数情况下提供帮助。 例如 float.as_integer_ratio() 方法会将浮点数表示为一个分数:

>>> x = 3.14159
>>> x.as_integer_ratio()
(3537115888337719, 1125899906842624)

由于这是一个精确的比值,它可以被用来无损地重建原始值:

>>> x == 3537115888337719 / 1125899906842624
True

float.hex() 方法会以十六进制(以 16 为基数)来表示浮点数,同样能给出保存在你的计算机中的精确值:

>>> x.hex()
'0x1.921f9f01b866ep+1'

这种精确的十六进制表示法可被用来精确地重建浮点值:

>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True

由于这种表示法是精确的,它适用于跨越不同版本(平台无关)的 Python 移植数值,以及与支持相同格式的其他语言(例如 Java 和 C99)交换数据.

另一个有用的工具是 math.fsum() 函数,它有助于减少求和过程中的精度损失。 它会在数值被添加到总计值的时候跟踪“丢失的位”。 这可以很好地保持总计值的精确度, 使得错误不会积累到能影响结果总数的程度:

>>> sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True

表示性错误

本小节将详细解释 “0.1” 的例子,并说明你可以怎样亲自对此类情况进行精确分析。 假定前提是已基本熟悉二进制浮点表示法。

表示性错误 是指某些(其实是大多数)十进制小数无法以二进制(以 2 为基数的计数制)精确表示这一事实造成的错误。 这就是为什么 Python(或者 Perl、C、C++、Java、Fortran 以及许多其他语言)经常不会显示你所期待的精确十进制数值的主要原因。

为什么会这样? 1/10 是无法用二进制小数精确表示的。 目前(2000年11月)几乎所有使用 IEEE-754 浮点运算标准的机器以及几乎所有系统平台都会将 Python 浮点数映射为 IEEE-754 “双精度类型”。 754 双精度类型包含 53 位精度,因此在输入时,计算会尽量将 0.1 转换为以 J/2**N 形式所能表示的最接近分数,其中 J 为恰好包含 53 个二进制位的整数。 重新将

1 / 10 ~= J / (2**N)

写为

J ~= 2**N / 10

并且由于 J 恰好有 53 位 (即 >= 2**52< 2**53),N 的最佳值为 56:

>>> 2**52 <=  2**56 // 10  < 2**53
True

也就是说,56 是唯一的 N 值能令 J 恰好有 53 位。 这样 J 的最佳可能值就是经过舍入的商:

>>> q, r = divmod(2**56, 10)
>>> r
6

由于余数超过 10 的一半,最佳近似值可通过四舍五入获得:

>>> q+1
7205759403792794

这样在 754 双精度下 1/10 的最佳近似值为:

7205759403792794 / 2 ** 56

分子和分母都除以二则结果小数为:

3602879701896397 / 2 ** 55

请注意由于我们做了向上舍入,这个结果实际上略大于 1/10;如果我们没有向上舍入,则商将会略小于 1/10。 但无论如何它都不会是 精确的 1/10!

因此计算永远不会“看到”1/10:它实际看到的就是上面所给出的小数,它所能达到的最佳 754 双精度近似值:

>>> 0.1 * 2 ** 55
3602879701896397.0

如果我们将该小数乘以 10**55,我们可以看到该值输出为 55 位的十进制数:

>>> 3602879701896397 * 10 ** 55 // 2 ** 55
1000000000000000055511151231257827021181583404541015625

这意味着存储在计算机中的确切数值等于十进制数值 0.1000000000000000055511151231257827021181583404541015625。 许多语言(包括较旧版本的 Python)都不会显示这个完整的十进制数值,而是将结果舍入为 17 位有效数字:

>>> format(0.1, '.17f')
'0.10000000000000001'

fractionsdecimal 模块可令进行此类计算更加容易:

>>> from decimal import Decimal
>>> from fractions import Fraction
>>> Fraction.from_float(0.1)
Fraction(3602879701896397, 36028797018963968)
>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)
>>> Decimal.from_float(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> format(Decimal.from_float(0.1), '.17')
'0.10000000000000001'

13. 交互模式

错误处理

当发生错误时,解释器会打印错误信息和错误堆栈。在交互模式下,将返回到主命令提示符;如果输入内容来自文件,在打印错误堆栈之后,程序会以非零状态退出。(这里所说的错误不包括 try 语句中由 except 所捕获的异常。)有些错误是无条件致命的,会导致程序以非零状态退出;比如内部逻辑矛盾或内存耗尽。所有错误信息都会被写入标准错误流;而命令的正常输出则被写入标准输出流。

将中断字符(通常为 Control-C 或 Delete )键入主要或辅助提示会取消输入并返回主提示符。 在执行命令时键入中断引发的 KeyboardInterrupt 异常,可以由 try 语句处理。

可执行的Python脚本

在BSD等类Unix系统上,Python脚本可以直接执行,就像shell脚本一样,第一行添加:

#!/usr/bin/env python3.5

(假设解释器位于用户的 PATH )脚本的开头,并将文件设置为可执行。 #! 必须是文件的前两个字符。在某些平台上,第一行必须以Unix样式的行结尾('\n')结束,而不是以Windows('\r\n')行结尾。请注意,散列或磅字符 '#' 在Python中代表注释开始。

可以使用 chmod 命令为脚本提供可执行模式或权限。

$ chmod +x myscript.py

在Windows系统上,没有“可执行模式”的概念。 Python安装程序自动将 .py 文件与 python.exe 相关联,这样双击Python文件就会将其作为脚本运行。扩展也可以是 .pyw ,在这种情况下,会隐藏通常出现的控制台窗口。

交互式启动文件

当您以交互方式使用Python时,每次启动解释器时都会执行一些标准命令,这通常很方便。您可以通过将名为 PYTHONSTARTUP 的环境变量设置为包含启动命令的文件名来实现。这类似于Unix shell的 .profile 功能。

此文件只会在交互式会话时读取,而非在 Python 从脚本读取指令或是在给定 /dev/tty 为指令的明确来源时(后者反而表现得像是一个交互式会话)。 该文件执行时所在的命名空间与交互式指令相同,所以它定义或导入的对象可以在交互式会话中直接使用。 你也可以在该文件中更改提示符 sys.ps1sys.ps2

如果你想从当前目录中读取一个额外的启动文件,你可以使用像 if os.path.isfile('.pythonrc.py'): exec(open('.pythonrc.py').read()) 这样的代码在全局启动文件中对它进行编程。如果要在脚本中使用启动文件,则必须在脚本中显式执行此操作:

import os
filename = os.environ.get('PYTHONSTARTUP')
if filename and os.path.isfile(filename):
    with open(filename) as fobj:
        startup_file = fobj.read()
    exec(startup_file)

定制模块

Python提供了两个钩子来让你自定义它:sitecustomizeusercustomize。要查看其工作原理,首先需要找到用户site-packages目录的位置。启动Python并运行此代码:

>>> import site
>>> site.getusersitepackages()
'/home/user/.local/lib/python3.5/site-packages'

现在,您可以在该目录中创建一个名为 usercustomize.py 的文件,并将所需内容放入其中。它会影响Python的每次启动,除非它以 -s 选项启动,以禁用自动导入。

sitecustomize 以相同的方式工作,但通常由计算机管理员在全局 site-packages 目录中创建,并在 usercustomize 之前被导入。

Python 语言参考手册

1. 概述

我希望尽可能地保证内容精确无误,但还是选择使用自然词句进行描述,正式的规格定义仅用于句法和词法解析。这样应该能使文档对于普通人来说更易理解,但也可能导致一些歧义。因此,如果你是来自火星并且想凭借这份文档把 Python 重新实现一遍,也许有时需要自行猜测,实际上最终大概会得到一个十分不同的语言。而在另一方面,如果你正在使用 Python 并且想了解有关该语言特定领域的精确规则,你应该能够在这里找到它们。如果你希望查看对该语言更正式的定义,也许你可以花些时间自己写上一份 —- 或者发明一台克隆机器 :-)

在语言参考文档里加入过多的实现细节是很危险的 —- 具体实现可能发生改变,对同一语言的其他实现可能使用不同的方式。而在另一方面,CPython 是得到广泛使用的 Python 实现 (然而其他一些实现的拥护者也在增加),其中的特殊细节有时也值得一提,特别是当其实现方式导致额外的限制时。因此,你会发现在正文里不时会跳出来一些简短的 “实现注释”。

每种 Python 实现都带有一些内置和标准的模块。少数内置模块也会在此提及,如果它们同语言描述存在明显的关联。

1.1. 其他实现

虽然官方 Python 实现差不多得到最广泛的欢迎,但也有一些其他实现对特定领域的用户来说更具吸引力。

知名的实现包括:

CPython

这是最早出现并持续维护的 Python 实现,以 C 语言编写。新的语言特性通常在此率先添加。

Jython

以 Java 语言编写的 Python 实现。此实现可以作为 Java 应用的一个脚本语言,或者可以用来创建需要 Java 类库支持的应用。想了解更多信息可访问 Jython 网站

Python for .NET

此实现实际上使用了 CPython 实现,但是属于 .NET 托管应用并且可以引入 .NET 类库。它的创造者是 Brian Lloyd。想了解详情可访问 Python for .NET 主页

IronPython

另一个 .NET 的 Python 实现,与 Python.NET 不同点在于它是生成 IL 的完全 Python 实现,并且将 Python 代码直接编译为 .NET 程序集。它的创造者就是当初创造 Jython 的 Jim Hugunin。想了解详情可访问 IronPython 网站

PyPy

完全使用 Python 语言编写的 Python 实现。它支持多个其他实现所没有的高级特性,例如非栈式支持和 JIT 编译器等。此项目的目标之一是通过允许方便地修改解释器 (因为它是用 Python 编写的),鼓励该对语言本身进行试验。想了解详情可访问 PyPy 项目主页

以上这些实现都可能在某些方面与此参考文档手册的描述有所差异,或是引入了超出标准 Python 文档范围的特定信息。请参考它们各自的专门文档,以确定你正在使用的这个实现有哪些你需要了解的东西。

1.2. 标注

句法和词法解析的描述采用经过改进的 BNF 语法标注。这包含以下定义样式:

name      ::=  lc_letter (lc_letter | "_")*
lc_letter ::=  "a"..."z"

第一行表示 namelc_letter 之后跟零个或多个 lc_letter 和下划线。而 lc_letter 则是任意单个 'a''z' 字符。(实际上在本文档中始终采用此规则来定义词法和语法规则的名称。)

每条规则的开头是一个名称 (即该规则所定义的名称) 加上 ::=。 竖线 (|) 被用来分隔可选项,它是此标注中绑定程度最低的操作符。 星号 (*) 表示前一项的零次或多次重复,类似地,加号 (+) 表示一次或多次重复,而由方括号括起的内容 ([ ]) 表示出现零次或一次 (或者说,这部分内容是可选的)。 *+ 操作符的绑定是最紧密的,圆括号用于分组。 字符串字面值包含在引号内。 空格的作用仅限于分隔形符。 每条规则通常为一行,有许多个可选项的规则可能会以竖线为界分为多行。

在词法定义中 (如上述示例),还额外使用了两个约定: 由三个点号分隔的两个字符字面值表示在指定 (闭) 区间范围内的任意单个 ASCII 字符。由尖括号 (<...>) 括起来的内容是对于所定义符号的非正式描述;即可以在必要时用来说明 ‘控制字符’ 的意图。

虽然所用的标注方式几乎相同,但是词法定义和句法定义是存在很大区别的: 词法定义作用于输入源中单独的字符,而句法定义则作用于由词法分析所生成的形符流。在下一章节 (“词法分析”) 中使用的 BNF 全部都是词法定义;在之后的章节中使用的则是句法定义。

2. 词法分析

Python 程序由 解析器 读取,输入解析器的是 词法分析器 生成的 形符 流。本章介绍词法分析器怎样把文件拆成形符。

Python 将读取的程序文本转为 Unicode 代码点;编码声明用于指定源文件的编码,默认为 UTF-8,详见 PEP 3120。源文件不能解码时,触发 SyntaxError

2.1. 行结构

Python 程序可以拆分为多个 逻辑行

2.1.1. 逻辑行

NEWLINE 形符表示结束逻辑行。语句不能超出逻辑行的边界,除非句法支持 NEWLINE (例如,复合语句中的多行子语句)。根据显式或隐式 行拼接 规则,一个或多个 物理行 可组成逻辑行。

2.1.2. 物理行

物理行是一序列字符,由行尾序列终止。源文件和字符串可使用任意标准平台行终止序列 - Unix ASCII 字符 LF (换行)、 Windows ASCII 字符序列 CR LF (回车换行)、或老式 Macintosh ASCII 字符 CR (回车)。不管在哪个平台,这些形式均可等价使用。输入结束也可以用作最终物理行的隐式终止符。

嵌入 Python 时,传入 Python API 的源码字符串应使用 C 标准惯例换行符(\n,代表 ASCII 字符 LF, 行终止符)。

2.1.3. 注释

注释以井号 (#) 开头,在物理行末尾截止。注意,井号不是字符串字面值。除非应用隐式行拼接规则,否则,注释代表逻辑行结束。句法不解析注释。

2.1.4. 编码声明

Python 脚本第一或第二行的注释匹配正则表达式 coding[=:]\s*([-\w.]+) 时,该注释会被当作编码声明;这个表达式的第一组指定了源码文件的编码。编码声明必须独占一行,在第二行时,则第一行必须也是注释。编码表达式的形式如下:

# -*- coding: <encoding-name> -*-

这也是 GNU Emacs 认可的形式,此外,还支持如下形式:

# vim:fileencoding=<encoding-name>

这是 Bram Moolenaar 的 VIM 认可的形式。

没有编码声明时,默认编码为 UTF-8。此外,如果文件的首字节为 UTF-8 字节顺序标志(b'\xef\xbb\xbf'),文件编码也声明为 UTF-8(这是 Microsoft 的 notepad 等软件支持的形式)。

声明的编码名称必须是 Python 能识别的。语义字符串、注释和标识符等词法分析都使用此编码。

2.1.5. 显式拼接行

两个及两个以上的物理行可用反斜杠(\)拼接为一个逻辑行,规则如下:以不在字符串或注释内的反斜杠结尾时,物理行将与下一行拼接成一个逻辑行,并删除反斜杠及其后的换行符。例如:

if 1900 < year < 2100 and 1 <= month <= 12 \
   and 1 <= day <= 31 and 0 <= hour < 24 \
   and 0 <= minute < 60 and 0 <= second < 60:   # Looks like a valid date
        return 1

以反斜杠结尾的行,不能加注释;反斜杠也不能拼接注释。除字符串字面值外,反斜杠不能拼接形符(如,除字符串字面值外,不能用反斜杠把形符切分至两个物理行)。反斜杠只能在代码的字符串字面值里,在其他任何位置都是非法的。

2.1.6. 隐式拼接行

圆括号、方括号、花括号内的表达式可以分成多个物理行,不必使用反斜杠。例如:

month_names = ['Januari', 'Februari', 'Maart',      # These are the
               'April',   'Mei',      'Juni',       # Dutch names
               'Juli',    'Augustus', 'September',  # for the months
               'Oktober', 'November', 'December']   # of the year

隐式行拼接可含注释;后续行的缩进并不重要;还支持空的后续行。隐式拼接行之间没有 NEWLINE 形符。三引号字符串支持隐式拼接行(见下文),但不支持注释。

2.1.7. 空白行

只包含空格符、制表符、换页符、注释的逻辑行会被忽略(即不生成 NEWLINE 形符)。交互模式输入语句时,空白行的处理方式可能因读取 - 求值 - 打印循环(REPL)的具体实现方式而不同。标准交互模式解释器中,完全空白的逻辑行(即连空格或注释都没有)将结束多行复合语句。

2.1.8. 缩进

逻辑行开头的空白符(空格符和制表符)用于计算该行的缩进层级,决定语句组块。

制表符(从左至右)被替换为一至八个空格,缩进空格的总数是八的倍数(与 Unix 的规则保持一致)。首个非空字符前的空格数决定了该行的缩进层次。缩进不能用反斜杠进行多行拼接;首个反斜杠之前的空白符决定了缩进的层次。

源文件混用制表符和空格符缩进时,因空格数量与制表符相关,由此产生的不一致将导致不能正常识别缩进层次,从而触发 TabError

跨平台兼容性说明: 鉴于非 UNIX 平台文本编辑器本身的特性,请勿在源文件中混用制表符和空格符。另外也请注意,不同平台有可能会显式限制最大缩进层级。

行首含换页符时,缩进计算将忽略该换页符。换页符在行首空白符内其他位置的效果未定义(例如,可能导致空格计数重置为零)。

连续行的缩进层级以堆栈形式生成 INDENT 和 DEDENT 形符,说明如下。

读取文件第一行前,先向栈推入一个零值,该零值不会被移除。推入栈的层级值从底至顶持续增加。每个逻辑行开头的行缩进层级将与栈顶行比较。如果相等,则不做处理。如果新行层级较高,则会被推入栈顶,并生成一个 INDENT 形符。如果新行层级较低,则 应当 是栈中的层级数值之一;栈中高于该层级的所有数值都将被移除,每移除一级数值生成一个 DEDENT 形符。文件末尾,栈中剩余的每个大于零的数值生成一个 DEDENT 形符。

下面的 Python 代码缩进示例虽然正确,但含混不清:

def perm(l):
        # Compute the list of all permutations of l
    if len(l) <= 1:
                  return [l]
    r = []
    for i in range(len(l)):
             s = l[:i] + l[i+1:]
             p = perm(s)
             for x in p:
              r.append(l[i:i+1] + x)
    return r

下例展示了多种缩进错误:

 def perm(l):                       # error: first line indented
for i in range(len(l)):             # error: not indented
    s = l[:i] + l[i+1:]
        p = perm(l[:i] + l[i+1:])   # error: unexpected indent
        for x in p:
                r.append(l[i:i+1] + x)
            return r                # error: inconsistent dedent

(实际上,解析器可以识别前三个错误;只有最后一个错误由词法分析器识别 —- return r 的缩进无法匹配从栈里移除的缩进层级。)

2.1.9. 形符间的空白字符

除非在逻辑行开头或字符串内,空格符、制表符、换页符等空白符都可以分隔形符。要把两个相连形符解读为不同形符,需要用空白符分隔(例如,ab 是一个形符,a b 则是两个形符)。

2.2. 其他形符

除 NEWLINE、INDENT、DEDENT 外,还有 标识符关键字字面值运算符分隔符 等形符。 空白符(前述的行终止符除外)不是形符,可用于分隔形符。存在二义性时,将从左至右,读取尽量长的字符串组成合法形符。

2.3. 标识符和关键字

标识符(也称为 名称)的词法定义说明如下。

Python 标识符的句法基于 Unicode 标准附件 UAX-31,并加入了下文定义的细化与修改;详见 PEP 3131

与 Python 2.x 一样,在 ASCII 范围内(U+0001..U+007F),有效标识符字符为: 大小写字母 AZ、下划线 _ 、数字 09,但不能以数字开头。

Python 3.0 引入了 ASCII 之外的更多字符(请参阅 PEP 3131)。这些字符的分类使用 unicodedata 模块中的 Unicode 字符数据库版本。

标识符的长度没有限制,但区分大小写。

identifier   ::=  xid_start xid_continue*
id_start     ::=  <all characters in general categories Lu, Ll, Lt, Lm, Lo, Nl, the underscore, and characters with the Other_ID_Start property>
id_continue  ::=  <all characters in id_start, plus characters in the categories Mn, Mc, Nd, Pc and others with the Other_ID_Continue property>
xid_start    ::=  <all characters in id_start whose NFKC normalization is in "id_start xid_continue*">
xid_continue ::=  <all characters in id_continue whose NFKC normalization is in "id_continue*">

上述 Unicode 类别码的含义:

  • Lu - 大写字母
  • Ll - 小写字母
  • Lt - 词首大写字母
  • Lm - 修饰符字母
  • Lo - 其他字母
  • Nl - 字母数字
  • Mn - 非空白标识
  • Mc - 含空白标识
  • Nd - 十进制数字
  • Pc - 连接标点
  • Other_ID_Start - 由 PropList.txt 定义的显式字符列表,用于支持向后兼容
  • Other_ID_Continue - 同上

在解析时,所有标识符都会被转换为规范形式 NFKC;标识符的比较都是基于 NFKC。

Unicode 4.1 中所有可用的标识符字符详见此 HTML 文件 https://www.unicode.org/Public/13.0.0/ucd/DerivedCoreProperties.txt

2.3.1. 关键字

以下标识符为保留字,或称 关键字,不可用于普通标识符。关键字的拼写必须与这里列出的完全一致:

False      await      else       import     pass
None       break      except     in         raise
True       class      finally    is         return
and        continue   for        lambda     try
as         def        from       nonlocal   while
assert     del        global     not        with
async      elif       if         or         yield

2.3.2. 软关键字

3.10 新版功能.

某些标识符仅在特定上下文中被保留。 它们被称为 软关键字match, case_ 等标识符在模式匹配语句相关的上下文中具有相当于关键字的语义,但这种区分是在解析器层级完成,而不是在形符化的时候。

作为软关键字,它们能够与模式匹配一起使用,同时仍然保持与使用 match, case_ 作为标识符名称的现有代码的兼容性。

2.3.3. 保留的标识符类

某些标识符类(除了关键字)具有特殊含义。这些类的命名模式以下划线字符开头,并以下划线结尾:

_*

from module import * 时,不会导入。交互式解释器中,特殊标识符 _ 用于存储最近一次求值的结果;该标识符保存在 builtins 模块里。不处于交互模式时,_ 没有特殊含义,也没有预定义。

注解

_ 常用于连接国际化文本。

__*__

系统定义的名称,通常简称为 “dunder” 。这些名称由解释器及其实现(包括标准库)定义。Python 未来版本中还将定义更多此类名称。任何情况下,任何 不显式遵从 __*__ 名称的文档用法,都可能导致无警告提示的错误。

__*

类的私有名称。类定义时,此类名称以一种混合形式重写,以避免基类及派生类的 “私有” 属性之间产生名称冲突。

2.4. 字面值

字面值是内置类型常量值的表示法。

2.4.1. 字符串与字节串字面值

字符串字面值的词法定义如下:

stringliteral   ::=  [stringprefix](shortstring | longstring)
stringprefix    ::=  "r" | "u" | "R" | "U" | "f" | "F"
                     | "fr" | "Fr" | "fR" | "FR" | "rf" | "rF" | "Rf" | "RF"
shortstring     ::=  "'" shortstringitem* "'" | '"' shortstringitem* '"'
longstring      ::=  "'''" longstringitem* "'''" | '"""' longstringitem* '"""'
shortstringitem ::=  shortstringchar | stringescapeseq
longstringitem  ::=  longstringchar | stringescapeseq
shortstringchar ::=  <any source character except "\" or newline or the quote>
longstringchar  ::=  <any source character except "\">
stringescapeseq ::=  "\" <any source character>

bytesliteral   ::=  bytesprefix(shortbytes | longbytes)
bytesprefix    ::=  "b" | "B" | "br" | "Br" | "bR" | "BR" | "rb" | "rB" | "Rb" | "RB"
shortbytes     ::=  "'" shortbytesitem* "'" | '"' shortbytesitem* '"'
longbytes      ::=  "'''" longbytesitem* "'''" | '"""' longbytesitem* '"""'
shortbytesitem ::=  shortbyteschar | bytesescapeseq
longbytesitem  ::=  longbyteschar | bytesescapeseq
shortbyteschar ::=  <any ASCII character except "\" or newline or the quote>
longbyteschar  ::=  <any ASCII character except "\">
bytesescapeseq ::=  "\" <any ASCII character>

上述产生式没有说明以下句法限制,stringprefixbytesprefix 与其他字面值之间不允许有空白符。源字符集由编码声明定义;源文件没有编码声明时,默认为 UTF-8。

通俗地说:两种字面值都可以用单引号(')或双引号(") 标注。也可以用三个单引号或双引号标注(俗称 三引号字符串)。反斜杠(\)用于转义特殊字符,例如,换行符、反斜杠本身、引号等。

字节串字面值要加前缀 'b''B';生成的是类型 bytes 的实例,不是类型 str 的实例;字节串只能包含 ASCII 字符;字节串数值大于等于 128 时,必须用转义表示。

字符串和字节串都可以加前缀 'r''R',称为 原始字符串,原始字符串把反斜杠当作原义字符,不执行转义操作。因此,原始字符串不转义 '\U''\u'。与 Python 2.x 的原始 unicode 字面值操作不同,Python 3.x 现已不支持 'ur' 句法。

3.3 新版功能: 新增原始字节串 'rb' 前缀,是 'br' 的同义词。

3.3 新版功能: 支持 unicode 字面值(u'value')遗留代码,简化 Python 2.x 和 3.x 并行代码库的维护工作。详见 PEP 414

前缀为 'f''F' 的字符串称为 格式字符串'f' 可与 'r' 连用,但不能与 'b''u' 连用,因此,可以使用原始格式字符串,但不能使用格式字节串字面值。

三引号字面值可以包含未转义的换行和引号(原样保留),除了连在一起的,用于终止字面值的,未经转义的三个引号。(”引号” 是启用字面值的字符,可以是 ',也可以是 "。)

如未标注 'r''R' 前缀,字符串和字节串字面值中,转义序列以类似 C 标准的规则进行解释。可用的转义序列如下:

转义序列 含意 备注
\newline 忽略反斜杠与换行符
\ 反斜杠(\
\’ 单引号(
\” 双引号(
\a ASCII 响铃(BEL)
\b ASCII 退格符(BS)
\f ASCII 换页符(FF)
\n ASCII 换行符(LF)
\r ASCII 回车符(CR)
\t ASCII 水平制表符(TAB)
\v ASCII 垂直制表符(VT)
\ooo 八进制数 ooo 字符 (1,3)
\xhh 十六进制数 hh 字符 (2,3)

字符串字面值专用的转义序列:

转义序列 含意 备注
\N{name} Unicode 数据库中名为 name 的字符 (4)
\uxxxx 16 位十六进制数 xxxx 码位的字符 (5)
\Uxxxxxxxx 32 位 16 进制数 xxxxxxxx 码位的字符 (6)

注释:

  1. 与 C 标准一致,接受最多三个八进制数字。
  2. 与 C 标准不同,必须为两个十六进制数字。
  3. 字节串 字面值中,十六进制数和八进制数的转义码以相应数值代表每个字节。字符串 字面值中,这些转义码以相应数值代表每个 Unicode 字符。
  4. 在 3.3 版更改: 加入了对别名的支持
  5. 必须为 4 个十六进制数码。
  6. 表示任意 Unicode 字符。必须为 8 个十六进制数码。

与 C 标准不同,无法识别的转义序列在字符串里原样保留,即,输出结果保留反斜杠。(调试时,这种方式很有用:输错转义序列时,更容易在输出结果中识别错误。)注意,在字节串字面值内,字符串字面值专用的转义序列属于无法识别的转义序列。

在 3.6 版更改: 无法识别的转义序列触发 DeprecationWarning。未来的 Python 发行版将改为触发 SyntaxWarning,最终会改为触发 SyntaxError

即使在原始字面值中,引号也可以用反斜杠转义,但反斜杠会保留在输出结果里;例如 r"\"" 是由两个字符组成的有效字符串字面值:反斜杠和双引号;r"\" 则不是有效字符串字面值(原始字符串也不能以奇数个反斜杠结尾)。尤其是,原始字面值不能以单个反斜杠结尾 (反斜杠会转义其后的引号)。还要注意,反斜杠加换行在字面值中被解释为两个字符,而 不是 连续行。

2.4.2. 字符串字面值合并

以空白符分隔的多个相邻字符串或字节串字面值,可用不同引号标注,等同于合并操作。因此,"hello" 'world' 等价于 "helloworld"。此功能不需要反斜杠,即可将长字符串分为多个物理行,还可以为不同部分的字符串添加注释,例如:

re.compile("[A-Za-z_]"       # letter or underscore
           "[A-Za-z0-9_]*"   # letter, digit or underscore
          )

注意,此功能在句法层面定义,在编译时实现。在运行时,合并字符串表达式必须使用 ‘+’ 运算符。还要注意,字面值合并可以为每个部分应用不同的引号风格(甚至混用原始字符串和三引号字符串),格式字符串字面值也可以与纯字符串字面值合并。

2.4.3. 格式字符串字面值

3.6 新版功能.

格式字符串字面值 或称 f-string 是标注了 'f''F' 前缀的字符串字面值。这种字符串可包含替换字段,即以 {} 标注的表达式。其他字符串字面值只是常量,格式字符串字面值则是可在运行时求值的表达式。

除非字面值标记为原始字符串,否则,与在普通字符串字面值中一样,转义序列也会被解码。解码后,用于字符串内容的语法如下:

f_string          ::=  (literal_char | "{{" | "}}" | replacement_field)*
replacement_field ::=  "{" f_expression ["="] ["!" conversion] [":" format_spec] "}"
f_expression      ::=  (conditional_expression | "*" or_expr)
                         ("," conditional_expression | "," "*" or_expr)* [","]
                       | yield_expression
conversion        ::=  "s" | "r" | "a"
format_spec       ::=  (literal_char | NULL | replacement_field)*
literal_char      ::=  <any code point except "{", "}" or NULL>

双花括号 '` 或 `' 被替换为单花括号,花括号外的字符串仍按字面值处理。单左花括号 '{' 标记以 Python 表达式开头的替换字段。在表达式后加等于号 '=',可在求值后,同时显示表达式文本及其结果(用于调试)。 随后是用叹号 '!' 标记的转换字段。还可以在冒号 ':' 后附加格式说明符。替换字段以右花括号 '}' 为结尾。

格式字符串字面值中,表达式的处理与圆括号中的常规 Python 表达式基本一样,但也有一些不同的地方。不允许使用空表达式;lambda 和赋值表达式 := 必须显式用圆括号标注;替换表达式可以包含换行(例如,三引号字符串中),但不能包含注释;在格式字符串字面值语境内,按从左至右的顺序,为每个表达式求值。

在 3.7 版更改: Python 3.7 以前, 因为实现的问题,不允许在格式字符串字面值表达式中使用 await 表达式与包含 async for 子句的推导式。

表达式里含等号 '=' 时,输出内容包括表达式文本、'=' 、求值结果。输出内容可以保留表达式中左花括号 '{' 后,及 '=' 后的空格。没有指定格式时,'=' 默认调用表达式的 repr()。指定了格式时,默认调用表达式的 str(),除非声明了转换字段 '!r'

3.8 新版功能: 等号 '='

指定了转换符时,表达式求值的结果会先转换,再格式化。转换符 '!s' 调用 str() 转换求值结果,'!r' 调用 repr()'!a' 调用 ascii()

输出结果的格式化使用 format() 协议。格式说明符传入表达式或转换结果的 __format__() 方法。省略格式说明符,则传入空字符串。然后,格式化结果包含在整个字符串的最终值里。

顶层格式说明符可以包含嵌套替换字段。嵌套字段也可以包含自己的转换字段和 格式说明符,但不可再包含更深层嵌套的替换字段。格式说明符微语言 与 str.format() 方法使用的微语言相同。

格式化字符串字面值可以拼接,但是一个替换字段不能拆分到多个字面值。

格式字符串字面值示例如下:

>>> name = "Fred"
>>> f"He said his name is {name!r}."
"He said his name is 'Fred'."
>>> f"He said his name is {repr(name)}."  # repr() is equivalent to !r
"He said his name is 'Fred'."
>>> width = 10
>>> precision = 4
>>> value = decimal.Decimal("12.34567")
>>> f"result: {value:{width}.{precision}}"  # nested fields
'result:      12.35'
>>> today = datetime(year=2017, month=1, day=27)
>>> f"{today:%B %d, %Y}"  # using date format specifier
'January 27, 2017'
>>> f"{today=:%B %d, %Y}" # using date format specifier and debugging
'today=January 27, 2017'
>>> number = 1024
>>> f"{number:#0x}"  # using integer format specifier
'0x400'
>>> foo = "bar"
>>> f"{ foo = }" # preserves whitespace
" foo = 'bar'"
>>> line = "The mill's closed"
>>> f"{line = }"
'line = "The mill\'s closed"'
>>> f"{line = :20}"
"line = The mill's closed   "
>>> f"{line = !r:20}"
'line = "The mill\'s closed" '

与常规字符串字面值的语法一样,替换字段中的字符不能与外层格式字符串字面值的引号冲突:

f"abc {a["x"]} def"    # error: outer string literal ended prematurely
f"abc {a['x']} def"    # workaround: use different quoting

格式表达式中不能有反斜杠,否则会报错:

f"newline: {ord('\n')}"  # raises SyntaxError

要使用反斜杠转义的值,则需创建临时变量。

>>> newline = ord('\n')
>>> f"newline: {newline}"
'newline: 10'

即便未包含表达式,格式字符串字面值也不能用作文档字符串。

>>> def foo():
...     f"Not a docstring"
...
>>> foo.__doc__ is None
True

参阅 PEP 498,了解格式字符串字面值的提案,以及与格式字符串机制相关的 str.format()

2.4.4. 数值字面值

数值字面值有三种类型:整数、浮点数、虚数。没有复数字面值(复数由实数加虚数构成)。

注意,数值字面值不含正负号;实际上,-1 等负数是由一元运算符 ‘-‘ 和字面值 1 合成的。

2.4.5. 整数字面值

整数字面值词法定义如下:

integer      ::=  decinteger | bininteger | octinteger | hexinteger
decinteger   ::=  nonzerodigit (["_"] digit)* | "0"+ (["_"] "0")*
bininteger   ::=  "0" ("b" | "B") (["_"] bindigit)+
octinteger   ::=  "0" ("o" | "O") (["_"] octdigit)+
hexinteger   ::=  "0" ("x" | "X") (["_"] hexdigit)+
nonzerodigit ::=  "1"..."9"
digit        ::=  "0"..."9"
bindigit     ::=  "0" | "1"
octdigit     ::=  "0"..."7"
hexdigit     ::=  digit | "a"..."f" | "A"..."F"

整数字面值的长度没有限制,能一直大到占满可用内存。

确定数值时,会忽略字面值中的下划线。下划线只是为了分组数字,让数字更易读。下划线可在数字之间,也可在 0x 等基数说明符后。

注意,除了 0 以外,十进制数字的开头不允许有零。以免与 Python 3.0 版之前使用的 C 样式八进制字面值混淆。

整数字面值示例如下:

7     2147483647                        0o177    0b100110111
3     79228162514264337593543950336     0o377    0xdeadbeef
      100_000_000_000                   0b_1110_0101

在 3.6 版更改: 现已支持在字面值中,用下划线分组数字。

2.4.6. 浮点数字面值

浮点数字面值词法定义如下:

floatnumber   ::=  pointfloat | exponentfloat
pointfloat    ::=  [digitpart] fraction | digitpart "."
exponentfloat ::=  (digitpart | pointfloat) exponent
digitpart     ::=  digit (["_"] digit)*
fraction      ::=  "." digitpart
exponent      ::=  ("e" | "E") ["+" | "-"] digitpart

注意,解析时,整数和指数部分总以 10 为基数。例如,077e010 是合法的,表示的数值与 77e10 相同。浮点数字面值的支持范围取决于具体实现。整数字面值支持用下划线分组数字。

浮点数字面值示例如下:

3.14    10.    .001    1e100    3.14e-10    0e0    3.14_15_93

在 3.6 版更改: 现已支持在字面值中,用下划线分组数字。

2.4.7. 虚数字面值

虚数字面值词法定义如下:

imagnumber ::=  (floatnumber | digitpart) ("j" | "J")

虚数字面值生成实部为 0.0 的复数。复数由一对浮点数表示,它们的取值范围相同。创建实部不为零的复数,则需添加浮点数,例如 (3+4j)。虚数字面值示例如下:

3.14j   10.j    10j     .001j   1e100j   3.14e-10j   3.14_15_93j

2.5. 运算符

运算符如下所示:

+       -       *       **      /       //      %      @
<<      >>      &       |       ^       ~       :=
<       >       <=      >=      ==      !=

2.6. 分隔符

以下形符在语法中为分隔符:

(       )       [       ]       {       }
,       :       .       ;       @       =       ->
+=      -=      *=      /=      //=     %=      @=
&=      |=      ^=      >>=     <<=     **=

句点也可以用于浮点数和虚数字面值。三个连续句点表示省略符。列表后半部分是增强赋值操作符,用作词法分隔符,但也可以执行运算。

以下 ASCII 字符具有特殊含义,对词法分析器有重要意义:

'       "       #       \

以下 ASCII 字符不用于 Python。在字符串字面值或注释外使用时,将直接报错:

$       ?       `

3. 数据模型

3.1. 对象、值与类型

对象 是 Python 中对数据的抽象。 Python 程序中的所有数据都是由对象或对象间关系来表示的。 (从某种意义上说,按照冯·诺依曼的“存储程序计算机”模型,代码本身也是由对象来表示的。)

每个对象都有各自的编号、类型和值。一个对象被创建后,它的 编号 就绝不会改变;你可以将其理解为该对象在内存中的地址。 ‘is‘ 运算符可以比较两个对象的编号是否相同;id() 函数能返回一个代表其编号的整型数。

CPython implementation detail: 在 CPython 中,id(x) 就是存放 x 的内存的地址。

对象的类型决定该对象所支持的操作 (例如 “对象是否有长度属性?”) 并且定义了该类型的对象可能的取值。type() 函数能返回一个对象的类型 (类型本身也是对象)。与编号一样,一个对象的 类型 也是不可改变的。

有些对象的 可以改变。值可以改变的对象被称为 可变的;值不可以改变的对象就被称为 不可变的。(一个不可变容器对象如果包含对可变对象的引用,当后者的值改变时,前者的值也会改变;但是该容器仍属于不可变对象,因为它所包含的对象集是不会改变的。因此,不可变并不严格等同于值不能改变,实际含义要更微妙。) 一个对象的可变性是由其类型决定的;例如,数字、字符串和元组是不可变的,而字典和列表是可变的。

对象绝不会被显式地销毁;然而,当无法访问时它们可能会被作为垃圾回收。允许具体的实现推迟垃圾回收或完全省略此机制 —- 如何实现垃圾回收是实现的质量问题,只要可访问的对象不会被回收即可。

CPython implementation detail: CPython 目前使用带有 (可选) 延迟检测循环链接垃圾的引用计数方案,会在对象不可访问时立即回收其中的大部分,但不保证回收包含循环引用的垃圾。请查看 gc 模块的文档了解如何控制循环垃圾的收集相关信息。其他实现会有不同的行为方式,CPython 现有方式也可能改变。不要依赖不可访问对象的立即终结机制 (所以你应当总是显式地关闭文件)。

注意:使用实现的跟踪或调试功能可能令正常情况下会被回收的对象继续存活。还要注意通过 ‘tryexcept‘ 语句捕捉异常也可能令对象保持存活。

有些对象包含对 “外部” 资源的引用,例如打开文件或窗口。当对象被作为垃圾回收时这些资源也应该会被释放,但由于垃圾回收并不确保发生,这些对象还提供了明确地释放外部资源的操作,通常为一个 close() 方法。强烈推荐在程序中显式关闭此类对象。’tryfinally‘ 语句和 ‘with‘ 语句提供了进行此种操作的更便捷方式。

有些对象包含对其他对象的引用;它们被称为 容器。容器的例子有元组、列表和字典等。这些引用是容器对象值的组成部分。在多数情况下,当谈论一个容器的值时,我们是指所包含对象的值而不是其编号;但是,当我们谈论一个容器的可变性时,则仅指其直接包含的对象的编号。因此,如果一个不可变容器 (例如元组) 包含对一个可变对象的引用,则当该可变对象被改变时容器的值也会改变。

类型会影响对象行为的几乎所有方面。甚至对象编号的重要性也在某种程度上受到影响: 对于不可变类型,会得出新值的运算实际上会返回对相同类型和取值的任一现有对象的引用,而对于可变类型来说这是不允许的。例如在 a = 1; b = 1 之后,ab 可能会也可能不会指向同一个值为一的对象,这取决于具体实现,但是在 c = []; d = [] 之后,cd 保证会指向两个不同、单独的新建空列表。(请注意 c = d = [] 则是将同一个对象赋值给 cd。)

3.2. 标准类型层级结构

以下是 Python 内置类型的列表。扩展模块 (具体实现会以 C, Java 或其他语言编写) 可以定义更多的类型。未来版本的 Python 可能会加入更多的类型 (例如有理数、高效存储的整型数组等等),不过新增类型往往都是通过标准库来提供的。

以下部分类型的描述中包含有 ‘特殊属性列表’ 段落。这些属性提供对具体实现的访问而非通常使用。它们的定义在未来可能会改变。

None

此类型只有一种取值。是一个具有此值的单独对象。此对象通过内置名称 None 访问。在许多情况下它被用来表示空值,例如未显式指明返回值的函数将返回 None。它的逻辑值为假。

NotImplemented

此类型只有一种取值。 是一个具有该值的单独对象。 此对象通过内置名称 NotImplemented 访问。 数值方法和丰富比较方法如未实现指定运算符表示的运算则应返回该值。 (解释器会根据具体运算符继续尝试反向运算或其他回退操作。) 它不应被解读为布尔值。

在 3.9 版更改: 作为布尔值来解读 NotImplemented 已被弃用。 虽然它目前会被解读为真值,但将同时发出 DeprecationWarning。 它将在未来的 Python 版本中引发 TypeError

Ellipsis

此类型只有一种取值。是一个具有此值的单独对象。此对象通过字面值 ... 或内置名称 Ellipsis 访问。它的逻辑值为真。

numbers.Number

此类对象由数字字面值创建,并会被作为算术运算符和算术内置函数的返回结果。数字对象是不可变的;一旦创建其值就不再改变。Python 中的数字当然非常类似数学中的数字,但也受限于计算机中的数字表示方法。

数字类的字符串表示形式,由 __repr__()__str__() 算出,具有以下属性:

  • 它们是有效的数字字面值,当被传给它们的类构造器时,将会产生具有原数字值的对象。
  • 表示形式会在可能的情况下采用 10 进制。
  • 开头的零,除小数点前可能存在的单个零之外,将不会被显示。
  • 末尾的零,除小数点后可能存在的单个零之外,将不会被显示。
  • 正负号仅在当数字为负值时会被显示。

Python 区分整型数、浮点型数和复数:

  • numbers.Integral

    此类对象表示数学中整数集合的成员 (包括正数和负数)。

    整型数可细分为两种类型:

    • 整型 (int)

      此类对象表示任意大小的数字,仅受限于可用的内存 (包括虚拟内存)。在变换和掩码运算中会以二进制表示,负数会以 2 的补码表示,看起来像是符号位向左延伸补满空位。

      布尔型 (bool)

      此类对象表示逻辑值 False 和 True。代表 FalseTrue 值的两个对象是唯二的布尔对象。布尔类型是整型的子类型,两个布尔值在各种场合的行为分别类似于数值 0 和 1,例外情况只有在转换为字符串时分别返回字符串 "False""True"

    整型数表示规则的目的是在涉及负整型数的变换和掩码运算时提供最为合理的解释。

    numbers.Real (float)

    此类对象表示机器级的双精度浮点数。其所接受的取值范围和溢出处理将受制于底层的机器架构 (以及 C 或 Java 实现)。Python 不支持单精度浮点数;支持后者通常的理由是节省处理器和内存消耗,但这点节省相对于在 Python 中使用对象的开销来说太过微不足道,因此没有理由包含两种浮点数而令该语言变得复杂。

    numbers.Complex (complex)

    此类对象以一对机器级的双精度浮点数来表示复数值。有关浮点数的附带规则对其同样有效。一个复数值 z 的实部和虚部可通过只读属性 z.realz.imag 来获取。

序列

此类对象表示以非负整数作为索引的有限有序集。内置函数 len() 可返回一个序列的条目数量。当一个序列的长度为 n 时,索引集包含数字 0, 1, …, n-1。序列 a 的条目 i 可通过 a[i] 选择。

序列还支持切片: a[i:j] 选择索引号为 k 的所有条目,i <= k < j。当用作表达式时,序列的切片就是一个与序列类型相同的新序列。新序列的索引还是从 0 开始。

有些序列还支持带有第三个 “step” 形参的 “扩展切片”: a[i:j:k] 选择 a 中索引号为 x 的所有条目,x = i + n*k, n >= 0i <= x < j

序列可根据其可变性来加以区分:

  • 不可变序列

    不可变序列类型的对象一旦创建就不能再改变。(如果对象包含对其他对象的引用,其中的可变对象就是可以改变的;但是,一个不可变对象所直接引用的对象集是不能改变的。)

    以下类型属于不可变对象:

    • 字符串

      字符串是由 Unicode 码位值组成的序列。范围在 U+0000 - U+10FFFF 之内的所有码位值都可在字符串中使用。Python 没有 char 类型;而是将字符串中的每个码位表示为一个长度为 1 的字符串对象。内置函数 ord() 可将一个码位由字符串形式转换成一个范围在 0 - 10FFFF 之内的整型数;chr() 可将一个范围在 0 - 10FFFF 之内的整型数转换为长度为 1 的对应字符串对象。str.encode() 可以使用指定的文本编码将 str 转换为 bytes,而 bytes.decode() 则可以实现反向的解码。

      元组

      一个元组中的条目可以是任意 Python 对象。包含两个或以上条目的元组由逗号分隔的表达式构成。只有一个条目的元组 (‘单项元组’) 可通过在表达式后加一个逗号来构成 (一个表达式本身不能创建为元组,因为圆括号要用来设置表达式分组)。一个空元组可通过一对内容为空的圆括号创建。

      字节串

      字节串对象是不可变的数组。其中每个条目都是一个 8 位字节,以取值范围 0 <= x < 256 的整型数表示。字节串字面值 (例如 b'abc') 和内置的 bytes() 构造器可被用来创建字节串对象。字节串对象还可以通过 decode() 方法解码为字符串。

  • 可变序列

    可变序列在被创建后仍可被改变。下标和切片标注可被用作赋值和 del (删除) 语句的目标。

    目前有两种内生可变序列类型:

    • 列表

      列表中的条目可以是任意 Python 对象。列表由用方括号括起并由逗号分隔的多个表达式构成。(注意创建长度为 0 或 1 的列表无需使用特殊规则。)

      字节数组

      字节数组对象属于可变数组。可以通过内置的 bytearray() 构造器来创建。除了是可变的 (因而也是不可哈希的),在其他方面字节数组提供的接口和功能都与不可变的 bytes 对象一致。

    扩展模块 array 提供了一个额外的可变序列类型示例,collections 模块也是如此。

集合类型

此类对象表示由不重复且不可变对象组成的无序且有限的集合。因此它们不能通过下标来索引。但是它们可被迭代,也可用内置函数 len() 返回集合中的条目数。集合常见的用处是快速成员检测,去除序列中的重复项,以及进行交、并、差和对称差等数学运算。

对于集合元素所采用的不可变规则与字典的键相同。注意数字类型遵循正常的数字比较规则: 如果两个数字相等 (例如 11.0),则同一集合中只能包含其中一个。

目前有两种内生集合类型:

  • 集合

    此类对象表示可变集合。它们可通过内置的 set() 构造器创建,并且创建之后可以通过方法进行修改,例如 add()

    冻结集合

    此类对象表示不可变集合。它们可通过内置的 frozenset() 构造器创建。由于 frozenset 对象不可变且 hashable,它可以被用作另一个集合的元素或是字典的键。

映射

此类对象表示由任意索引集合所索引的对象的集合。通过下标 a[k] 可在映射 a 中选择索引为 k 的条目;这可以在表达式中使用,也可作为赋值或 del 语句的目标。内置函数 len() 可返回一个映射中的条目数。

目前只有一种内生映射类型:

  • 字典

    此类对象表示由几乎任意值作为索引的有限个对象的集合。不可作为键的值类型只有包含列表或字典或其他可变类型,通过值而非对象编号进行比较的值,其原因在于高效的字典实现需要使用键的哈希值以保持一致性。用作键的数字类型遵循正常的数字比较规则: 如果两个数字相等 (例如 11.0) 则它们均可来用来索引同一个字典条目。

    字典会保留插入顺序,这意味着键将以它们被添加的顺序在字典中依次产生。 替换某个现有的键不会改变其顺序,但是移除某个键再重新插入则会将其添加到末尾而不会保留其原有位置。

    字典是可变的;它们可通过 {...} 标注来创建 。

    扩展模块 dbm.ndbmdbm.gnu 提供了额外的映射类型示例,collections 模块也是如此。

    在 3.7 版更改: 在 Python 3.6 版之前字典不会保留插入顺序。 在 CPython 3.6 中插入顺序会被保留,但这在当时被当作是一个实现细节而非确定的语言特性。

可调用类型

此类型可以被应用于函数调用操作 :

  • 用户定义函数

    用户定义函数对象可通过函数定义来创建 。它被调用时应附带一个参数列表,其中包含的条目应与函数所定义的形参列表一致。

    特殊属性:

    属性 含意
    doc 该函数的文档字符串,没有则为 None;不会被子类继承。 可写
    name 该函数的名称。 可写
    qualname 该函数的 qualified name。3.3 新版功能. 可写
    module 该函数所属模块的名称,没有则为 None 可写
    defaults 由具有默认值的参数的默认参数值组成的元组,如无任何参数具有默认值则为 None 可写
    code 表示编译后的函数体的代码对象。 可写
    globals 对存放该函数中全局变量的字典的引用 —- 函数所属模块的全局命名空间。 只读
    dict 命名空间支持的函数属性。 可写
    closure None 或包含该函数可用变量的绑定的单元的元组。有关 cell contents 属性的详情见下。 只读
    annotations 包含形参标注的字典。 字典的键是形参名,而如果提供了 ‘return’ 则是用于返回值标注。 可写
    _kwdefaults 仅包含关键字参数默认值的字典。 可写

    大部分标有 “Writable” 的属性均会检查赋值的类型。

    函数对象也支持获取和设置任意属性,例如这可以被用来给函数附加元数据。使用正规的属性点号标注获取和设置此类属性。注意当前实现仅支持用户定义函数属性。未来可能会增加支持内置函数属性。

    单元对象具有 cell_contents 属性。这可被用来获取以及设置单元的值。

    有关函数定义的额外信息可以从其代码对象中提取;参见下文对内部类型的描述。 cell 类型可以在 types 模块中访问。

    实例方法

    实例方法用于结合类、类实例和任何可调用对象 (通常为用户定义函数)。

    特殊的只读属性: __self__ 为类实例对象本身,__func__ 为函数对象;__doc__ 为方法的文档 (与 __func__.__doc__ 作用相同);__name__ 为方法名称 (与 __func__.__name__ 作用相同);__module__ 为方法所属模块的名称,没有则为 None

    方法还支持获取 (但不能设置) 下层函数对象的任意函数属性。

    用户定义方法对象可在获取一个类的属性时被创建 (也可能通过该类的一个实例),如果该属性为用户定义函数对象或类方法对象。

    当通过从类实例获取一个用户定义函数对象的方式创建一个实例方法对象时,类实例对象的 __self__ 属性即为该实例,并会绑定方法对象。该新建方法的 __func__ 属性就是原来的函数对象。

    当通过从类或实例获取一个类方法对象的方式创建一个实例对象时,实例对象的 __self__ 属性为该类本身,其 __func__ 属性为类方法对应的下层函数对象。

    当一个实例方法对象被调用时,会调用对应的下层函数 (__func__),并将类实例 (__self__) 插入参数列表的开头。例如,当 C 是一个包含了 f() 函数定义的类,而 xC 的一个实例,则调用 x.f(1) 就等同于调用 C.f(x, 1)

    当一个实例方法对象是衍生自一个类方法对象时,保存在 __self__ 中的 “类实例” 实际上会是该类本身,因此无论是调用 x.f(1) 还是 C.f(1) 都等同于调用 f(C,1),其中 f 为对应的下层函数。

    请注意从函数对象到实例方法对象的变换会在每一次从实例获取属性时发生。在某些情况下,一种高效的优化方式是将属性赋值给一个本地变量并调用该本地变量。还要注意这样的变换只发生于用户定义函数;其他可调用对象 (以及所有不可调用对象) 在被获取时都不会发生变换。还有一个需要关注的要点是作为一个类实例属性的用户定义函数不会被转换为绑定方法;这样的变换 仅当 函数是类属性时才会发生。

    生成器函数

    一个使用 yield 语句 的函数或方法被称作一个 生成器函数。 这样的函数在被调用时,总是返回一个可以执行函数体的迭代器对象:调用该迭代器的 iterator.__next__() 方法将会导致这个函数一直运行直到它使用 yield 语句提供了一个值为止。 当这个函数执行 return 语句或者执行到末尾时,将引发 StopIteration 异常并且这个迭代器将到达所返回的值集合的末尾。

    协程函数

    使用 async def 来定义的函数或方法就被称为 协程函数。这样的函数在被调用时会返回一个 coroutine 对象。它可能包含 await 表达式以及 async withasync for 语句。

    异步生成器函数

    使用 async def 来定义并包含 yield 语句的函数或方法就被称为 异步生成器函数。这样的函数在被调用时会返回一个异步迭代器对象,该对象可在 async for 语句中用来执行函数体。

    调用异步迭代器的 aiterator.__anext__() 方法将会返回一个 awaitable,此对象会在被等待时执行直到使用 yield 表达式输出一个值。当函数执行时到空的 return 语句或是最后一条语句时,将会引发 StopAsyncIteration 异常,异步迭代器也会到达要输出的值集合的末尾。

    内置函数

    内置函数对象是对于 C 函数的外部封装。内置函数的例子包括 len()math.sin() (math 是一个标准内置模块)。内置函数参数的数量和类型由 C 函数决定。特殊的只读属性: __doc__ 是函数的文档字符串,如果没有则为 None; __name__ 是函数的名称; __self__ 设定为 None (参见下一条目); __module__ 是函数所属模块的名称,如果没有则为 None

    内置方法

    此类型实际上是内置函数的另一种形式,只不过还包含了一个传入 C 函数的对象作为隐式的额外参数。内置方法的一个例子是 alist.append(),其中 alist 为一个列表对象。在此示例中,特殊的只读属性 __self__ 会被设为 alist 所标记的对象。

    类是可调用的。此种对象通常是作为“工厂”来创建自身的实例,类也可以有重载 __new__() 的变体类型。调用的参数会传给 __new__(),而且通常也会传给 __init__() 来初始化新的实例。

    类实例

    任意类的实例通过在所属类中定义 __call__() 方法即能成为可调用的对象。

模块

模块是 Python 代码的基本组织单元,由 导入系统 创建,由 import 语句发起调用,或者通过 importlib.import_module() 和内置的 __import__() 等函数发起调用。 模块对象具有由字典对象实现的命名空间(这是被模块中定义的函数的 __globals__ 属性引用的字典)。 属性引用被转换为该字典中的查找,例如 m.x 相当于 m.__dict__["x"]。 模块对象不包含用于初始化模块的代码对象(因为初始化完成后不需要它)。

属性赋值会更新模块的命名空间字典,例如 m.x = 1 等同于 m.__dict__["x"] = 1

预先定义的(可写)属性:

  • __name__

    模块的名称。

    __doc__

    模块的文档字符串,如果不可用则为 None

    __file__

    被加载模块所对应文件的路径名称,如果它是从文件加载的话。 对于某些类型的模块来说 __file__ 属性可能是缺失的,例如被静态链接到解释器中的 C 模块。 对于从共享库动态加载的扩展模块来说,它将是共享库文件的路径名称。

    __annotations__

    包含在模块体执行期间收集的 变量标注 的字典。

特殊的只读属性: __dict__ 为以字典对象表示的模块命名空间。

CPython implementation detail: 由于 CPython 清理模块字典的设定,当模块离开作用域时模块字典将会被清理,即使该字典还有活动的引用。想避免此问题,可复制该字典或保持模块状态以直接使用其字典。

自定义类

自定义类这种类型一般通过类定义来创建。每个类都有通过一个字典对象实现的独立命名空间。类属性引用会被转化为在此字典中查找,例如 C.x 会被转化为 C.__dict__["x"] (不过也存在一些钩子对象以允许其他定位属性的方式)。当未在其中发现某个属性名称时,会继续在基类中查找。这种基类查找使用 C3 方法解析顺序,即使存在 ‘钻石形’ 继承结构即有多条继承路径连到一个共同祖先也能保持正确的行为。有关 Python 使用的 C3 MRO 的详情可查看配合 2.3 版发布的文档 https://www.python.org/download/releases/2.3/mro/.

当一个类属性引用 (假设类名为 C) 会产生一个类方法对象时,它将转化为一个 __self__ 属性为 C 的实例方法对象。当其会产生一个静态方法对象时,它将转化为该静态方法对象所封装的对象。

类属性赋值会更新类的字典,但不会更新基类的字典。

类对象可被调用 (见上文) 以产生一个类实例 (见下文)。

特殊属性:

  • __name__

    类的名称。

    __module__

    类定义所在模块的名称。

    __dict__

    包含类命名空间的字典。

    __bases__

    包含基类的元组,按它们在基类列表中的出现先后排序。

    __doc__

    类的文档字符串,如果未定义则为 None

    __annotations__

    包含在类体执行期间收集的 变量标注 的字典。

类实例

类实例可通过调用类对象来创建 (见上文)。每个类实例都有通过一个字典对象实现的独立命名空间,属性引用会首先在此字典中查找。当未在其中发现某个属性,而实例对应的类中有该属性时,会继续在类属性中查找。如果找到的类属性为一个用户定义函数对象,它会被转化为实例方法对象,其 __self__ 属性即该实例。静态方法和类方法对象也会被转化;参见上文 “Classes” 一节。这样得到的属性可能与实际存放于类的 __dict__ 中的对象不同。如果未找到类属性,而对象对应的类具有 __getattr__() 方法,则会调用该方法来满足查找要求。

属性赋值和删除会更新实例的字典,但不会更新对应类的字典。如果类具有 __setattr__()__delattr__() 方法,则将调用方法而不再直接更新实例的字典。

如果类实例具有某些特殊名称的方法,就可以伪装为数字、序列或映射。

特殊属性: __dict__ 为属性字典; __class__ 为实例对应的类。

I/O 对象 (或称文件对象)

file object 表示一个打开的文件。有多种快捷方式可用来创建文件对象: open() 内置函数,以及 os.popen(), os.fdopen() 和 socket 对象的 makefile() 方法 (还可能使用某些扩展模块所提供的其他函数或方法)。

sys.stdin, sys.stdoutsys.stderr 会初始化为对应于解释器标准输入、输出和错误流的文件对象;它们都会以文本模式打开,因此都遵循 io.TextIOBase 抽象类所定义的接口。

内部类型

某些由解释器内部使用的类型也被暴露给用户。它们的定义可能随未来解释器版本的更新而变化,为内容完整起见在此处一并介绍。

  • 代码对象

    代码对象表示 编译为字节的 可执行 Python 代码,或称 bytecode。代码对象和函数对象的区别在于函数对象包含对函数全局对象 (函数所属的模块) 的显式引用,而代码对象不包含上下文;而且默认参数值会存放于函数对象而不是代码对象内 (因为它们表示在运行时算出的值)。与函数对象不同,代码对象不可变,也不包含对可变对象的引用 (不论是直接还是间接)。

    特殊的只读属性: co_name 给出了函数名; co_argcount 为位置参数的总数量 (包括仅限位置参数和带有默认值的参数); co_posonlyargcount 为仅限位置参数的数量 (包括带有默认值的参数); co_kwonlyargcount 为仅限关键字参数的数量 (包括带有默认值的参数); co_nlocals 为函数使用的局部变量的数量 (包括参数); co_varnames 为一个包含局部变量名称的元组 (参数名排在最前面); co_cellvars 为一个包含被嵌套函数所引用的局部变量名称的元组; co_freevars 为一个包含自由变量名称的元组; co_code 为一个表示字节码指令序列的字符口中; co_consts 为一个包含字节码所使用的字面值的元组; co_names 为一个包含字节码所使用的名称的元组; co_filename 为被编码代码所在的文件名; co_firstlineno 为函数首行的行号; co_lnotab 为一个字符串,其中编码了从字节码偏移量到行号的映射 (详情参见解释器的源代码); co_stacksize 为要求的栈大小; co_flags 为一个整数,其中编码了解释器所用的多个旗标。

    以下是可用于 co_flags 的标志位定义:如果函数使用 *arguments 语法来接受任意数量的位置参数,则 0x04 位被设置;如果函数使用 **keywords 语法来接受任意数量的关键字参数,则 0x08 位被设置;如果函数是一个生成器,则 0x20 位被设置。

    未来特性声明 (from __future__ import division) 也使用 co_flags 中的标志位来指明代码对象的编译是否启用特定的特性: 如果函数编译时启用未来除法特性则设置 0x2000 位; 在更早的 Python 版本中则使用 0x100x1000 位。

    co_flags 中的其他位被保留为内部使用。

    如果代码对象表示一个函数,co_consts 中的第一项将是函数的文档字符串,如果未定义则为 None

  • 帧对象

    帧对象表示执行帧。它们可能出现在回溯对象中 (见下文),还会被传递给注册跟踪函数。

    特殊的只读属性: f_back 为前一堆栈帧 (指向调用者),如是最底层堆栈帧则为 None; f_code 为此帧中所执行的代码对象; f_locals 为用于查找本地变量的字典; f_globals 则用于查找全局变量; f_builtins 用于查找内置 (固有) 名称; f_lasti 给出精确指令 (这是代码对象的字节码字符串的一个索引)。

    访问 f_code 会引发一个 审计事件 object.__getattr__,附带参数 obj"f_code"

    特殊的可写属性: f_trace,如果不为 None,则是在代码执行期间调用各类事件的函数 (由调试器使用)。通常每个新源码行会触发一个事件 - 这可以通过将 f_trace_lines 设为 False 来禁用。

    具体的实现 可能 会通过将 f_trace_opcodes 设为 True 来允许按操作码请求事件。请注意如果跟踪函数引发的异常逃逸到被跟踪的函数中,这可能会导致未定义的解释器行为。

    f_lineno 为帧的当前行号 —- 在这里写入从一个跟踪函数内部跳转的指定行 (仅用于最底层的帧)。调试器可以通过写入 f_lineno 实现一个 Jump 命令 (即设置下一语句)。

    帧对象支持一个方法:

    • frame.clear()

      此方法清除该帧持有的全部对本地变量的引用。而且如果该帧属于一个生成器,生成器会被完成。这有助于打破包含帧对象的循环引用 (例如当捕获一个异常并保存其回溯在之后使用)。

      如果该帧当前正在执行则会引发 RuntimeError

      3.4 新版功能.

  • 回溯对象

    回溯对象表示一个异常的栈跟踪记录。当异常发生时会隐式地创建一个回溯对象,也可能通过调用 types.TracebackType 显式地创建。

    对于隐式地创建的回溯对象,当查找异常句柄使得执行栈展开时,会在每个展开层级的当前回溯之前插入一个回溯对象。当进入一个异常句柄时,栈跟踪将对程序启用。 它可作为 sys.exc_info() 所返回的元组的第三项,以及所捕获异常的 __traceback__ 属性被获取。

    当程序不包含可用的句柄时,栈跟踪会 (以良好的格式) 写入标准错误流;如果解释器处于交互模式,它也可作为 sys.last_traceback 对用户启用。

    对于显式创建的回溯对象,则由回溯对象的创建者来决定应该如何链接 tb_next 属性来构成完整的栈跟踪。

    特殊的只读属性: tb_frame 指向当前层级的执行帧; tb_lineno 给出发生异常所在的行号; tb_lasti 标示具体指令。如果异常发生于没有匹配的 except 子句或有 finally 子句的 try 语句中,回溯对象中的行号和最后指令可能与相应帧对象中行号不同。

    访问 tb_frame 会引发一个 审计事件 object.__getattr__,附带参数 obj"tb_frame"

    特殊的可写属性: tb_next 为栈跟踪中的下一层级 (通往发生异常的帧),如果没有下一层级则为 None

    在 3.7 版更改: 回溯对象现在可以使用 Python 代码显式地实例化,现有实例的 tb_next 属性可以被更新。

    切片对象

    切片对象用来表示 __getitem__() 方法用到的切片。 该对象也可使用内置的 slice() 函数来创建。

    特殊的只读属性: start 为下界; stop 为上界; step 为步长值; 各值如省略则为 None。这些属性可具有任意类型。

    切片对象支持一个方法:

    • slice.indices(self, length)

      此方法接受一个整型参数 length 并计算在切片对象被应用到 length 指定长度的条目序列时切片的相关信息应如何描述。 其返回值为三个整型数组成的元组;这些数分别为切片的 startstop 索引号以及 step 步长值。索引号缺失或越界则按照与正规切片相一致的方式处理。

    静态方法对象

    静态方法对象提供了一种胜过上文所述将函数对象转换为方法对象的方式。 静态方法对象是对任意其他对象的包装器,通常用来包装用户自定义的方法对象。 当从类或类实例获取一个静态方法对象时,实际返回的是经过包装的对象,它不会被进一步转换。 静态方法对象也是可调用对象。 静态方法对象可通过内置的 staticmethod() 构造器来创建。

    类方法对象

    类方法对象和静态方法一样是对其他对象的封装,会改变从类或类实例获取该对象的方式。类方法对象在此类获取操作中的行为已在上文 “用户定义方法” 一节中描述。类方法对象可通过内置的 classmethod() 构造器来创建。

3.3. 特殊方法名称

一个类可以通过定义具有特殊名称的方法来实现由特殊语法所引发的特定操作 (例如算术运算或下标与切片)。这是 Python 实现 操作符重载 的方式,允许每个类自行定义基于操作符的特定行为。例如,如果一个类定义了名为 __getitem__() 的方法,并且 x 为该类的一个实例,则 x[i] 基本就等同于 type(x).__getitem__(x, i)。除非有说明例外情况,在没有定义适当方法的情况下尝试执行一种操作将引发一个异常 (通常为 AttributeErrorTypeError)。

将一个特殊方法设为 None 表示对应的操作不可用。例如,如果一个类将 __iter__() 设为 None,则该类就是不可迭代的,因此对其实例调用 iter() 将引发一个 TypeError (而不会回退至 __getitem__()).

在实现模拟任何内置类型的类时,很重要的一点是模拟的实现程度对于被模拟对象来说应当是有意义的。例如,提取单个元素的操作对于某些序列来说是适宜的,但提取切片可能就没有意义。(这种情况的一个实例是 W3C 的文档对象模型中的 NodeList 接口。)

3.3.1. 基本定制

object.__new__(cls[, ])

调用以创建一个 cls 类的新实例。__new__() 是一个静态方法 (因为是特例所以你不需要显式地声明),它会将所请求实例所属的类作为第一个参数。其余的参数会被传递给对象构造器表达式 (对类的调用)。__new__() 的返回值应为新对象实例 (通常是 cls 的实例)。

典型的实现会附带适宜的参数使用 super().__new__(cls[, ...]),通过超类的 __new__() 方法来创建一个类的新实例,然后根据需要修改新创建的实例再将其返回。

If __new__() is invoked during object construction and it returns an instance of cls, then the new instance’s __init__() method will be invoked like __init__(self[, ...]), where self is the new instance and the remaining arguments are the same as were passed to the object constructor.

如果 __new__() 未返回一个 cls 的实例,则新实例的 __init__() 方法就不会被执行。

__new__() 的目的主要是允许不可变类型的子类 (例如 int, str 或 tuple) 定制实例创建过程。它也常会在自定义元类中被重载以便定制类创建过程。

object.__init__(self[, ])

在实例 (通过 __new__()) 被创建之后,返回调用者之前调用。其参数与传递给类构造器表达式的参数相同。一个基类如果有 __init__() 方法,则其所派生的类如果也有 __init__() 方法,就必须显式地调用它以确保实例基类部分的正确初始化;例如: super().__init__([args...]).

因为对象是由 __new__()__init__() 协作构造完成的 (由 __new__() 创建,并由 __init__() 定制),所以 __init__() 返回的值只能是 None,否则会在运行时引发 TypeError

object.__del__(self)

在实例将被销毁时调用。 这还被称为终结器或析构器(不适当)。 如果一个基类具有 __del__() 方法,则其所派生的类如果也有 __del__() 方法,就必须显式地调用它以确保实例基类部分的正确清除。

__del__() 方法可以 (但不推荐!) 通过创建一个该实例的新引用来推迟其销毁。这被称为对象 重生__del__() 是否会在重生的对象将被销毁时再次被调用是由具体实现决定的 ;当前的 CPython 实现只会调用一次。

当解释器退出时不会确保为仍然存在的对象调用 __del__() 方法。

注解

del x 并不直接调用 x.__del__() —- 前者会将 x 的引用计数减一,而后者仅会在 x 的引用计数变为零时被调用。

CPython implementation detail: It is possible for a reference cycle to prevent the reference count of an object from going to zero. In this case, the cycle will be later detected and deleted by the cyclic garbage collector. A common cause of reference cycles is when an exception has been caught in a local variable. The frame’s locals then reference the exception, which references its own traceback, which references the locals of all frames caught in the traceback.

警告

由于调用 __del__() 方法时周边状况已不确定,在其执行期间发生的异常将被忽略,改为打印一个警告到 sys.stderr。特别地:

  • __del__() 可在任意代码被执行时启用,包括来自任意线程的代码。如果 __del__() 需要接受锁或启用其他阻塞资源,可能会发生死锁,例如该资源已被为执行 __del__() 而中断的代码所获取。
  • __del__() 可以在解释器关闭阶段被执行。因此,它需要访问的全局变量(包含其他模块)可能已被删除或设为 None。Python 会保证先删除模块中名称以单个下划线打头的全局变量再删除其他全局变量;如果已不存在其他对此类全局变量的引用,这有助于确保导入的模块在 __del__() 方法被调用时仍然可用。

object.__repr__(self)

repr() 内置函数调用以输出一个对象的“官方”字符串表示。如果可能,这应类似一个有效的 Python 表达式,能被用来重建具有相同取值的对象(只要有适当的环境)。如果这不可能,则应返回形式如 <...some useful description...> 的字符串。返回值必须是一个字符串对象。如果一个类定义了 __repr__() 但未定义 __str__(),则在需要该类的实例的“非正式”字符串表示时也会使用 __repr__()

此方法通常被用于调试,因此确保其表示的内容包含丰富信息且无歧义是很重要的。

object.__str__(self)

通过 str(object) 以及内置函数 format()print() 调用以生成一个对象的“非正式”或格式良好的字符串表示。返回值必须为一个 字符串 对象。

此方法与 object.__repr__() 的不同点在于 __str__() 并不预期返回一个有效的 Python 表达式:可以使用更方便或更准确的描述信息。

内置类型 object 所定义的默认实现会调用 object.__repr__()

object.__bytes__(self)

通过 bytes 调用以生成一个对象的字节串表示。这应该返回一个 bytes 对象。

object.__format__(self, format_spec)

通过 format() 内置函数、扩展、格式化字符串字面值 的求值以及 str.format() 方法调用以生成一个对象的“格式化”字符串表示。 format_spec 参数为包含所需格式选项描述的字符串。 format_spec 参数的解读是由实现 __format__() 的类型决定的,不过大多数类或是将格式化委托给某个内置类型,或是使用相似的格式化选项语法。

返回值必须为一个字符串对象。

在 3.4 版更改: object 本身的 format 方法如果被传入任何非空字符,将会引发一个 TypeError

在 3.7 版更改: object.__format__(x, '') 现在等同于 str(x) 而不再是 format(str(x), '')

object.__lt__(self, other)

object.__le__(self, other)

object.__eq__(self, other)

object.__ne__(self, other)

object.__gt__(self, other)

object.__ge__(self, other)

以上这些被称为“富比较”方法。运算符号与方法名称的对应关系如下:x<y 调用 x.__lt__(y)x<=y 调用 x.__le__(y)x==y 调用 x.__eq__(y)x!=y 调用 x.__ne__(y)x>y 调用 x.__gt__(y)x>=y 调用 x.__ge__(y)

如果指定的参数对没有相应的实现,富比较方法可能会返回单例对象 NotImplemented。按照惯例,成功的比较会返回 FalseTrue。不过实际上这些方法可以返回任意值,因此如果比较运算符是要用于布尔值判断(例如作为 if 语句的条件),Python 会对返回值调用 bool() 以确定结果为真还是假。

在默认情况下,object 通过使用 is 来实现 __eq__(),并在比较结果为假值时返回 NotImplemented: True if x is y else NotImplemented。 对于 __ne__(),默认会委托给 __eq__() 并对结果取反,除非结果为 NotImplemented。 比较运算符之间没有其他隐含关系或默认实现;例如,(x<y or x==y) 为真并不意味着 x<=y

这些方法并没有对调参数版本(在左边参数不支持该操作但右边参数支持时使用);而是 __lt__()__gt__() 互为对方的反射, __le__()__ge__() 互为对方的反射,而 __eq__()__ne__() 则是它们自己的反射。如果两个操作数的类型不同,且右操作数类型是左操作数类型的直接或间接子类,则优先选择右操作数的反射方法,否则优先选择左操作数的方法。虚拟子类不会被考虑。

object.__hash__(self)

通过内置函数 hash() 调用以对哈希集的成员进行操作,属于哈希集的类型包括 setfrozenset 以及 dict__hash__() 应该返回一个整数。对象比较结果相同所需的唯一特征属性是其具有相同的哈希值;建议的做法是把参与比较的对象全部组件的哈希值混在一起,即将它们打包为一个元组并对该元组做哈希运算。例如:

def __hash__(self):    
    return hash((self.name, self.nick, self.color))

注解

hash() 会从一个对象自定义的 __hash__() 方法返回值中截断为 Py_ssize_t 的大小。通常对 64 位构建为 8 字节,对 32 位构建为 4 字节。如果一个对象的 __hash__() 必须在不同位大小的构建上进行互操作,请确保检查全部所支持构建的宽度。做到这一点的简单方法是使用 python -c "import sys; print(sys.hash_info.width)"

如果一个类没有定义 __eq__() 方法,那么也不应该定义 __hash__() 操作;如果它定义了 __eq__() 但没有定义 __hash__(),则其实例将不可被用作可哈希集的项。如果一个类定义了可变对象并实现了 __eq__() 方法,则不应该实现 __hash__(),因为可哈希集的实现要求键的哈希集是不可变的(如果对象的哈希值发生改变,它将处于错误的哈希桶中)。

用户定义的类默认带有 __eq__()__hash__() 方法;使用它们与任何对象(自己除外)比较必定不相等,并且 x.__hash__() 会返回一个恰当的值以确保 x == y 同时意味着 x is yhash(x) == hash(y)

一个类如果重载了 __eq__() 且没有定义 __hash__() 则会将其 __hash__() 隐式地设为 None。当一个类的 __hash__() 方法为 None 时,该类的实例将在一个程序尝试获取其哈希值时正确地引发 TypeError,并会在检测 isinstance(obj, collections.abc.Hashable) 时被正确地识别为不可哈希对象。

如果一个重载了 __eq__() 的类需要保留来自父类的 __hash__() 实现,则必须通过设置 __hash__ = <ParentClass>.__hash__ 来显式地告知解释器。

如果一个没有重载 __eq__() 的类需要去掉哈希支持,则应该在类定义中包含 __hash__ = None。一个自定义了 __hash__() 以显式地引发 TypeError 的类会被 isinstance(obj, collections.abc.Hashable) 调用错误地识别为可哈希对象。

注解

在默认情况下,str 和 bytes 对象的 __hash__() 值会使用一个不可预知的随机值“加盐”。 虽然它们在一个单独 Python 进程中会保持不变,但它们的值在重复运行的 Python 间是不可预测的。

This is intended to provide protection against a denial-of-service caused by carefully-chosen inputs that exploit the worst case performance of a dict insertion, O(n2) complexity. See http://www.ocert.org/advisories/ocert-2011-003.html for details.

改变哈希值会影响集合的迭代次序。Python 也从不保证这个次序不会被改变(通常它在 32 位和 64 位构建上是不一致的)。

在 3.3 版更改: 默认启用哈希随机化。

object.__bool__(self)

调用此方法以实现真值检测以及内置的 bool() 操作;应该返回 FalseTrue。如果未定义此方法,则会查找并调用 __len__() 并在其返回非零值时视对象的逻辑值为真。如果一个类既未定义 __len__() 也未定义 __bool__() 则视其所有实例的逻辑值为真。

3.3.2. 自定义属性访问

可以定义下列方法来自定义对类实例属性访问(x.name 的使用、赋值或删除)的具体含义.

object.__getattr__(self, name)

当默认属性访问因引发 AttributeError 而失败时被调用 (可能是调用 __getattribute__() 时由于 name 不是一个实例属性或 self 的类关系树中的属性而引发了 AttributeError;或者是对 name 特性属性调用 __get__() 时引发了 AttributeError)。此方法应当返回(找到的)属性值或是引发一个 AttributeError 异常。

请注意如果属性是通过正常机制找到的,__getattr__() 就不会被调用。(这是在 __getattr__()__setattr__() 之间故意设置的不对称性。)这既是出于效率理由也是因为不这样设置的话 __getattr__() 将无法访问实例的其他属性。要注意至少对于实例变量来说,你不必在实例属性字典中插入任何值(而是通过插入到其他对象)就可以模拟对它的完全控制。请参阅下面的 __getattribute__() 方法了解真正获取对属性访问的完全控制权的办法。

object.__getattribute__(self, name)

此方法会无条件地被调用以实现对类实例属性的访问。如果类还定义了 __getattr__(),则后者不会被调用,除非 __getattribute__() 显式地调用它或是引发了 AttributeError。此方法应当返回(找到的)属性值或是引发一个 AttributeError 异常。为了避免此方法中的无限递归,其实现应该总是调用具有相同名称的基类方法来访问它所需要的任何属性,例如 object.__getattribute__(self, name)

注解

此方法在作为通过特定语法或内置函数隐式地调用的结果的情况下查找特殊方法时仍可能会被跳过。

引发一个 审计事件 object.__getattr__,附带参数 obj, name

object.__setattr__(self, name, value)

此方法在一个属性被尝试赋值时被调用。这个调用会取代正常机制(即将值保存到实例字典)。 name 为属性名称, value 为要赋给属性的值。

如果 __setattr__() 想要赋值给一个实例属性,它应该调用同名的基类方法,例如 object.__setattr__(self, name, value)

引发一个 审计事件 object.__setattr__,附带参数 obj, name, value

object.__delattr__(self, name)

类似于 __setattr__() 但其作用为删除而非赋值。此方法应该仅在 del obj.name 对于该对象有意义时才被实现。

引发一个 审计事件 object.__delattr__,附带参数 obj, name

object.__dir__(self)

此方法会在对相应对象调用 dir() 时被调用。返回值必须为一个序列。 dir() 会把返回的序列转换为列表并对其排序。

3.3.2.1. 自定义模块属性访问

特殊名称 __getattr____dir__ 还可被用来自定义对模块属性的访问。模块层级的 __getattr__ 函数应当接受一个参数,其名称为一个属性名,并返回计算结果值或引发一个 AttributeError。如果通过正常查找即 object.__getattribute__() 未在模块对象中找到某个属性,则 __getattr__ 会在模块的 __dict__ 中查找,未找到时会引发一个 AttributeError。如果找到,它会以属性名被调用并返回结果值。

__dir__ 函数应当不接受任何参数,并且返回一个表示模块中可访问名称的字符串序列。 此函数如果存在,将会重载一个模块中的标准 dir() 查找。

想要更细致地自定义模块的行为(设置属性和特性属性等待),可以将模块对象的 __class__ 属性设置为一个 types.ModuleType 的子类。例如:

import sys
from types import ModuleType
class VerboseModule(ModuleType):
    def __repr__(self):
        return f'Verbose {self.__name__}'
    def __setattr__(self, attr, value):
        print(f'Setting {attr}...')
        super().__setattr__(attr, value)
sys.modules[__name__].__class__ = VerboseModule

注解

定义模块的 __getattr__ 和设置模块的 __class__ 只会影响使用属性访问语法进行的查找 — 直接访问模块全局变量(不论是通过模块内的代码还是通过对模块全局字典的引用)是不受影响的。

在 3.5 版更改: __class__ 模块属性改为可写。

3.7 新版功能: __getattr____dir__ 模块属性。

参见

PEP 562 - 模块 getattrdir

描述用于模块的 __getattr____dir__ 函数。

3.3.2.2. 实现描述器

以下方法仅当一个包含该方法的类(称为 描述器 类)的实例出现于一个 所有者 类中的时候才会起作用(该描述器必须在所有者类或其某个上级类的字典中)。在以下示例中,“属性”指的是名称为所有者类 __dict__ 中的特征属性的键名的属性。

object.__get__(self, instance, owner=None)

调用此方法以获取所有者类的属性(类属性访问)或该类的实例的属性(实例属性访问)。 可选的 owner 参数是所有者类而 instance 是被用来访问属性的实例,如果通过 owner 来访问属性则返回 None

此方法应当返回计算得到的属性值或是引发 AttributeError 异常。

PEP 252 指明 __get__() 为带有一至二个参数的可调用对象。 Python 自身内置的描述器支持此规格定义;但是,某些第三方工具可能要求必须带两个参数。 Python 自身的 __getattribute__() 实现总是会传入两个参数,无论它们是否被要求提供。

object.__set__(self, instance, value)

调用此方法以设置 instance 指定的所有者类的实例的属性为新值 value

请注意,添加 __set__()__delete__() 会将描述器变成“数据描述器”。

object.__delete__(self, instance)

调用此方法以删除 instance 指定的所有者类的实例的属性。

属性 __objclass__ 会被 inspect 模块解读为指定此对象定义所在的类(正确设置此属性有助于动态类属性的运行时内省)。对于可调用对象来说,它可以指明预期或要求提供一个特定类型(或子类)的实例作为第一个位置参数(例如,CPython 会为实现于 C 中的未绑定方法设置此属性)。

3.3.2.3. 调用描述器

总的说来,描述器就是具有“绑定行为”的对象属性,其属性访问已被描述器协议中的方法所重载,包括 __get__(), __set__()__delete__()。如果一个对象定义了以上方法中的任意一个,它就被称为描述器。

属性访问的默认行为是从一个对象的字典中获取、设置或删除属性。例如,a.x 的查找顺序会从 a.__dict__['x'] 开始,然后是 type(a).__dict__['x'],接下来依次查找 type(a) 的上级基类,不包括元类。

但是,如果找到的值是定义了某个描述器方法的对象,则 Python 可能会重载默认行为并转而发起调用描述器方法。这具体发生在优先级链的哪个环节则要根据所定义的描述器方法及其被调用的方式来决定。

描述器发起调用的开始点是一个绑定 a.x。参数的组合方式依 a 而定:

直接调用

最简单但最不常见的调用方式是用户代码直接发起调用一个描述器方法: x.__get__(a)

实例绑定

如果绑定到一个对象实例,a.x 会被转换为调用: type(a).__dict__['x'].__get__(a, type(a))

类绑定

如果绑定到一个类,A.x 会被转换为调用: A.__dict__['x'].__get__(None, A)

超绑定

如果 asuper 的一个实例,则绑定 super(B, obj).m() 会在 obj.__class__.__mro__ 中搜索 B 的直接上级基类 A 然后通过以下调用发起调用描述器: A.__dict__['m'].__get__(obj, obj.__class__)

对于实例绑定,发起描述器调用的优先级取决于定义了哪些描述器方法。 一个描述器可以定义 __get__(), __set__()__delete__() 的任意组合。 如果它没有定义 __get__(),则访问属性将返回描述器对象自身,除非对象的实例字典中有相应属性值。 如果描述器定义了 __set__() 和/或 __delete__(),则它是一个数据描述器;如果以上两种都未定义,则它是一个非数据描述器。 通常,数据描述器会同时定义 __get__()__set__(),而非数据描述器则只有 __get__() 方法。 定义了 __get__()__set__() (和/或 __delete__()) 的数据描述器总是会重载实例字典中的定义。 与之相对地,非数据描述器则可被实例所重载。

Python 方法 (包括 staticmethod()classmethod()) 都是作为非数据描述器来实现的。 因此实例可以重定义并重载方法。 这允许单个实例获得与相同类的其他实例不一样的行为。

property() 函数是作为数据描述器来实现的。因此实例不能重载特性属性的行为。

3.3.2.4. slots

slots 允许我们显式地声明数据成员(例如特征属性)并禁止创建 dictweakref (除非是在 slots 中显式地声明或是在父类中可用。)

相比使用 dict 此方式可以显著地节省空间。 属性查找速度也可得到显著的提升。

object.__slots__

这个类变量可赋值为字符串、可迭代对象或由实例使用的变量名构成的字符串序列。 slots 会为已声明的变量保留空间,并阻止自动为每个实例创建 dictweakref

使用 slots 的注意事项

  • 当继承自一个未定义 slots 的类时,实例的 dictweakref 属性将总是可访问。
  • 没有 dict 变量,实例就不能给未在 slots 定义中列出的新变量赋值。尝试给一个未列出的变量名赋值将引发 AttributeError。新变量需要动态赋值,就要将 '__dict__' 加入到 slots 声明的字符串序列中。
  • 如果未给每个实例设置 weakref 变量,定义了 slots 的类就不支持对其实际的弱引用。如果需要弱引用支持,就要将 '__weakref__' 加入到 slots 声明的字符串序列中。
  • slots 是通过为每个变量名创建描述器 (实现描述器) 在类层级上实现的。因此,类属性不能被用来为通过 slots 定义的实例变量设置默认值;否则,类属性就会覆盖描述器赋值。
  • slots 声明的作用不只限于定义它的类。在父类中声明的 slots 在其子类中同样可用。不过,子类将会获得 dictweakref 除非它们也定义了 slots (其中应该仅包含对任何 额外 名称的声明位置)。
  • 如果一个类定义的位置在某个基类中也有定义,则由基类位置定义的实例变量将不可访问(除非通过直接从基类获取其描述器的方式)。这会使得程序的含义变成未定义。未来可能会添加一个防止此情况的检查。
  • 非空的 slots 不适用于派生自“可变长度”内置类型例如 intbytestuple 的派生类。
  • 任何非字符串可迭代对象都可以被赋值给 slots。映射也可以被使用;不过,未来可能会分别赋给每个键具有特殊含义的值。
  • class 赋值仅在两个类具有相同的 slots 时才会起作用。
  • 带有多个父类声明位置的多重继承也是可用的,但仅允许一个父类具有由声明位置创建的属性(其他基类必须具有空的位置布局) —— 违反规则将引发 TypeError
  • 如果为 slots 使用了一个迭代器,则会为迭代器的每个值创建描述器。 但是 slots 属性将为一个空迭代器。

3.3.3. 自定义类创建

当一个类继承其他类时,那个类的 init_subclass 会被调用。这样就可以编写能够改变子类行为的类。这与类装饰器有紧密的关联,但是类装饰器是影响它们所应用的特定类,而 __init_subclass__ 则只作用于定义了该方法的类所派生的子类。

classmethod object.__init_subclass__(cls)

当所在类派生子类时此方法就会被调用。cls 将指向新的子类。如果定义为一个普通实例方法,此方法将被隐式地转换为类方法。

传入一个新类的关键字参数会被传给父类的 __init_subclass__。为了与其他使用 __init_subclass__ 的类兼容,应当根据需要去掉部分关键字参数再将其余的传给基类,例如:

class Philosopher:
    def __init_subclass__(cls, /, default_name, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.default_name = default_name
class AustralianPhilosopher(Philosopher, default_name="Bruce"):
    pass

object.__init_subclass__ 的默认实现什么都不做,只在带任意参数调用时引发一个错误。

注解

元类提示 metaclass 将被其它类型机制消耗掉,并不会被传给 __init_subclass__ 的实现。实际的元类(而非显式的提示)可通过 type(cls) 访问。

3.6 新版功能.

When a class is created, type.__new__() scans the class variables and makes callbacks to those with a __set_name__() hook.

object.__set_name__(self, owner, name)

Automatically called at the time the owning class owner is created. The object has been assigned to name in that class:

class A:
    x = C()  # Automatically calls: x.__set_name__(A, 'x')

If the class variable is assigned after the class is created, __set_name__() will not be called automatically. If needed, __set_name__() can be called directly:

class A:
   pass
c = C()
A.x = c                  # The hook is not called
c.__set_name__(A, 'x')   # Manually invoke the hook

3.6 新版功能.

3.3.3.1. 元类

默认情况下,类是使用 type() 来构建的。类体会在一个新的命名空间内执行,类名会被局部绑定到 type(name, bases, namespace) 的结果。

类创建过程可通过在定义行传入 metaclass 关键字参数,或是通过继承一个包含此参数的现有类来进行定制。在以下示例中,MyClassMySubclass 都是 Meta 的实例:

class Meta(type):
    pass
class MyClass(metaclass=Meta):
    pass
class MySubclass(MyClass):
    pass

在类定义内指定的任何其他关键字参数都会在下面所描述的所有元类操作中进行传递。

当一个类定义被执行时,将发生以下步骤:

  • 解析 MRO 条目;
  • 确定适当的元类;
  • 准备类命名空间;
  • 执行类主体;
  • 创建类对象。
3.3.3.2. 解析 MRO 条目

如果在类定义中出现的基类不是 type 的实例,则使用 __mro_entries__ 方法对其进行搜索,当找到结果时,它会以原始基类元组做参数进行调用。此方法必须返回类的元组以替代此基类被使用。元组可以为空,在此情况下原始基类将被忽略。

参见

PEP 560 - 对 typing 模块和泛型类型的核心支持

3.3.3.3. 确定适当的元类

为一个类定义确定适当的元类是根据以下规则:

  • 如果没有基类且没有显式指定元类,则使用 type()
  • 如果给出一个显式元类而且 不是 type() 的实例,则其会被直接用作元类;
  • 如果给出一个 type() 的实例作为显式元类,或是定义了基类,则使用最近派生的元类。

最近派生的元类会从显式指定的元类(如果有)以及所有指定的基类的元类(即 type(cls))中选取。最近派生的元类应为 所有 这些候选元类的一个子类型。如果没有一个候选元类符合该条件,则类定义将失败并抛出 TypeError

3.3.3.4. 准备类命名空间

一旦确定了适当的元类,则将准备好类命名空间。 如果元类具有 __prepare__ 属性,它会以 namespace = metaclass.__prepare__(name, bases, **kwds) 的形式被调用(其中如果有任何额外的关键字参数,则应当来自类定义)。 __prepare__ 方法应该被实现为 classmethod()__prepare__ 所返回的命名空间会被传入 __new__,但是当最终的类对象被创建时,该命名空间会被拷贝到一个新的 dict 中。

如果元类没有 __prepare__ 属性,则类命名空间将初始化为一个空的有序映射。

参见

PEP 3115 - Python 3000 中的元类

引入 __prepare__ 命名空间钩子

3.3.3.5. 执行类主体

类主体会以(类似于) exec(body, globals(), namespace) 的形式被执行。普通调用与 exec() 的关键区别在于当类定义发生于函数内部时,词法作用域允许类主体(包括任何方法)引用来自当前和外部作用域的名称。

但是,即使当类定义发生于函数内部时,在类内部定义的方法仍然无法看到在类作用域层次上定义的名称。类变量必须通过实例的第一个形参或类方法来访问,或者是通过下一节中描述的隐式词法作用域的 __class__ 引用。

3.3.3.6. 创建类对象

一旦执行类主体完成填充类命名空间,将通过调用 metaclass(name, bases, namespace, **kwds) 创建类对象(此处的附加关键字参数与传入 __prepare__ 的相同)。

如果类主体中有任何方法引用了 __class__super,这个类对象会通过零参数形式的 super(). __class__ 所引用,这是由编译器所创建的隐式闭包引用。这使用零参数形式的 super() 能够正确标识正在基于词法作用域来定义的类,而被用于进行当前调用的类或实例则是基于传递给方法的第一个参数来标识的。

CPython implementation detail: 在 CPython 3.6 及之后的版本中,__class__ 单元会作为类命名空间中的 __classcell__ 条目被传给元类。 如果存在,它必须被向上传播给 type.__new__ 调用,以便能正确地初始化该类。 如果不这样做,在 Python 3.8 中将引发 RuntimeError

When using the default metaclass type, or any metaclass that ultimately calls type.__new__, the following additional customization steps are invoked after creating the class object:

  1. The type.__new__ method collects all of the attributes in the class namespace that define a __set_name__() method;
  2. Those __set_name__ methods are called with the class being defined and the assigned name of that particular attribute;
  3. The __init_subclass__() hook is called on the immediate parent of the new class in its method resolution order.

在类对象创建之后,它会被传给包含在类定义中的类装饰器(如果有的话),得到的对象将作为已定义的类绑定到局部命名空间。

当通过 type.__new__ 创建一个新类时,提供以作为命名空间形参的对象会被复制到一个新的有序映射并丢弃原对象。这个新副本包装于一个只读代理中,后者则成为类对象的 __dict__ 属性。

参见

PEP 3135 - 新的超类型

描述隐式的 __class__ 闭包引用

3.3.3.7. 元类的作用

元类的潜在作用非常广泛。已经过尝试的设想包括枚举、日志、接口检查、自动委托、自动特征属性创建、代理、框架以及自动资源锁定/同步等等。

3.3.4. 自定义实例及子类检查

以下方法被用来重载 isinstance()issubclass() 内置函数的默认行为。

特别地,元类 abc.ABCMeta 实现了这些方法以便允许将抽象基类(ABC)作为“虚拟基类”添加到任何类或类型(包括内置类型),包括其他 ABC 之中。

class.__instancecheck__(self, instance)

如果 instance 应被视为 class 的一个(直接或间接)实例则返回真值。如果定义了此方法,则会被调用以实现 isinstance(instance, class)

class.__subclasscheck__(self, subclass)

Return true 如果 subclass 应被视为 class 的一个(直接或间接)子类则返回真值。如果定义了此方法,则会被调用以实现 issubclass(subclass, class)

请注意这些方法的查找是基于类的类型(元类)。它们不能作为类方法在实际的类中被定义。这与基于实例被调用的特殊方法的查找是一致的,只有在此情况下实例本身被当作是类。

参见

PEP 3119 - 引入抽象基类

新增功能描述,通过 __instancecheck__()__subclasscheck__() 来定制 isinstance()issubclass() 行为,加入此功能的动机是出于向该语言添加抽象基类的内容。

3.3.5. 模拟泛型类型

通过定义一个特殊方法,可以实现由 PEP 484 所规定的泛型类语法 (例如 List[int]):

classmethod object.__class_getitem__(cls, key)

按照 key 参数指定的类型返回一个表示泛型类的专门化对象。

此方法的查找会基于对象自身,并且当定义于类体内部时,此方法将隐式地成为类方法。请注意,此机制主要是被保留用于静态类型提示,不鼓励在其他场合使用。

参见

PEP 560 - 对 typing 模块和泛型类型的核心支持

3.3.6. 模拟可调用对象

object.__call__(self[, args…])

此方法会在实例作为一个函数被“调用”时被调用;如果定义了此方法,则 x(arg1, arg2, ...) 就大致可以被改写为 type(x).__call__(x, arg1, ...)

3.3.7. 模拟容器类型

可以定义下列方法来实现容器对象。 容器通常属于序列(如列表或元组)或映射(如字典),但也存在其他形式的容器。 前几个方法集被用于模拟序列或是模拟映射;两者的不同之处在于序列允许的键应为整数 k0 <= k < N 其中 N 是序列或定义指定区间的项的切片对象的长度。 此外还建议让映射提供 keys(), values(), items(), get(), clear(), setdefault(), pop(), popitem(), copy() 以及 update() 等方法,它们的行为应与 Python 标准字典对象的相应方法类似。 此外 collections.abc 模块提供了一个 MutableMapping 抽象基类以便根据由 __getitem__(), __setitem__(), __delitem__(), 和 keys() 组成的基本集来创建所需的方法。 可变序列还应像 Python 标准列表对象那样提供 append(), count(), index(), extend(), insert(), pop(), remove(), reverse()sort() 等方法。 最后,序列类型还应通过定义下文描述的 __add__(), __radd__(), __iadd__(), __mul__(), __rmul__()__imul__() 等方法来实现加法(指拼接)和乘法(指重复);它们不应定义其他数值运算符。 此外还建议映射和序列都实现 __contains__() 方法以允许高效地使用 in 运算符;对于映射,in 应该搜索映射的键;对于序列,则应搜索其中的值。 另外还建议映射和序列都实现 __iter__() 方法以允许高效地迭代容器中的条目;对于映射,__iter__() 应当迭代对象的键;对于序列,则应当迭代其中的值。

object.__len__(self)

调用此方法以实现内置函数 len()。应该返回对象的长度,以一个 >= 0 的整数表示。此外,如果一个对象未定义 __bool__() 方法而其 __len__() 方法返回值为零,则在布尔运算中会被视为假值。

CPython implementation detail: 在 CPython 中,要求长度最大为 sys.maxsize。如果长度大于 sys.maxsize 则某些特性 (例如 len()) 可能会引发 OverflowError。要通过真值检测来防止引发 OverflowError,对象必须定义 __bool__() 方法。

object.__length_hint__(self)

调用此方法以实现 operator.length_hint()。 应该返回对象长度的估计值(可能大于或小于实际长度)。 此长度应为一个 >= 0 的整数。 返回值也可以为 NotImplemented,这会被视作与 __length_hint__ 方法完全不存在时一样处理。 此方法纯粹是为了优化性能,并不要求正确无误。

3.4 新版功能.

注解

切片是通过下述三个专门方法完成的。以下形式的调用

a[1:2] = b

会为转写为

a[slice(1, 2, None)] = b

其他形式以此类推。略去的切片项总是以 None 补全。

object.__getitem__(self, key)

调用此方法以实现 self[key] 的求值。对于序列类型,接受的键应为整数和切片对象。请注意负数索引(如果类想要模拟序列类型)的特殊解读是取决于 __getitem__() 方法。如果 key 的类型不正确则会引发 TypeError 异常;如果为序列索引集范围以外的值(在进行任何负数索引的特殊解读之后)则应引发 IndexError 异常。对于映射类型,如果 key 找不到(不在容器中)则应引发 KeyError 异常。

注解

for 循环在有不合法索引时会期待捕获 IndexError 以便正确地检测到序列的结束。

object.__setitem__(self, key, value)

调用此方法以实现向 self[key] 赋值。注意事项与 __getitem__() 相同。为对象实现此方法应该仅限于需要映射允许基于键修改值或添加键,或是序列允许元素被替换时。不正确的 key 值所引发的异常应与 __getitem__() 方法的情况相同。

object.__delitem__(self, key)

调用此方法以实现 self[key] 的删除。注意事项与 __getitem__() 相同。为对象实现此方法应该权限于需要映射允许移除键,或是序列允许移除元素时。不正确的 key 值所引发的异常应与 __getitem__() 方法的情况相同。

object.__missing__(self, key)

此方法由 dict.__getitem__() 在找不到字典中的键时调用以实现 dict 子类的 self[key]

object.__iter__(self)

此方法在需要为容器创建迭代器时被调用。此方法应该返回一个新的迭代器对象,它能够逐个迭代容器中的所有对象。对于映射,它应该逐个迭代容器中的键。

迭代器对象也需要实现此方法;它们需要返回对象自身。

object.__reversed__(self)

此方法(如果存在)会被 reversed() 内置函数调用以实现逆向迭代。它应当返回一个新的以逆序逐个迭代容器内所有对象的迭代器对象。

如果未提供 __reversed__() 方法,则 reversed() 内置函数将回退到使用序列协议 (__len__()__getitem__())。支持序列协议的对象应当仅在能够提供比 reversed() 所提供的实现更高效的实现时才提供 __reversed__() 方法。

成员检测运算符 (innot in) 通常以对容器进行逐个迭代的方式来实现。 不过,容器对象可以提供以下特殊方法并采用更有效率的实现,这样也不要求对象必须为可迭代对象。

object.__contains__(self, item)

调用此方法以实现成员检测运算符。如果 itemself 的成员则应返回真,否则返回假。对于映射类型,此检测应基于映射的键而不是值或者键值对。

对于未定义 __contains__() 的对象,成员检测将首先尝试通过 __iter__() 进行迭代,然后再使用 __getitem__() 的旧式序列迭代协议。

3.3.8. 模拟数字类型

定义以下方法即可模拟数字类型。特定种类的数字不支持的运算(例如非整数不能进行位运算)所对应的方法应当保持未定义状态。

object.__add__(self, other)

object.__sub__(self, other)

object.__mul__(self, other)

object.__matmul__(self, other)

object.__truediv__(self, other)

object.__floordiv__(self, other)

object.__mod__(self, other)

object.__divmod__(self, other)

object.__pow__(self, other[, modulo])

object.__lshift__(self, other)

object.__rshift__(self, other)

object.__and__(self, other)

object.__xor__(self, other)

object.__or__(self, other)

调用这些方法来实现二进制算术运算 (+, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |)。例如,求表达式 x + y 的值,其中 x 是具有 __add__() 方法的类的一个实例,则会调用 x.__add__(y)__divmod__() 方法应该等价于使用 __floordiv__()__mod__(),它不应该被关联到 __truediv__()。请注意如果要支持三元版本的内置 pow() 函数,则 __pow__() 的定义应该接受可选的第三个参数。

如果这些方法中的某一个不支持与所提供参数进行运算,它应该返回 NotImplemented

object.__radd__(self, other)

object.__rsub__(self, other)

object.__rmul__(self, other)

object.__rmatmul__(self, other)

object.__rtruediv__(self, other)

object.__rfloordiv__(self, other)

object.__rmod__(self, other)

object.__rdivmod__(self, other)

object.__rpow__(self, other[, modulo])

object.__rlshift__(self, other)

object.__rrshift__(self, other)

object.__rand__(self, other)

object.__rxor__(self, other)

object.__ror__(self, other)

调用这些方法来实现具有反射(交换)操作数的二进制算术运算 (+, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |)。这些成员函数仅会在左操作数不支持相应运算且两个操作数类型不同时被调用。例如,求表达式 x - y 的值,其中 y 是具有 __rsub__() 方法的类的一个实例,则当 x.__sub__(y) 返回 NotImplemented 时会调用 y.__rsub__(x)

请注意三元版的 pow() 并不会尝试调用 __rpow__() (因为强制转换规则会太过复杂)。

注解

如果右操作数类型为左操作数类型的一个子类,且该子类提供了指定运算的反射方法,则此方法将先于左操作数的非反射方法被调用。 此行为可允许子类重载其祖先类的运算符。

object.__iadd__(self, other)

object.__isub__(self, other)

object.__imul__(self, other)

object.__imatmul__(self, other)

object.__itruediv__(self, other)

object.__ifloordiv__(self, other)

object.__imod__(self, other)

object.__ipow__(self, other[, modulo])

object.__ilshift__(self, other)

object.__irshift__(self, other)

object.__iand__(self, other)

object.__ixor__(self, other)

object.__ior__(self, other)

调用这些方法来实现扩展算术赋值 (+=, -=, *=, @=, /=, //=, %=, **=, <<=, >>=, &=, ^=, |=)。这些方法应该尝试进行自身操作 (修改 self) 并返回结果 (结果应该但并非必须为 self)。如果某个方法未被定义,相应的扩展算术赋值将回退到普通方法。例如,如果 x 是具有 __iadd__() 方法的类的一个实例,则 x += y 就等价于 x = x.__iadd__(y)。否则就如 x + y 的求值一样选择 x.__add__(y)y.__radd__(x)。在某些情况下,扩展赋值可导致未预期的错误,但此行为实际上是数据模型的一个组成部分。

object.__neg__(self)

object.__pos__(self)

object.__abs__(self)

object.__invert__(self)

调用此方法以实现一元算术运算 (-, +, abs()~)。

object.__complex__(self)

object.__int__(self)

object.__float__(self)

调用这些方法以实现内置函数 complex(), int()float()。应当返回一个相应类型的值。

object.__index__(self)

调用此方法以实现 operator.index() 以及 Python 需要无损地将数字对象转换为整数对象的场合(例如切片或是内置的 bin(), hex()oct() 函数)。 存在此方法表明数字对象属于整数类型。 必须返回一个整数。

如果未定义 __int__(), __float__()__complex__() 则相应的内置函数 int(), float()complex() 将回退为 __index__()

object.__round__(self[, ndigits])

object.__trunc__(self)

object.__floor__(self)

object.__ceil__(self)

调用这些方法以实现内置函数 round() 以及 math 函数 trunc(), floor()ceil()。 除了将 ndigits 传给 __round__() 的情况之外这些方法的返回值都应当是原对象截断为 Integral (通常为 int)。

如果未定义 __int__() 则内置函数 int() 会回退到 __trunc__()

3.3.9. with 语句上下文管理器

上下文管理器 是一个对象,它定义了在执行 with 语句时要建立的运行时上下文。 上下文管理器处理进入和退出所需运行时上下文以执行代码块。 通常使用 with 语句(在 with 语句 中描述),但是也可以通过直接调用它们的方法来使用。

上下文管理器的典型用法包括保存和恢复各种全局状态,锁定和解锁资源,关闭打开的文件等等。

object.__enter__(self)

进入与此对象相关的运行时上下文。 with 语句将会绑定这个方法的返回值到 as 子句中指定的目标,如果有的话。

object.__exit__(self, exc_type, exc_value, traceback)

退出关联到此对象的运行时上下文。 各个参数描述了导致上下文退出的异常。 如果上下文是无异常地退出的,三个参数都将为 None

如果提供了异常,并且希望方法屏蔽此异常(即避免其被传播),则应当返回真值。 否则的话,异常将在退出此方法时按正常流程处理。

请注意 __exit__() 方法不应该重新引发被传入的异常,这是调用者的责任。

参见

PEP 343 - “with” 语句

Python with 语句的规范描述、背景和示例。

3.3.10. 定制类模式匹配中的位置参数

当在模式中使用类名称时,默认不允许模式中出现位置参数,例如 case MyClass(x, y) 通常是无效的,除非 MyClass 提供了特别支持。 要能使用这样的模式,类必须定义一个 match_args 属性。

object.__match_args__

该类变量可以被赋值为一个字符串元组。 当该类被用于带位置参数的类模式时,每个位置参数都将被转换为关键字参数,并使用 match_args 中的对应值作为关键字。 缺失此属性就等价于将其设为 ()

举例来说,如果 MyClass.__match_args__("left", "center", "right") 则意味着 case MyClass(x, y) 就等价于 case MyClass(left=x, center=y)。 请注意模式中参数的数量必须小于等于 match_args 中元素的数量;如果前者大于后者,则尝试模式匹配时将引发 TypeError

3.10 新版功能.

参见

PEP 634 - 结构化模式匹配

有关 Python match 语句的规范说明。

3.3.11. 特殊方法查找

对于自定义类来说,特殊方法的隐式发起调用仅保证在其定义于对象类型中时能正确地发挥作用,而不能定义在对象实例字典中。 该行为就是以下代码会引发异常的原因。:

>>> class C:
...     pass
...
>>> c = C()
>>> c.__len__ = lambda: 5
>>> len(c)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'C' has no len()

此行为背后的原理在于包括类型对象在内的所有对象都会实现的几个特殊方法,例如 __hash__()__repr__()。 如果这些方法的隐式查找使用了传统的查找过程,它们会在对类型对象本身发起调用时失败:

>>> 1 .__hash__() == hash(1)
True
>>> int.__hash__() == hash(int)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: descriptor '__hash__' of 'int' object needs an argument

以这种方式不正确地尝试发起调用一个类的未绑定方法有时被称为‘元类混淆’,可以通过在查找特殊方法时绕过实例的方式来避免:

>>> type(1).__hash__(1) == hash(1)
True
>>> type(int).__hash__(int) == hash(int)
True

除了为了正确性而绕过任何实例属性之外,隐式特殊方法查找通常也会绕过 __getattribute__() 方法,甚至包括对象的元类:

>>> class Meta(type):
...     def __getattribute__(*args):
...         print("Metaclass getattribute invoked")
...         return type.__getattribute__(*args)
...
>>> class C(object, metaclass=Meta):
...     def __len__(self):
...         return 10
...     def __getattribute__(*args):
...         print("Class getattribute invoked")
...         return object.__getattribute__(*args)
...
>>> c = C()
>>> c.__len__()                 # Explicit lookup via instance
Class getattribute invoked
10
>>> type(c).__len__(c)          # Explicit lookup via type
Metaclass getattribute invoked
10
>>> len(c)                      # Implicit lookup
10

以这种方式绕过 __getattribute__() 机制为解析器内部的速度优化提供了显著的空间,其代价则是牺牲了处理特殊方法时的一些灵活性(特殊方法 必须 设置在类对象本身上以便始终一致地由解释器发起调用)。

3.4. 协程

3.4.1. 可等待对象

awaitable 对象主要实现了 __await__() 方法。 从 async def 函数返回的 协程对象 即属于可等待对象。

注解

从带有 types.coroutine()asyncio.coroutine() 装饰器的生成器返回的 generator iterator 对象也属于可等待对象,但它们并未实现 __await__()

object.__await__(self)

必须返回一个 iterator。 应当被用来实现 awaitable 对象。 例如,asyncio.Future 实现了此方法以与 await 表达式相兼容。

3.5 新版功能.

参见

PEP 492 了解有关可等待对象的详细信息。

3.4.2. 协程对象

协程对象 属于 awaitable 对象。 协程的执行可通过调用 __await__() 并迭代其结果来控制。 当协程结束执行并返回时,迭代器会引发 StopIteration,该异常的 value 属性将存放返回值。 如果协程引发了异常,它会被迭代器所传播。 协程不应直接引发未处理的 StopIteration 异常。

协程也具有下面列出的方法,它们类似于生成器的对应方法。 但是,与生成器不同,协程并不直接支持迭代。

在 3.5.2 版更改: 等待一个协程超过一次将引发 RuntimeError

coroutine.send(value)

开始或恢复协程的执行。 如果 valueNone,则这相当于前往 __await__() 所返回迭代器的下一项。 如果 value 不为 None,此方法将委托给导致协程挂起的迭代器的 send() 方法。 其结果(返回值,StopIteration 或是其他异常)将与上述对 __await__() 返回值进行迭代的结果相同。

coroutine.throw(type[, value[, traceback]])

在协程内引发指定的异常。 此方法将委托给导致协程挂起的迭代器的 throw() 方法,如果存在该方法。 否则的话,异常会在挂起点被引发。 其结果(返回值,StopIteration 或是其他异常)将与上述对 __await__() 返回值进行迭代的结果相同。 如果异常未在协程内被捕获,则将回传给调用者。

coroutine.close()

此方法会使得协程清理自身并退出。 如果协程被挂起,此方法会先委托给导致协程挂起的迭代器的 close() 方法,如果存在该方法。 然后它会在挂起点引发 GeneratorExit,使得协程立即清理自身。 最后,协程会被标记为已结束执行,即使它根本未被启动。

当协程对象将要被销毁时,会使用以上处理过程来自动关闭。

3.4.3. 异步迭代器

异步迭代器 可以在其 __anext__ 方法中调用异步代码。

异步迭代器可在 async for 语句中使用。

object.__aiter__(self)

必须返回一个 异步迭代器 对象。

object.__anext__(self)

必须返回一个 可迭代对象 输出迭代器的下一结果值。 当迭代结束时应该引发 StopAsyncIteration 错误。

异步可迭代对象的一个示例:

class Reader:
    async def readline(self):
        ...
    def __aiter__(self):
        return self
    async def __anext__(self):
        val = await self.readline()
        if val == b'':
            raise StopAsyncIteration
        return val

3.5 新版功能.

在 3.7 版更改: 在 Python 3.7 之前,__aiter__ 可以返回一个 可迭代对象 并解析为 异步迭代器。

从 Python 3.7 开始,__aiter__ 必须 返回一个异步迭代器对象。 返回任何其他对象都将导致 TypeError 错误。

3.4.4. 异步上下文管理器

异步上下文管理器上下文管理器 的一种,它能够在其 __aenter____aexit__ 方法中暂停执行。

异步上下文管理器可在 async with 语句中使用。

object.__aenter__(self)

在语义上类似于 __enter__(),仅有的区别是它必须返回一个 可等待对象

object.__aexit__(self, exc_type, exc_value, traceback)

在语义上类似于 __exit__(),仅有的区别是它必须返回一个 可等待对象

异步上下文管理器类的一个示例:

class AsyncContextManager:
    async def __aenter__(self):
        await log('entering context')
    async def __aexit__(self, exc_type, exc, tb):
        await log('exiting context')

3.5 新版功能.

注:

  • 在某些情况下 有可能 基于可控的条件改变一个对象的类型。 但这通常不是个好主意,因为如果处理不当会导致一些非常怪异的行为。
  • __hash__(), __iter__(), __reversed__() 以及 __contains__() 方法对此有特殊处理;其他方法仍会引发 TypeError,但可能依靠 None 属于不可调用对象的行为来做到这一点。
  • 这里的“不支持”是指该类无此方法,或方法返回 NotImplemented。 如果你想强制回退到右操作数的反射方法,请不要设置方法为 None — 那会造成显式地 阻塞 此种回退的相反效果。
  • 对于相同类型的操作数,如果非反射方法 — 例如 __add__() — 失败则会认为整个运算都不被支持,这就是反射方法未被调用的原因。

4. 执行模型

4.1. 程序的结构

Python 程序是由代码块构成的。 代码块 是被作为一个单元来执行的一段 Python 程序文本。 以下几个都属于代码块:模块、函数体和类定义。 交互式输入的每条命令都是代码块。 一个脚本文件(作为标准输入发送给解释器或是作为命令行参数发送给解释器的文件)也是代码块。 一条脚本命令(通过 -c 选项在解释器命令行中指定的命令)也是代码块。 通过在命令行中使用 -m 参数作为最高层级脚本(即 __main__ 模块)运行的模块也是代码块。 传递给内置函数 eval()exec() 的字符串参数也是代码块。

代码块在 执行帧 中被执行。 一个帧会包含某些管理信息(用于调试)并决定代码块执行完成后应前往何处以及如何继续执行。

4.2. 命名与绑定

4.2.1. 名称的绑定

名称 用于指代对象。 名称是通过名称绑定操作来引入的。

以下构造会绑定名称:传给函数的正式形参,import 语句,类与函数定义(这会在定义的代码块中绑定类或函数名称)以及发生以标识符为目标的赋值,for 循环的开头,或 with 语句和 except 子句的 as 之后。 import 语句的 from ... import * 形式会绑定在被导入模块中定义的所有名称,那些以下划线开头的除外。 这种形式仅在模块层级上被使用。

del 语句的目标也被视作一种绑定(虽然其实际语义为解除名称绑定)。

每条赋值或导入语句均发生于类或函数内部定义的代码块中,或是发生于模块层级(即最高层级的代码块)。

如果名称绑定在一个代码块中,则为该代码块的局部变量,除非声明为 nonlocalglobal。 如果名称绑定在模块层级,则为全局变量。 (模块代码块的变量既为局部变量又为全局变量。) 如果变量在一个代码块中被使用但不是在其中定义,则为 自由变量

每个在程序文本中出现的名称是指由以下名称解析规则所建立的对该名称的 绑定

4.2.2. 名称的解析

作用域 定义了一个代码块中名称的可见性。 如果代码块中定义了一个局部变量,则其作用域包含该代码块。 如果定义发生于函数代码块中,则其作用域会扩展到该函数所包含的任何代码块,除非有某个被包含代码块引入了对该名称的不同绑定。

当一个名称在代码块中被使用时,会由包含它的最近作用域来解析。 对一个代码块可见的所有这种作用域的集合称为该代码块的 环境

当一个名称完全找不到时,将会引发 NameError 异常。 如果当前作用域为函数作用域,且该名称指向一个局部变量,而此变量在该名称被使用的时候尚未绑定到特定值,将会引发 UnboundLocalError 异常。 UnboundLocalErrorNameError 的一个子类。

如果一个代码块内的任何位置发生名称绑定操作,则代码块内所有对该名称的使用会被认为是对当前代码块的引用。 当一个名称在其被绑定前就在代码块内被使用时则会导致错误。 这个一个很微妙的规则。 Python 缺少声明语法,并允许名称绑定操作发生于代码块内的任何位置。 一个代码块的局部变量可通过在整个代码块文本中扫描名称绑定操作来确定。

如果 global 语句出现在一个代码块中,则所有对该语句所指定名称的使用都是在最高层级命名空间内对该名称绑定的引用。 名称在最高层级命名内的解析是通过全局命名空间,也就是包含该代码块的模块的命名空间,以及内置命名空间即 builtins 模块的命名空间。 全局命名空间会先被搜索。 如果未在其中找到指定名称,再搜索内置命名空间。 global 语句必须位于所有对其所指定名称的使用之前。

global 语句与同一代码块中名称绑定具有相同的作用域。 如果一个自由变量的最近包含作用域中有一条 global 语句,则该自由变量也会被当作是全局变量。

nonlocal 语句会使得相应的名称指向之前在最近包含函数作用域中绑定的变量。 如果指定名称不存在于任何包含函数作用域中则将在编译时引发 SyntaxError

模块的作用域会在模块第一次被导入时自动创建。 一个脚本的主模块总是被命名为 __main__

类定义代码块以及传给 exec()eval() 的参数是名称解析上下文中的特殊情况。 类定义是可能使用并定义名称的可执行语句。 这些引用遵循正常的名称解析规则,例外之处在于未绑定的局部变量将会在全局命名空间中查找。 类定义的命名空间会成为该类的属性字典。 在类代码块中定义的名称的作用域会被限制在类代码块中;它不会扩展到方法的代码块中 — 这也包括推导式和生成器表达式,因为它们都是使用函数作用域实现的。 这意味着以下代码将会失败:

class A:
    a = 42
    b = list(a + i for i in range(10))

4.2.3. 内置命名空间和受限的执行

CPython implementation detail: 用户不应该接触 __builtins__,严格说来它属于实现细节。 用户如果要重载内置命名空间中的值则应该 import builtins 并相应地修改该模块中的属性。

与一个代码块的执行相关联的内置命名空间实际上是通过在其全局命名空间中搜索名称 __builtins__ 来找到的;这应该是一个字典或一个模块(在后一种情况下会使用该模块的字典)。 默认情况下,当在 __main__ 模块中时,__builtins__ 就是内置模块 builtins;当在任何其他模块中时,__builtins__ 则是 builtins 模块自身的字典的一个别名。

4.2.4. 与动态特性的交互

自由变量的名称解析发生于运行时而不是编译时。 这意味着以下代码将打印出 42:

i = 10
def f():
    print(i)
i = 42
f()

eval()exec() 函数没有对完整环境的访问权限来解析名称。 名称可以在调用者的局部和全局命名空间中被解析。 自由变量的解析不是在最近包含命名空间中,而是在全局命名空间中。 exec()eval() 函数有可选参数用来重载全局和局部命名空间。 如果只指定一个命名空间,则它会同时作用于两者。

4.3. 异常

异常是中断代码块的正常控制流程以便处理错误或其他异常条件的一种方式。 异常会在错误被检测到的位置 引发,它可以被当前包围代码块或是任何直接或间接发起调用发生错误的代码块的其他代码块所 处理

Python 解析器会在检测到运行时错误(例如零作为被除数)的时候引发异常。 Python 程序也可以通过 raise 语句显式地引发异常。 异常处理是通过 tryexcept 语句来指定的。 该语句的 finally 子句可被用来指定清理代码,它并不处理异常,而是无论之前的代码是否发生异常都会被执行。

Python 的错误处理采用的是“终止”模型:异常处理器可以找出发生了什么问题,并在外层继续执行,但它不能修复错误的根源并重试失败的操作(除非通过从顶层重新进入出错的代码片段)。

当一个异常完全未被处理时,解释器会终止程序的执行,或者返回交互模式的主循环。 无论是哪种情况,它都会打印栈回溯信息,除非是当异常为 SystemExit 的时候。

异常是通过类实例来标识的。 except 子句会依据实例的类来选择:它必须引用实例的类或是其所属的基类。 实例可通过处理器被接收,并可携带有关异常条件的附加信息。

注解

异常消息不是 Python API 的组成部分。 其内容可能在 Python 升级到新版本时不经警告地发生改变,不应该被需要在多版本解释器中运行的代码所依赖。

5. 导入系统

一个 module 内的 Python 代码通过 importing 操作就能够访问另一个模块内的代码。 import 语句是发起调用导入机制的最常用方式,但不是唯一的方式。 importlib.import_module() 以及内置的 __import__() 等函数也可以被用来发起调用导入机制。

import 语句结合了两个操作;它先搜索指定名称的模块,然后将搜索结果绑定到当前作用域中的名称。 import 语句的搜索操作被定义为对 __import__() 函数的调用并带有适当的参数。 __import__() 的返回值会被用于执行 import 语句的名称绑定操作。 请参阅 import 语句了解名称绑定操作的更多细节。

__import__() 的直接调用将仅执行模块搜索以及在找到时的模块创建操作。 不过也可能产生某些副作用,例如导入父包和更新各种缓存 (包括 sys.modules),只有 import 语句会执行名称绑定操作。

import 语句被执行时,标准的内置 __import__() 函数会被调用。 其他发起调用导入系统的机制 (例如 importlib.import_module()) 可能会选择绕过 __import__() 并使用它们自己的解决方案来实现导入机制。

当一个模块首次被导入时,Python 会搜索该模块,如果找到就创建一个 module 对象 1 并初始化它。 如果指定名称的模块未找到,则会引发 ModuleNotFoundError。 当发起调用导入机制时,Python 会实现多种策略来搜索指定名称的模块。 这些策略可以通过使用使用下文所描述的多种钩子来加以修改和扩展。

在 3.3 版更改: 导入系统已被更新以完全实现 PEP 302 中的第二阶段要求。 不会再有任何隐式的导入机制 —— 整个导入系统都通过 sys.meta_path 暴露出来。 此外,对原生命名空间包的支持也已被实现 (参见 PEP 420)。

5.1. importlib

importlib 模块提供了一个丰富的 API 用来与导入系统进行交互。 例如 importlib.import_module() 提供了相比内置的 __import__() 更推荐、更简单的 API 用来发起调用导入机制。

5.2. 包

Python 只有一种模块对象类型,所有模块都属于该类型,无论模块是用 Python、C 还是别的语言实现。 为了帮助组织模块并提供名称层次结构,Python 还引入了 包 的概念。

你可以把包看成是文件系统中的目录,并把模块看成是目录中的文件,但请不要对这个类比做过于字面的理解,因为包和模块不是必须来自于文件系统。 为了方便理解本文档,我们将继续使用这种目录和文件的类比。 与文件系统一样,包通过层次结构进行组织,在包之内除了一般的模块,还可以有子包。

要注意的一个重点概念是所有包都是模块,但并非所有模块都是包。 或者换句话说,包只是一种特殊的模块。 特别地,任何具有 __path__ 属性的模块都会被当作是包。

所有模块都有自己的名字。 子包名与其父包名以点号分隔,与 Python 的标准属性访问语法一致。 例如你可能看到一个名为 sys 的模块以及一个名为 email 的包,这个包中又有一个名为 email.mime 的子包和该子包中的名为 email.mime.text 的子包。

5.2.1. 常规包

Python 定义了两种类型的包,常规包 和 命名空间包。 常规包是传统的包类型,它们在 Python 3.2 及之前就已存在。 常规包通常以一个包含 __init__.py 文件的目录形式实现。 当一个常规包被导入时,这个 __init__.py 文件会隐式地被执行,它所定义的对象会被绑定到该包命名空间中的名称。__init__.py 文件可以包含与任何其他模块中所包含的 Python 代码相似的代码,Python 将在模块被导入时为其添加额外的属性。

例如,以下文件系统布局定义了一个最高层级的 parent 包和三个子包:

parent/
    __init__.py
    one/
        __init__.py
    two/
        __init__.py
    three/
        __init__.py

导入 parent.one 将隐式地执行 parent/__init__.pyparent/one/__init__.py。 后续导入 parent.twoparent.three 则将分别执行 parent/two/__init__.pyparent/three/__init__.py

5.2.2. 命名空间包

命名空间包是由多个 部分 构成的,每个部分为父包增加一个子包。 各个部分可能处于文件系统的不同位置。 部分也可能处于 zip 文件中、网络上,或者 Python 在导入期间可以搜索的其他地方。 命名空间包并不一定会直接对应到文件系统中的对象;它们有可能是无实体表示的虚拟模块。

命名空间包的 __path__ 属性不使用普通的列表。 而是使用定制的可迭代类型,如果其父包的路径 (或者最高层级包的 sys.path) 发生改变,这种对象会在该包内的下一次导入尝试时自动执行新的对包部分的搜索。

命名空间包没有 parent/__init__.py 文件。 实际上,在导入搜索期间可能找到多个 parent 目录,每个都由不同的部分所提供。 因此 parent/one 的物理位置不一定与 parent/two 相邻。 在这种情况下,Python 将为顶级的 parent 包创建一个命名空间包,无论是它本身还是它的某个子包被导入。

另请参阅 PEP 420 了解对命名空间包的规格描述。

5.3. 搜索

为了开始搜索,Python 需要被导入模块(或者包,对于当前讨论来说两者没有差别)的完整 限定名称。 此名称可以来自 import 语句所带的各种参数,或者来自传给 importlib.import_module()__import__() 函数的形参。

此名称会在导入搜索的各个阶段被使用,它也可以是指向一个子模块的带点号路径,例如 foo.bar.baz。 在这种情况下,Python 会先尝试导入 foo,然后是 foo.bar,最后是 foo.bar.baz。 如果这些导入中的任何一个失败,都会引发 ModuleNotFoundError

5.3.1. 模块缓存

在导入搜索期间首先会被检查的地方是 sys.modules。 这个映射起到缓存之前导入的所有模块的作用(包括其中间路径)。 因此如果之前导入过 foo.bar.baz,则 sys.modules 将包含 foo, foo.barfoo.bar.baz 条目。 每个键的值就是相应的模块对象。

在导入期间,会在 sys.modules 查找模块名称,如存在则其关联的值就是需要导入的模块,导入过程完成。 然而,如果值为 None,则会引发 ModuleNotFoundError。 如果找不到指定模块名称,Python 将继续搜索该模块。

sys.modules 是可写的。删除键可能不会破坏关联的模块(因为其他模块可能会保留对它的引用),但它会使命名模块的缓存条目无效,导致 Python 在下次导入时重新搜索命名模块。键也可以赋值为 None ,强制下一次导入模块导致 ModuleNotFoundError

但是要小心,因为如果你还保有对某个模块对象的引用,同时停用其在 sys.modules 中的缓存条目,然后又再次导入该名称的模块,则前后两个模块对象将 不是 同一个。 相反地,importlib.reload() 将重用 同一个 模块对象,并简单地通过重新运行模块的代码来重新初始化模块内容。

5.3.2. 查找器和加载器

如果指定名称的模块在 sys.modules 找不到,则将发起调用 Python 的导入协议以查找和加载该模块。 此协议由两个概念性模块构成,即 查找器 和 加载器。 查找器的任务是确定是否能使用其所知的策略找到该名称的模块。 同时实现这两种接口的对象称为 导入器 —— 它们在确定能加载所需的模块时会返回其自身。

Python 包含了多个默认查找器和导入器。 第一个知道如何定位内置模块,第二个知道如何定位冻结模块。 第三个默认查找器会在 import path 中搜索模块。 import path 是一个由文件系统路径或 zip 文件组成的位置列表。 它还可以扩展为搜索任意可定位资源,例如由 URL 指定的资源。

导入机制是可扩展的,因此可以加入新的查找器以扩展模块搜索的范围和作用域。

查找器并不真正加载模块。 如果它们能找到指定名称的模块,会返回一个 模块规格说明,这是对模块导入相关信息的封装,供后续导入机制用于在加载模块时使用。

以下各节描述了有关查找器和加载器协议的更多细节,包括你应该如何创建并注册新的此类对象来扩展导入机制。

在 3.4 版更改: 在之前的 Python 版本中,查找器会直接返回 加载器,现在它们则返回模块规格说明,其中 包含 加载器。 加载器仍然在导入期间被使用,但负担的任务有所减少。

5.3.3. 导入钩子

导入机制被设计为可扩展;其中的基本机制是 导入钩子*。 导入钩子有两种类型: *元钩子导入路径钩子

元钩子在导入过程开始时被调用,此时任何其他导入过程尚未发生,但 sys.modules 缓存查找除外。 这允许元钩子重载 sys.path 过程、冻结模块甚至内置模块。 元钩子的注册是通过向 sys.meta_path 添加新的查找器对象,具体如下所述。

导入路径钩子是作为 sys.path (或 package.__path__) 过程的一部分,在遇到它们所关联的路径项的时候被调用。 导入路径钩子的注册是通过向 sys.path_hooks 添加新的可调用对象,具体如下所述。

5.3.4. 元路径

当指定名称的模块在 sys.modules 中找不到时,Python 会接着搜索 sys.meta_path,其中包含元路径查找器对象列表。 这些查找器按顺序被查询以确定它们是否知道如何处理该名称的模块。 元路径查找器必须实现名为 find_spec() 的方法,该方法接受三个参数:名称、导入路径和目标模块(可选)。 元路径查找器可使用任何策略来确定它是否能处理指定名称的模块。

如果元路径查找器知道如何处理指定名称的模块,它将返回一个说明对象。 如果它不能处理该名称的模块,则会返回 None。 如果 sys.meta_path 处理过程到达列表末尾仍未返回说明对象,则将引发 ModuleNotFoundError。 任何其他被引发异常将直接向上传播,并放弃导入过程。

元路径查找器的 find_spec() 方法调用带有两到三个参数。 第一个是被导入模块的完整限定名称,例如 foo.bar.baz。 第二个参数是供模块搜索使用的路径条目。 对于最高层级模块,第二个参数为 None,但对于子模块或子包,第二个参数为父包 __path__ 属性的值。 如果相应的 __path__ 属性无法访问,将引发 ModuleNotFoundError。 第三个参数是一个将被作为稍后加载目标的现有模块对象。 导入系统仅会在重加载期间传入一个目标模块。

对于单个导入请求可以多次遍历元路径。 例如,假设所涉及的模块都尚未被缓存,则导入 foo.bar.baz 将首先执行顶级的导入,在每个元路径查找器 (mpf) 上调用 mpf.find_spec("foo", None, None)。 在导入 foo 之后,foo.bar 将通过第二次遍历元路径来导入,调用 mpf.find_spec("foo.bar", foo.__path__, None)。 一旦 foo.bar 完成导入,最后一次遍历将调用 mpf.find_spec("foo.bar.baz", foo.bar.__path__, None)

有些元路径查找器只支持顶级导入。 当把 None 以外的对象作为第三个参数传入时,这些导入器将总是返回 None

Python 的默认 sys.meta_path 具有三种元路径查找器,一种知道如何导入内置模块,一种知道如何导入冻结模块,还有一种知道如何导入来自 import path 的模块 (即 path based finder)。

在 3.4 版更改: 元路径查找器的 find_spec() 方法替代了 find_module(),后者现已弃用,它将继续可用但不会再做改变,导入机制仅会在查找器未实现 find_spec() 时尝试使用它。

在 3.10 版更改: 导入系统使用 find_module() 现在将会引发 ImportWarning

5.4. 加载

当一个模块说明被找到时,导入机制将在加载该模块时使用它(及其所包含的加载器)。 下面是导入的加载部分所发生过程的简要说明:

module = None
if spec.loader is not None and hasattr(spec.loader, 'create_module'):
    # It is assumed 'exec_module' will also be defined on the loader.
    module = spec.loader.create_module(spec)
if module is None:
    module = ModuleType(spec.name)
# The import-related module attributes get set here:
_init_module_attrs(spec, module)
if spec.loader is None:
    # unsupported
    raise ImportError
if spec.origin is None and spec.submodule_search_locations is not None:
    # namespace package
    sys.modules[spec.name] = module
elif not hasattr(spec.loader, 'exec_module'):
    module = spec.loader.load_module(spec.name)
    # Set __loader__ and __package__ if missing.
else:
    sys.modules[spec.name] = module
    try:
        spec.loader.exec_module(module)
    except BaseException:
        try:
            del sys.modules[spec.name]
        except KeyError:
            pass
        raise
return sys.modules[spec.name]

请注意以下细节:

  • 如果在 sys.modules 中存在指定名称的模块对象,导入操作会已经将其返回。
  • 在加载器执行模块代码之前,该模块将存在于 sys.modules 中。 这一点很关键,因为该模块代码可能(直接或间接地)导入其自身;预先将其添加到 sys.modules 可防止在最坏情况下的无限递归和最好情况下的多次加载。
  • 如果加载失败,则该模块 — 只限加载失败的模块 — 将从 sys.modules 中移除。 任何已存在于 sys.modules 缓存的模块,以及任何作为附带影响被成功加载的模块仍会保留在缓存中。 这与重新加载不同,后者会把即使加载失败的模块也保留在 sys.modules 中。
  • 在模块创建完成但还未执行之前,导入机制会设置导入相关模块属性(在上面的示例伪代码中为 “_init_module_attrs”)。
  • 模块执行是加载的关键时刻,在此期间将填充模块的命名空间。 执行会完全委托给加载器,由加载器决定要填充的内容和方式。
  • 在加载过程中创建并传递给 exec_module() 的模块并不一定就是在导入结束时返回的模块。

在 3.4 版更改: 导入系统已经接管了加载器建立样板的责任。 这些在以前是由 importlib.abc.Loader.load_module() 方法来执行的。

5.4.1. 加载器

模块加载器提供关键的加载功能:模块执行。 导入机制调用 importlib.abc.Loader.exec_module() 方法并传入一个参数来执行模块对象。 从 exec_module() 返回的任何值都将被忽略。

加载器必须满足下列要求:

  • 如果模块是一个 Python 模块(而非内置模块或动态加载的扩展),加载器应该在模块的全局命名空间 (module.__dict__) 中执行模块的代码。
  • 如果加载器无法执行指定模块,它应该引发 ImportError,不过在 exec_module() 期间引发的任何其他异常也会被传播。

在许多情况下,查找器和加载器可以是同一对象;在此情况下 find_spec() 方法将返回一个规格说明,其中加载器会被设为 self

模块加载器可以选择通过实现 create_module() 方法在加载期间创建模块对象。 它接受一个参数,即模块规格说明,并返回新的模块对象供加载期间使用。 create_module() 不需要在模块对象上设置任何属性。 如果模块返回 None,导入机制将自行创建新模块。

3.4 新版功能: 加载器的 create_module() 方法。

在 3.4 版更改: load_module() 方法被 exec_module() 所替代,导入机制会对加载的所有样板责任作出假定。

为了与现有的加载器兼容,导入机制会使用导入器的 load_module() 方法,如果它存在且导入器也未实现 exec_module()。 但是,load_module() 现已弃用,加载器应该转而实现 exec_module()

除了执行模块之外,load_module() 方法必须实现上文描述的所有样板加载功能。 所有相同的限制仍然适用,并带有一些附加规定:

  • 如果 sys.modules 中存在指定名称的模块对象,加载器必须使用已存在的模块。 (否则 importlib.reload() 将无法正确工作。) 如果该名称模块不存在于 sys.modules 中,加载器必须创建一个新的模块对象并将其加入 sys.modules
  • 在加载器执行模块代码之前,模块 必须 存在于 sys.modules 之中,以防止无限递归或多次加载。
  • 如果加载失败,加载器必须移除任何它已加入到 sys.modules 中的模块,但它必须 仅限 移除加载失败的模块,且所移除的模块应为加载器自身显式加载的。

在 3.5 版更改: 当 exec_module() 已定义但 create_module() 未定义时将引发 DeprecationWarning

在 3.6 版更改: 当 exec_module() 已定义但 create_module() 未定义时将引发 ImportError

在 3.10 版更改: 使用 load_module() 将引发 ImportWarning

5.4.2. 子模块

当使用任意机制 (例如 importlib API, importimport-from 语句或者内置的 __import__()) 加载一个子模块时,父模块的命名空间中会添加一个对子模块对象的绑定。 例如,如果包 spam 有一个子模块 foo,则在导入 spam.foo 之后,spam 将具有一个 绑定到相应子模块的 foo 属性。 假如现在有如下的目录结构:

spam/
    __init__.py
    foo.py
    bar.py

并且 spam/__init__.py 中有如下几行内容:

from .foo import Foo
from .bar import Bar

则执行如下代码将在 spam 模块中添加对 foobar 的名称绑定:

>>> import spam
>>> spam.foo
<module 'spam.foo' from '/tmp/imports/spam/foo.py'>
>>> spam.bar
<module 'spam.bar' from '/tmp/imports/spam/bar.py'>

按照通常的 Python 名称绑定规则,这看起来可能会令人惊讶,但它实际上是导入系统的一个基本特性。 保持不变的一点是如果你有 sys.modules['spam']sys.modules['spam.foo'] (例如在上述导入之后就是如此),则后者必须显示为前者的 foo 属性。

5.4.3. 模块规格说明

导入机制在导入期间会使用有关每个模块的多种信息,特别是加载之前。 大多数信息都是所有模块通用的。 模块规格说明的目的是基于每个模块来封装这些导入相关信息。

在导入期间使用规格说明可允许状态在导入系统各组件之间传递,例如在创建模块规格说明的查找器和执行模块的加载器之间。 最重要的一点是,它允许导入机制执行加载的样板操作,在没有模块规格说明的情况下这是加载器的责任。

模块的规格说明会作为模块对象的 __spec__ 属性对外公开。

3.4 新版功能.

5.4.4. 导入相关的模块属性

导入机制会在加载期间会根据模块的规格说明填充每个模块对象的这些属性,并在加载器执行模块之前完成。

__name__

__name__ 属性必须被设为模块的完整限定名称。 此名称被用来在导入系统中唯一地标识模块。

__loader__

__loader__ 属性必须被设为导入系统在加载模块时使用的加载器对象。 这主要是用于内省,但也可用于额外的加载器专用功能,例如获取关联到加载器的数据。

__package__

模块的 __package__ 属性必须设定。 其取值必须为一个字符串,但可以与 __name__ 取相同的值。 当模块是包时,其 __package__ 值应该设为其 __name__ 值。 当模块不是包时,对于最高层级模块 __package__ 应该设为空字符串,对于子模块则应该设为其父包名。 更多详情可参阅 PEP 366

该属性取代 __name__ 被用来为主模块计算显式相对导入,相关定义见 PEP 366。 预期它与 __spec__.parent 具有相同的值。

在 3.6 版更改: __package__ 预期与 __spec__.parent 具有相同的值。

__spec__

__spec__ 属性必须设为在导入模块时要使用的模块规格说明。 对 __spec__ 的正确设定将同时作用于 解释器启动期间初始化的模块。 唯一的例外是 __main__,其中的 __spec__ 会 在某些情况下设为 None.

__package__ 未定义时, __spec__.parent 会被用作回退项。

3.4 新版功能.

在 3.6 版更改: 当 __package__ 未定义时,__spec__.parent 会被用作回退项。

__path__

如果模块为包(不论是正规包还是命名空间包),则必须设置模块对象的 __path__ 属性。 属性值必须为可迭代对象,但如果 __path__ 没有进一步的用处则可以为空。 如果 __path__ 不为空,则在迭代时它应该产生字符串。

不是包的模块不应该具有 __path__ 属性。

__file__
__cached__

__file__ 是可选项。 如果设置,此属性的值必须为字符串。 导入系统可以选择在其没有语法意义时不设置 __file__ (例如从数据库加载的模块)。

如果设置了 __file__,则也可以再设置 __cached__ 属性,后者取值为编译版本代码(例如字节码文件)所在的路径。 设置此属性不要求文件已存在;该路径可以简单地指向应该存放编译文件的位置 (参见 PEP 3147)。

当未设置 __file__ 时也可以设置 __cached__。 但是,那样的场景很不典型。 最终,加载器会使用 __file__ 和/或 __cached__。 因此如果一个加载器可以从缓存加载模块但是不能从文件加载,那种非典型场景就是适当的。

5.4.5. module.path

根据定义,如果一个模块具有 __path__ 属性,它就是包。

包的 __path__ 属性会在导入其子包期间被使用。 在导入机制内部,它的功能与 sys.path 基本相同,即在导入期间提供一个模块搜索位置列表。 但是,__path__ 通常会比 sys.path 受到更多限制。

__path__ 必须是由字符串组成的可迭代对象,但它也可以为空。 作用于 sys.path 的规则同样适用于包的 __path__,并且 sys.path_hooks 会在遍历包的 __path__ 时被查询。

包的 __init__.py 文件可以设置或更改包的 __path__ 属性,而且这是在 PEP 420 之前实现命名空间包的典型方式。 随着 PEP 420 的引入,命名空间包不再需要提供仅包含 __path__ 操控代码的 __init__.py 文件;导入机制会自动为命名空间包正确地设置 __path__

5.4.6. 模块的 repr

默认情况下,全部模块都具有一个可用的 repr,但是你可以依据上述的属性设置,在模块的规格说明中更为显式地控制模块对象的 repr。

如果模块具有 spec (__spec__),导入机制将尝试用它来生成一个 repr。 如果生成失败或找不到 spec,导入系统将使用模块中的各种可用信息来制作一个默认 repr。 它将尝试使用 module.__name__, module.__file__ 以及 module.__loader__ 作为 repr 的输入,并将任何丢失的信息赋为默认值。

以下是所使用的确切规则:

  • 如果模块具有 __spec__ 属性,其中的规格信息会被用来生成 repr。 被查询的属性有 “name”, “loader”, “origin” 和 “has_location” 等等。
  • 如果模块具有 __file__ 属性,这会被用作模块 repr 的一部分。
  • 如果模块没有 __file__ 但是有 __loader__ 且取值不为 None,则加载器的 repr 会被用作模块 repr 的一部分。
  • 对于其他情况,仅在 repr 中使用模块的 __name__

在 3.4 版更改: loader.module_repr() 已弃用,导入机制现在使用模块规格说明来生成模块 repr。

为了向后兼容 Python 3.3,如果加载器定义了 module_repr() 方法,则会在尝试上述两种方式之前先调用该方法来生成模块 repr。 但请注意此方法已弃用。

在 3.10 版更改: 对 module_repr() 的调用现在会在尝试使用模块的 __spec__ 属性之后但在回退至 __file__ 之前发生。 module_repr() 的使用预定会在 Python 3.12 中停止。

5.4.7. 已缓存字节码的失效

在 Python 从 .pyc 文件加载已缓存字节码之前,它会检查缓存是否由最新的 .py 源文件所生成。 默认情况下,Python 通过在所写入缓存文件中保存源文件的最新修改时间戳和大小来实现这一点。 在运行时,导入系统会通过比对缓存文件中保存的元数据和源文件的元数据确定该缓存的有效性。

Python 也支持“基于哈希的”缓存文件,即保存源文件内容的哈希值而不是其元数据。 存在两种基于哈希的 .pyc 文件:检查型和非检查型。 对于检查型基于哈希的 .pyc 文件,Python 会通过求哈希源文件并将结果哈希值与缓存文件中的哈希值比对来确定缓存有效性。 如果检查型基于哈希的缓存文件被确定为失效,Python 会重新生成并写入一个新的检查型基于哈希的缓存文件。 对于非检查型 .pyc 文件,只要其存在 Python 就会直接认定缓存文件有效。 确定基于哈希的 .pyc 文件有效性的行为可通过 --check-hash-based-pycs 旗标来重载。

在 3.7 版更改: 增加了基于哈希的 .pyc 文件。在此之前,Python 只支持基于时间戳来确定字节码缓存的有效性。

5.5. 基于路径的查找器

在之前已经提及,Python 带有几种默认的元路径查找器。 其中之一是 path based finder (PathFinder),它会搜索包含一个 路径条目 列表的 import path。 每个路径条目指定一个用于搜索模块的位置。

基于路径的查找器自身并不知道如何进行导入。 它只是遍历单独的路径条目,将它们各自关联到某个知道如何处理特定类型路径的路径条目查找器。

默认的路径条目查找器集合实现了在文件系统中查找模块的所有语义,可处理多种特殊文件类型例如 Python 源码 (.py 文件),Python 字节码 (.pyc 文件) 以及共享库 (例如 .so 文件)。 在标准库中 zipimport 模块的支持下,默认路径条目查找器还能处理所有来自 zip 文件的上述文件类型。

路径条目不必仅限于文件系统位置。 它们可以指向 URL、数据库查询或可以用字符串指定的任何其他位置。

基于路径的查找器还提供了额外的钩子和协议以便能扩展和定制可搜索路径条目的类型。 例如,如果你想要支持网络 URL 形式的路径条目,你可以编写一个实现 HTTP 语义在网络上查找模块的钩子。 这个钩子(可调用对象)应当返回一个支持下述协议的 path entry finder,以被用来获取一个专门针对来自网络的模块的加载器。

预先的警告:本节和上节都使用了 查找器 这一术语,并通过 meta path finder 和 path entry finder 两个术语来明确区分它们。 这两种类型的查找器非常相似,支持相似的协议,且在导入过程中以相似的方式运作,但关键的一点是要记住它们是有微妙差异的。 特别地,元路径查找器作用于导入过程的开始,主要是启动 sys.meta_path 遍历。

相比之下,路径条目查找器在某种意义上说是基于路径的查找器的实现细节,实际上,如果需要从 sys.meta_path 移除基于路径的查找器,并不会有任何路径条目查找器被发起调用。

5.5.1. 路径条目查找器

path based finder 会负责查找和加载通过 path entry 字符串来指定位置的 Python 模块和包。 多数路径条目所指定的是文件系统中的位置,但它们并不必受限于此。

作为一种元路径查找器,path based finder 实现了上文描述的 find_spec() 协议,但是它还对外公开了一些附加钩子,可被用来定制模块如何从 import path 查找和加载。

有三个变量由 path based finder, sys.path, sys.path_hookssys.path_importer_cache 所使用。 包对象的 __path__ 属性也会被使用。 它们提供了可用于定制导入机制的额外方式。

sys.path 包含一个提供模块和包搜索位置的字符串列表。 它初始化自 PYTHONPATH 环境变量以及多种其他特定安装和实现的默认设置。 sys.path 条目可指定的名称有文件系统中的目录、zip 文件和其他可用于搜索模块的潜在“位置”,例如 URL 或数据库查询等。 在 sys.path 中只能出现字符串和字节串;所有其他数据类型都会被忽略。 字节串条目使用的编码由单独的 路径条目查找器 来确定。

path based finder 是一种 meta path finder,因此导入机制会通过调用上文描述的基于路径的查找器的 find_spec() 方法来启动 import path 搜索。 当要向 find_spec() 传入 path 参数时,它将是一个可遍历的字符串列表 —— 通常为用来在其内部进行导入的包的 __path__ 属性。 如果 path 参数为 None,这表示最高层级的导入,将会使用 sys.path

基于路径的查找器会迭代搜索路径中的每个条目,并且每次都查找与路径条目对应的 path entry finder (PathEntryFinder)。 因为这种操作可能很耗费资源(例如搜索会有 stat() 调用的开销),基于路径的查找器会维持一个缓存来将路径条目映射到路径条目查找器。 这个缓存放于 sys.path_importer_cache (尽管如此命名,但这个缓存实际存放的是查找器对象而非仅限于 importer 对象)。 通过这种方式,对特定 path entry 位置的 path entry finder 的高耗费搜索只需进行一次。 用户代码可以自由地从 sys.path_importer_cache 移除缓存条目,以强制基于路径的查找器再次执行路径条目搜索 。

如果路径条目不存在于缓存中,基于路径的查找器会迭代 sys.path_hooks 中的每个可调用对象。 对此列表中的每个 路径条目钩子 的调用会带有一个参数,即要搜索的路径条目。 每个可调用对象或是返回可处理路径条目的 path entry finder,或是引发 ImportError。 基于路径的查找器使用 ImportError 来表示钩子无法找到与 path entry 相对应的 path entry finder。 该异常会被忽略并继续进行 import path 的迭代。 每个钩子应该期待接收一个字符串或字节串对象;字节串对象的编码由钩子决定(例如可以是文件系统使用的编码 UTF-8 或其它编码),如果钩子无法解码参数,它应该引发 ImportError

如果 sys.path_hooks 迭代结束时没有返回 path entry finder,则基于路径的查找器 find_spec() 方法将在 sys.path_importer_cache 中存入 None (表示此路径条目没有对应的查找器) 并返回 None,表示此 meta path finder 无法找到该模块。

如果 sys.path_hooks 中的某个 path entry hook 可调用对象的返回值 一个 path entry finder,则以下协议会被用来向查找器请求一个模块的规格说明,并在加载该模块时被使用。

当前工作目录 — 由一个空字符串表示 — 的处理方式与 sys.path 中的其他条目略有不同。 首先,如果发现当前工作目录不存在,则 sys.path_importer_cache 中不会存放任何值。 其次,每个模块查找会对当前工作目录的值进行全新查找。 第三,由 sys.path_importer_cache 所使用并由 importlib.machinery.PathFinder.find_spec() 所返回的路径将是实际的当前工作目录而非空字符串。

5.5.2. 路径条目查找器协议

为了支持模块和已初始化包的导入,也为了给命名空间包提供组成部分,路径条目查找器必须实现 find_spec() 方法。

find_spec() 接受两个参数,即要导入模块的完整限定名称,以及(可选的)目标模块。 find_spec() 返回模块的完全填充好的规格说明。 这个规格说明总是包含“加载器”集合(但有一个例外)。

为了向导入机制提示该规格说明代表一个命名空间 portion,路径条目查找器会将 “submodule_search_locations” 设为一个包含该部分的列表。

在 3.4 版更改: find_spec() 替代了 find_loader()find_module(),后两者现在都已弃用,但会在 find_spec() 未定义时被使用。

较旧的路径条目查找器可能会实现这两个已弃用的方法中的一个而没有实现 find_spec()。 为保持向后兼容,这两个方法仍会被接受。 但是,如果在路径条目查找器上实现了 find_spec(),这两个遗留方法就会被忽略。

find_loader() 接受一个参数,即要导入模块的完整限定名称。 find_loader() 返回一个 2 元组,其中第一项是加载器而第二项是命名空间 portion。

为了向后兼容其他导入协议的实现,许多路径条目查找器也同样支持元路径查找器所支持的传统 find_module() 方法。 但是路径条目查找器 find_module() 方法的调用绝不会带有 path 参数(它们被期望记录来自对路径钩子初始调用的恰当路径信息)。

路径条目查找器的 find_module() 方法已弃用,因为它不允许路径条目查找器为命名空间包提供部分。 如果 find_loader()find_module() 同时存在于一个路径条目查找器中,导入系统将总是调用 find_loader() 而不选择 find_module()

在 3.10 版更改: 导入系统调用 find_module()find_loader() 将引发 ImportWarning

5.6. 替换标准导入系统

替换整个导入系统的最可靠机制是移除 sys.meta_path 的默认内容,,将其完全替换为自定义的元路径钩子。

一个可行的方式是仅改变导入语句的行为而不影响访问导入系统的其他 API,那么替换内置的 __import__() 函数可能就够了。 这种技巧也可以在模块层级上运用,即只在某个模块内部改变导入语句的行为。

想要选择性地预先防止在元路径上从一个钩子导入某些模块(而不是完全禁用标准导入系统),只需直接从 find_spec() 引发 ModuleNotFoundError 而非返回 None 就足够了。 返回后者表示元路径搜索应当继续,而引发异常则会立即终止搜索。

5.7. 包相对导入

相对导入使用前缀点号。 一个前缀点号表示相对导入从当前包开始。 两个或更多前缀点号表示对当前包的上级包的相对导入,第一个点号之后的每个点号代表一级。 例如,给定以下的包布局结构:

package/
    __init__.py
    subpackage1/
        __init__.py
        moduleX.py
        moduleY.py
    subpackage2/
        __init__.py
        moduleZ.py
    moduleA.py

不论是在 subpackage1/moduleX.py 还是 subpackage1/__init__.py 中,以下导入都是有效的:

from .moduleY import spam
from .moduleY import spam as ham
from . import moduleY
from ..subpackage1 import moduleY
from ..subpackage2.moduleZ import eggs
from ..moduleA import foo

绝对导入可以使用 import <>from <> import <> 语法,但相对导入只能使用第二种形式;其中的原因在于:

import XXX.YYY.ZZZ

应当提供 XXX.YYY.ZZZ 作为可用表达式,但 .moduleY 不是一个有效的表达式。

5.8. 有关 main 的特殊事项

对于 Python 的导入系统来说 __main__ 模块是一个特殊情况。 __main__ 模块是在解释器启动时直接初始化的,与 sysbuiltins 很类似。 但是,与那两者不同,它并不被严格归类为内置模块。 这是因为 __main__ 被初始化的方式依赖于发起调用解释器所附带的旗标和其他选项。

5.8.1. main.spec

根据 __main__ 被初始化的方式,__main__.__spec__ 会被设置相应值或是 None

当 Python 附加 -m 选项启动时,__spec__ 会被设为相应模块或包的模块规格说明。 __spec__ 也会在 __main__ 模块作为执行某个目录,zip 文件或其它 sys.path 条目的一部分加载时被填充。

在 其余的情况 下 __main__.__spec__ 会被设为 None,因为用于填充 __main__ 的代码不直接与可导入的模块相对应:

  • 交互型提示
  • -c 选项
  • 从 stdin 运行
  • 直接从源码或字节码文件运行

请注意在最后一种情况中 __main__.__spec__ 总是为 None即使 文件从技术上说可以作为一个模块被导入。 如果想要让 __main__ 中的元数据生效,请使用 -m 开关。

还要注意即使是在 __main__ 对应于一个可导入模块且 __main__.__spec__ 被相应地设定时,它们仍会被视为 不同的 模块。 这是由于以下事实:使用 if __name__ == "__main__": 检测来保护的代码块仅会在模块被用来填充 __main__ 命名空间时而非普通的导入时被执行。

5.9. 开放问题项

XXX 最好是能增加一个图表。

XXX * (import_machinery.rst) 是否要专门增加一节来说明模块和包的属性,也许可以扩展或移植数据模型参考页中的相关条目?

XXX 库手册中的 runpy 和 pkgutil 等等应该都在页面顶端增加指向新的导入系统章节的“另请参阅”链接。

XXX 是否要增加关于初始化 __main__ 的不同方式的更多解释?

XXX 增加更多有关 __main__ 怪异/坑人特性的信息 (例如直接从 PEP 395 复制)。

5.10. 参考文献

导入机制自 Python 诞生之初至今已发生了很大的变化。 原始的 包规格说明 仍然可以查阅,但在撰写该文档之后许多相关细节已被修改。

原始的 sys.meta_path 规格说明见 PEP 302,后续的扩展说明见 PEP 420

PEP 420 为 Python 3.3 引入了 命名空间包。 PEP 420 还引入了 find_loader() 协议作为 find_module() 的替代。

PEP 366 描述了新增的 __package__ 属性,用于在模块中的显式相对导入。

PEP 328 引入了绝对和显式相对导入,并初次提出了 __name__ 语义,最终由 PEP 366__package__ 加入规范描述。

PEP 338 定义了将模块作为脚本执行。

PEP 451 在 spec 对象中增加了对每个模块导入状态的封装。 它还将加载器的大部分样板责任移交回导入机制中。 这些改变允许弃用导入系统中的一些 API 并为查找器和加载器增加一些新的方法。

注:

  • importlib 实现避免直接使用返回值。 而是通过在 sys.modules 中查找模块名称来获取模块对象。 这种方式的间接影响是被导入的模块可能在 sys.modules 中替换其自身。 这属于具体实现的特定行为,不保证能在其他 Python 实现中起作用。
  • 在遗留代码中,有可能在 sys.path_importer_cache 中找到 imp.NullImporter 的实例。 建议将这些代码修改为使用 None 代替。

6. 表达式

本章将解释 Python 中组成表达式的各种元素的的含义。

语法注释: 在本章和后续章节中,会使用扩展 BNF 标注来描述语法而不是词法分析。 当(某种替代的)语法规则具有如下形式

name ::=  othername

并且没有给出语义,则这种形式的 name 在语法上与 othername 相同。

6.1. 算术转换

当对下述某个算术运算符的描述中使用了“数值参数被转换为普通类型”这样的说法,这意味着内置类型的运算符实现采用了如下运作方式:

  • 如果任一参数为复数,另一参数会被转换为复数;
  • 否则,如果任一参数为浮点数,另一参数会被转换为浮点数;
  • 否则,两者应该都为整数,不需要进行转换。

某些附加规则会作用于特定运算符(例如,字符串作为 ‘%’ 运算符的左运算参数)。 扩展必须定义它们自己的转换行为。

6.2. 原子

“原子”指表达式的最基本构成元素。 最简单的原子是标识符和字面值。 以圆括号、方括号或花括号包括的形式在语法上也被归类为原子。 原子的句法为:

atom      ::=  identifier | literal | enclosure
enclosure ::=  parenth_form | list_display | dict_display | set_display
               | generator_expression | yield_atom

6.2.1. 标识符(名称)

作为原子出现的标识符叫做名称。 请参看 标识符和关键字 一节了解其词法定义,以及 命名与绑定 获取有关命名与绑定的文档。

当名称被绑定到一个对象时,对该原子求值将返回相应对象。 当名称未被绑定时,尝试对其求值将引发 NameError 异常。

私有名称转换: 当以文本形式出现在类定义中的一个标识符以两个或更多下划线开头并且不以两个或更多下划线结尾,它会被视为该类的 私有名称。 私有名称会在为其生成代码之前被转换为一种更长的形式。 转换时会插入类名,移除打头的下划线再在名称前增加一个下划线。 例如,出现在一个名为 Ham 的类中的标识符 __spam 会被转换为 _Ham__spam。 这种转换独立于标识符所使用的相关句法。 如果转换后的名称太长(超过 255 个字符),可能发生由具体实现定义的截断。 如果类名仅由下划线组成,则不会进行转换。

6.2.2. 字面值

Python 支持字符串和字节串字面值,以及几种数字字面值:

literal ::=  stringliteral | bytesliteral
             | integer | floatnumber | imagnumber

对字面值求值将返回一个该值所对应类型的对象(字符串、字节串、整数、浮点数、复数)。 对于浮点数和虚数(复数)的情况,该值可能为近似值。

所有字面值都对应与不可变数据类型,因此对象标识的重要性不如其实际值。 多次对具有相同值的字面值求值(不论是发生在程序文本的相同位置还是不同位置)可能得到相同对象或是具有相同值的不同对象。

6.2.3. 带圆括号的形式

带圆括号的形式是包含在圆括号中的可选表达式列表。

parenth_form ::=  "(" [starred_expression] ")"

带圆括号的表达式列表将返回该表达式列表所产生的任何东西:如果该列表包含至少一个逗号,它会产生一个元组;否则,它会产生该表达式列表所对应的单一表达式。

一对内容为空的圆括号将产生一个空的元组对象。 由于元组是不可变对象,因此适用与字面值相同的规则(即两次出现的空元组产生的对象可能相同也可能不同)。

请注意元组并不是由圆括号构建,实际起作用的是逗号操作符。 例外情况是空元组,这时圆括号 才是 必须的 —- 允许在表达式中使用不带圆括号的 “空” 会导致歧义,并会造成常见输入错误无法被捕获。

6.2.4. 列表、集合与字典的显示

为了构建列表、集合或字典,Python 提供了名为“显示”的特殊句法,每个类型各有两种形式:

  • 第一种是显式地列出容器内容
  • 第二种是通过一组循环和筛选指令计算出来,称为 推导式

推导式的常用句法元素为:

comprehension ::=  assignment_expression comp_for
comp_for      ::=  ["async"] "for" target_list "in" or_test [comp_iter]
comp_iter     ::=  comp_for | comp_if
comp_if       ::=  "if" or_test [comp_iter]

推导式的结构是一个单独表达式后面加至少一个 for 子句以及零个或更多个 forif 子句。 在这种情况下,新容器的元素产生方式是将每个 forif 子句视为一个代码块,按从左至右的顺序嵌套,然后每次到达最内层代码块时就对表达式进行求值以产生一个元素。

不过,除了最左边 for 子句中的可迭代表达式,推导式是在另一个隐式嵌套的作用域内执行的。 这能确保赋给目标列表的名称不会“泄露”到外层的作用域。

最左边的 for 子句中的可迭代对象表达式会直接在外层作用域中被求值,然后作为一个参数被传给隐式嵌套的作用域。 后续的 for 子句以及最左侧 for 子句中的任何筛选条件不能在外层作用域中被求值,因为它们可能依赖于从最左侧可迭代对象中获得的值。 例如: [x*y for x in range(10) for y in range(x, x+10)]

为了确保推导式得出的结果总是一个类型正确的容器,在隐式嵌套作用域内禁止使用 yieldyield from 表达式。

从 Python 3.6 开始,在 async def 函数中可以使用 async for 子句来迭代 asynchronous iterator。 在 async def 函数中构建推导式可以通过在打头的表达式后加上 forasync for 子句,也可能包含额外的 forasync for 子句,还可能使用 await 表达式。 如果一个推导式包含 async for 子句或者 await 表达式,则被称为 异步推导式。 异步推导式可以暂停执行它所在的协程函数。 另请参阅 PEP 530

3.6 新版功能: 引入了异步推导式。

在 3.8 版更改: yieldyield from 在隐式嵌套的作用域中已被禁用。

6.2.5. 列表显示

列表显示是一个用方括号括起来的可能为空的表达式系列:

list_display ::=  "[" [starred_list | comprehension] "]"

列表显示会产生一个新的列表对象,其内容通过一系列表达式或一个推导式来指定。 当提供由逗号分隔的一系列表达式时,其元素会从左至右被求值并按此顺序放入列表对象。 当提供一个推导式时,列表会根据推导式所产生的结果元素进行构建。

6.2.6. 集合显示

集合显示是用花括号标明的,与字典显示的区别在于没有冒号分隔的键和值:

set_display ::=  "{" (starred_list | comprehension) "}"

集合显示会产生一个新的可变集合对象,其内容通过一系列表达式或一个推导式来指定。 当提供由逗号分隔的一系列表达式时,其元素会从左至右被求值并加入到集合对象。 当提供一个推导式时,集合会根据推导式所产生的结果元素进行构建。

空集合不能用 {} 来构建;该字面值所构建的是一个空字典。

6.2.7. 字典显示

字典显示是一个用花括号括起来的可能为空的键/数据对系列:

dict_display       ::=  "{" [key_datum_list | dict_comprehension] "}"
key_datum_list     ::=  key_datum ("," key_datum)* [","]
key_datum          ::=  expression ":" expression | "**" or_expr
dict_comprehension ::=  expression ":" expression comp_for

字典显示会产生一个新的字典对象。

如果给出一个由逗号分隔的键/数据对序列,它们会从左至右被求值以定义字典的条目:每个键对象会被用作在字典中存放相应数据的键。 这意味着你可以在键/数据对序列中多次指定相同的键,最终字典的值将由最后一次给出的键决定。

双星号 ** 表示 字典拆包。 它的操作数必须是一个 mapping。 每个映射项被会加入新的字典。 后续的值会替代先前的键/数据对和先前的字典拆包所设置的值。

3.5 新版功能: 拆包到字典显示,最初由 PEP 448 提出。

字典推导式与列表和集合推导式有所不同,它需要以冒号分隔的两个表达式,后面带上标准的 “for” 和 “if” 子句。 当推导式被执行时,作为结果的键和值元素会按它们的产生顺序被加入新的字典。

(总的说来,键的类型应该为 hashable,这就把所有可变对象都排除在外。) 重复键之间的冲突不会被检测;指定键所保存的最后一个数据 (即在显示中排最右边的文本) 为最终有效数据。

在 3.8 版更改: 在 Python 3.8 之前的字典推导式中,并没有定义好键和值的求值顺序。 在 CPython 中,值会先于键被求值。 根据 PEP 572 的提议,从 3.8 开始,键会先于值被求值。

6.2.8. 生成器表达式

生成器表达式是用圆括号括起来的紧凑形式生成器标注。

generator_expression ::=  "(" expression comp_for ")"

生成器表达式会产生一个新的生成器对象。 其句法与推导式相同,区别在于它是用圆括号而不是用方括号或花括号括起来的。

在生成器表达式中使用的变量会在为生成器对象调用 __next__() 方法的时候以惰性方式被求值(即与普通生成器相同的方式)。 但是,最左侧 for 子句内的可迭代对象是会被立即求值的,因此它所造成的错误会在生成器表达式被定义时被检测到,而不是在获取第一个值时才出错。 后续的 for 子句以及最左侧 for 子句内的任何筛选条件无法在外层作用域内被求值,因为它们可能会依赖于从最左侧可迭代对象获取的值。 例如: (x*y for x in range(10) for y in range(x, x+10)).

圆括号在只附带一个参数的调用中可以被省略。

为了避免干扰到生成器表达式本身的预期操作,禁止在隐式定义的生成器中使用 yieldyield from 表达式。

如果生成器表达式包含 async for 子句或 await 表达式,则称为 异步生成器表达式。 异步生成器表达式会返回一个新的异步生成器对象,此对象属于异步迭代器 。

3.6 新版功能: 引入了异步生成器表达式。

在 3.7 版更改: 在 Python 3.7 之前,异步生成器表达式只能在 async def 协和中出现。 从 3.7 开始,任何函数都可以使用异步生成器表达式。

在 3.8 版更改: yieldyield from 在隐式嵌套的作用域中已被禁用。

6.2.9. yield 表达式

yield_atom       ::=  "(" yield_expression ")"
yield_expression ::=  "yield" [expression_list | "from" expression]

yield 表达式在定义 generator 函数或是 asynchronous generator 的时候才会用到。 因此只能在函数定义的内部使用yield表达式。 在一个函数体内使用 yield 表达式会使这个函数变成一个生成器,并且在一个 async def 定义的函数体内使用 yield 表达式会让协程函数变成异步的生成器。 比如说:

def gen():  # defines a generator function
    yield 123
async def agen(): # defines an asynchronous generator function
    yield 123

由于它们会对外层作用域造成附带影响,yield 表达式不被允许作为用于实现推导式和生成器表达式的隐式定义作用域的一部分。

在 3.8 版更改: 禁止在实现推导式和生成器表达式的隐式嵌套作用域中使用 yield 表达式。

下面是对生成器函数的描述,异步生成器函数会在 异步生成器函数 一节中单独介绍。

当一个生成器函数被调用的时候,它返回一个迭代器,称为生成器。然后这个生成器来控制生成器函数的执行。当这个生成器的某一个方法被调用的时候,生成器函数开始执行。这时会一直执行到第一个 yield 表达式,在此执行再次被挂起,给生成器的调用者返回 expression_list 的值。挂起后,我们说所有局部状态都被保留下来,包括局部变量的当前绑定,指令指针,内部求值栈和任何异常处理的状态。通过调用生成器的某一个方法,生成器函数继续执行。此时函数的运行就和 yield 表达式只是一个外部函数调用的情况完全一致。恢复后 yield 表达式的值取决于调用的哪个方法来恢复执行。 如果用的是 __next__() (通常通过语言内置的 for 或是 next() 来调用) 那么结果就是 None. 否则,如果用 send(), 那么结果就是传递给send方法的值。

所有这些使生成器函数与协程非常相似;它们 yield 多次,它们具有多个入口点,并且它们的执行可以被挂起。唯一的区别是生成器函数不能控制在它在 yield 后交给哪里继续执行;控制权总是转移到生成器的调用者。

try 结构中的任何位置都允许yield表达式。如果生成器在(因为引用计数到零或是因为被垃圾回收)销毁之前没有恢复执行,将调用生成器-迭代器的 close() 方法. close 方法允许任何挂起的 finally 子句执行。

当使用 yield from <expr> 时,所提供的表达式必须是一个可迭代对象。 迭代该可迭代对象所产生的值会被直接传递给当前生成器方法的调用者。 任何通过 send() 传入的值以及任何通过 throw() 传入的异常如果有适当的方法则会被传给下层迭代器。 如果不是这种情况,那么 send() 将引发 AttributeErrorTypeError,而 throw() 将立即引发所转入的异常。

当下层迭代器完成时,被引发的 StopIteration 实例的 value 属性会成为 yield 表达式的值。 它可以在引发 StopIteration 时被显式地设置,也可以在子迭代器是一个生成器时自动地设置(通过从子生成器返回一个值)。

在 3.3 版更改: 添加 yield from <expr> 以委托控制流给一个子迭代器。

当yield表达式是赋值语句右侧的唯一表达式时,括号可以省略。

参见

PEP 255 - 简单生成器

在 Python 中加入生成器和 yield 语句的提议。

PEP 342 - 通过增强型生成器实现协程

增强生成器 API 和语法的提议,使其可以被用作简单的协程。

PEP 380 - 委托给子生成器的语法

引入 yield_from 语法以方便地委托给子生成器的提议。

PEP 525 - 异步生成器

通过给协程函数加入生成器功能对 PEP 492 进行扩展的提议。

6.2.9.1. 生成器-迭代器的方法

这个子小节描述了生成器迭代器的方法。 它们可被用于控制生成器函数的执行。

请注意在生成器已经在执行时调用以下任何方法都会引发 ValueError 异常。

generator.__next__()

开始一个生成器函数的执行或是从上次执行的 yield 表达式位置恢复执行。 当一个生成器函数通过 __next__() 方法恢复执行时,当前的 yield 表达式总是取值为 None。 随后会继续执行到下一个 yield 表达式,其 expression_list 的值会返回给 __next__() 的调用者。 如果生成器没有产生下一个值就退出,则将引发 StopIteration 异常。

此方法通常是隐式地调用,例如通过 for 循环或是内置的 next() 函数。

generator.send(value)

恢复执行并向生成器函数“发送”一个值。 value 参数将成为当前 yield 表达式的结果。 send() 方法会返回生成器所产生的下一个值,或者如果生成器没有产生下一个值就退出则会引发 StopIteration。 当调用 send() 来启动生成器时,它必须以 None 作为调用参数,因为这时没有可以接收值的 yield 表达式。

generator.throw(type[, value[, traceback]])

在生成器暂停的位置引发 type 类型的异常,并返回该生成器函数所产生的下一个值。 如果生成器没有产生下一个值就退出,则将引发 StopIteration 异常。 如果生成器函数没有捕获传入的异常,或引发了另一个异常,则该异常会被传播给调用者。

generator.close()

在生成器函数暂停的位置引发 GeneratorExit。 如果之后生成器函数正常退出、关闭或引发 GeneratorExit (由于未捕获该异常) 则关闭并返回其调用者。 如果生成器产生了一个值,关闭会引发 RuntimeError。 如果生成器引发任何其他异常,它会被传播给调用者。 如果生成器已经由于异常或正常退出则 close() 不会做任何事。

6.2.9.2. 例子

这里是一个简单的例子,演示了生成器和生成器函数的行为:

>>> def echo(value=None):
...     print("Execution starts when 'next()' is called for the first time.")
...     try:
...         while True:
...             try:
...                 value = (yield value)
...             except Exception as e:
...                 value = e
...     finally:
...         print("Don't forget to clean up when 'close()' is called.")
...
>>> generator = echo(1)
>>> print(next(generator))
Execution starts when 'next()' is called for the first time.
1
>>> print(next(generator))
None
>>> print(generator.send(2))
2
>>> generator.throw(TypeError, "spam")
TypeError('spam',)
>>> generator.close()
Don't forget to clean up when 'close()' is called.
6.2.9.3. 异步生成器函数

在一个使用 async def 定义的函数或方法中出现的 yield 表达式会进一步将该函数定义为一个 asynchronous generator 函数。

当一个异步生成器函数被调用时,它会返回一个名为异步生成器对象的异步迭代器。 此对象将在之后控制该生成器函数的执行。 异步生成器对象通常被用在协程函数的 async for 语句中,类似于在 for 语句中使用生成器对象。

调用异步生成器的方法之一将返回 awaitable 对象,执行会在此对象被等待时启动。 到那时,执行将前往第一个 yield 表达式,在那里它会再次暂停,将 expression_list 的值返回给等待中的协程。 与生成器一样,挂起意味着局部的所有状态会被保留,包括局部变量的当前绑定、指令的指针、内部求值的堆栈以及任何异常处理的状态。 当执行在等待异步生成器的方法返回下一个对象后恢复时,该函数可以从原状态继续进行,就仿佛 yield 表达式只是另一个外部调用。 恢复执行之后 yield 表达式的值取决于恢复执行所用的方法。 如果使用 __anext__() 则结果为 None。 否则的话,如果使用 asend() 则结果将是传递给该方法的值。

如果一个异步生成器恰好因 break、调用方任务被取消,或是其他异常而提前退出,生成器的异步清理代码将会运行并可能引发异常或访问意外上下文中的上下文变量 — 也许是在它所依赖的任务的生命周期之后,或是在异步生成器垃圾回收钩子被调用时的事件循环关闭期间。 为了防止这种情况,调用方必须通过调用 aclose() 方法来显式地关闭异步生成器以终结生成器并最终从事件循环中将其分离。

在异步生成器函数中,yield 表达式允许出现在 try 结构的任何位置。 但是,如果一个异步生成器在其被终结(由于引用计数达到零或被作为垃圾回收)之前未被恢复,则then a yield expression within a try 结构中的 yield 表达式可能导致挂起的 finally 子句执行失败。 在此情况下,应由运行该异步生成器的事件循环或任务调度器来负责调用异步生成器-迭代器的 aclose() 方法并运行所返回的协程对象,从而允许任何挂起的 finally 子句得以执行。

为了能在事件循环终结时执行最终化处理,事件循环应当定义一个 终结器 函数,它接受一个异步生成器迭代器并将调用 aclose() 且执行该协程。 这个 终结器 可以通过调用 sys.set_asyncgen_hooks() 来注册。 当首次迭代时,异步生成器迭代器将保存已注册的 终结器 以便在最终化时调用。 有关 终结器 方法的参考示例请查看在 Lib/asyncio/base_events.py 的中的 asyncio.Loop.shutdown_asyncgens 实现。

yield from <expr> 表达式如果在异步生成器函数中使用会引发语法错误。

6.2.9.4. 异步生成器-迭代器方法

这个子小节描述了异步生成器迭代器的方法,它们可被用于控制生成器函数的执行。

coroutine agen.__anext__()

返回一个可等待对象,它在运行时会开始执行该异步生成器或是从上次执行的 yield 表达式位置恢复执行。 当一个异步生成器函数通过 __anext__() 方法恢复执行时,当前的 yield 表达式所返回的可等待对象总是取值为 None,它在运行时将继续执行到下一个 yield 表达式。 该 yield 表达式的 expression_list 的值会是完成的协程所引发的 StopIteration 异常的值。 如果异步生成器没有产生下一个值就退出,则该可等待对象将引发 StopAsyncIteration 异常,提示该异步迭代操作已完成。

此方法通常是通过 async for 循环隐式地调用。

coroutine agen.asend(value)

返回一个可等待对象,它在运行时会恢复该异步生成器的执行。 与生成器的 send() 方法一样,此方法会“发送”一个值给异步生成器函数,其 value 参数会成为当前 yield 表达式的结果值。 asend() 方法所返回的可等待对象将返回生成器产生的下一个值,其值为所引发的 StopIteration,或者如果异步生成器没有产生下一个值就退出则引发 StopAsyncIteration。 当调用 asend() 来启动异步生成器时,它必须以 None 作为调用参数,因为这时没有可以接收值的 yield 表达式。

coroutine agen.athrow(type[, value[, traceback]])

返回一个可等待对象,它会在异步生成器暂停的位置引发 type 类型的异常,并返回该生成器函数所产生的下一个值,其值为所引发的 StopIteration 异常。 如果异步生成器没有产生下一个值就退出,则将由该可等待对象引发 StopAsyncIteration 异步。 如果生成器函数没有捕获传入的异常,或引发了另一个异常,则当可等待对象运行时该异常会被传播给可等待对象的调用者。

coroutine agen.aclose()

返回一个可等待对象,它会在运行时向异步生成器函数暂停的位置抛入一个 GeneratorExit。 如果该异步生成器函数正常退出、关闭或引发 GeneratorExit (由于未捕获该异常) 则返回的可等待对象将引发 StopIteration 异常。 后续调用异步生成器所返回的任何其他可等待对象将引发 StopAsyncIteration 异常。 如果异步生成器产生了一个值,该可等待对象会引发 RuntimeError。 如果异步生成器引发任何其他异常,它会被传播给可等待对象的调用者。 如果异步生成器已经由于异常或正常退出则后续调用 aclose() 将返回一个不会做任何事的可等待对象。

6.3. 原型

原型代表编程语言中最紧密绑定的操作。 它们的句法如下:

primary ::=  atom | attributeref | subscription | slicing | call

6.3.1. 属性引用

属性引用是后面带有一个句点加一个名称的原型:

attributeref ::=  primary "." identifier

此原型必须求值为一个支持属性引用的类型的对象,多数对象都支持属性引用。 随后该对象会被要求产生以指定标识符为名称的属性。 这个产生过程可通过重载 __getattr__() 方法来自定义。 如果这个属性不可用,则将引发 AttributeError 异常。 否则的话,所产生对象的类型和值会根据该对象来确定。 对同一属性引用的多次求值可能产生不同的对象。

6.3.2. 抽取

对序列(字符串、元组或列表)或映射(字典)对象的抽取操作通常就是从相应的多项集中选择一项:

subscription ::=  primary "[" expression_list "]"

此原型必须求值为一个支持抽取操作的对象(例如列表或字典)。 用户定义的对象可通过定义 __getitem__() 方法来支持抽取操作。

对于内置对象,有两种类型的对象支持抽取操作:

如果原型为映射,表达式列表必须求值为一个以该映射的键为值的对象,抽取操作会在映射中选出该键所对应的值。(表达式列表为一个元组,除非其中只有一项。)

如果原型为序列,表达式列表必须求值为一个整数或一个切片(详情见下节)。

正式句法规则并没有在序列中设置负标号的特殊保留条款;但是,内置序列所提供的 __getitem__() 方法都可通过在索引中添加序列长度来解析负标号 (这样 x[-1] 会选出 x 中的最后一项)。 结果值必须为一个小于序列中项数的非负整数,抽取操作会选出标号为该值的项(从零开始数)。 由于对负标号和切片的支持存在于对象的 __getitem__() 方法,重载此方法的子类需要显式地添加这种支持。

字符串的项是字符。 字符不是单独的数据类型而是仅有一个字符的字符串。

对特定 类 或 类型 的抽取操作会创建一个 泛型别名。 在此情况下,用户自定义类型可通过提供 __class_getitem__() 类方法来支持抽取操作。

6.3.3. 切片

切片就是在序列对象(字符串、元组或列表)中选择某个范围内的项。 切片可被用作表达式以及赋值或 del 语句的目标。 切片的句法如下:

slicing      ::=  primary "[" slice_list "]"
slice_list   ::=  slice_item ("," slice_item)* [","]
slice_item   ::=  expression | proper_slice
proper_slice ::=  [lower_bound] ":" [upper_bound] [ ":" [stride] ]
lower_bound  ::=  expression
upper_bound  ::=  expression
stride       ::=  expression

此处的正式句法中存在一点歧义:任何形似表达式列表的东西同样也会形似切片列表,因此任何抽取操作也可以被解析为切片。 为了不使句法更加复杂,于是通过定义将此情况解析为抽取优先于解析为切片来消除这种歧义(切片列表未包含正确的切片就属于此情况)。

切片的语义如下所述。 元型通过一个根据下面的切片列表来构造的键进行索引(与普通抽取一样使用 __getitem__() 方法)。 如果切片列表包含至少一个逗号,则键将是一个包含切片项转换的元组;否则的话,键将是单个切片项的转换。 切片项如为一个表达式,则其转换就是该表达式。 一个正确切片的转换就是一个切片对象,该对象的 start, stopstep 属性将分别为表达式所给出的下界、上界和步长值,省略的表达式将用 None 来替换。

6.3.4. 调用

所谓调用就是附带可能为空的一系列 参数 来执行一个可调用对象 (例如 function):

call                 ::=  primary "(" [argument_list [","] | comprehension] ")"
argument_list        ::=  positional_arguments ["," starred_and_keywords]
                            ["," keywords_arguments]
                          | starred_and_keywords ["," keywords_arguments]
                          | keywords_arguments
positional_arguments ::=  positional_item ("," positional_item)*
positional_item      ::=  assignment_expression | "*" expression
starred_and_keywords ::=  ("*" expression | keyword_item)
                          ("," "*" expression | "," keyword_item)*
keywords_arguments   ::=  (keyword_item | "**" expression)
                          ("," keyword_item | "," "**" expression)*
keyword_item         ::=  identifier "=" expression

一个可选项为在位置和关键字参数后加上逗号而不影响语义。

此原型必须求值为一个可调用对象(用户定义的函数,内置函数,内置对象的方法,类对象,类实例的方法以及任何具有 __call__() 方法的对象都是可调用对象)。 所有参数表达式将在尝试调用前被求值。

如果存在关键字参数,它们会先通过以下操作被转换为位置参数。 首先,为正式参数创建一个未填充空位的列表. 如果有 N 个位置参数,则将它们放入前 N 个空位。 然后,对于每个关键字参数,使用标识符来确定其对应的空位(如果标识符与第一个正式参数名相同则使用第一个个空位,依此类推)。 如果空位已被填充,则会引发 TypeError 异常。 否则,将参数值放入空位进行填充(即使表达式为 None 也会填充空位)。 当所有参数处理完毕时,尚未填充的空位将用来自函数定义的相应默认值来填充。 (函数一旦定义其参数默认值就会被计算;因此,当列表或字典这类可变对象被用作默认值时,将会被所有未指定相应空位参数值的调用所共享;这种情况通常应当避免。) 如果任何一个未填充空位没有指定默认值,则会引发 TypeError 异常。 否则的话,已填充空位的列表会被作为调用的参数列表。

CPython implementation detail: 某些实现可能提供位置参数没有名称的内置函数,即使它们在文档说明的场合下有“命名”,因此不能以关键字形式提供参数。 在 CPython 中,以 C 编写并使用 PyArg_ParseTuple() 来解析其参数的函数实现就属于这种情况。

如果存在比正式参数空位多的位置参数,将会引发 TypeError 异常,除非有一个正式参数使用了 *identifier 句法;在此情况下,该正式参数将接受一个包含了多余位置参数的元组(如果没有多余位置参数则为一个空元组)。

如果任何关键字参数没有与之对应的正式参数名称,将会引发 TypeError 异常,除非有一个正式参数使用了 **identifier 句法,该正式参数将接受一个包含了多余关键字参数的字典(使用关键字作为键而参数值作为与键对应的值),如果没有多余关键字参数则为一个(新的)空字典。

如果函数调用中出现了 *expression 句法,expression 必须求值为一个 iterable。 来自该可迭代对象的元素会被当作是额外的位置参数。 对于 f(x1, x2, *y, x3, x4) 调用,如果 y 求值为一个序列 y1, …, yM*,则它就等价于一个带有 M+4 个位置参数 *x1, x2, y1, …, yM, x3, x4 的调用。

这样做的一个后果是虽然 *expression 句法可能出现于显式的关键字参数 之后,但它会在关键字参数(以及任何 `*expression` 参数 — 见下文) *之前 被处理。 因此:

>>> def f(a, b):
...     print(a, b)
...
>>> f(b=1, *(2,))
2 1
>>> f(a=1, *(2,))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() got multiple values for keyword argument 'a'
>>> f(1, *(2,))
1 2

在同一个调用中同时使用关键字参数和 *expression 句法并不常见,因此实际上这样的混淆不会发生。

如果函数调用中出现了 **expression 句法,expression 必须求值为一个 mapping,其内容会被当作是额外的关键字参数。 如果一个关键字已存在(作为显式关键字参数,或来自另一个拆包),则将引发 TypeError 异常。

使用 *identifier**identifier 句法的正式参数不能被用作位置参数空位或关键字参数名称。

在 3.5 版更改: 函数调用接受任意数量的 *** 拆包,位置参数可能跟在可迭代对象拆包 (*) 之后,而关键字参数可能跟在字典拆包 (**) 之后。 由 PEP 448 发起最初提议。

除非引发了异常,调用总是会有返回值,返回值也可能为 None。 返回值的计算方式取决于可调用对象的类型。

如果类型为—-

用户自定义函数:

函数的代码块会被执行,并向其传入参数列表。 代码块所做的第一件事是将正式形参绑定到对应参数。 当代码块执行 return 语句时,由其指定函数调用的返回值。

内置函数或方法:

具体结果依赖于解释器。

类对象:

返回该类的一个新实例。

类实例方法:

调用相应的用户自定义函数,向其传入的参数列表会比调用的参数列表多一项:该实例将成为第一个参数。

类实例:

该类必须定义有 __call__() 方法;作用效果将等价于调用该方法。

6.4. await 表达式

挂起 coroutine 的执行以等待一个 awaitable 对象。 只能在 coroutine function 内部使用。

await_expr ::=  "await" primary

3.5 新版功能.

6.5. 幂运算符

幂运算符的绑定比在其左侧的一元运算符更紧密;但绑定紧密程度不及在其右侧的一元运算符。 句法如下:

power ::=  (await_expr | primary) ["**" u_expr]

因此,在一个未加圆括号的幂运算符和单目运算符序列中,运算符将从右向左求值(这不会限制操作数的求值顺序): -1**2 结果将为 -1

幂运算符与附带两个参数调用内置 pow() 函数具有相同的语义:结果为对其左参数进行其右参数所指定幂次的乘方运算。 数值参数会先转换为相同类型,结果也为转换后的类型。

对于 int 类型的操作数,结果将具有与操作数相同的类型,除非第二个参数为负数;在那种情况下,所有参数会被转换为 float 类型并输出 float 类型的结果。 例如,10**2 返回 100,而 10**-2 返回 0.01

0.0 进行负数幂次运算将导致 ZeroDivisionError。 对负数进行分数幂次运算将返回 complex 数值。 (在早期版本中这将引发 ValueError。)

此运算符可使用特殊的 __pow__() 方法来自定义。

6.6. 一元算术和位运算

所有算术和位运算具有相同的优先级:

u_expr ::=  power | "-" u_expr | "+" u_expr | "~" u_expr

一元的 - (负值) 运算符会产生其数字参数的负值;该运算可通过 __neg__() 特殊方法来重载。

一元的 + (正值) 运算符会原样输出其数字参数;该运算可通过 __pos__() 特殊方法来重载。

一元的 ~ (取反) 运算符会对其整数参数按位取反。 x 的按位取反被定义为 -(x+1)。 它只作用于整数或是重载了 __invert__() 特殊方法的自定义对象。

在所有三种情况下,如果参数的类型不正确,将引发 TypeError 异常。

6.7. 二元算术运算符

二元算术运算符遵循传统的优先级。 请注意某些此类运算符也作用于特定的非数字类型。 除幂运算符以外只有两个优先级别,一个作用于乘法型运算符,另一个作用于加法型运算符:

m_expr ::=  u_expr | m_expr "*" u_expr | m_expr "@" m_expr |
            m_expr "//" u_expr | m_expr "/" u_expr |
            m_expr "%" u_expr
a_expr ::=  m_expr | a_expr "+" m_expr | a_expr "-" m_expr

运算符 * (乘) 将输出其参数的乘积。 两个参数或者必须都为数字,或者一个参数必须为整数而另一个参数必须为序列。 在前一种情况下,两个数字将被转换为相同类型然后相乘。 在后一种情况下,将执行序列的重复;重复因子为负数将输出空序列。

此运算可使用特殊的 __mul__()__rmul__() 方法来自定义。

运算符 @ (at) 的目标是用于矩阵乘法。 没有内置 Python 类型实现此运算符。

3.5 新版功能.

运算符 / (除) 和 // (整除) 将输出其参数的商。 两个数字参数将先被转换为相同类型。 整数相除会输出一个 float 值,整数相整除的结果仍是整数;整除的结果就是使用 ‘floor’ 函数进行算术除法的结果。 除以零的运算将引发 ZeroDivisionError 异常。

This operation can be customized using the special __truediv__() and __floordiv__() methods.

运算符 % (模) 将输出第一个参数除以第二个参数的余数。 两个数字参数将先被转换为相同类型。 右参数为零将引发 ZeroDivisionError 异常。 参数可以为浮点数,例如 3.14%0.7 等于 0.34 (因为 3.14 等于 4*0.7 + 0.34)。 模运算符的结果的正负总是与第二个操作数一致(或是为零);结果的绝对值一定小于第二个操作数的绝对值 。

整除与模运算符的联系可通过以下等式说明: x == (x//y)*y + (x%y)。 此外整除与模也可通过内置函数 divmod() 来同时进行: divmod(x, y) == (x//y, x%y)

除了对数字执行模运算,运算符 % 还被字符串对象重载用于执行旧式的字符串格式化(又称插值)。 字符串格式化句法的描述参见 Python 库参考的 printf 风格的字符串格式化 一节。

取余 运算可使用特殊的 __mod__() 方法来自定义。

整除运算符,模运算符和 divmod() 函数未被定义用于复数。 如果有必要可以使用 abs() 函数将其转换为浮点数。

运算符 + (addition) 将输出其参数的和。 两个参数或者必须都为数字,或者都为相同类型的序列。 在前一种情况下,两个数字将被转换为相同类型然后相加。 在后一种情况下,将执行序列拼接操作。

此运算可使用特殊的 __add__()__radd__() 方法来自定义。

运算符 - (减) 将输出其参数的差。 两个数字参数将先被转换为相同类型。

此运算可使用特殊的 __sub__() 方法来自定义。

6.8. 移位运算

移位运算的优先级低于算术运算:

shift_expr ::=  a_expr | shift_expr ("<<" | ">>") a_expr

这些运算符接受整数参数。 它们会将第一个参数左移或右移第二个参数所指定的比特位数。

此运算可使用特殊的 __lshift__()__rshift__() 方法来自定义。

右移 n 位被定义为被 pow(2,n) 整除。 左移 n 位被定义为乘以 pow(2,n)

6.9. 二元位运算

三种位运算具有各不相同的优先级:

and_expr ::=  shift_expr | and_expr "&" shift_expr
xor_expr ::=  and_expr | xor_expr "^" and_expr
or_expr  ::=  xor_expr | or_expr "|" xor_expr

& 运算符会对其参数执行按位 AND,参数必须都为整数或者其中之一必须为重载了 __and__()__rand__() 特殊方法的自定义对象。

^ 运算符会对其参数执行按位 XOR (异 OR),参数必须都为整数或者其中之一必须为重载了 __xor__()__rxor__() 特殊方法的自定义对象。

| 运算符会对其参数执行按位 (合并) OR,参数必须都为整数或者其中之一必须为重载了 __or__()__ror__() 特殊方法的自定义对象。

6.10. 比较运算

与 C 不同,Python 中所有比较运算的优先级相同,低于任何算术、移位或位运算。 另一个与 C 不同之处在于 a < b < c 这样的表达式会按传统算术法则来解读:

comparison    ::=  or_expr (comp_operator or_expr)*
comp_operator ::=  "<" | ">" | "==" | ">=" | "<=" | "!="
                   | "is" ["not"] | ["not"] "in"

比较运算会产生布尔值: TrueFalse。 自定义的 富比较方法 可能返回非布尔值。 在此情况下 Python 将在布尔运算上下文中对该值调用 bool()

比较运算可以任意串连,例如 x < y <= z 等价于 x < y and y <= z,除了 y 只被求值一次(但在两种写法下当 x < y 值为假时 z 都不会被求值)。

正式的说法是这样:如果 a, b, c, …, y, z 为表达式而 op1, op2, …, opN 为比较运算符,则 a op1 b op2 c ... y opN z 就等价于 a op1 b and b op2 c and ... y opN z,不同点在于每个表达式最多只被求值一次。

请注意 a op1 b op2 c 不意味着在 ac 之间进行任何比较,因此,如 x < y > z 这样的写法是完全合法的(虽然也许不太好看)。

6.10.1. 值比较

运算符 <, >, ==, >=, <=!= 将比较两个对象的值。 两个对象不要求为相同类型。

对象值在 Python 中是一个相当抽象的概念:例如,对象值并没有一个规范的访问方法。 而且,对象值并不要求具有特定的构建方式,例如由其全部数据属性组成等。 比较运算符实现了一个特定的对象值概念。 人们可以认为这是通过实现对象比较间接地定义了对象值。

由于所有类型都是 object 的(直接或间接)子类型,它们都从 object 继承了默认的比较行为。 类型可以通过实现 丰富比较方法 例如 __lt__() 来定义自己的比较行为。

默认的一致性比较 (==!=) 是基于对象的标识号。 因此,具有相同标识号的实例一致性比较结果为相等,具有不同标识号的实例一致性比较结果为不等。 规定这种默认行为的动机是希望所有对象都应该是自反射的 (即 x is y 就意味着 x == y)。

次序比较 (<, >, <=>=) 默认没有提供;如果尝试比较会引发 TypeError。 规定这种默认行为的原因是缺少与一致性比较类似的固定值。

按照默认的一致性比较行为,具有不同标识号的实例总是不相等,这可能不适合某些对象值需要有合理定义并有基于值的一致性的类型。 这样的类型需要定制自己的比较行为,实际上,许多内置类型都是这样做的。

以下列表描述了最主要内置类型的比较行为。

  • 内置数值类型 (数字类型 —- int, float, complex) 以及标准库类型 fractions.Fractiondecimal.Decimal 可进行类型内部和跨类型的比较,例外限制是复数不支持次序比较。 在类型相关的限制以内,它们会按数学(算法)规则正确进行比较且不会有精度损失。

    非数字值 float('NaN')decimal.Decimal('NaN') 属于特例。 任何数字与非数字值的排序比较均返回假值。 还有一个反直觉的结果是非数字值不等于其自身。 举例来说,如果 x = float('NaN')3 < x, x < 3x == x 均为假值,而 x != x 则为真值。 此行为是遵循 IEEE 754 标准的。

  • NoneNotImplemented 都是单例对象。 PEP 8 建议单例对象的比较应当总是通过 isis not 而不是等于运算符来进行。

  • 二进制码序列 (bytesbytearray 的实例) 可进行类型内部和跨类型的比较。 它们使用其元素的数字值按字典顺序进行比较。

  • 字符串 (str 的实例) 使用其字符的 Unicode 码位数字值 (内置函数 ord() 的结果) 按字典顺序进行比较.

    字符串和二进制码序列不能直接比较。

  • 序列 (tuple, listrange 的实例) 只可进行类型内部的比较,range 还有一个限制是不支持次序比较。 以上对象的跨类型一致性比较结果将是不相等,跨类型次序比较将引发 TypeError

    序列比较是按字典序对相应元素进行逐个比较。 内置容器通常设定同一对象与其自身是相等的。 这使得它们能跳过同一对象的相等性检测以提升运行效率并保持它们的内部不变性。

    内置多项集间的字典序比较规则如下:

    • 两个多项集若要相等,它们必须为相同类型、相同长度,并且每对相应的元素都必须相等(例如,[1,2] == (1,2) 为假值,因为类型不同)。
    • 对于支持次序比较的多项集,排序与其第一个不相等元素的排序相同(例如 [1,2,x] <= [1,2,y] 的值与x <= y 相同)。 如果对应元素不存在,较短的多项集排序在前(例如 [1,2] < [1,2,3] 为真值)。
  • 两个映射 (dict 的实例) 若要相等,必须当且仅当它们具有相同的 (键, 值) 对。 键和值的一致性比较强制规定自反射性。

    次序比较 (<, >, <=>=) 将引发 TypeError

  • 集合 (setfrozenset 的实例) 可进行类型内部和跨类型的比较。

    它们将比较运算符定义为子集和超集检测。 这类关系没有定义完全排序(例如 {1,2}{2,3} 两个集合不相等,即不为彼此的子集,也不为彼此的超集。 相应地,集合不适宜作为依赖于完全排序的函数的参数(例如如果给出一个集合列表作为 min(), max()sorted() 的输入将产生未定义的结果)。

    集合的比较强制规定其元素的自反射性。

  • 大多数其他内置类型没有实现比较方法,因此它们会继承默认的比较行为。

在可能的情况下,用户定义类在定制其比较行为时应当遵循一些一致性规则:

  • 相等比较应该是自反射的。 换句话说,相同的对象比较时应该相等:

    x is y 意味着 x == y

  • 比较应该是对称的。 换句话说,下列表达式应该有相同的结果:

    x == yy == x

    x != yy != x

    x < yy > x

    x <= yy >= x

  • 比较应该是可传递的。 下列(简要的)例子显示了这一点:

    x > y and y > z 意味着 x > z

    x < y and y <= z 意味着 x < z

  • 反向比较应该导致布尔值取反。 换句话说,下列表达式应该有相同的结果:

    x == ynot x != y

    x < ynot x >= y (对于完全排序)

    x > ynot x <= y (对于完全排序)

    最后两个表达式适用于完全排序的多项集(即序列而非集合或映射)。

  • hash() 的结果应该与是否相等一致。 相等的对象应该或者具有相同的哈希值,或者标记为不可哈希。

Python 并不强制要求这些一致性规则。 实际上,非数字值就是一个不遵循这些规则的例子。

6.10.2. 成员检测运算

运算符 innot in 用于成员检测。 如果 xs 的成员则 x in s 求值为 True,否则为 Falsex not in s 返回 x in s 取反后的值。 所有内置序列和集合类型以及字典都支持此运算,对于字典来说 in 检测其是否有给定的键。 对于 list, tuple, set, frozenset, dict 或 collections.deque 这样的容器类型,表达式 x in y 等价于 any(x is e or x == e for e in y)

对于字符串和字节串类型来说,当且仅当 xy 的子串时 x in yTrue。 一个等价的检测是 y.find(x) != -1。 空字符串总是被视为任何其他字符串的子串,因此 "" in "abc" 将返回 True

对于定义了 __contains__() 方法的用户自定义类来说,如果 y.__contains__(x) 返回真值则 x in y 返回 True,否则返回 False

对于未定义 __contains__() 但定义了 __iter__() 的用户自定义类来说,如果在对 y 进行迭代时产生了值 z 使得表达式 x is z or x == z 为真,则 x in yTrue。 如果在迭代期间引发了异常,则等同于 in 引发了该异常。

最后将会尝试旧式的迭代协议:如果一个类定义了 __getitem__(),则当且仅当存在非负整数索引号 i 使得 x is y[i] or x == y[i] 并且没有更小的索引号引发 IndexError 异常时 x in yTrue。 (如果引发了任何其他异常,则等同于 in 引发了该异常)。

运算符 not in 被定义为具有与 in 相反的逻辑值。

6.10.3. 标识号比较

运算符 isis not 用于检测对象的标识号:当且仅当 xy 是同一对象时 x is y 为真。 一个对象的标识号可使用 id() 函数来确定。 x is not y 会产生相反的逻辑值。

6.11. 布尔运算

or_test  ::=  and_test | or_test "or" and_test
and_test ::=  not_test | and_test "and" not_test
not_test ::=  comparison | "not" not_test

在执行布尔运算的情况下,或是当表达式被用于流程控制语句时,以下值会被解析为假值: False, None, 所有类型的数字零,以及空字符串和空容器(包括字符串、元组、列表、字典、集合与冻结集合)。 所有其他值都会被解析为真值。 用户自定义对象可通过提供 __bool__() 方法来定制其逻辑值。

运算符 not 将在其参数为假值时产生 True,否则产生 False

表达式 x and y 首先对 x 求值;如果 x 为假则返回该值;否则对 y 求值并返回其结果值。

表达式 x or y 首先对 x 求值;如果 x 为真则返回该值;否则对 y 求值并返回其结果值。

请注意 andor 都不限制其返回的值和类型必须为 FalseTrue,而是返回最终求值的参数。 此行为是有必要的,例如假设 s 为一个当其为空时应被替换为某个默认值的字符串,表达式 s or 'foo' 将产生希望的值。 由于 not 必须创建一个新值,不论其参数为何种类型它都会返回一个布尔值(例如,not 'foo' 结果为 False 而非 ''。)

6.12. 赋值表达式

assignment_expression ::=  [identifier ":="] expression

赋值表达式(有时又被叫做“命名表达式”或“海象表达式”)将一个 expression 赋值给一个 identifier,同时还返回 expression 的值。

一个常见用例是在处理匹配的正则表达式的时候:

if matching := pattern.search(data):
    do_something(matching)

或者是在处理分块的文件流的时候:

while chunk := file.read(9000):
    process(chunk)

3.8 新版功能: 请参阅 PEP 572 了解有关赋值表达式的详情。

6.13. 条件表达式

conditional_expression ::=  or_test ["if" or_test "else" expression]
expression             ::=  conditional_expression | lambda_expr

条件表达式(有时称为“三元运算符”)在所有 Python 运算中具有最低的优先级。

表达式 x if C else y 首先是对条件 C 而非 x 求值。 如果 C 为真,x 将被求值并返回其值;否则将对 y 求值并返回其值。

请参阅 PEP 308 了解有关条件表达式的详情。

6.14. lambda 表达式

lambda_expr ::=  "lambda" [parameter_list] ":" expression

lambda 表达式(有时称为 lambda 构型)被用于创建匿名函数。 表达式 lambda parameters: expression 会产生一个函数对象 。 该未命名对象的行为类似于用以下方式定义的函数:

def <lambda>(parameters):
    return expression

请注意通过 lambda 表达式创建的函数不能包含语句或标注。

6.15. 表达式列表

expression_list    ::=  expression ("," expression)* [","]
starred_list       ::=  starred_item ("," starred_item)* [","]
starred_expression ::=  expression | (starred_item ",")* [starred_item]
starred_item       ::=  assignment_expression | "*" or_expr

除了作为列表或集合显示的一部分,包含至少一个逗号的表达式列表将生成一个元组。 元组的长度就是列表中表达式的数量。 表达式将从左至右被求值。

一个星号 * 表示 可迭代拆包。 其操作数必须为一个 iterable。 该可迭代对象将被拆解为迭代项的序列,并被包含于在拆包位置上新建的元组、列表或集合之中。

3.5 新版功能: 表达式列表中的可迭代对象拆包,最初由 PEP 448 提出。

末尾的逗号仅在创建单独元组 (或称 单例) 时需要;在所有其他情况下都是可选项。 没有末尾逗号的单独表达式不会创建一个元组,而是产生该表达式的值。 (要创建一个空元组,应使用一对内容为空的圆括号: ()。)

6.16. 求值顺序

Python 按从左至右的顺序对表达式求值。 但注意在对赋值操作求值时,右侧会先于左侧被求值。

在以下几行中,表达式将按其后缀的算术优先顺序被求值。:

expr1, expr2, expr3, expr4
(expr1, expr2, expr3, expr4)
{expr1: expr2, expr3: expr4}
expr1 + expr2 * (expr3 - expr4)
expr1(expr2, expr3, *expr4, **expr5)
expr3, expr4 = expr1, expr2

6.17. 运算符优先级

下表对The following table summarizes the operator precedence in Python 中运算符的优先顺序进行了总结,从最高优先级(最先绑定)到最低优先级(最后绑定)。 相同单元格内的运算符具有相同优先级。 除非句法显式地给出,否则运算符均指二元运算。 相同单元格内的运算符从左至右分组(除了幂运算是从右至左分组)。

请注意比较、成员检测和标识号检测均为相同优先级。

运算符 描述
(expressions…),[expressions…], {key: value…}, {expressions…} 绑定或加圆括号的表达式,列表显示,字典显示,集合显示
x[index], x[index:index], x(arguments…), x.attribute 抽取,切片,调用,属性引用
await x await 表达式
* 乘方
+x, -x, ~x 正,负,按位非 NOT
``, @, /, //, % 乘,矩阵乘,除,整除,取余
+, - 加和减
<<, >> 移位
& 按位与 AND
^ 按位异或 XOR
` `
in, not in, is, is not, <, <=, >, >=, !=, == 比较运算,包括成员检测和标识号检测
not x 布尔逻辑非 NOT
and 布尔逻辑与 AND
or 布尔逻辑或 OR
ifelse 条件表达式
lambda lambda 表达式
:= 赋值表达式

注:

  • 虽然 abs(x%y) < abs(y) 在数学中必为真,但对于浮点数而言,由于舍入的存在,其在数值上未必为真。 例如,假设在某个平台上的 Python 浮点数为一个 IEEE 754 双精度数值,为了使 -1e-100 % 1e100 具有与 1e100 相同的正负性,计算结果将是 -1e-100 + 1e100,这在数值上正好等于 1e100。 函数 math.fmod() 返回的结果则会具有与第一个参数相同的正负性,因此在这种情况下将返回 -1e-100。 何种方式更适宜取决于具体的应用。

  • 如果 x 恰好非常接近于 y 的整数倍,则由于舍入的存在 x//y 可能会比 (x-x%y)//y 大。 在这种情况下,Python 会返回后一个结果,以便保持令 divmod(x,y)[0] * y + x % y 尽量接近 x.

  • Unicode 标准明确区分 码位 (例如 U+0041) 和 抽象字符 (例如 “大写拉丁字母 A”)。 虽然 Unicode 中的大多数抽象字符都只用一个码位来代表,但也存在一些抽象字符可使用由多个码位组成的序列来表示。 例如,抽象字符 “带有下加符的大写拉丁字母 C” 可以用 U+00C7 码位上的单个 预设字符 来表示,也可以用一个 U+0043 码位上的 基础字符 (大写拉丁字母 C) 加上一个 U+0327 码位上的 组合字符 (组合下加符) 组成的序列来表示。

    对于字符串,比较运算符会按 Unicode 码位级别进行比较。 这可能会违反人类的直觉。 例如,"\u00C7" == "\u0043\u0327"False,虽然两个字符串都代表同一个抽象字符 “带有下加符的大写拉丁字母 C”。

    要按抽象字符级别(即对人类来说更直观的方式)对字符串进行比较,应使用 unicodedata.normalize()

  • 由于存在自动垃圾收集、空闲列表以及描述器的动态特性,你可能会注意到在特定情况下使用 is 运算符会出现看似不正常的行为,例如涉及到实例方法或常量之间的比较时就是如此。 更多信息请查看有关它们的文档。

  • 幂运算符 ** 绑定的紧密程度低于在其右侧的算术或按位一元运算符,也就是说 2**-10.5

  • % 运算符也被用于字符串格式化;在此场合下会使用同样的优先级。

7. 简单语句

简单语句由一个单独的逻辑行构成。 多条简单语句可以存在于同一行内并以分号分隔。 简单语句的句法为:

simple_stmt ::=  expression_stmt
                 | assert_stmt
                 | assignment_stmt
                 | augmented_assignment_stmt
                 | annotated_assignment_stmt
                 | pass_stmt
                 | del_stmt
                 | return_stmt
                 | yield_stmt
                 | raise_stmt
                 | break_stmt
                 | continue_stmt
                 | import_stmt
                 | future_stmt
                 | global_stmt
                 | nonlocal_stmt

7.1. 表达式语句

表达式语句用于计算和写入值(大多是在交互模式下),或者(通常情况)调用一个过程 (过程就是不返回有意义结果的函数;在 Python 中,过程的返回值为 None)。 表达式语句的其他使用方式也是允许且有特定用处的。 表达式语句的句法为:

expression_stmt ::=  starred_expression

表达式语句会对指定的表达式列表(也可能为单一表达式)进行求值。

在交互模式下,如果结果值不为 None,它会通过内置的 repr() 函数转换为一个字符串,该结果字符串将以单独一行的形式写入标准输出(例外情况是如果结果为 None,则该过程调用不产生任何输出。)

7.2. 赋值语句

赋值语句用于将名称(重)绑定到特定值,以及修改属性或可变对象的成员项:

assignment_stmt ::=  (target_list "=")+ (starred_expression | yield_expression)
target_list     ::=  target ("," target)* [","]
target          ::=  identifier
                     | "(" [target_list] ")"
                     | "[" [target_list] "]"
                     | attributeref
                     | subscription
                     | slicing
                     | "*" target

赋值语句会对指定的表达式列表进行求值(注意这可能为单一表达式或是由逗号分隔的列表,后者将产生一个元组)并将单一结果对象从左至右逐个赋值给目标列表。

赋值是根据目标(列表)的格式递归地定义的。 当目标为一个可变对象(属性引用、抽取或切片)的组成部分时,该可变对象必须最终执行赋值并决定其有效性,如果赋值操作不可接受也可能引发异常。 各种类型可用的规则和引发的异常通过对象类型的定义给出.

对象赋值的目标对象可以包含于圆括号或方括号内,具体操作按以下方式递归地定义。

  • 如果目标列表为后面不带逗号、可以包含于圆括号内的单一目标,则将对象赋值给该目标。
  • 否则:该对象必须为具有与目标列表相同项数的可迭代对象,这些项将按从左至右的顺序被赋值给对应的目标。
    • 如果目标列表包含一个带有星号前缀的目标,这称为“加星”目标:则该对象至少必须为与目标列表项数减一相同项数的可迭代对象。 该可迭代对象前面的项将按从左至右的顺序被赋值给加星目标之前的目标。 该可迭代对象末尾的项将被赋值给加星目标之后的目标。 然后该可迭代对象中剩余项的列表将被赋值给加星目标(该列表可以为空)。
    • 否则:该对象必须为具有与目标列表相同项数的可迭代对象,这些项将按从左至右的顺序被赋值给对应的目标。

对象赋值给单个目标的操作按以下方式递归地定义。

  • 如果目标为标识符(名称):

    • 如果该名称未出现于当前代码块的 globalnonlocal 语句中:该名称将被绑定到当前局部命名空间的对象。
    • 否则:该名称将被分别绑定到全局命名空间或由 nonlocal 所确定的外层命名空间的对象。

    如果该名称已经被绑定则将被重新绑定。 这可能导致之前被绑定到该名称的对象的引用计数变为零,造成该对象进入释放过程并调用其析构器(如果存在)。

  • 如果该对象为属性引用:引用中的原型表达式会被求值。 它应该产生一个具有可赋值属性的对象;否则将引发 TypeError。 该对象会被要求将可赋值对象赋值给指定的属性;如果它无法执行赋值,则会引发异常 (通常应为 AttributeError 但并不强制要求)。

    注意:如果该对象为类实例并且属性引用在赋值运算符的两侧都出现,则右侧表达式 a.x 可以访问实例属性或(如果实例属性不存在)类属性。 左侧目标 a.x 将总是设定为实例属性,并在必要时创建该实例属性。 因此 a.x 的两次出现不一定指向相同的属性:如果右侧表达式指向一个类属性,则左侧会创建一个新的实例属性作为赋值的目标:

    class Cls:
        x = 3             # class variable
    inst = Cls()
    inst.x = inst.x + 1   # writes inst.x as 4 leaving Cls.x as 3

    此描述不一定作用于描述器属性,例如通过 property() 创建的特征属性。

  • 如果目标为一个抽取项:引用中的原型表达式会被求值。 它应当产生一个可变序列对象(例如列表)或一个映射对象(例如字典)。 接下来,该抽取表达式会被求值。

    如果原型为一个可变序列对象(例如列表),抽取应产生一个整数。 如其为负值,则再加上序列长度。 结果值必须为一个小于序列长度的非负整数,序列将把被赋值对象赋值给该整数指定索引号的项。 如果索引超出范围,将会引发 IndexError (给被抽取序列赋值不能向列表添加新项)。

    如果原型为一个映射对象(例如字典),抽取必须具有与该映射的键类型相兼容的类型,然后映射中会创建一个将抽取映射到被赋值对象的键/值对。 这可以是替换一个现有键/值对并保持相同键值,也可以是插入一个新键/值对(如果具有相同值的键不存在)。

    对于用户定义对象,会调用 __setitem__() 方法并附带适当的参数。

  • 如果目标为一个切片:引用中的原型表达式会被求值。 它应当产生一个可变序列对象(例如列表)。 被赋值对象应当是一个相同类型的序列对象。 接下来,下界与上界表达式如果存在的话将被求值;默认值分别为零和序列长度。 上下边界值应当为整数。 如果某一边界为负值,则会加上序列长度。 求出的边界会被裁剪至介于零和序列长度的开区间中。 最后,将要求序列对象以被赋值序列的项替换该切片。 切片的长度可能与被赋值序列的长度不同,这会在目标序列允许的情况下改变目标序列的长度。

CPython implementation detail: 在当前实现中,目标的句法被当作与表达式的句法相同,无效的句法会在代码生成阶段被拒绝,导致不太详细的错误信息。

虽然赋值的定义意味着左手边与右手边的重叠是“同时”进行的(例如 a, b = b, a 会交换两个变量的值),但在赋值给变量的多项集 之内 的重叠是从左至右进行的,这有时会令人混淆。 例如,以下程序将会打印出 [0, 2]:

x = [0, 1]
i = 0
i, x[i] = 1, 2         # i is updated, then x[i] is updated
print(x)

参见

PEP 3132 - 扩展的可迭代对象拆包

*target 特性的规范说明。

7.2.1. 增强赋值语句

增强赋值语句就是在单个语句中将二元运算和赋值语句合为一体:

augmented_assignment_stmt ::=  augtarget augop (expression_list | yield_expression)
augtarget                 ::=  identifier | attributeref | subscription | slicing
augop                     ::=  "+=" | "-=" | "*=" | "@=" | "/=" | "//=" | "%=" | "**="
                               | ">>=" | "<<=" | "&=" | "^=" | "|="

增强赋值语句将对目标和表达式列表求值(与普通赋值语句不同的是,前者不能为可迭代对象拆包),对两个操作数相应类型的赋值执行指定的二元运算,并将结果赋值给原始目标。 目标仅会被求值一次。

增强赋值语句例如 x += 1 可以改写为 x = x + 1 获得类似但并非完全等价的效果。 在增强赋值的版本中,x 仅会被求值一次。 而且,在可能的情况下,实际的运算是 原地 执行的,也就是说并不是创建一个新对象并将其赋值给目标,而是直接修改原对象。

不同于普通赋值,增强赋值会在对右手边求值 之前 对左手边求值。 例如,a[i] += f(x) 首先查找 a[i],然后对 f(x) 求值并执行加法操作,最后将结果写回到 a[i]

除了在单个语句中赋值给元组和多个目标的例外情况,增强赋值语句的赋值操作处理方式与普通赋值相同。 类似地,除了可能存在 原地 操作行为的例外情况,增强赋值语句执行的二元运算也与普通二元运算相同。

对于属性引用类目标,针对常规赋值的 关于类和实例属性的警告 也同样适用。

7.2.2. 带标注的赋值语句

标注 赋值就是在单个语句中将变量或属性标注和可选的赋值语句合为一体:

annotated_assignment_stmt ::=  augtarget ":" expression
                               ["=" (starred_expression | yield_expression)]

与普通 赋值语句 的差别在于仅允许单个目标。

对于将简单名称作为赋值目标的情况,如果是在类或模块作用域中,标注会被求值并存入一个特殊的类或模块属性 __annotations__ 中,这是一个将变量名称(如为私有会被移除)映射到被求值标注的字典。 此属性为可写并且在类或模块体开始执行时如果静态地发现标注就会自动创建。

对于将表达式作为赋值目标的情况,如果是在类或模块作用域中,标注会被求值,但不会保存。

如果一个名称在函数作用域内被标注,则该名称为该作用域的局部变量。 标注绝不会在函数作用域内被求值和保存。

如果存在右手边,带标注的赋值会在对标注求值之前(如果适用)执行实际的赋值。 如果用作表达式目标的右手边不存在,则解释器会对目标求值,但最后的 __setitem__()__setattr__() 调用除外。

参见

PEP 526 - 变量标注的语法

该提议增加了标注变量(也包括类变量和实例变量)类型的语法,而不再是通过注释来进行表达。

PEP 484 - 类型提示

该提议增加了 typing 模块以便为类型标注提供标准句法,可被静态分析工具和 IDE 所使用。

在 3.8 版更改: 现在带有标注的赋值允许在右边以同样的表达式作为常规赋值。 之前,某些表达式(例如未加圆括号的元组表达式)会导致语法错误。

7.3. assert 语句

assert 语句是在程序中插入调试性断言的简便方式:

assert_stmt ::=  "assert" expression ["," expression]

简单形式 assert expression 等价于

if __debug__:    if not expression: raise AssertionError

扩展形式 assert expression1, expression2 等价于

if __debug__:    if not expression1: raise AssertionError(expression2)

以上等价形式假定 __debug__AssertionError 指向具有指定名称的内置变量。 在当前实现中,内置变量 __debug__ 在正常情况下为 True,在请求优化时为 False (对应命令行选项为 -O)。 如果在编译时请求优化,当前代码生成器不会为 assert 语句发出任何代码。 请注意不必在错误信息中包含失败表达式的源代码;它会被作为栈追踪的一部分被显示。

赋值给 __debug__ 是非法的。 该内置变量的值会在解释器启动时确定。

7.4. pass 语句

pass_stmt ::=  "pass"

pass 是一个空操作 —- 当它被执行时,什么都不发生。 它适合当语法上需要一条语句但并不需要执行任何代码时用来临时占位,例如:

def f(arg): pass    # a function that does nothing (yet)
class C: pass       # a class with no methods (yet)

7.5. del 语句

del_stmt ::=  "del" target_list

删除是递归定义的,与赋值的定义方式非常类似。 此处不再详细说明,只给出一些提示。

目标列表的删除将从左至右递归地删除每一个目标。

名称的删除将从局部或全局命名空间中移除该名称的绑定,具体作用域的确定是看该名称是否有在同一代码块的 global 语句中出现。 如果该名称未被绑定,将会引发 NameError

属性引用、抽取和切片的删除会被传递给相应的原型对象;删除一个切片基本等价于赋值为一个右侧类型的空切片(但即便这一点也是由切片对象决定的)。

在 3.2 版更改: 在之前版本中,如果一个名称作为被嵌套代码块中的自由变量出现,则将其从局部命名空间中删除是非法的。

7.6. return 语句

return_stmt ::=  "return" [expression_list]

return 在语法上只会出现于函数定义所嵌套的代码,不会出现于类定义所嵌套的代码。

如果提供了表达式列表,它将被求值,否则以 None 替代。

return 会离开当前函数调用,并以表达式列表 (或 None) 作为返回值。

return 将控制流传出一个带有 finally 子句的 try 语句时,该 finally 子句会先被执行然后再真正离开该函数。

在一个生成器函数中,return 语句表示生成器已完成并将导致 StopIteration 被引发。 返回值(如果有的话)会被当作一个参数用来构建 StopIteration 并成为 StopIteration.value 属性。

在一个异步生成器函数中,一个空的 return 语句表示异步生成器已完成并将导致 StopAsyncIteration 被引发。 一个非空的 return 语句在异步生成器函数中会导致语法错误。

7.7. yield 语句

yield_stmt ::=  yield_expression

yield 语句在语义上等同于 yield 表达式。 yield 语句可用来省略在使用等效的 yield 表达式语句时所必须的圆括号。 例如,以下 yield 语句

yield <expr>
yield from <expr>

等同于以下 yield 表达式语句

(yield <expr>)
(yield from <expr>)

yield 表达式和语句仅在定义 generator 函数时使用,并且仅被用于生成器函数的函数体内部。 在函数定义中使用 yield 就足以使得该定义创建的是生成器函数而非普通函数。

7.8. raise 语句

raise_stmt ::=  "raise" [expression ["from" expression]]

如果不带表达式,raise 会重新引发当前作用域内最后一个激活的异常。 如果当前作用域内没有激活的异常,将会引发 RuntimeError 来提示错误。

否则的话,raise 会将第一个表达式求值为异常对象。 它必须为 BaseException 的子类或实例。 如果它是一个类,当需要时会通过不带参数地实例化该类来获得异常的实例。

异常的 类型 为异常实例的类, 为实例本身。

当异常被引发时通常会自动创建一个回溯对象并将其关联到可写的 __traceback__ 属性。 你可以创建一个异常并同时使用 with_traceback() 异常方法(该方法将返回同一异常实例,并将回溯对象设为其参数)设置自己的回溯,就像这样:

raise Exception("foo occurred").with_traceback(tracebackobj)

from 子句用于异常串连:如果有该子句,则第二个 表达式 必须为另一个异常类或实例。 如果第二个表达式是一个异常实例,它将作为可写的 __cause__ 属性被关联到所引发的异常。 如果该表达式是一个异常类,这个类将被实例化且所生成的异常实例将作为 __cause__ 属性被关联到所引发的异常。 如果所引发的异常未被处理,则两个异常都将被打印出来:

>>> try:
...     print(1 / 0)
... except Exception as exc:
...     raise RuntimeError("Something bad happened") from exc
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
RuntimeError: Something bad happened

如果一个异常在异常处理器或 finally clause: 中被引发,类似的机制会隐式地发挥作用,之前的异常将被关联到新异常的 __context__ 属性:

>>> try:
...     print(1 / 0)
... except:
...     raise RuntimeError("Something bad happened")
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
RuntimeError: Something bad happened

异常串连可通过在 from 子句中指定 None 来显式地加以抑制:

>>> try:
...     print(1 / 0)
... except:
...     raise RuntimeError("Something bad happened") from None
...
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
RuntimeError: Something bad happened

在 3.3 版更改: None 现在允许被用作 raise X from Y 中的 Y

3.3 新版功能: 使用 __suppress_context__ 属性来抑制异常上下文的自动显示。

7.9. break 语句

break_stmt ::=  "break"

break 在语法上只会出现于 forwhile 循环所嵌套的代码,但不会出现于该循环内部的函数或类定义所嵌套的代码。

它会终结最近的外层循环,如果循环有可选的 else 子句,也会跳过该子句。

如果一个 for 循环被 break 所终结,该循环的控制目标会保持其当前值。

break 将控制流传出一个带有 finally 子句的 try 语句时,该 finally 子句会先被执行然后再真正离开该循环。

7.10. continue 语句

continue_stmt ::=  "continue"

continue 在语法上只会出现于 forwhile 循环所嵌套的代码中,但不会出现于该循环内部的函数或类定义中。 它会继续执行最近的外层循环的下一个轮次。

continue 将控制流传出一个带有 finally 子句的 try 语句时,该 finally 子句会先被执行然后再真正开始循环的下一个轮次。

7.11. import 语句

import_stmt     ::=  "import" module ["as" identifier] ("," module ["as" identifier])*
                     | "from" relative_module "import" identifier ["as" identifier]
                     ("," identifier ["as" identifier])*
                     | "from" relative_module "import" "(" identifier ["as" identifier]
                     ("," identifier ["as" identifier])* [","] ")"
                     | "from" relative_module "import" "*"
module          ::=  (identifier ".")* identifier
relative_module ::=  "."* module | "."+

基本的 import 语句(不带 from 子句)会分两步执行:

  1. 查找一个模块,如果有必要还会加载并初始化模块。
  2. 在局部命名空间中为 import 语句发生位置所处的作用域定义一个或多个名称。

当语句包含多个子句(由逗号分隔)时这两个步骤将对每个子句分别执行,如同这些子句被分成独立的 import 语句一样。

第一个步骤即查找和加载模块,其中也描述了可被导入的多种类型的包和模块,以及可用于定制导入系统的所有钩子对象。 请注意这一步如果失败,则可能说明模块无法找到,或者 是在初始化模块,包括执行模块代码期间发生了错误。

如果成功获取到请求的模块,则可以通过以下三种方式一之在局部命名空间中使用它:

  • 模块名后使用 as 时,直接把 as 后的名称与导入模块绑定。
  • 如果没有指定其他名称,且被导入的模块为最高层级模块,则模块的名称将被绑定到局部命名空间作为对所导入模块的引用。
  • 如果被导入的模块 不是 最高层级模块,则包含该模块的最高层级包的名称将被绑定到局部命名空间作为对该最高层级包的引用。 所导入的模块必须使用其完整限定名称来访问而不能直接访问。

from 形式使用的过程略微繁复一些:

  1. 查找 from 子句中指定的模块,如有必要还会加载并初始化模块;
  2. 对于 import 子句中指定的每个标识符:
    1. 检查被导入模块是否有该名称的属性
    2. 如果没有,尝试导入具有该名称的子模块,然后再次检查被导入模块是否有该属性
    3. 如果未找到该属性,则引发 ImportError
    4. 否则的话,将对该值的引用存入局部命名空间,如果有 as 子句则使用其指定的名称,否则使用该属性的名称

示例:

import foo                 # foo imported and bound locally
import foo.bar.baz         # foo.bar.baz imported, foo bound locally
import foo.bar.baz as fbb  # foo.bar.baz imported and bound as fbb
from foo.bar import baz    # foo.bar.baz imported and bound as baz
from foo import attr       # foo imported and foo.attr bound as attr

如果标识符列表改为一个星号 ('*'),则在模块中定义的全部公有名称都将按 import 语句所在的作用域被绑定到局部命名空间。

一个模块所定义的 公有名称 是由在模块的命名空间中检测一个名为 __all__ 的变量来确定的;如果有定义,它必须是一个字符串列表,其中的项为该模块所定义或导入的名称。 在 __all__ 中所给出的名称都会被视为公有并且应当存在。 如果 __all__ 没有被定义,则公有名称的集合将包含在模块的命名空间中找到的所有不以下划线字符 ('_') 打头的名称。 __all__ 应当包括整个公有 API。 它的目标是避免意外地导出不属于 API 的一部分的项(例如在模块内部被导入和使用的库模块)。

通配符形式的导入 —- from module import * —- 仅在模块层级上被允许。 尝试在类或函数定义中使用它将引发 SyntaxError

当指定要导入哪个模块时,你不必指定模块的绝对名称。 当一个模块或包被包含在另一个包之中时,可以在同一个最高层级包中进行相对导入,而不必提及包名称。 通过在 from 之后指定的模块或包中使用前缀点号,你可以在不指定确切名称的情况下指明在当前包层级结构中要上溯多少级。 一个前缀点号表示是执行导入的模块所在的当前包,两个点号表示上溯一个包层级。 三个点号表示上溯两级,依此类推。 因此如果你执行 from . import mod 时所处位置为 pkg 包内的一个模块,则最终你将导入 pkg.mod。 如果你执行 from ..subpkg2 import mod 时所处位置为 pkg.subpkg1 则你将导入 pkg.subpkg2.mod。 有关相对导入的规范说明包含在 包相对导入 一节中。

importlib.import_module() 被提供用来为动态地确定要导入模块的应用提供支持。

引发一个 审计事件 import 附带参数 module, filename, sys.path, sys.meta_path, sys.path_hooks

7.11.1. future 语句

future 语句 是一种针对编译器的指令,指明某个特定模块应当使用在特定的未来某个 Python 发行版中成为标准特性的语法或语义。

future 语句的目的是使得向在语言中引入了不兼容改变的 Python 未来版本的迁移更为容易。 它允许基于每个模块在某种新特性成为标准之前的发行版中使用该特性。

future_stmt ::=  "from" "__future__" "import" feature ["as" identifier]
                 ("," feature ["as" identifier])*
                 | "from" "__future__" "import" "(" feature ["as" identifier]
                 ("," feature ["as" identifier])* [","] ")"
feature     ::=  identifier

future 语句必须在靠近模块开头的位置出现。 可以出现在 future 语句之前行只有:

  • 模块的文档字符串(如果存在),
  • 注释,
  • 空行,以及
  • 其他 future 语句。

唯一需要使用 future 语句的特性是 标注 (参见 PEP 563)。

future 语句所启用的所有历史特性仍然为 Python 3 所认可。 其中包括 absolute_import, division, generators, generator_stop, unicode_literals, print_function, nested_scopeswith_statement。 它们都已成为冗余项,因为它们总是为已启用状态,保留它们只是为了向后兼容。

future 语句在编译时会被识别并做特殊对待:对核心构造语义的改变常常是通过生成不同的代码来实现。 新的特性甚至可能会引入新的不兼容语法(例如新的保留字),在这种情况下编译器可能需要以不同的方式来解析模块。 这样的决定不能推迟到运行时方才作出。

对于任何给定的发布版本,编译器要知道哪些特性名称已被定义,如果某个 future 语句包含未知的特性则会引发编译时错误。

直接运行时的语义与任何 import 语句相同:存在一个后文将详细说明的标准模块 __future__,它会在执行 future 语句时以通常的方式被导入。

相应的运行时语义取决于 future 语句所启用的指定特性。

请注意以下语句没有任何特别之处:

import __future__ [as name]

这并非 future 语句;它只是一条没有特殊语义或语法限制的普通 import 语句。

在默认情况下,通过对Code compiled by calls to the 内置函数 exec()compile() 的调用所编译的代码如果出现于一个包含有 future 语句的模块 M 之中,就会使用 future 语句所关联的语法和语义。 此行为可以通过 compile() 的可选参数加以控制 —- 请参阅该函数的文档以了解详情。

在交互式解释器提示符中键入的 future 语句将在解释器会话此后的交互中有效。 如果一个解释器的启动使用了 -i 选项启动,并传入了一个脚本名称来执行,且该脚本包含 future 语句,它将在交互式会话开始执行脚本之后保持有效。

参见

PEP 236 - 回到 future

有关 future 机制的最初提议。

7.12. global 语句

global_stmt ::=  "global" identifier ("," identifier)*

global 语句是作用于整个当前代码块的声明。 它意味着所列出的标识符将被解读为全局变量。 要给全局变量赋值不可能不用到 global 关键字,不过自由变量也可以指向全局变量而不必声明为全局变量。

global 语句中列出的名称不得在同一代码块内该 global 语句之前的位置中使用。

global 语句中列出的名称不能被定义为形式参数,也不能被作为 with 语句或 except 子句的目标,以及 for 循环的目标列表、class 定义、函数定义、import 语句或变量标注等等。

CPython implementation detail: 当前的实现并未强制要求所有的上述限制,但程序不应当滥用这样的自由,因为未来的实现可能会改为强制要求,并静默地改变程序的含义。

程序员注意事项: global 是对解析器的指令。 它仅对与 global 语句同时被解析的代码起作用。 特别地,包含在提供给内置 exec() 函数字符串或代码对象中的 global 语句并不会影响 包含 该函数调用的代码块,而包含在这种字符串中的代码也不会受到包含该函数调用的代码中的 global 语句影响。 这同样适用于 eval()compile() 函数。

7.13. nonlocal 语句

nonlocal_stmt ::=  "nonlocal" identifier ("," identifier)*

nonlocal 语句会使得所列出的名称指向之前在最近的包含作用域中绑定的除全局变量以外的变量。 这种功能很重要,因为绑定的默认行为是先搜索局部命名空间。 这个语句允许被封装的代码重新绑定局部作用域以外且非全局(模块)作用域当中的变量。

global 语句中列出的名称不同,nonlocal 语句中列出的名称必须指向之前存在于包含作用域之中的绑定(在这个应当用来创建新绑定的作用域不能被无歧义地确定)。

nonlocal 语句中列出的名称不得与之前存在于局部作用域中的绑定相冲突。

参见

PEP 3104 - 访问外层作用域中的名称

有关 nonlocal 语句的规范说明。

8. 复合语句

复合语句是包含其它语句(语句组)的语句;它们会以某种方式影响或控制所包含其它语句的执行。 通常,复合语句会跨越多行,虽然在某些简单形式下整个复合语句也可能包含于一行之内。

if, whilefor 语句用来实现传统的控制流程构造。 try 语句为一组语句指定异常处理和/和清理代码,而 with 语句允许在一个代码块周围执行初始化和终结化代码。 函数和类定义在语法上也属于复合语句。

一条复合语句由一个或多个‘子句’组成。 一个子句则包含一个句头和一个‘句体’。 特定复合语句的子句头都处于相同的缩进层级。 每个子句头以一个作为唯一标识的关键字开始并以一个冒号结束。 子句体是由一个子句控制的一组语句。 子句体可以是在子句头的冒号之后与其同处一行的一条或由分号分隔的多条简单语句,或者也可以是在其之后缩进的一行或多行语句。 只有后一种形式的子句体才能包含嵌套的复合语句;以下形式是不合法的,这主要是因为无法分清某个后续的 else 子句应该属于哪个 if 子句:

if test1: if test2: print(x)

还要注意的是在这种情形下分号的绑定比冒号更紧密,因此在以下示例中,所有 print() 调用或者都不执行,或者都执行:

if x < y < z: print(x); print(y); print(z)

总结:

compound_stmt ::=  if_stmt
                   | while_stmt
                   | for_stmt
                   | try_stmt
                   | with_stmt
                   | match_stmt
                   | funcdef
                   | classdef
                   | async_with_stmt
                   | async_for_stmt
                   | async_funcdef
suite         ::=  stmt_list NEWLINE | NEWLINE INDENT statement+ DEDENT
statement     ::=  stmt_list NEWLINE | compound_stmt
stmt_list     ::=  simple_stmt (";" simple_stmt)* [";"]

请注意语句总是以 NEWLINE 结束,之后可能跟随一个 DEDENT。 还要注意可选的后续子句总是以一个不能作为语句开头的关键字作为开头,因此不会产生歧义(‘悬空的 else’问题在 Python 中是通过要求嵌套的 if 语句必须缩进来解决的)。

为了保证清晰,以下各节中语法规则采用将每个子句都放在单独行中的格式。

8.1. if 语句

if 语句用于有条件的执行:

if_stmt ::=  "if" assignment_expression ":" suite
             ("elif" assignment_expression ":" suite)*
             ["else" ":" suite]

它通过对表达式逐个求值直至找到一个真值在子句体中选择唯一匹配的一个;然后执行该子句体(而且 if 语句的其他部分不会被执行或求值)。 如果所有表达式均为假值,则如果 else 子句体如果存在就会被执行。

8.2. while 语句

while 语句用于在表达式保持为真的情况下重复地执行:

while_stmt ::=  "while" assignment_expression ":" suite
                ["else" ":" suite]

这将重复地检验表达式,并且如果其值为真就执行第一个子句体;如果表达式值为假(这可能在第一次检验时就发生)则如果 else 子句体存在就会被执行并终止循环。

第一个子句体中的 break 语句在执行时将终止循环且不执行 else 子句体。 第一个子句体中的 continue 语句在执行时将跳过子句体中的剩余部分并返回检验表达式。

8.3. for 语句

for 语句用于对序列(例如字符串、元组或列表)或其他可迭代对象中的元素进行迭代:

for_stmt ::=  "for" target_list "in" expression_list ":" suite
              ["else" ":" suite]

表达式列表会被求值一次;它应该产生一个可迭代对象。 系统将为 expression_list 的结果创建一个迭代器,然后将为迭代器所提供的每一项执行一次子句体,具体次序与迭代器的返回顺序一致。 每一项会按标准赋值规则被依次赋值给目标列表,然后子句体将被执行。 当所有项被耗尽时 (这会在序列为空或迭代器引发 StopIteration 异常时立刻发生),else 子句的子句体如果存在将会被执行,并终止循环。

第一个子句体中的 break 语句在执行时将终止循环且不执行 else 子句体。 第一个子句体中的 continue 语句在执行时将跳过子句体中的剩余部分并转往下一项继续执行,或者在没有下一项时转往 else 子句执行。

for 循环会对目标列表中的变量进行赋值。 这将覆盖之前对这些变量的所有赋值,包括在 for 循环体中的赋值:

for i in range(10):
    print(i)
    i = 5             # this will not affect the for-loop
                      # because i will be overwritten with the next
                      # index in the range

目标列表中的名称在循环结束时不会被删除,但如果序列为空,则它们根本不会被循环所赋值。 提示:内置函数 range() 会返回一个可迭代的整数序列,适用于模拟 Pascal 中的 for i := a to b do 这种效果;例如 list(range(3)) 会返回列表 [0, 1, 2]

注解

当序列在循环中被修改时会有一个微妙的问题(这只可能发生于可变序列例如列表中)。 会有一个内部计数器被用来跟踪下一个要使用的项,每次迭代都会使计数器递增。 当计数器值达到序列长度时循环就会终止。 这意味着如果语句体从序列中删除了当前(或之前)的一项,下一项就会被跳过(因为其标号将变成已被处理的当前项的标号)。 类似地,如果语句体在序列当前项的前面插入一个新项,当前项会在循环的下一轮中再次被处理。 这会导致麻烦的程序错误,避免此问题的办法是对整个序列使用切片来创建一个临时副本,例如

for x in a[:]:
    if x < 0: a.remove(x)

8.4. try 语句

try 语句可为一组语句指定异常处理器和/或清理代码:

try_stmt  ::=  try1_stmt | try2_stmt
try1_stmt ::=  "try" ":" suite
               ("except" [expression ["as" identifier]] ":" suite)+
               ["else" ":" suite]
               ["finally" ":" suite]
try2_stmt ::=  "try" ":" suite
               "finally" ":" suite

except 子句指定一个或多个异常处理程序。 当 try 子句中没有发生异常时,没有任何异常处理程序会被执行。 当 try 子句中发生异常时,将启动对异常处理程序的搜索。 此搜索会逐一检查 except 子句直至找到与该异常相匹配的子句。 如果存在无表达式的 except 子句,它必须是最后一个;它将匹配任何异常。 对于带有表达式的 except 子句,该表达式会被求值,如果结果对象与发生的异常“兼容”则该子句将匹配该异常。 如果一个对象是异常对象所属的类或基类,或者是包含兼容该异常的项的元组则两者就是兼容的。

如果没有 except 子句与异常相匹配,则会在周边代码和发起调用栈上继续搜索异常处理器。

如果在对 except 子句头中的表达式求值时引发了异常,则原来对处理器的搜索会被取消,并在周边代码和调用栈上启动对新异常的搜索(它会被视作是整个 try 语句所引发的异常)。

当找到一个匹配的 except 子句时,该异常将被赋值给该 except 子句在 as 关键字之后指定的目标,如果存在此关键字的话,并且该 except 子句体将被执行。 所有 except 子句都必须有可执行的子句体。 当到达子句体的末尾时,通常会转向整个 try 语句之后继续执行。 (这意味着如果对于同一异常存在有嵌套的两个处理器,而异常发生于内层处理器的 try 子句中,则外层处理器将不会处理该异常。)

当使用 as 将目标赋值为一个异常时,它将在 except 子句结束时被清除。 这就相当于

except E as N:
    foo

被转写为

except E as N:
    try:
        foo
    finally:
        del N

这意味着异常必须赋值给一个不同的名称才能在 except 子句之后引用它。 异常会被清除是因为在附加了回溯信息的情况下,它们会形成堆栈帧的循环引用,使得所有局部变量保持存活直到发生下一次垃圾回收。

Before an except clause’s suite is executed, details about the exception are stored in the sys module and can be accessed via sys.exc_info(). sys.exc_info() returns a 3-tuple consisting of the exception class, the exception instance and a traceback object (see section 标准类型层级结构) identifying the point in the program where the exception occurred. The details about the exception accessed via sys.exc_info() are restored to their previous values when leaving an exception handler:

>>> print(sys.exc_info())
(None, None, None)
>>> try:
...     raise TypeError
... except:
...     print(sys.exc_info())
...     try:
...          raise ValueError
...     except:
...         print(sys.exc_info())
...     print(sys.exc_info())
...
(<class 'TypeError'>, TypeError(), <traceback object at 0x10efad080>)
(<class 'ValueError'>, ValueError(), <traceback object at 0x10efad040>)
(<class 'TypeError'>, TypeError(), <traceback object at 0x10efad080>)
>>> print(sys.exc_info())
(None, None, None)

如果控制流离开 try 子句体时没有引发异常,并且没有执行 return, continuebreak 语句,可选的 else 子句将被执行。 else 语句中的异常不会由之前的 except 子句处理。

如果存在 finally,它将指定一个‘清理’处理程序。 try 子句会被执行,包括任何 exceptelse 子句。 如果在这些子句中发生任何未处理的异常,该异常会被临时保存。 finally 子句将被执行。 如果存在被保存的异常,它会在 finally 子句的末尾被重新引发。 如果 finally 子句引发了另一个异常,被保存的异常会被设为新异常的上下文。 如果 finally 子句执行了 return, breakcontinue 语句,则被保存的异常会被丢弃:

>>> def f():
...     try:
...         1/0
...     finally:
...         return 42
...
>>> f()
42

finally 子句执行期间,程序不能获取异常信息。

return, breakcontinue 语句在一个 tryfinally 语句的 try 子语句体中被执行时,finally 子语句也会‘在离开时’被执行。

函数的返回值是由最后被执行的 return 语句所决定的。 由于 finally 子句总是被执行,因此在 finally 子句中被执行的 return 语句总是最后被执行的:

>>> def foo():
...     try:
...         return 'try'
...     finally:
...         return 'finally'
...
>>> foo()
'finally'

在 3.8 版更改: 在 Python 3.8 之前,continue 语句不允许在 finally 子句中使用,这是因为具体实现存在一个问题。

8.5. with 语句

with 语句用于包装带有使用上下文管理器 定义的方法的代码块的执行。 这允许对普通的 tryexceptfinally 使用模式进行封装以方便地重用。

with_stmt          ::=  "with" ( "(" with_stmt_contents ","? ")" | with_stmt_contents ) ":" suite
with_stmt_contents ::=  with_item ("," with_item)*
with_item          ::=  expression ["as" target]

带有一个“项目”的 with 语句的执行过程如下:

  1. 对上下文表达式 (在 with_item 中给出的表达式) 求值以获得一个上下文管理器。

  2. 载入上下文管理器的 __enter__() 以便后续使用。

  3. 载入上下文管理器的 __exit__() 以便后续使用。

  4. 发起调用上下文管理器的 __enter__() 方法。

  5. 如果 with 语句中包含一个目标,来自 __enter__() 的返回值将被赋值给它。

    注解

    with 语句会保证如果 __enter__() 方法返回时未发生错误,则 __exit__() 将总是被调用。 因此,如果在对目标列表赋值期间发生错误,则会将其视为在语句体内部发生的错误。 参见下面的第 6 步。

  6. 执行语句体。

  7. 发起调用上下文管理器的 __exit__() 方法。 如果语句体的退出是由异常导致的,则其类型、值和回溯信息将被作为参数传递给 __exit__()。 否则的话,将提供三个 None 参数。

    如果语句体的退出是由异常导致的,并且来自 __exit__() 方法的返回值为假,则该异常会被重新引发。 如果返回值为真,则该异常会被抑制,并会继续执行 with 语句之后的语句。

    如果语句体由于异常以外的任何原因退出,则来自 __exit__() 的返回值会被忽略,并会在该类退出正常的发生位置继续执行。

以下代码:

with EXPRESSION as TARGET:
    SUITE

在语义上等价于:

manager = (EXPRESSION)
enter = type(manager).__enter__
exit = type(manager).__exit__
value = enter(manager)
hit_except = False
try:
    TARGET = value
    SUITE
except:
    hit_except = True
    if not exit(manager, *sys.exc_info()):
        raise
finally:
    if not hit_except:
        exit(manager, None, None, None)

如果有多个项目,则会视作存在多个 with 语句嵌套来处理多个上下文管理器:

with A() as a, B() as b:    
    SUITE

在语义上等价于:

with A() as a:
    with B() as b:
        SUITE

You can also write multi-item context managers in multiple lines if the items are surrounded by parentheses. For example:

with (
    A() as a,
    B() as b,
):
    SUITE

在 3.1 版更改: 支持多个上下文表达式。

在 3.10 版更改: Support for using grouping parentheses to break the statement in multiple lines.

参见

PEP 343 - “with” 语句

Python with 语句的规范描述、背景和示例。

8.6. The match statement

3.10 新版功能.

The match statement is used for pattern matching. Syntax:

match_stmt   ::=  'match' subject_expr ":" NEWLINE INDENT case_block+ DEDENT
subject_expr ::=  star_named_expression "," star_named_expressions?
                  | named_expression
case_block   ::=  'case' patterns [guard] ":" block

注解

This section uses single quotes to denote soft keywords.

Pattern matching takes a pattern as input (following case) and a subject value (following match). The pattern (which may contain subpatterns) is matched against the subject value. The outcomes are:

  • A match success or failure (also termed a pattern success or failure).
  • Possible binding of matched values to a name. The prerequisites for this are further discussed below.

The match and case keywords are soft keywords.

参见

  • PEP 634 — Structural Pattern Matching: Specification
  • PEP 636 — Structural Pattern Matching: Tutorial

8.6.1. 概述

Here’s an overview of the logical flow of a match statement:

  1. The subject expression subject_expr is evaluated and a resulting subject value obtained. If the subject expression contains a comma, a tuple is constructed using the standard rules.

  2. Each pattern in a case_block is attempted to match with the subject value. The specific rules for success or failure are described below. The match attempt can also bind some or all of the standalone names within the pattern. The precise pattern binding rules vary per pattern type and are specified below. Name bindings made during a successful pattern match outlive the executed block and can be used after the match statement.

    注解

    During failed pattern matches, some subpatterns may succeed. Do not rely on bindings being made for a failed match. Conversely, do not rely on variables remaining unchanged after a failed match. The exact behavior is dependent on implementation and may vary. This is an intentional decision made to allow different implementations to add optimizations.

  3. If the pattern succeeds, the corresponding guard (if present) is evaluated. In this case all name bindings are guaranteed to have happened.

    • If the guard evaluates as true or is missing, the block inside case_block is executed.
    • Otherwise, the next case_block is attempted as described above.
    • If there are no further case blocks, the match statement is completed.

注解

Users should generally never rely on a pattern being evaluated. Depending on implementation, the interpreter may cache values or use other optimizations which skip repeated evaluations.

A sample match statement:

>>> flag = False
>>> match (100, 200):
...    case (100, 300):  # Mismatch: 200 != 300
...        print('Case 1')
...    case (100, 200) if flag:  # Successful match, but guard fails
...        print('Case 2')
...    case (100, y):  # Matches and binds y to 200
...        print(f'Case 3, y: {y}')
...    case _:  # Pattern not attempted
...        print('Case 4, I match anything!')
...
Case 3, y: 200

In this case, if flag is a guard. Read more about that in the next section.

8.6.2. Guards

guard ::=  "if" named_expression

A guard (which is part of the case) must succeed for code inside the case block to execute. It takes the form: if followed by an expression.

The logical flow of a case block with a guard follows:

  1. Check that the pattern in the case block succeeded. If the pattern failed, the guard is not evaluated and the next case block is checked.
  2. If the pattern succeeded, evaluate the guard.
    • If the guard condition evaluates as true, the case block is selected.
    • If the guard condition evaluates as false, the case block is not selected.
    • If the guard raises an exception during evaluation, the exception bubbles up.

Guards are allowed to have side effects as they are expressions. Guard evaluation must proceed from the first to the last case block, one at a time, skipping case blocks whose pattern(s) don’t all succeed. (I.e., guard evaluation must happen in order.) Guard evaluation must stop once a case block is selected.

8.6.3. Irrefutable Case Blocks

An irrefutable case block is a match-all case block. A match statement may have at most one irrefutable case block, and it must be last.

A case block is considered irrefutable if it has no guard and its pattern is irrefutable. A pattern is considered irrefutable if we can prove from its syntax alone that it will always succeed. Only the following patterns are irrefutable:

  • AS Patterns whose left-hand side is irrefutable
  • OR Patterns containing at least one irrefutable pattern
  • Capture Patterns
  • Wildcard Patterns
  • parenthesized irrefutable patterns

8.6.4. Patterns

注解

This section uses grammar notations beyond standard EBNF:

  • the notation SEP.RULE+ is shorthand for RULE (SEP RULE)*
  • the notation !RULE is shorthand for a negative lookahead assertion

The top-level syntax for patterns is:

patterns       ::=  open_sequence_pattern | pattern
pattern        ::=  as_pattern | or_pattern
closed_pattern ::=  | literal_pattern
                    | capture_pattern
                    | wildcard_pattern
                    | value_pattern
                    | group_pattern
                    | sequence_pattern
                    | mapping_pattern
                    | class_pattern

The descriptions below will include a description “in simple terms” of what a pattern does for illustration purposes (credits to Raymond Hettinger for a document that inspired most of the descriptions). Note that these descriptions are purely for illustration purposes and may not reflect the underlying implementation. Furthermore, they do not cover all valid forms.

8.6.4.1. OR Patterns

An OR pattern is two or more patterns separated by vertical bars |. Syntax:

or_pattern ::=  "|".closed_pattern+

Only the final subpattern may be irrefutable, and each subpattern must bind the same set of names to avoid ambiguity.

An OR pattern matches each of its subpatterns in turn to the subject value, until one succeeds. The OR pattern is then considered successful. Otherwise, if none of the subpatterns succeed, the OR pattern fails.

In simple terms, P1 | P2 | ... will try to match P1, if it fails it will try to match P2, succeeding immediately if any succeeds, failing otherwise.

8.6.4.2. AS Patterns

An AS pattern matches an OR pattern on the left of the as keyword against a subject. Syntax:

as_pattern ::=  or_pattern "as" capture_pattern

If the OR pattern fails, the AS pattern fails. Otherwise, the AS pattern binds the subject to the name on the right of the as keyword and succeeds. capture_pattern cannot be a a _.

In simple terms P as NAME will match with P, and on success it will set NAME = <subject>.

8.6.4.3. Literal Patterns

A literal pattern corresponds to most literals in Python. Syntax:

literal_pattern ::=  signed_number
                     | signed_number "+" NUMBER
                     | signed_number "-" NUMBER
                     | strings
                     | "None"
                     | "True"
                     | "False"
                     | signed_number: NUMBER | "-" NUMBER

The rule strings and the token NUMBER are defined in the standard Python grammar. Triple-quoted strings are supported. Raw strings and byte strings are supported. 格式字符串字面值 are not supported.

The forms signed_number '+' NUMBER and signed_number '-' NUMBER are for expressing complex numbers; they require a real number on the left and an imaginary number on the right. E.g. 3 + 4j.

In simple terms, LITERAL will succeed only if <subject> == LITERAL. For the singletons None, True and False, the is operator is used.

8.6.4.4. Capture Patterns

A capture pattern binds the subject value to a name. Syntax:

capture_pattern ::=  !'_' NAME

A single underscore _ is not a capture pattern (this is what !'_' expresses). It is instead treated as a wildcard_pattern.

In a given pattern, a given name can only be bound once. E.g. case x, x: ... is invalid while case [x] | x: ... is allowed.

Capture patterns always succeed. The binding follows scoping rules established by the assignment expression operator in PEP 572; the name becomes a local variable in the closest containing function scope unless there’s an applicable global or nonlocal statement.

In simple terms NAME will always succeed and it will set NAME = <subject>.

8.6.4.5. Wildcard Patterns

A wildcard pattern always succeeds (matches anything) and binds no name. Syntax:

wildcard_pattern ::=  '_'

_ is a soft keyword within any pattern, but only within patterns. It is an identifier, as usual, even within match subject expressions, guards, and case blocks.

In simple terms, _ will always succeed.

8.6.4.6. Value Patterns

A value pattern represents a named value in Python. Syntax:

value_pattern ::=  attr
attr          ::=  name_or_attr "." NAME
name_or_attr  ::=  attr | NAME

The dotted name in the pattern is looked up using standard Python name resolution rules. The pattern succeeds if the value found compares equal to the subject value (using the == equality operator).

In simple terms NAME1.NAME2 will succeed only if <subject> == NAME1.NAME2

注解

If the same value occurs multiple times in the same match statement, the interpreter may cache the first value found and reuse it rather than repeat the same lookup. This cache is strictly tied to a given execution of a given match statement.

8.6.4.7. Group Patterns

A group pattern allows users to add parentheses around patterns to emphasize the intended grouping. Otherwise, it has no additional syntax. Syntax:

group_pattern ::=  "(" pattern ")"

In simple terms (P) has the same effect as P.

8.6.4.8. Sequence Patterns

A sequence pattern contains several subpatterns to be matched against sequence elements. The syntax is similar to the unpacking of a list or tuple.

sequence_pattern       ::=  "[" [maybe_sequence_pattern] "]"
                            | "(" [open_sequence_pattern] ")"
open_sequence_pattern  ::=  maybe_star_pattern "," [maybe_sequence_pattern]
maybe_sequence_pattern ::=  ",".maybe_star_pattern+ ","?
maybe_star_pattern     ::=  star_pattern | pattern
star_pattern           ::=  "*" (capture_pattern | wildcard_pattern)

There is no difference if parentheses or square brackets are used for sequence patterns (i.e. (...) vs [...] ).

注解

A single pattern enclosed in parentheses without a trailing comma (e.g. (3 | 4)) is a group pattern. While a single pattern enclosed in square brackets (e.g. [3 | 4]) is still a sequence pattern.

At most one star subpattern may be in a sequence pattern. The star subpattern may occur in any position. If no star subpattern is present, the sequence pattern is a fixed-length sequence pattern; otherwise it is a variable-length sequence pattern.

The following is the logical flow for matching a sequence pattern against a subject value:

  1. If the subject value is not a sequence, the sequence pattern fails.

  2. If the subject value is an instance of str, bytes or bytearray the sequence pattern fails.

  3. The subsequent steps depend on whether the sequence pattern is fixed or variable-length.

    If the sequence pattern is fixed-length:

    1. If the length of the subject sequence is not equal to the number of subpatterns, the sequence pattern fails
    2. Subpatterns in the sequence pattern are matched to their corresponding items in the subject sequence from left to right. Matching stops as soon as a subpattern fails. If all subpatterns succeed in matching their corresponding item, the sequence pattern succeeds.

    Otherwise, if the sequence pattern is variable-length:

    1. If the length of the subject sequence is less than the number of non-star subpatterns, the sequence pattern fails.
    2. The leading non-star subpatterns are matched to their corresponding items as for fixed-length sequences.
    3. If the previous step succeeds, the star subpattern matches a list formed of the remaining subject items, excluding the remaining items corresponding to non-star subpatterns following the star subpattern.
    4. Remaining non-star subpatterns are matched to their corresponding subject items, as for a fixed-length sequence.

    注解

    The length of the subject sequence is obtained via len() (i.e. via the __len__() protocol). This length may be cached by the interpreter in a similar manner as value patterns.

In simple terms [P1, P2, P3,, P<N>] matches only if all the following happens:

  • check <subject> is a sequence
  • len(subject) == <N>
  • P1 matches <subject>[0] (note that this match can also bind names)
  • P2 matches <subject>[1] (note that this match can also bind names)
  • … and so on for the corresponding pattern/element.
8.6.4.9. Mapping Patterns

A mapping pattern contains one or more key-value patterns. The syntax is similar to the construction of a dictionary. Syntax:

mapping_pattern     ::=  "{" [items_pattern] "}"
items_pattern       ::=  ",".key_value_pattern+ ","?
key_value_pattern   ::=  (literal_pattern | value_pattern) ":" pattern
                         | double_star_pattern
double_star_pattern ::=  "**" capture_pattern

At most one double star pattern may be in a mapping pattern. The double star pattern must be the last subpattern in the mapping pattern.

Duplicate keys in mapping patterns are disallowed. Duplicate literal keys will raise a SyntaxError. Two keys that otherwise have the same value will raise a ValueError at runtime.

The following is the logical flow for matching a mapping pattern against a subject value:

  1. If the subject value is not a mapping ,the mapping pattern fails.
  2. If every key given in the mapping pattern is present in the subject mapping, and the pattern for each key matches the corresponding item of the subject mapping, the mapping pattern succeeds.
  3. If duplicate keys are detected in the mapping pattern, the pattern is considered invalid. A SyntaxError is raised for duplicate literal values; or a ValueError for named keys of the same value.

注解

Key-value pairs are matched using the two-argument form of the mapping subject’s get() method. Matched key-value pairs must already be present in the mapping, and not created on-the-fly via __missing__() or __getitem__().

In simple terms {KEY1: P1, KEY2: P2, ... } matches only if all the following happens:

  • check <subject> is a mapping
  • KEY1 in <subject>
  • P1 matches <subject>[KEY1]
  • … and so on for the corresponding KEY/pattern pair.
8.6.4.10. Class Patterns

A class pattern represents a class and its positional and keyword arguments (if any). Syntax:

class_pattern       ::=  name_or_attr "(" [pattern_arguments ","?] ")"
pattern_arguments   ::=  positional_patterns ["," keyword_patterns]
                         | keyword_patterns
positional_patterns ::=  ",".pattern+
keyword_patterns    ::=  ",".keyword_pattern+
keyword_pattern     ::=  NAME "=" pattern

The same keyword should not be repeated in class patterns.

The following is the logical flow for matching a mapping pattern against a subject value:

  1. If name_or_attr is not an instance of the builtin type , raise TypeError.

  2. If the subject value is not an instance of name_or_attr (tested via isinstance()), the class pattern fails.

  3. If no pattern arguments are present, the pattern succeeds. Otherwise, the subsequent steps depend on whether keyword or positional argument patterns are present.

    For a number of built-in types (specified below), a single positional subpattern is accepted which will match the entire subject; for these types keyword patterns also work as for other types.

    If only keyword patterns are present, they are processed as follows, one by one:

    I. The keyword is looked up as an attribute on the subject.

    • If this raises an exception other than AttributeError, the exception bubbles up.
    • If this raises AttributeError, the class pattern has failed.
    • Else, the subpattern associated with the keyword pattern is matched against the subject’s attribute value. If this fails, the class pattern fails; if this succeeds, the match proceeds to the next keyword.

    II. If all keyword patterns succeed, the class pattern succeeds.

    If any positional patterns are present, they are converted to keyword patterns using the __match_args__ attribute on the class name_or_attr before matching:

    I. The equivalent of getattr(cls, "__match_args__", ()) is called.

    • If this raises an exception, the exception bubbles up.
    • If the returned value is not a tuple, the conversion fails and TypeError is raised.
    • If there are more positional patterns than len(cls.__match_args__), TypeError is raised.
    • Otherwise, positional pattern i is converted to a keyword pattern using __match_args__[i] as the keyword. __match_args__[i] must be a string; if not TypeError is raised.
    • If there are duplicate keywords, TypeError is raised.

    II. Once all positional patterns have been converted to keyword patterns,

    the match proceeds as if there were only keyword patterns.

    For the following built-in types the handling of positional subpatterns is different:

    • bool
    • bytearray
    • bytes
    • dict
    • float
    • frozenset
    • int
    • list
    • set
    • str
    • tuple

    These classes accept a single positional argument, and the pattern there is matched against the whole object rather than an attribute. For example int(0|1) matches the value 0, but not the values 0.0 or False.

In simple terms CLS(P1, attr=P2) matches only if the following happens:

  • isinstance(<subject>, CLS)
  • convert P1 to a keyword pattern using CLS.__match_args__
  • For each keyword argument attr=P2:
    • hasattr(<subject>, "attr")
    • P2 matches <subject>.attr
  • … and so on for the corresponding keyword argument/pattern pair.

参见

  • PEP 634 — Structural Pattern Matching: Specification
  • PEP 636 — Structural Pattern Matching: Tutorial

8.7. 函数定义

函数定义就是对用户自定义函数的定义:

funcdef                   ::=  [decorators] "def" funcname "(" [parameter_list] ")"
                               ["->" expression] ":" suite
decorators                ::=  decorator+
decorator                 ::=  "@" assignment_expression NEWLINE
parameter_list            ::=  defparameter ("," defparameter)* "," "/" ["," [parameter_list_no_posonly]]
                                 | parameter_list_no_posonly
parameter_list_no_posonly ::=  defparameter ("," defparameter)* ["," [parameter_list_starargs]]
                               | parameter_list_starargs
parameter_list_starargs   ::=  "*" [parameter] ("," defparameter)* ["," ["**" parameter [","]]]
                               | "**" parameter [","]
parameter                 ::=  identifier [":" expression]
defparameter              ::=  parameter ["=" expression]
funcname                  ::=  identifier

函数定义是一条可执行语句。 它执行时会在当前局部命名空间中将函数名称绑定到一个函数对象(函数可执行代码的包装器)。 这个函数对象包含对当前全局命名空间的引用,作为函数被调用时所使用的全局命名空间。

函数定义并不会执行函数体;只有当函数被调用时才会执行此操作。

一个函数定义可以被一个或多个 decorator 表达式所包装。 当函数被定义时将在包含该函数定义的作用域中对装饰器表达式求值。 求值结果必须是一个可调用对象,它会以该函数对象作为唯一参数被发起调用。 其返回值将被绑定到函数名称而非函数对象。 多个装饰器会以嵌套方式被应用。 例如以下代码

@f1(arg)
@f2
def func(): pass

大致等价于

def func(): pass
func = f1(arg)(f2(func))

不同之处在于原始函数并不会被临时绑定到名称 func

在 3.9 版更改: 函数可使用任何有效的 assignment_expression 来装饰。 在之前版本中,此语法则更为受限,详情参见 PEP 614

当一个或多个 形参 具有 形参 = 表达式 这样的形式时,该函数就被称为具有“默认形参值”。 对于一个具有默认值的形参,其对应的 argument 可以在调用中被省略,在此情况下会用形参的默认值来替代。 如果一个形参具有默认值,后续所有在 “*“ 之前的形参也必须具有默认值 —- 这个句法限制并未在语法中明确表达。

Default parameter values are evaluated from left to right when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that the same “pre-computed” value is used for each call. This is especially important to understand when a default parameter value is a mutable object, such as a list or a dictionary: if the function modifies the object (e.g. by appending an item to a list), the default parameter value is in effect modified. This is generally not what was intended. A way around this is to use None as the default, and explicitly test for it in the body of the function, e.g.:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

函数调用总是会给形参列表中列出的所有形参赋值,或是用位置参数,或是用关键字参数,或是用默认值。 如果存在 “*identifier“ 这样的形式,它会被初始化为一个元组来接收任何额外的位置参数,默认为一个空元组。 如果存在 “**identifier“ 这样的形式,它会被初始化为一个新的有序映射来接收任何额外的关键字参数,默认为一个相同类型的空映射。 在 “*“ 或 “*identifier“ 之后的形参都是仅限关键字形参因而只能通过关键字参数传入。 在 “/“ 之前的形参都是仅限位置形参因而只能通过位置参数传入。

在 3.8 版更改: 可以使用 / 函数形参语法来标示仅限位置形参。 请参阅 PEP 570 了解详情。

形参可以带有 标注,其形式为在形参名称后加上 “: expression“。 任何形参都可以带有标注,甚至 *identifier**identifier 这样的形参也可以。 函数可以带有“返回”标注,其形式为在形参列表后加上 “-> expression“。 这些标注可以是任何有效的 Python 表达式。 标注的存在不会改变函数的语义。 标注值可以作为函数对象的 __annotations__ 属性中以对应形参名称为键的字典值被访问。 如果使用了 annotations import from __future__ 的方式,则标注会在运行时保存为字符串以启用延迟求值特性。 否则,它们会在执行函数定义时被求值。 在这种情况下,标注的求值顺序可能与它们在源代码中出现的顺序不同。

创建匿名函数(未绑定到一个名称的函数)以便立即在表达式中使用也是可能的。 这需要使用 lambda 表达式。 请注意 lambda 只是简单函数定义的一种简化写法;在 “def“ 语句中定义的函数也可以像用 lambda 表达式定义的函数一样被传递或赋值给其他名称。 “def“ 形式实际上更为强大,因为它允许执行多条语句和使用标注。

程序员注意事项: 函数属于一类对象。 在一个函数内部执行的 “def“ 语句会定义一个局部函数并可被返回或传递。 在嵌套函数中使用的自由变量可以访问包含该 def 语句的函数的局部变量。

参见

PEP 3107 - 函数标注

最初的函数标注规范说明。

PEP 484 - 类型提示

标注的标准含意定义:类型提示。

PEP 526 - 变量标注的语法

变量声明的类型提示功能,包括类变量和实例变量

PEP 563 - 延迟的标注求值

支持在运行时通过以字符串形式保存标注而非不是即求值来实现标注内部的向前引用。

8.8. 类定义

类定义就是对类对象的定义

classdef    ::=  [decorators] "class" classname [inheritance] ":" suite
inheritance ::=  "(" [argument_list] ")"
classname   ::=  identifier

类定义是一条可执行语句。 其中继承列表通常给出基类的列表 (进阶用法请参见 元类),列表中的每一项都应当被求值为一个允许子类的类对象。 没有继承列表的类默认继承自基类 object;因此,:

class Foo:    
    pass

等价于

class Foo(object):    
    pass

随后类体将在一个新的执行帧中被执行,使用新创建的局部命名空间和原有的全局命名空间。 (通常,类体主要包含函数定义。) 当类体结束执行时,其执行帧将被丢弃而其局部命名空间会被保存。 一个类对象随后会被创建,其基类使用给定的继承列表,属性字典使用保存的局部命名空间。 类名称将在原有的全局命名空间中绑定到该类对象。

在类体内定义的属性的顺序保存在新类的 __dict__ 中。 请注意此顺序的可靠性只限于类刚被创建时,并且只适用于使用定义语法所定义的类。

类的创建可使用 元类 进行重度定制。

类也可以被装饰:就像装饰函数一样,:

@f1(arg)
@f2
class Foo: pass

大致等价于

class Foo: pass
Foo = f1(arg)(f2(Foo))

装饰器表达式的求值规则与函数装饰器相同。 结果随后会被绑定到类名称。

在 3.9 版更改: 类可使用任何有效的 assignment_expression 来装饰。 在之前版本中,此语法则更为受限,详情参见 PEP 614

程序员注意事项: 在类定义内定义的变量是类属性;它们将被类实例所共享。 实例属性可通过 self.name = value 在方法中设定。 类和实例属性均可通过 “self.name“ 表示法来访问,当通过此方式访问时实例属性会隐藏同名的类属性。 类属性可被用作实例属性的默认值,但在此场景下使用可变值可能导致未预期的结果。 可以使用 描述器 来创建具有不同实现细节的实例变量。

参见

PEP 3115 - Python 3000 中的元类

将元类声明修改为当前语法的提议,以及关于如何构建带有元类的类的语义描述。

PEP 3129 - 类装饰器

增加类装饰器的提议。 函数和方法装饰器是在 PEP 318 中被引入的。

8.9. 协程

3.5 新版功能.

8.9.1. 协程函数定义

async_funcdef ::=  [decorators] "async" "def" funcname "(" [parameter_list] ")"
                   ["->" expression] ":" suite

Execution of Python coroutines can be suspended and resumed at many points (see coroutine). await expressions, async for and async with can only be used in the body of a coroutine function.

使用 async def 语法定义的函数总是为协程函数,即使它们不包含 awaitasync 关键字。

在协程函数体中使用 yield from 表达式将引发 SyntaxError

协程函数的例子:

async def func(param1, param2):
    do_stuff()
    await some_coroutine()

在 3.7 版更改: await and async are now keywords; previously they were only treated as such inside the body of a coroutine function.

8.9.2. async for 语句

async_for_stmt ::=  "async" for_stmt

asynchronous iterable 提供了 __aiter__ 方法,该方法会直接返回 asynchronous iterator,它可以在其 __anext__ 方法中调用异步代码。

async for 语句允许方便地对异步可迭代对象进行迭代。

以下代码:

async for TARGET in ITER:
    SUITE
else:
    SUITE2

在语义上等价于:

iter = (ITER)
iter = type(iter).__aiter__(iter)
running = True
while running:
    try:
        TARGET = await type(iter).__anext__(iter)
    except StopAsyncIteration:
        running = False
    else:
        SUITE
else:
    SUITE2

在协程函数体之外使用 async for 语句将引发 SyntaxError

8.9.3. async with 语句

async_with_stmt ::=  "async" with_stmt

asynchronous context manager 是一种 context manager,能够在其 enterexit 方法中暂停执行。

以下代码:

async with EXPRESSION as TARGET:    
    SUITE

在语义上等价于:

manager = (EXPRESSION)
aenter = type(manager).__aenter__
aexit = type(manager).__aexit__
value = await aenter(manager)
hit_except = False
try:
    TARGET = value
    SUITE
except:
    hit_except = True
    if not await aexit(manager, *sys.exc_info()):
        raise
finally:
    if not hit_except:
        await aexit(manager, None, None, None)

另请参阅 __aenter__()__aexit__() 了解详情。

在协程函数体之外使用 async with 语句将引发 SyntaxError

参见

PEP 492 - 使用 async 和 await 语法实现协程

将协程作为 Python 中的一个正式的单独概念,并增加相应的支持语法。

注:

  • 异常会被传播给发起调用栈,除非存在一个 finally 子句正好引发了另一个异常。 新引发的异常将导致旧异常的丢失。

  • In pattern matching, a sequence is defined as one of the following:

    • a class that inherits from collections.abc.Sequence
    • a Python class that has been registered as collections.abc.Sequence
    • a builtin class that has its (CPython) Py_TPFLAGS_SEQUENCE bit set
    • a class that inherits from any of the above

    The following standard library classes are sequences:

    • array.array
    • collections.deque
    • list
    • memoryview
    • range
    • tuple

    注解

    Subject values of type str, bytes, and bytearray do not match sequence patterns.

  • In pattern matching, a mapping is defined as one of the following:

    • a class that inherits from collections.abc.Mapping
    • a Python class that has been registered as collections.abc.Mapping
    • a builtin class that has its (CPython) Py_TPFLAGS_MAPPING bit set
    • a class that inherits from any of the above

    The standard library classes dict and types.MappingProxyType are mappings.

  • 作为函数体的第一条语句出现的字符串字面值会被转换为函数的 __doc__ 属性,也就是该函数的 docstring。

  • 作为类体的第一条语句出现的字符串字面值会被转换为命名空间的 __doc__ 条目,也就是该类的 docstring。

9. 顶级组件

Python 解释器可以从多种源获得输入:作为标准输入或程序参数传入的脚本,以交互方式键入的语句,导入的模块源文件等等。 这一章将给出在这些情况下所用的语法。

9.1. 完整的 Python 程序

虽然语言规范描述不必规定如何发起调用语言解释器,但对完整的 Python 程序加以说明还是很有用的。 一个完整的 Python 程序会在最小初始化环境中被执行:所有内置和标准模块均为可用,但均处于未初始化状态,只有 sys (各种系统服务), builtins (内置函数、异常以及 None) 和 __main__ 除外。 最后一个模块用于为完整程序的执行提供局部和全局命名空间。

适用于一个完整 Python 程序的语法即下节所描述的文件输入。

解释器也可以通过交互模式被发起调用;在此情况下,它并不读取和执行一个完整程序,而是每次读取和执行一条语句(可能为复合语句)。 此时的初始环境与一个完整程序的相同;每条语句会在 __main__ 的命名空间中被执行。

一个完整程序可通过三种形式被传递给解释器:使用 -c 字符串 命令行选项,使用一个文件作为第一个命令行参数,或者使用标准输入。 如果文件或标准输入是一个 tty 设置,解释器会进入交互模式;否则的话,它会将文件当作一个完整程序来执行。

9.2. 文件输入

所有从非交互式文件读取的输入都具有相同的形式:

file_input ::=  (NEWLINE | statement)*

此语法用于下列几种情况:

  • 解析一个完整 Python 程序时(从文件或字符串);
  • 解析一个模块时;
  • 解析一个传递给 exec() 函数的字符串时;

9.3. 交互式输入

交互模式下的输入使用以下语法进行解析:

interactive_input ::=  [stmt_list] NEWLINE | compound_stmt NEWLINE

请注意在交互模式下一条(最高层级)复合语句必须带有一个空行;这对于帮助解析器确定输入的结束是必须的。

9.4. 表达式输入

eval() 被用于表达式输入。 它会忽略开头的空白。 传递给 eval() 的字符串参数必须具有以下形式:

eval_input ::=  expression_list NEWLINE*

10. 完整的语法规范

这是完整的 Python 语法规范,直接提取自用于生成 CPython 解析器的语法 。 这里显示的版本省略了有关代码生成和错误恢复的细节。

该标记法是 EBNF 和 PEG 的混合体。 特别地,& 后跟一个符号、形符或带括号的分组来表示正向前视(即要求匹配但不会消耗掉),而 ! 表示负向前视(即 不要求 匹配)。 我们使用 | 分隔符来表示 PEG 的“有序选择” (在传统 PEG 语法中写作 /)。 请参阅 PEP 617 了解有关该语法规则的更多细节。

# PEG grammar for Python
file: [statements] ENDMARKER 
interactive: statement_newline 
eval: expressions NEWLINE* ENDMARKER 
func_type: '(' [type_expressions] ')' '->' expression NEWLINE* ENDMARKER 
fstring: star_expressions
# type_expressions allow */** but ignore them
type_expressions:
    | ','.expression+ ',' '*' expression ',' '**' expression 
    | ','.expression+ ',' '*' expression 
    | ','.expression+ ',' '**' expression 
    | '*' expression ',' '**' expression 
    | '*' expression 
    | '**' expression 
    | ','.expression+ 
statements: statement+ 
statement: compound_stmt  | simple_stmts 
statement_newline:
    | compound_stmt NEWLINE 
    | simple_stmts
    | NEWLINE 
    | ENDMARKER 
simple_stmts:
    | simple_stmt !';' NEWLINE  # Not needed, there for speedup
    | ';'.simple_stmt+ [';'] NEWLINE 
# NOTE: assignment MUST precede expression, else parsing a simple assignment
# will throw a SyntaxError.
simple_stmt:
    | assignment
    | star_expressions 
    | return_stmt
    | import_stmt
    | raise_stmt
    | 'pass' 
    | del_stmt
    | yield_stmt
    | assert_stmt
    | 'break' 
    | 'continue' 
    | global_stmt
    | nonlocal_stmt
compound_stmt:
    | function_def
    | if_stmt
    | class_def
    | with_stmt
    | for_stmt
    | try_stmt
    | while_stmt
    | match_stmt
# NOTE: annotated_rhs may start with 'yield'; yield_expr must start with 'yield'
assignment:
    | NAME ':' expression ['=' annotated_rhs ] 
    | ('(' single_target ')' 
         | single_subscript_attribute_target) ':' expression ['=' annotated_rhs ] 
    | (star_targets '=' )+ (yield_expr | star_expressions) !'=' [TYPE_COMMENT] 
    | single_target augassign ~ (yield_expr | star_expressions) 
augassign:
    | '+=' 
    | '-=' 
    | '*=' 
    | '@=' 
    | '/=' 
    | '%=' 
    | '&=' 
    | '|=' 
    | '^=' 
    | '<<=' 
    | '>>=' 
    | '**=' 
    | '//=' 
global_stmt: 'global' ','.NAME+ 
nonlocal_stmt: 'nonlocal' ','.NAME+ 
yield_stmt: yield_expr 
assert_stmt: 'assert' expression [',' expression ] 
del_stmt:
    | 'del' del_targets &(';' | NEWLINE) 
import_stmt: import_name | import_from
import_name: 'import' dotted_as_names 
# note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS
import_from:
    | 'from' ('.' | '...')* dotted_name 'import' import_from_targets 
    | 'from' ('.' | '...')+ 'import' import_from_targets 
import_from_targets:
    | '(' import_from_as_names [','] ')' 
    | import_from_as_names !','
    | '*' 
import_from_as_names:
    | ','.import_from_as_name+ 
import_from_as_name:
    | NAME ['as' NAME ] 
dotted_as_names:
    | ','.dotted_as_name+ 
dotted_as_name:
    | dotted_name ['as' NAME ] 
dotted_name:
    | dotted_name '.' NAME 
    | NAME
if_stmt:
    | 'if' named_expression ':' block elif_stmt 
    | 'if' named_expression ':' block [else_block] 
elif_stmt:
    | 'elif' named_expression ':' block elif_stmt 
    | 'elif' named_expression ':' block [else_block] 
else_block:
    | 'else' ':' block 
while_stmt:
    | 'while' named_expression ':' block [else_block] 
for_stmt:
    | 'for' star_targets 'in' ~ star_expressions ':' [TYPE_COMMENT] block [else_block] 
    | ASYNC 'for' star_targets 'in' ~ star_expressions ':' [TYPE_COMMENT] block [else_block] 
with_stmt:
    | 'with' '(' ','.with_item+ ','? ')' ':' block 
    | 'with' ','.with_item+ ':' [TYPE_COMMENT] block 
    | ASYNC 'with' '(' ','.with_item+ ','? ')' ':' block 
    | ASYNC 'with' ','.with_item+ ':' [TYPE_COMMENT] block 
with_item:
    | expression 'as' star_target &(',' | ')' | ':') 
    | expression 
try_stmt:
    | 'try' ':' block finally_block 
    | 'try' ':' block except_block+ [else_block] [finally_block] 
except_block:
    | 'except' expression ['as' NAME ] ':' block 
    | 'except' ':' block 
finally_block:
    | 'finally' ':' block 
match_stmt:
    | "match" subject_expr ':' NEWLINE INDENT case_block+ DEDENT 
subject_expr:
    | star_named_expression ',' star_named_expressions? 
    | named_expression
case_block:
    | "case" patterns guard? ':' block 
guard: 'if' named_expression 
patterns:
    | open_sequence_pattern 
    | pattern
pattern:
    | as_pattern
    | or_pattern
as_pattern:
    | or_pattern 'as' pattern_capture_target 
or_pattern:
    | '|'.closed_pattern+ 
closed_pattern:
    | literal_pattern
    | capture_pattern
    | wildcard_pattern
    | value_pattern
    | group_pattern
    | sequence_pattern
    | mapping_pattern
    | class_pattern
# Literal patterns are used for equality and identity constraints
literal_pattern:
    | signed_number !('+' | '-') 
    | complex_number 
    | strings 
    | 'None' 
    | 'True' 
    | 'False' 
# Literal expressions are used to restrict permitted mapping pattern keys
literal_expr:
    | signed_number !('+' | '-')
    | complex_number
    | strings
    | 'None' 
    | 'True' 
    | 'False' 
complex_number:
    | signed_real_number '+' imaginary_number 
    | signed_real_number '-' imaginary_number  
signed_number:
    | NUMBER
    | '-' NUMBER 
signed_real_number:
    | real_number
    | '-' real_number 
real_number:
    | NUMBER 
imaginary_number:
    | NUMBER 
capture_pattern:
    | pattern_capture_target 
pattern_capture_target:
    | !"_" NAME !('.' | '(' | '=') 
wildcard_pattern:
    | "_" 
value_pattern:
    | attr !('.' | '(' | '=') 
attr:
    | name_or_attr '.' NAME 
name_or_attr:
    | attr
    | NAME
group_pattern:
    | '(' pattern ')' 
sequence_pattern:
    | '[' maybe_sequence_pattern? ']' 
    | '(' open_sequence_pattern? ')' 
open_sequence_pattern:
    | maybe_star_pattern ',' maybe_sequence_pattern? 
maybe_sequence_pattern:
    | ','.maybe_star_pattern+ ','? 
maybe_star_pattern:
    | star_pattern
    | pattern
star_pattern:
    | '*' pattern_capture_target 
    | '*' wildcard_pattern 
mapping_pattern:
    | '{' '}' 
    | '{' double_star_pattern ','? '}' 
    | '{' items_pattern ',' double_star_pattern ','? '}' 
    | '{' items_pattern ','? '}' 
items_pattern:
    | ','.key_value_pattern+
key_value_pattern:
    | (literal_expr | attr) ':' pattern 
double_star_pattern:
    | '**' pattern_capture_target 
class_pattern:
    | name_or_attr '(' ')' 
    | name_or_attr '(' positional_patterns ','? ')' 
    | name_or_attr '(' keyword_patterns ','? ')' 
    | name_or_attr '(' positional_patterns ',' keyword_patterns ','? ')' 
positional_patterns:
    | ','.pattern+ 
keyword_patterns:
    | ','.keyword_pattern+
keyword_pattern:
    | NAME '=' pattern 
return_stmt:
    | 'return' [star_expressions] 
raise_stmt:
    | 'raise' expression ['from' expression ] 
    | 'raise' 
function_def:
    | decorators function_def_raw 
    | function_def_raw
function_def_raw:
    | 'def' NAME '(' [params] ')' ['->' expression ] ':' [func_type_comment] block 
    | ASYNC 'def' NAME '(' [params] ')' ['->' expression ] ':' [func_type_comment] block 
func_type_comment:
    | NEWLINE TYPE_COMMENT &(NEWLINE INDENT)   # Must be followed by indented block
    | TYPE_COMMENT
params:
    | parameters
parameters:
    | slash_no_default param_no_default* param_with_default* [star_etc] 
    | slash_with_default param_with_default* [star_etc] 
    | param_no_default+ param_with_default* [star_etc] 
    | param_with_default+ [star_etc] 
    | star_etc 
# Some duplication here because we can't write (',' | &')'),
# which is because we don't support empty alternatives (yet).
#
slash_no_default:
    | param_no_default+ '/' ',' 
    | param_no_default+ '/' &')' 
slash_with_default:
    | param_no_default* param_with_default+ '/' ',' 
    | param_no_default* param_with_default+ '/' &')' 
star_etc:
    | '*' param_no_default param_maybe_default* [kwds] 
    | '*' ',' param_maybe_default+ [kwds] 
    | kwds 
kwds: '**' param_no_default 
# One parameter.  This *includes* a following comma and type comment.
#
# There are three styles:
# - No default
# - With default
# - Maybe with default
#
# There are two alternative forms of each, to deal with type comments:
# - Ends in a comma followed by an optional type comment
# - No comma, optional type comment, must be followed by close paren
# The latter form is for a final parameter without trailing comma.
#
param_no_default:
    | param ',' TYPE_COMMENT? 
    | param TYPE_COMMENT? &')' 
param_with_default:
    | param default ',' TYPE_COMMENT? 
    | param default TYPE_COMMENT? &')' 
param_maybe_default:
    | param default? ',' TYPE_COMMENT? 
    | param default? TYPE_COMMENT? &')' 
param: NAME annotation? 
annotation: ':' expression 
default: '=' expression 
decorators: ('@' named_expression NEWLINE )+ 
class_def:
    | decorators class_def_raw 
    | class_def_raw
class_def_raw:
    | 'class' NAME ['(' [arguments] ')' ] ':' block 
block:
    | NEWLINE INDENT statements DEDENT 
    | simple_stmts
star_expressions:
    | star_expression (',' star_expression )+ [','] 
    | star_expression ',' 
    | star_expression
star_expression:
    | '*' bitwise_or 
    | expression
star_named_expressions: ','.star_named_expression+ [','] 
star_named_expression:
    | '*' bitwise_or 
    | named_expression
assigment_expression:
    | NAME ':=' ~ expression 
named_expression:
    | assigment_expression
    | expression !':='
annotated_rhs: yield_expr | star_expressions
expressions:
    | expression (',' expression )+ [','] 
    | expression ',' 
    | expression
expression:
    | disjunction 'if' disjunction 'else' expression 
    | disjunction
    | lambdef
lambdef:
    | 'lambda' [lambda_params] ':' expression 
lambda_params:
    | lambda_parameters
# lambda_parameters etc. duplicates parameters but without annotations
# or type comments, and if there's no comma after a parameter, we expect
# a colon, not a close parenthesis.  (For more, see parameters above.)
#
lambda_parameters:
    | lambda_slash_no_default lambda_param_no_default* lambda_param_with_default* [lambda_star_etc] 
    | lambda_slash_with_default lambda_param_with_default* [lambda_star_etc] 
    | lambda_param_no_default+ lambda_param_with_default* [lambda_star_etc] 
    | lambda_param_with_default+ [lambda_star_etc] 
    | lambda_star_etc 
lambda_slash_no_default:
    | lambda_param_no_default+ '/' ',' 
    | lambda_param_no_default+ '/' &':' 
lambda_slash_with_default:
    | lambda_param_no_default* lambda_param_with_default+ '/' ',' 
    | lambda_param_no_default* lambda_param_with_default+ '/' &':' 
lambda_star_etc:
    | '*' lambda_param_no_default lambda_param_maybe_default* [lambda_kwds] 
    | '*' ',' lambda_param_maybe_default+ [lambda_kwds] 
    | lambda_kwds 
lambda_kwds: '**' lambda_param_no_default 
lambda_param_no_default:
    | lambda_param ',' 
    | lambda_param &':' 
lambda_param_with_default:
    | lambda_param default ',' 
    | lambda_param default &':' 
lambda_param_maybe_default:
    | lambda_param default? ',' 
    | lambda_param default? &':' 
lambda_param: NAME 
disjunction:
    | conjunction ('or' conjunction )+ 
    | conjunction
conjunction:
    | inversion ('and' inversion )+ 
    | inversion
inversion:
    | 'not' inversion 
    | comparison
comparison:
    | bitwise_or compare_op_bitwise_or_pair+ 
    | bitwise_or
compare_op_bitwise_or_pair:
    | eq_bitwise_or
    | noteq_bitwise_or
    | lte_bitwise_or
    | lt_bitwise_or
    | gte_bitwise_or
    | gt_bitwise_or
    | notin_bitwise_or
    | in_bitwise_or
    | isnot_bitwise_or
    | is_bitwise_or
eq_bitwise_or: '==' bitwise_or 
noteq_bitwise_or:
    | ('!=' ) bitwise_or 
lte_bitwise_or: '<=' bitwise_or 
lt_bitwise_or: '<' bitwise_or 
gte_bitwise_or: '>=' bitwise_or 
gt_bitwise_or: '>' bitwise_or 
notin_bitwise_or: 'not' 'in' bitwise_or 
in_bitwise_or: 'in' bitwise_or 
isnot_bitwise_or: 'is' 'not' bitwise_or 
is_bitwise_or: 'is' bitwise_or 
bitwise_or:
    | bitwise_or '|' bitwise_xor 
    | bitwise_xor
bitwise_xor:
    | bitwise_xor '^' bitwise_and 
    | bitwise_and
bitwise_and:
    | bitwise_and '&' shift_expr 
    | shift_expr
shift_expr:
    | shift_expr '<<' sum 
    | shift_expr '>>' sum 
    | sum
sum:
    | sum '+' term 
    | sum '-' term 
    | term
term:
    | term '*' factor 
    | term '/' factor 
    | term '//' factor 
    | term '%' factor 
    | term '@' factor 
    | factor
factor:
    | '+' factor 
    | '-' factor 
    | '~' factor 
    | power
power:
    | await_primary '**' factor 
    | await_primary
await_primary:
    | AWAIT primary 
    | primary
primary:
    | invalid_primary  # must be before 'primay genexp' because of invalid_genexp
    | primary '.' NAME 
    | primary genexp 
    | primary '(' [arguments] ')' 
    | primary '[' slices ']' 
    | atom
slices:
    | slice !',' 
    | ','.slice+ [','] 
slice:
    | [expression] ':' [expression] [':' [expression] ] 
    | named_expression 
atom:
    | NAME
    | 'True' 
    | 'False' 
    | 'None' 
    | strings
    | NUMBER
    | (tuple | group | genexp)
    | (list | listcomp)
    | (dict | set | dictcomp | setcomp)
    | '...' 
strings: STRING+ 
list:
    | '[' [star_named_expressions] ']' 
listcomp:
    | '[' named_expression for_if_clauses ']' 
tuple:
    | '(' [star_named_expression ',' [star_named_expressions]  ] ')' 
group:
    | '(' (yield_expr | named_expression) ')' 
genexp:
    | '(' ( assigment_expression | expression !':=') for_if_clauses ')' 
set: '{' star_named_expressions '}' 
setcomp:
    | '{' named_expression for_if_clauses '}' 
dict:
    | '{' [double_starred_kvpairs] '}' 
    | '{' invalid_double_starred_kvpairs '}'
dictcomp:
    | '{' kvpair for_if_clauses '}' 
double_starred_kvpairs: ','.double_starred_kvpair+ [','] 
double_starred_kvpair:
    | '**' bitwise_or 
    | kvpair
kvpair: expression ':' expression 
for_if_clauses:
    | for_if_clause+ 
for_if_clause:
    | ASYNC 'for' star_targets 'in' ~ disjunction ('if' disjunction )* 
    | 'for' star_targets 'in' ~ disjunction ('if' disjunction )* 
yield_expr:
    | 'yield' 'from' expression 
    | 'yield' [star_expressions] 
arguments:
    | args [','] &')' 
args:
    | ','.(starred_expression | ( assigment_expression | expression !':=') !'=')+ [',' kwargs ] 
    | kwargs 
kwargs:
    | ','.kwarg_or_starred+ ',' ','.kwarg_or_double_starred+ 
    | ','.kwarg_or_starred+
    | ','.kwarg_or_double_starred+
starred_expression:
    | '*' expression 
kwarg_or_starred:
    | NAME '=' expression 
    | starred_expression 
kwarg_or_double_starred:
    | NAME '=' expression 
    | '**' expression 
# NOTE: star_targets may contain *bitwise_or, targets may not.
star_targets:
    | star_target !',' 
    | star_target (',' star_target )* [','] 
star_targets_list_seq: ','.star_target+ [','] 
star_targets_tuple_seq:
    | star_target (',' star_target )+ [','] 
    | star_target ',' 
star_target:
    | '*' (!'*' star_target) 
    | target_with_star_atom
target_with_star_atom:
    | t_primary '.' NAME !t_lookahead 
    | t_primary '[' slices ']' !t_lookahead 
    | star_atom
star_atom:
    | NAME 
    | '(' target_with_star_atom ')' 
    | '(' [star_targets_tuple_seq] ')' 
    | '[' [star_targets_list_seq] ']' 
single_target:
    | single_subscript_attribute_target
    | NAME 
    | '(' single_target ')' 
single_subscript_attribute_target:
    | t_primary '.' NAME !t_lookahead 
    | t_primary '[' slices ']' !t_lookahead 
del_targets: ','.del_target+ [','] 
del_target:
    | t_primary '.' NAME !t_lookahead 
    | t_primary '[' slices ']' !t_lookahead 
    | del_t_atom
del_t_atom:
    | NAME 
    | '(' del_target ')' 
    | '(' [del_targets] ')' 
    | '[' [del_targets] ']' 
t_primary:
    | t_primary '.' NAME &t_lookahead 
    | t_primary '[' slices ']' &t_lookahead 
    | t_primary genexp &t_lookahead 
    | t_primary '(' [arguments] ')' &t_lookahead 
    | atom &t_lookahead 
t_lookahead: '(' | '[' | '.'

分发与安装 Python 模块

分发

作为一个流行的开源开发项目,Python拥有一个活跃的贡献者和用户支持社区,这些社区也可以让他们的软件可供其他Python开发人员在开源许可条款下使用。

这允许Python用户有效地共享和协作,从其他人已经创建的解决方案中受益于常见(有时甚至是罕见的)问题,以及可以提供他们自己的解决方案。

关键术语

  • the Python Package Index is a public repository of open source licensed packages made available for use by other Python users
  • Python Packaging Authority 是负责标准打包工具以及相关元数据和文件格式标准维护与改进的开发人员和文档作者团队。 他们基于 GitHubBitbucket 这两个平台维护着各种工具、文档和问题追踪系统。
  • distutils 是 1998 年首次添加到 Python 标准库的原始构建和分发系统。 虽然直接使用 distutils 正在逐步淘汰,但它仍然为当前的打包和分发基础架构奠定了基础它不仅仍然是标准库的一部分,而且它的名称还以其他方式存在(例如用于协调 Python 打包标准开发的邮件列表的名称)。
  • setuptools (在很大程度上)是作为 distutils 的取代者,于 2004 年首次发布。 它对未经修改的 distutils 工具最重要的补充是能够声明对其他包的依赖。 目前它被推荐用来替代 distutils,其更新更为频繁,在更为多样的 Python 版本之上为最新的打包标准提供持续支持。
  • wheel (在此上下文中)是一个将 bdist_wheel 命令添加到 distutils/setuptools 的项目。这产生了一个跨平台的二进制打包格式(称为“轮子”或“轮子文件”,并在 PEP 427 中定义),它允许在系统上安装Python库,甚至包括二进制扩展的库,而不需在本地进行构建。

开源许可与协作

在世界上大多数地方,软件自动受版权保护。这意味着其他开发人员需要明确的权限来复制,使用,修改和重新分发软件。

开源许可是一种以相对一致的方式明确授予此类权限的方式,允许开发人员通过为各种问题免费提供通用解决方案来有效地共享和协作。这使得许多开发人员可以将更多时间用于关注他们特定情况相对独特的问题。

Python提供的分发工具旨在使开发人员选择开源时,可以合理地直接将其自己的贡献回馈到该公共软件池。

无论该软件是否作为开源软件发布,相同的分发工具也可用于在组织内分发软件。

安装相关工具

标准库不包括支持现代Python打包标准的构建工具,因为核心开发团队已经发现,即使在旧版本的Python上,使用一致工作的标准工具也很重要。

可以通过在命令行调用 pip 模块来安装当前推荐的构建和分发工具:

python -m pip install setuptools wheel twine

注解

For POSIX users (including macOS and Linux users), these instructions assume the use of a virtual environment.

对于Windows用户,这些说明假定在安装Python时选择了调整系统PATH环境变量的选项。

安装

作为一个流行的开源开发项目,Python拥有一个活跃的贡献者和用户支持社区,这些社区也可以让他们的软件可供其他Python开发人员在开源许可条款下使用。

这允许Python用户有效地共享和协作,从其他人已经创建的解决方案中受益于常见(有时甚至是罕见的)问题,以及可以提供他们自己的解决方案。

关键术语

  • pip 是首选的安装程序。从Python 3.4开始,它默认包含在Python二进制安装程序中。
  • virtual environment 是一种半隔离的 Python 环境,允许为特定的应用安装各自的包,而不是安装到整个系统。
  • venv 是创建虚拟环境的标准工具,从 Python 3.3 开始成为 Python 的组成部分。 从 Python 3.4 开始,它会默认安装 pip 到所创建的全部虚拟环境。
  • virtualenvvenv 的第三方替代(及其前身)。 它允许在 Python 3.4 之前的版本中使用虚拟环境,那些版本或是完全不提供 venv,或是不会自动安装 pip 到所创建的虚拟环境。
  • The Python Package Index is a public repository of open source licensed packages made available for use by other Python users.
  • Python Packaging Authority 是负责标准打包工具以及相关元数据和文件格式标准维护与改进的开发人员和文档作者团队。 他们基于 GitHubBitbucket 这两个平台维护着各种工具、文档和问题追踪系统。
  • distutils 是最初的构建和分发系统,于 1998 年首次加入 Python 标准库。 虽然直接使用 distutils 的方式已被淘汰,它仍然是当前打包和分发架构的基础,而且它不仅仍然是标准库的一部分,这个名称还以其他方式存在(例如用于协调 Python 打包标准开发流程的邮件列表就以此命名)。

在 3.5 版更改: 现在推荐使用 venv 来创建虚拟环境。

基本使用

标准打包工具完全是针对命令行使用方式来设计的。

The following command will install the latest version of a module and its dependencies from the Python Package Index:

python -m pip install SomePackage

注解

For POSIX users (including macOS and Linux users), the examples in this guide assume the use of a virtual environment.

对于 Windows 用户,本指南中的示例假定在安装 Python 时选择了修改系统 PATH 环境变量。

在命令行中指定一个准确或最小版本也是可以的。 当使用比较运算符例如 >, < 或其他某些可以被终端所解析的特殊字符时,包名称与版本号应当用双引号括起来:

python -m pip install SomePackage==1.0.4    # specific version
python -m pip install "SomePackage>=1.0.4"  # minimum version

通常,如果一个匹配的模块已安装,尝试再次安装将不会有任何效果。 要升级现有模块必须显式地发出请求:

python -m pip install --upgrade SomePackage

虚拟环境的创建可使用 venv 模块来完成。 向已激活虚拟环境安装软件包可使用上文所介绍的命令。

我应如何 …?

这是一些常见任务的快速解答或相关链接。

… 在 Python 3.4 之前的 Python 版本中安装 pip

Python 捆绑 pip 是从 Python 3.4 才开始的。 对于更早的版本,pip 需要“引导安装”,具体说明参见 Python 软件包用户指南。

… 只为当前用户安装软件包?

--user 选项传入 python -m pip install 将只为当前用户而非为系统中的所有用户安装软件包。

… 安装科学计算类 Python 软件包?

许多科学计算类 Python 软件包都有复杂的二进制编译文件依赖,直接使用 pip 安装目前并不太容易。 在当前情况下,通过 其他方式 而非尝试用 pip 安装这些软件包对用户来说通常会更容易。

… 使用并行安装的多个 Python 版本?

On Linux, macOS, and other POSIX systems, use the versioned Python commands in combination with the -m switch to run the appropriate copy of pip:

python2   -m pip install SomePackage  # default Python 2
python2.7 -m pip install SomePackage  # specifically Python 2.7
python3   -m pip install SomePackage  # default Python 3
python3.4 -m pip install SomePackage  # specifically Python 3.4

也可以使用带特定版本号的 pip 命令。

在 Windows 中,使用 py Python 启动器命令配合 -m 开关选项:

py -2   -m pip install SomePackage  # default Python 2
py -2.7 -m pip install SomePackage  # specifically Python 2.7
py -3   -m pip install SomePackage  # default Python 3
py -3.4 -m pip install SomePackage  # specifically Python 3.4

常见的安装问题

在 Linux 的系统 Python 版本上安装

Linux 系统通常会将某个 Python 版本作为发行版的一部分包含在内。 将软件包安装到这个 Python 版本上需要系统 root 权限,并可能会干扰到系统包管理器和其他系统组件的运作,如果这些组件在使用 pip 时被意外升级的话。

在这样的系统上,通过 pip 安装软件包通常最好是使用虚拟环境或分用户安装。

未安装 pip

默认情况下可能未安装 pip,一种可选解决方案是:

python -m ensurepip --default-pip

还有其他资源可用来 安装 pip

安装二进制编译扩展

Python 通常非常依赖基于源代码的发布方式,也就是期望最终用户在安装过程中使用源码来编译生成扩展模块。

With the introduction of support for the binary wheel format, and the ability to publish wheels for at least Windows and macOS through the Python Package Index, this problem is expected to diminish over time, as users are more regularly able to install pre-built extensions rather than needing to build them themselves.

某些用来安装 科学计算类软件包 的解决方案对于尚未提供预编译 wheel 文件的那些扩展模块来说,也有助于用户在无需进行本机编译的情况下获取二进制码扩展模块。

扩展和嵌入 Python 解释器

本指南仅介绍了作为此 CPython 版本的一部分提供的创建扩展的基本工具。 第三方工具,如 CythoncffiSWIGNumba 提供了更简单和更复杂的方法来为 Python 创建 C 和 C ++ 扩展。

1. 使用 C 或 C++ 扩展 Python

如果你会用 C,添加新的 Python 内置模块会很简单。以下两件不能用 Python 直接做的事,可以通过 extension modules 来实现:实现新的内置对象类型;调用 C 的库函数和系统调用。

为了支持扩展,Python API(应用程序编程接口)定义了一系列函数、宏和变量,可以访问 Python 运行时系统的大部分内容。Python 的 API 可以通过在一个 C 源文件中引用 "Python.h" 头文件来使用。

扩展模块的编写方式取决与你的目的以及系统设置。

注解

C扩展接口特指CPython,扩展模块无法在其他Python实现上工作。在大多数情况下,应该避免写C扩展,来保持可移植性。举个例子,如果你的用例调用了C库或系统调用,你应该考虑使用 ctypes 模块或 cffi 库,而不是自己写C代码。这些模块允许你写Python代码来接口C代码,而且可移植性更好。不知为何编译失败了。

1.1. 一个简单的例子

让我们创建一个扩展模块 spam (Monty Python 粉丝最喜欢的食物…) 并且想要创建对应 C 库函数 system() 的 Python 接口。 这个函数接受一个以 null 结尾的字符串参数并返回一个整数。 我们希望可以在 Python 中以如下方式调用此函数:

>>> import spam
>>> status = spam.system("ls -l")

首先创建一个 spammodule.c 文件。(传统上,如果一个模块叫 spam,则对应实现它的 C 文件叫 spammodule.c;如果这个模块名字非常长,比如 spammify,则这个模块的文件可以直接叫 spammify.c。)

文件中开始的两行是:

#define PY_SSIZE_T_CLEAN
#include <Python.h>

这会导入 Python API(如果你喜欢,你可以在这里添加描述模块目标和版权信息的注释)。

注解

由于 Python 可能会定义一些能在某些系统上影响标准头文件的预处理器定义,因此在包含任何标准头文件之前,你 必须 先包含 Python.h

推荐总是在 Python.h 前定义 PY_SSIZE_T_CLEAN

所有在 Python.h 中定义的用户可见的符号都具有 PyPY 前缀,已在标准头文件中定义的那些除外。 考虑到便利性,也由于其在 Python 解释器中被广泛使用,"Python.h" 还包含了一些标准头文件: <stdio.h><string.h><errno.h><stdlib.h>。 如果后面的头文件在你的系统上不存在,它还会直接声明函数 malloc()free()realloc()

下面添加C函数到扩展模块,当调用 spam.system(string) 时会做出响应,(我们稍后会看到调用):

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;
    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = system(command);
    return PyLong_FromLong(sts);
}

有个直接翻译参数列表的方法(举个例子,单独的 "ls -l" )到要传递给C函数的参数。C函数总是有两个参数,通常名字是 selfargs

对模块级函数, self 参数指向模块对象;对于方法则指向对象实例。

args 参数是指向一个 Python 的 tuple 对象的指针,其中包含参数。 每个 tuple 项对应一个调用参数。 这些参数也全都是 Python 对象 —- 要在我们的 C 函数中使用它们就需要先将其转换为 C 值。 Python API 中的函数 PyArg_ParseTuple() 会检查参数类型并将其转换为 C 值。 它使用模板字符串确定需要的参数类型以及存储被转换的值的 C 变量类型。 细节将稍后说明。

PyArg_ParseTuple() 在所有参数都有正确类型且组成部分按顺序放在传递进来的地址里时,返回真(非零)。其在传入无效参数时返回假(零)。在后续例子里,还会抛出特定异常,使得调用的函数可以理解返回 NULL (也就是例子里所见)。

1.2. 关于错误和异常

在 Python 解释器中有一个重要的惯例:当一个函数出错时,它应当设置异常条件并返回错误值(通常为 NULL 指针)。 异常存储于解释器内部的静态全局变量中;如此变量为 NULL 表示未发生异常。 还有第二个全局变量用于保存异常的“关联值”(即 raise 的第二个参数)。 第三个变量包含 Python 代码产生错误情况下的栈回溯信息。 这三个变量是 Python 中 sys.exc_info() 的结果在 C 中的对应物。 了解它们对于理解错误的传递方式是非常重要的。

Python API中定义了一些函数来设置这些变量。

最常用的就是 PyErr_SetString()。 其参数是异常对象和 C 字符串。 异常对象一般是像 PyExc_ZeroDivisionError 这样的预定义对象。 C 字符串指明异常原因,并被转换为一个 Python 字符串对象存储为异常的“关联值”。

另一个有用的函数是 PyErr_SetFromErrno() ,仅接受一个异常对象,异常描述包含在全局变量 errno 中。最通用的函数还是 PyErr_SetObject() ,包含两个参数,分别为异常对象和异常描述。你不需要使用 Py_INCREF() 来增加传递到其他函数的参数对象的引用计数。

你可以通过 PyErr_Occurred() 在不造成破坏的情况下检测是否设置了异常。 这将返回当前异常对象,或者如果未发生异常则返回 NULL。 你通常不需要调用 PyErr_Occurred() 来查看函数调用中是否发生了错误,因为你应该能从返回值中看出来。

当一个函数 f 调用另一个函数 g 时检测到后者出错了,f 应当自己返回一个错误值 (通常为 NULL-1)。 它 不应该 调用某个 PyErr_* 函数 —- 这类函数已经被 g 调用过了。 f 的调用者随后也应当返回一个错误来提示 它的 调用者,同样 不应该 调用 PyErr_*,依此类推 —- 错误的最详细原因已经由首先检测到它的函数报告了。 一旦这个错误到达 Python 解释器的主循环,它会中止当前执行的 Python 代码并尝试找出由 Python 程序员所指定的异常处理程序。

(在某些情况下,当模块确实能够通过调用其它 PyErr_* 函数给出更加详细的错误消息,并且在这些情况是可以这样做的。 但是按照一般规则,这是不必要的,并可能导致有关错误原因的信息丢失:大多数操作会由于种种原因而失败。)

想要忽略由一个失败的函数调用所设置的异常,异常条件必须通过调用 PyErr_Clear() 显式地被清除。 C 代码应当调用 PyErr_Clear() 的唯一情况是如果它不想将错误传给解释器而是想完全由自己来处理它(可能是尝试其他方法,或是假装没有出错)。

每次失败的 malloc() 调用必须转换为一个异常。 malloc() (或 realloc() )的直接调用者必须调用 PyErr_NoMemory() 来返回错误来提示。所有对象创建函数(例如 PyLong_FromLong() )已经这么做了,所以这个提示仅用于直接调用 malloc() 的情况。

还要注意的是,除了 PyArg_ParseTuple() 等重要的例外,返回整数状态码的函数通常都是返回正值或零来表示成功,而以 -1 表示失败,如同 Unix 系统调用一样。

最后,当你返回一个错误指示器时要注意清理垃圾(通过为你已经创建的对象执行 Py_XDECREF()Py_DECREF() 调用)!

选择引发哪个异常完全取决于你的喜好。 所有内置的 Python 异常都有对应的预声明 C 对象,例如 PyExc_ZeroDivisionError,你可以直接使用它们。 当然,你应当明智地选择异常 —- 不要使用 PyExc_TypeError 来表示一个文件无法被打开 (那大概应该用 PyExc_IOError)。 如果参数列表有问题,PyArg_ParseTuple() 函数通常会引发 PyExc_TypeError。 如果你想要一个参数的值必须处于特定范围之内或必须满足其他条件,则适宜使用 PyExc_ValueError

你也可以为你的模块定义一个唯一的新异常。需要在文件前部声明一个静态对象变量,如:

static PyObject *SpamError;

并且在你的模块的初始化函数 (PyInit_spam()) 中使用一个异常对象来初始化:

PyMODINIT_FUNC
PyInit_spam(void)
{
    PyObject *m;
    m = PyModule_Create(&spammodule);
    if (m == NULL)
        return NULL;
    SpamError = PyErr_NewException("spam.error", NULL, NULL);
    Py_XINCREF(SpamError);
    if (PyModule_AddObject(m, "error", SpamError) < 0) {
        Py_XDECREF(SpamError);
        Py_CLEAR(SpamError);
        Py_DECREF(m);
        return NULL;
    }
    return m;
}

注意异常对象的Python名字是 spam.error 。而 PyErr_NewException() 函数可以创建一个类,其基类为 Exception (除非是另一个类传入以替换 NULL ) 。

同样注意的是创建类保存了 SpamError 的一个引用,这是有意的。为了防止被垃圾回收掉,否则 SpamError 随时会成为野指针。

一会讨论 PyMODINIT_FUNC 作为函数返回类型的用法。

spam.error 异常可以在扩展模块中抛出,通过 PyErr_SetString() 函数调用,如下:

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;
    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = system(command);
    if (sts < 0) {
        PyErr_SetString(SpamError, "System command failed");
        return NULL;
    }
    return PyLong_FromLong(sts);
}

1.3. 回到例子

回到前面的例子,你应该明白下面的代码:

if (!PyArg_ParseTuple(args, "s", &command))
    return NULL;

如果在参数列表中检测到错误,它将返回 NULL (该值是返回对象指针的函数所使用的错误提示),这取决于 PyArg_ParseTuple() 设置的异常。 在其他情况下参数的字符串值会被拷贝到局部变量 command。 这是一个指针赋值并且你不应该修改它所指向的字符串 (因此在标准 C 中,变量 command 应当被正确地声明为 const char *command)。

下一个语句使用UNIX系统函数 system() ,传递给他的参数是刚才从 PyArg_ParseTuple() 取出的:

sts = system(command);

我们的 spam.system() 函数必须返回 sts 的值作为Python对象。这通过使用函数 PyLong_FromLong() 来实现。

return PyLong_FromLong(sts);

在这种情况下,会返回一个整数对象,(这个对象会在Python堆里面管理)。

如果你的 C 函数没有有用的返回值 (返回 void 的函数),则对应的 Python 函数必须返回 None。 你必须使用这种写法(可以通过 Py_RETURN_NONE 宏来实现):

Py_INCREF(Py_None);
return Py_None;

Py_None 是特殊 Python 对象 None 所对应的 C 名称。 它是一个真正的 Python 对象而不是 NULL 指针,如我们所见,后者在大多数上下文中都意味着“错误”。

1.4. 模块方法表和初始化函数

为了展示 spam_system() 如何被Python程序调用。把函数声明为可以被Python调用,需要先定义一个方法表 “method table” 。

static PyMethodDef SpamMethods[] = {
    ...
    {"system",  spam_system, METH_VARARGS,
     "Execute a shell command."},
    ...
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

注意第三个参数 ( METH_VARARGS ) ,这个标志指定会使用C的调用惯例。可选值有 METH_VARARGSMETH_VARARGS | METH_KEYWORDS 。值 0 代表使用 PyArg_ParseTuple() 的陈旧变量。

如果单独使用 METH_VARARGS ,函数会等待Python传来tuple格式的参数,并最终使用 PyArg_ParseTuple() 进行解析。

METH_KEYWORDS 值表示接受关键字参数。这种情况下C函数需要接受第三个 PyObject * 对象,表示字典参数,使用 PyArg_ParseTupleAndKeywords() 来解析出参数。

这个方法表必须被模块定义结构所引用。

static struct PyModuleDef spammodule = {
    PyModuleDef_HEAD_INIT,
    "spam",   /* name of module */
    spam_doc, /* module documentation, may be NULL */
    -1,       /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
    SpamMethods
};

这个结构体必须传递给解释器的模块初始化函数。初始化函数必须命名为 PyInit_name() ,其中 name 是模块的名字,并应该定义为非 static ,且在模块文件里:

PyMODINIT_FUNC
PyInit_spam(void)
{
    return PyModule_Create(&spammodule);
}

注意 PyMODINIT_FUNC 将函数声明为 PyObject * 返回类型,声明了任何平台所要求的特殊链接声明,并针对 C++ 将函数声明为 extern "C"

当 Python 程序首次导入 spam 模块时, PyInit_spam() 会被调用。 它将调用 PyModule_Create(),该函数会返回一个模块对象,并基于在模块定义中找到的表将内置函数对象插入到新创建的模块中(该表是一个 PyMethodDef 结构体的数组)。 PyModule_Create() 返回一个指向它所创建的模块对象的指针。 它可能会因程度严重的特定错误而中止,或者在模块无法成功初始化时返回 NULL。 初始化函数必须返回模块对象给其调用者,这样它就可以被插入到 sys.modules 中。

当嵌入Python时, PyInit_spam() 函数不会被自动调用,除非放在 PyImport_Inittab 表里。要添加模块到初始化表,使用 PyImport_AppendInittab() ,可选的跟着一个模块的导入。

int main(int argc, char *argv[])
{
    wchar_t *program = Py_DecodeLocale(argv[0], NULL);
    if (program == NULL) {
        fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
        exit(1);
    }
    /* Add a built-in module, before Py_Initialize */
    if (PyImport_AppendInittab("spam", PyInit_spam) == -1) {
        fprintf(stderr, "Error: could not extend in-built modules table\n");
        exit(1);
    }
    /* Pass argv[0] to the Python interpreter */
    Py_SetProgramName(program);
    /* Initialize the Python interpreter.  Required.
       If this step fails, it will be a fatal error. */
    Py_Initialize();
    /* Optionally import the module; alternatively,
       import can be deferred until the embedded script
       imports it. */
    PyObject *pmodule = PyImport_ImportModule("spam");
    if (!pmodule) {
        PyErr_Print();
        fprintf(stderr, "Error: could not import module 'spam'\n");
    }
    ...
    PyMem_RawFree(program);
    return 0;
}

注解

要从 sys.modules 删除实体或导入已编译模块到一个进程里的多个解释器(或使用 fork() 而没用 exec() )会在一些扩展模块上产生错误。扩展模块作者可以在初始化内部数据结构时给出警告。

更多关于模块的现实的例子包含在Python源码包的 Modules/xxmodule.c 中。这些文件可以用作你的代码模板,或者学习。脚本 modulator.py 包含在源码发行版或Windows安装中,提供了一个简单的GUI,用来声明需要实现的函数和对象,并且可以生成供填入的模板。脚本在 Tools/modulator/ 目录。查看README以了解用法。

注解

不像我们的 spam 例子, xxmodule 使用了 多阶段初始化 (Python3.5开始引入), PyInit_spam 会返回一个 PyModuleDef 结构体,然后创建的模块放到导入机制。细节参考 PEP 489 的多阶段初始化。

1.5. 编译和链接

在你能使用你的新写的扩展之前,你还需要做两件事情:使用 Python 系统来编译和链接。如果你使用动态加载,这取决于你使用的操作系统的动态加载机制;更多信息请参考编译扩展模块的章节,以及在 Windows 上编译需要的额外信息(。

如果你不使用动态加载,或者想要让模块永久性的作为Python解释器的一部分,就必须修改配置设置,并重新构建解释器。幸运的是在Unix上很简单,只需要把你的文件 ( spammodule.c 为例) 放在解压缩源码发行包的 Modules/ 目录下,添加一行到 Modules/Setup.local 来描述你的文件:

spam spammodule.o

然后在顶层目录运行 make 来重新构建解释器。你也可以在 Modules/ 子目录使用 make,但是你必须先重建 Makefile 文件,然后运行 ‘make Makefile’ 命令。(你每次修改 Setup 文件都需要这样操作。)

如果你的模块需要额外的链接,这些内容可以列出在配置文件里,举个实例:

spam spammodule.o -lX11

1.6. 在C中调用Python函数

迄今为止,我们一直把注意力集中于让Python调用C函数,其实反过来也很有用,就是用C调用Python函数。这在回调函数中尤其有用。如果一个C接口使用回调,那么就要实现这个回调机制。

幸运的是,Python解释器是比较方便回调的,并给标准Python函数提供了标准接口。(这里就不再详述解析Python字符串作为输入的方式,如果有兴趣可以参考 Python/pythonmain.c 中的 -c 命令行代码。)

调用Python函数很简单,首先Python程序要传递Python函数对象。应该提供个函数(或其他接口)来实现。当调用这个函数时,用全局变量保存Python函数对象的指针,还要调用 (Py_INCREF()) 来增加引用计数,当然不用全局变量也没什么关系。举个例子,如下函数可能是模块定义的一部分:

static PyObject *my_callback = NULL;
static PyObject *
my_set_callback(PyObject *dummy, PyObject *args)
{
    PyObject *result = NULL;
    PyObject *temp;
    if (PyArg_ParseTuple(args, "O:set_callback", &temp)) {
        if (!PyCallable_Check(temp)) {
            PyErr_SetString(PyExc_TypeError, "parameter must be callable");
            return NULL;
        }
        Py_XINCREF(temp);         /* Add a reference to new callback */
        Py_XDECREF(my_callback);  /* Dispose of previous callback */
        my_callback = temp;       /* Remember new callback */
        /* Boilerplate to return "None" */
        Py_INCREF(Py_None);
        result = Py_None;
    }
    return result;
}

这个函数必须使用 METH_VARARGS 标志注册到解释器.

Py_XINCREF()Py_XDECREF() 这两个宏可增加/减少一个对象的引用计数,并且当存在 NULL 指针时仍可保证安全 (但请注意在这个上下文中 temp 将不为 NULL)。

随后,当要调用此函数时,你将调用 C 函数 PyObject_CallObject()。 该函数有两个参数,它们都属于指针,指向任意 Python 对象:即 Python 函数,及其参数列表。 参数列表必须总是一个元组对象,其长度即参数的个数量。 要不带参数地调用 Python 函数,则传入 NULL 或一个空元组;要带一个参数调用它,则传入一个单元组。 Py_BuildValue() 会在其格式字符串包含一对圆括号内的零个或多个格式代码时返回一个元组。 例如:

int arg;
PyObject *arglist;
PyObject *result;
...
arg = 123;
...
/* Time to call the callback */
arglist = Py_BuildValue("(i)", arg);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);

PyObject_CallObject() 返回Python对象指针,这也是Python函数的返回值。 PyObject_CallObject() 是一个对其参数 “引用计数无关” 的函数。例子中新的元组创建用于参数列表,并且在 PyObject_CallObject() 之后立即使用了 Py_DECREF()

PyEval_CallObject() 的返回值总是“新”的:要么是一个新建的对象;要么是已有对象,但增加了引用计数。所以除非你想把结果保存在全局变量中,你需要对这个值使用 Py_DECREF(),即使你对里面的内容(特别!)不感兴趣。

但是在你这么做之前,很重要的一点是检查返回值不是 NULL。 如果是的话,Python 函数会终止并引发异常。 如果调用 PyObject_CallObject() 的 C 代码是在 Python 中发起调用的,它应当立即返回一个错误来告知其 Python 调用者,以便解释器能打印栈回溯信息,或者让调用方 Python 代码能处理该异常。 如果这无法做到或不合本意,则应当通过调用 PyErr_Clear() 来清除异常。 例如:

if (result == NULL)
    return NULL; /* Pass error back */
...use result...
Py_DECREF(result);

依赖于具体的回调函数,你还要提供一个参数列表到 PyEval_CallObject() 。在某些情况下参数列表是由Python程序提供的,通过接口再传到回调函数对象。这样就可以不改变形式直接传递。另外一些时候你要构造一个新的元组来传递参数。最简单的方法就是 Py_BuildValue() 函数构造tuple。举个例子,你要传递一个事件代码时可以用如下代码:

PyObject *arglist;
...
arglist = Py_BuildValue("(l)", eventcode);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
if (result == NULL)
    return NULL; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);

注意 Py_DECREF(arglist) 所在处会立即调用,在错误检查之前。当然还要注意一些常规的错误,比如 Py_BuildValue() 可能会遭遇内存不足等等。

当你调用函数时还需要注意,用关键字参数调用 PyObject_Call() ,需要支持普通参数和关键字参数。有如如上例子中,我们使用 Py_BuildValue() 来构造字典。

PyObject *dict;
...
dict = Py_BuildValue("{s:i}", "name", val);
result = PyObject_Call(my_callback, NULL, dict);
Py_DECREF(dict);
if (result == NULL)
    return NULL; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);

1.7. 提取扩展函数的参数

函数 PyArg_ParseTuple() 的声明如下:

int PyArg_ParseTuple(PyObject *arg, const char *format, ...);

参数 arg 必须是一个元组对象,包含从 Python 传递给 C 函数的参数列表。format 参数必须是一个格式字符串。剩余参数是各个变量的地址,类型要与格式字符串对应。

注意 PyArg_ParseTuple() 会检测他需要的Python参数类型,却无法检测传递给他的C变量地址,如果这里出错了,可能会在内存中随机写入东西,小心。

注意任何由调用者提供的 Python 对象引用是 借来的 引用;不要递减它们的引用计数!

一些调用的例子:

#define PY_SSIZE_T_CLEAN  /* Make "s#" use Py_ssize_t rather than int. */
#include <Python.h>
int ok;
int i, j;
long k, l;
const char *s;
Py_ssize_t size;
ok = PyArg_ParseTuple(args, ""); /* No arguments */
    /* Python call: f() */
ok = PyArg_ParseTuple(args, "s", &s); /* A string */
    /* Possible Python call: f('whoops!') */

ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* Two longs and a string */
    /* Possible Python call: f(1, 2, 'three') */

ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
    /* A pair of ints and a string, whose size is also returned */
    /* Possible Python call: f((1, 2), 'three') */ 
{
    const char *file;
    const char *mode = "r";
    int bufsize = 0;
    ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
    /* A string, and optionally another string and an integer */
    /* Possible Python calls:
       f('spam')
       f('spam', 'w')
       f('spam', 'wb', 100000) */
}
{
    int left, top, right, bottom, h, v;
    ok = PyArg_ParseTuple(args, "((ii)(ii))(ii)",
             &left, &top, &right, &bottom, &h, &v);
    /* A rectangle and a point */
    /* Possible Python call:
       f(((0, 0), (400, 300)), (10, 10)) */
}
{
    Py_complex c;
    ok = PyArg_ParseTuple(args, "D:myfunction", &c);
    /* a complex, also providing a function name for errors */
    /* Possible Python call: myfunction(1+2j) */
}

1.8. 给扩展函数的关键字参数

函数 PyArg_ParseTupleAndKeywords() 声明如下:

int PyArg_ParseTupleAndKeywords(PyObject *arg, PyObject *kwdict,
                                const char *format, char *kwlist[], ...);

argformat 形参与 PyArg_ParseTuple() 函数所定义的一致。 kwdict 形参是作为第三个参数从 Python 运行时接收的关键字字典。 kwlist 形参是以 NULL 结尾的字符串列表,它被用来标识形参;名称从左至右与来自 format 的类型信息相匹配。 如果执行成功,PyArg_ParseTupleAndKeywords() 会返回真值,否则返回假值并引发一个适当的异常。

注解

嵌套的元组在使用关键字参数时无法生效,不在 kwlist 中的关键字参数会导致 TypeError 异常。

如下例子是使用关键字参数的例子模块:

#define PY_SSIZE_T_CLEAN  /* Make "s#" use Py_ssize_t rather than int. */
#include <Python.h>
static PyObject *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
    int voltage;
    const char *state = "a stiff";
    const char *action = "voom";
    const char *type = "Norwegian Blue";
    static char *kwlist[] = {"voltage", "state", "action", "type", NULL};
    if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|sss", kwlist,
                                     &voltage, &state, &action, &type))
        return NULL;
    printf("-- This parrot wouldn't %s if you put %i Volts through it.\n",
           action, voltage);
    printf("-- Lovely plumage, the %s -- It's %s!\n", type, state);
    Py_RETURN_NONE;
}
static PyMethodDef keywdarg_methods[] = {
    /* The cast of the function is necessary since PyCFunction values
     * only take two PyObject* parameters, and keywdarg_parrot() takes
     * three.
     */
    {"parrot", (PyCFunction)(void(*)(void))keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,
     "Print a lovely skit to standard output."},
    {NULL, NULL, 0, NULL}   /* sentinel */
};
static struct PyModuleDef keywdargmodule = {
    PyModuleDef_HEAD_INIT,
    "keywdarg",
    NULL,
    -1,
    keywdarg_methods
};
PyMODINIT_FUNC
PyInit_keywdarg(void)
{
    return PyModule_Create(&keywdargmodule);
}

1.9. 构造任意值

这个函数与 PyArg_ParseTuple() 很相似,声明如下:

PyObject *Py_BuildValue(const char *format, ...);

接受一个格式字符串,与 PyArg_ParseTuple() 相同,但是参数必须是原变量的地址指针(输入给函数,而非输出)。最终返回一个Python对象适合于返回C函数调用给Python代码。

一个与 PyArg_ParseTuple() 的不同是,后面可能需要的要求返回一个元组(Python参数里诶包总是在内部描述为元组),比如用于传递给其他Python函数以参数。 Py_BuildValue() 并不总是生成元组,在多于1个格式字符串时会生成元组,而如果格式字符串为空则返回 None ,一个参数则直接返回该参数的对象。如果要求强制生成一个长度为0的元组,或包含一个元素的元组,需要在格式字符串中加上括号。

例子(左侧是调用,右侧是Python值结果):

Py_BuildValue("")                        None
Py_BuildValue("i", 123)                  123
Py_BuildValue("iii", 123, 456, 789)      (123, 456, 789)
Py_BuildValue("s", "hello")              'hello'
Py_BuildValue("y", "hello")              b'hello'
Py_BuildValue("ss", "hello", "world")    ('hello', 'world')
Py_BuildValue("s#", "hello", 4)          'hell'
Py_BuildValue("y#", "hello", 4)          b'hell'
Py_BuildValue("()")                      ()
Py_BuildValue("(i)", 123)                (123,)
Py_BuildValue("(ii)", 123, 456)          (123, 456)
Py_BuildValue("(i,i)", 123, 456)         (123, 456)
Py_BuildValue("[i,i]", 123, 456)         [123, 456]
Py_BuildValue("{s:i,s:i}",
              "abc", 123, "def", 456)    {'abc': 123, 'def': 456}
Py_BuildValue("((ii)(ii)) (ii)",
              1, 2, 3, 4, 5, 6)          (((1, 2), (3, 4)), (5, 6))

1.10. 引用计数

在C/C++语言中,程序员负责动态分配和回收堆heap当中的内存。在C里,通过函数 malloc()free() 来完成。在C++里是操作 newdelete 来实现相同的功能。

每个由 malloc() 分配的内存块,最终都要由 free() 退回到可用内存池里面去。而调用 free() 的时机非常重要,如果一个内存块忘了 free() 则会导致内存泄漏,这块内存在程序结束前将无法重新使用。这叫做 内存泄漏 。而如果对同一内存块 free() 了以后,另外一个指针再次访问,则再次使用 malloc() 复用这块内存会导致冲突。这叫做 野指针 。等同于使用未初始化的数据,core dump,错误结果,神秘的崩溃等。

内存泄露往往发生在一些并不常见的代码流程上面。比如一个函数申请了内存以后,做了些计算,然后释放内存块。现在一些对函数的修改可能增加对计算的测试并检测错误条件,然后过早的从函数返回了。这很容易忘记在退出前释放内存,特别是后期修改的代码。这种内存泄漏,一旦引入,通常很长时间都难以检测到,错误退出被调用的频度较低,而现代电脑又有非常巨大的虚拟内存,所以泄漏仅在长期运行或频繁调用泄漏函数时才会变得明显。因此,有必要避免内存泄漏,通过代码规范会策略来最小化此类错误。

Python通过 malloc()free() 包含大量的内存分配和释放,同样需要避免内存泄漏和野指针。他选择的方法就是 引用计数 。其原理比较简单:每个对象都包含一个计数器,计数器的增减与对象引用的增减直接相关,当引用计数为0时,表示对象已经没有存在的意义了,对象就可以删除了。

另一个叫法是 自动垃圾回收 。(有时引用计数也被看作是垃圾回收策略,于是这里的”自动”用以区分两者)。自动垃圾回收的优点是用户不需要明确的调用 free() 。(另一个优点是改善速度或内存使用,然而这并不难)。缺点是对C,没有可移植的自动垃圾回收器,而引用计数则可以可移植的实现(只要 malloc()free() 函数是可用的,这也是C标准担保的)。也许以后有一天会出现可移植的自动垃圾回收器,但在此前我们必须与引用计数一起工作。

Python使用传统的引用计数实现,也提供了循环监测器,用以检测引用循环。这使得应用无需担心直接或间接的创建了循环引用,这是引用计数垃圾收集的一个弱点。引用循环是对象(可能直接)的引用了本身,所以循环中的每个对象的引用计数都不是0。典型的引用计数实现无法回收处于引用循环中的对象,或者被循环所引用的对象,哪怕没有循环以外的引用了。

循环检测器能够检测垃圾回收循环并能回收它们。 gc 模块提供了一种运行该检测器的方式 (collect() 函数),以及多个配置接口和在运行时禁用该检测器的功能。

1.10.1. Python中的引用计数

有两个宏 Py_INCREF(x)Py_DECREF(x) ,会处理引用计数的增减。 Py_DECREF() 也会在引用计数到达0时释放对象。为了灵活,并不会直接调用 free() ,而是通过对象的 类型对象 的函数指针来调用。为了这个目的(或其他的),每个对象同时包含一个指向自身类型对象的指针。

最大的问题依旧:何时使用 Py_INCREF(x)Py_DECREF(x) ?我们首先引入一些概念。没有人”拥有”一个对象,你可以 拥有一个引用 到一个对象。一个对象的引用计数定义为拥有引用的数量。引用的拥有者有责任调用 Py_DECREF() ,在引用不再需要时。引用的拥有关系可以被传递。有三种办法来处置拥有的引用:传递、存储、调用 Py_DECREF() 。忘记处置一个拥有的引用会导致内存泄漏。

还可以 借用 一个对象的引用。借用的引用不应该调用 Py_DECREF() 。借用者必须确保不能持有对象超过拥有者借出的时间。在拥有者处置对象后使用借用的引用是有风险的,应该完全避免 。

借用相对于引用的优点是你无需担心整条路径上代码的引用,或者说,通过借用你无需担心内存泄漏的风险。借用的缺点是一些看起来正确代码上的借用可能会在拥有者处置后使用对象。

借用可以变为拥有引用,通过调用 Py_INCREF() 。这不会影响已经借出的拥有者的状态。这回创建一个新的拥有引用,并给予完全的拥有者责任(新的拥有者必须恰当的处置引用,就像之前的拥有者那样)。

1.10.2. 拥有规则

当一个对象引用传递进出一个函数时,函数的接口应该指定拥有关系的传递是否包含引用。

大多数函数返回一个对象的引用,并传递引用拥有关系。通常,所有创建对象的函数,例如 PyLong_FromLong()Py_BuildValue() ,会传递拥有关系给接收者。即便是对象不是真正新的,你仍然可以获得对象的新引用。一个实例是 PyLong_FromLong() 维护了一个流行值的缓存,并可以返回已缓存项目的新引用。

很多另一个对象提取对象的函数,也会传递引用关系,例如 PyObject_GetAttrString() 。这里的情况不够清晰,一些不太常用的例程是例外的 PyTuple_GetItem()PyList_GetItem()PyDict_GetItem()PyDict_GetItemString() 都是返回从元组、列表、字典里借用的引用。

函数 PyImport_AddModule() 也会返回借用的引用,哪怕可能会返回创建的对象:这个可能因为一个拥有的引用对象是存储在 sys.modules 里。

当你传递一个对象引用到另一个函数时,通常函数是借用出去的。如果需要存储,就使用 Py_INCREF() 来变成独立的拥有者。这个规则有两个重要的例外: PyTuple_SetItem()PyList_SetItem() 。这些函数接受传递来的引用关系,哪怕会失败!(注意 PyDict_SetItem() 及其同类不会接受引用关系,他们是”正常的”)。

当一个C函数被Python调用时,会从调用方传来的参数借用引用。调用者拥有对象的引用,所以借用的引用生命周期可以保证到函数返回。只要当借用的引用需要存储或传递时,就必须转换为拥有的引用,通过调用 Py_INCREF()

Python调用从C函数返回的对象引用时必须是拥有的引用—-拥有关系被从函数传递给调用者。

1.10.3. 危险的薄冰

有少数情况下,借用的引用看起来无害,但却可能导致问题。这通常是因为解释器的隐式调用,并可能导致引用拥有者处置这个引用。

首先需要特别注意的情况是使用 Py_DECREF() 到一个无关对象,而这个对象的引用是借用自一个列表的元素。举个实例:

void bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);
    PyList_SetItem(list, 1, PyLong_FromLong(0L));
    PyObject_Print(item, stdout, 0); /* BUG! */
}

这个函数首先借用一个引用 list[0] ,然后替换 list[1] 为值 0 ,最后打印借用的引用。看起来无害是吧,但却不是。

我们跟着控制流进入 PyList_SetItem() 。列表拥有者引用了其所有成员,所以当成员1被替换时,就必须处置原来的成员1。现在假设原来的成员1是用户定义类的实例,且假设这个类定义了 __del__() 方法。如果这个类实例的引用计数是1,那么处置动作就会调用 __del__() 方法。

既然是Python写的, __del__() 方法可以执行任意Python代码。是否可能在 bug()item 废止引用呢,是的。假设列表传递到 bug() 会被 __del__() 方法所访问,就可以执行一个语句来实现 del list[0] ,然后假设这是最后一个对对象的引用,就需要释放内存,从而使得 item 无效化。

解决方法是,当你知道了问题的根源,就容易了:临时增加引用计数。正确版本的函数代码如下:

void no_bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);
    Py_INCREF(item);
    PyList_SetItem(list, 1, PyLong_FromLong(0L));
    PyObject_Print(item, stdout, 0);
    Py_DECREF(item);
}

这是个真实的故事。一个旧版本的Python包含了这个bug的变种,而一些人花费了大量时间在C调试器上去寻找为什么 __del__() 方法会失败。

这个问题的第二种情况是借用的引用涉及线程的变种。通常,Python解释器里多个线程无法进入对方的路径,因为有个全局锁保护着Python整个对象空间。但可以使用宏 Py_BEGIN_ALLOW_THREADS 来临时释放这个锁,重新获取锁用 Py_END_ALLOW_THREADS 。这通常围绕在阻塞I/O调用外,使得其他线程可以在等待I/O期间使用处理器。显然,如下函数会跟之前那个有一样的问题:

void bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);
    Py_BEGIN_ALLOW_THREADS
    ...some blocking I/O call...
    Py_END_ALLOW_THREADS
    PyObject_Print(item, stdout, 0); /* BUG! */
}

1.10.4. NULL指针

通常,接受对象引用作为参数的函数不希望你传给它们 NULL 指针,并且当你这样做时将会转储核心(或在以后导致核心转储)。 返回对象引用的函数通常只在要指明发生了异常时才返回 NULL。 不检测 NULL 参数的原因在于这些函数经常要将它们所接收的对象传给其他函数 —- 如果每个函数都检测 NULL,将会导致大量的冗余检测而使代码运行得更缓慢。

更好的做法是仅在“源头”上检测 NULL,即在接收到一个可能为 NULL 的指针,例如来自 malloc() 或是一个可能引发异常的函数的时候。

Py_INCREF()Py_DECREF() 等宏不会检测 NULL 指针 —但是,它们的变种 Py_XINCREF()Py_XDECREF() 则会检测。

用于检测特定对象类型的宏 (Pytype_Check()) 不会检测 NULL 指针 —- 同样地,有大量代码会连续调用这些宏来测试一个对象是否为几种不同预期类型之一,这将会生成冗余的测试。 不存在带有 NULL 检测的变体。

C 函数调用机制会保证传给 C 函数的参数列表 (本示例中为 args) 绝不会为 NULL —- 实际上它会保证其总是为一个元组。

任何时候将 NULL 指针“泄露”给 Python 用户都会是个严重的错误。

1.11. 在C++中编写扩展

还可以在C++中编写扩展模块,只是有些限制。如果主程序(Python解释器)是使用C编译器来编译和链接的,全局或静态对象的构造器就不能使用。而如果是C++编译器来链接的就没有这个问题。函数会被Python解释器调用(通常就是模块初始化函数)必须声明为 extern "C" 。而是否在 extern "C" {...} 里包含Python头文件则不是那么重要,因为如果定义了符号 __cplusplus 则已经是这么声明的了(所有现代C++编译器都会定义这个符号)。

1.12. 给扩展模块提供C API

很多扩展模块提供了新的函数和类型供Python使用,但有时扩展模块里的代码也可以被其他扩展模块使用。例如,一个扩展模块可以实现一个类型 “collection” 看起来是没有顺序的。就像是Python列表类型,拥有C API允许扩展模块来创建和维护列表,这个新的集合类型可以有一堆C函数用于给其他扩展模块直接使用。

开始看起来很简单:只需要编写函数(无需声明为 static ),提供恰当的头文件,以及C API的文档。实际上在所有扩展模块都是静态链接到Python解释器时也是可以正常工作的。当模块以共享库链接时,一个模块中的符号定义对另一个模块不可见。可见的细节依赖于操作系统,一些系统的Python解释器使用全局命名空间(例如Windows),有些则在链接时需要一个严格的已导入符号列表(一个例子是AIX),或者提供可选的不同策略(如Unix系列)。即便是符号是全局可见的,你要调用的模块也可能尚未加载。

可移植性需要不能对符号可见性做任何假设。这意味着扩展模块里的所有符号都应该声明为 static ,除了模块的初始化函数,来避免与其他扩展模块的命名冲突。这意味着符号应该 必须 通过其他导出方式来供其他扩展模块访问。

Python提供了一个特别的机制来传递C级别信息(指针),从一个扩展模块到另一个:Capsules。一个Capsule是一个Python数据类型,会保存指针( void* )。Capsule只能通过其C API来创建和访问,但可以像其他Python对象一样的传递。通常,我们可以指定一个扩展模块命名空间的名字。其他扩展模块可以导入这个模块,获取这个名字的值,然后从Capsule获取指针。

Capsule可以用多种方式导出C API给扩展模块。每个函数可以用自己的Capsule,或者所有C API指针可以存储在一个数组里,数组地址再发布给Capsule。存储和获取指针也可以用多种方式,供客户端模块使用。

无论你选择哪个方法,正确地为你的 Capsule 命名都很重要。 函数 PyCapsule_New() 接受一个名称形参 (const char*);允许你传入一个 NULL 作为名称,但我们强烈建议你指定一个名称。 正确地命名的 Capsule 提供了一定程序的运行时类型安全;没有可行的方式能区分两个未命名的 Capsule。

通常来说,Capsule用于暴露C API,其名字应该遵循如下规范:

modulename.attributename

便利函数 PyCapsule_Import() 可以方便的载入通过Capsule提供的C API,仅在Capsule的名字匹配时。这个行为为C API用户提供了高度的确定性来载入正确的C API。

如下例子展示了将大部分负担交由导出模块作者的方法,适用于常用的库模块。其会存储所有C API指针(例子里只有一个)在 void 指针的数组里,并使其值变为Capsule。对应的模块头文件提供了宏来管理导入模块和获取C API指针;客户端模块只需要在访问C API前调用这个宏即可。

导出的模块修改自 spam 模块。函数 spam.system() 不会直接调用C库函数 system() ,但一个函数 PySpam_System() 会负责调用,当然现实中会更复杂些(例如添加 “spam” 到每个命令)。函数 PySpam_System() 也会导出给其他扩展模块。

函数 PySpam_System() 是个纯C函数,声明 static 就像其他地方那样:

static int
PySpam_System(const char *command)
{
    return system(command);
}

函数 spam_system() 按照如下方式修改:

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;
    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = PySpam_System(command);
    return PyLong_FromLong(sts);
}

在模块开头,在此行后:

#include <Python.h>

添加另外两行:

#define SPAM_MODULE
#include "spammodule.h"

#define 用于告知头文件需要包含给导出的模块,而不是客户端模块。最终,模块的初始化函数必须负责初始化C API指针数组:

PyMODINIT_FUNC
PyInit_spam(void)
{
    PyObject *m;
    static void *PySpam_API[PySpam_API_pointers];
    PyObject *c_api_object;
    m = PyModule_Create(&spammodule);
    if (m == NULL)
        return NULL;
    /* Initialize the C API pointer array */
    PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;
    /* Create a Capsule containing the API pointer array's address */
    c_api_object = PyCapsule_New((void *)PySpam_API, "spam._C_API", NULL);
    if (PyModule_AddObject(m, "_C_API", c_api_object) < 0) {
        Py_XDECREF(c_api_object);
        Py_DECREF(m);
        return NULL;
    }
    return m;
}

注意 PySpam_API 声明为 static ;此外指针数组会在 PyInit_spam() 结束后消失!

头文件 spammodule.h 里的一堆工作,看起来如下所示:

#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif
/* Header file for spammodule */
/* C API functions */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (const char *command)
/* Total number of C API pointers */
#define PySpam_API_pointers 1
#ifdef SPAM_MODULE
/* This section is used when compiling spammodule.c */
static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;
#else
/* This section is used in modules that use spammodule's API */
static void **PySpam_API;
#define PySpam_System \
 (*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])
/* Return -1 on error, 0 on success.
 * PyCapsule_Import will set an exception if there's an error.
 */
static int
import_spam(void)
{
    PySpam_API = (void **)PyCapsule_Import("spam._C_API", 0);
    return (PySpam_API != NULL) ? 0 : -1;
}
#endif
#ifdef __cplusplus
}
#endif
#endif /* !defined(Py_SPAMMODULE_H) */

客户端模块必须在其初始化函数里按顺序调用函数 import_spam() (或其他宏)才能访问函数 PySpam_System()

PyMODINIT_FUNC
PyInit_client(void)
{
    PyObject *m;
    m = PyModule_Create(&clientmodule);
    if (m == NULL)
        return NULL;
    if (import_spam() < 0)
        return NULL;
    /* additional initialization can happen here */
    return m;
}

这种方法的主要缺点是,文件 spammodule.h 过于复杂。当然,对每个要导出的函数,基本结构是相似的,所以只需要学习一次。

最后需要提醒的是Capsule提供了额外的功能,用于存储在Capsule里的指针的内存分配和释放。

2. 自定义扩展类型

Python 允许编写 C 扩展模块定义可以从 Python 代码中操纵的新类型,这很像内置的 strlist 类型。

2.1. 基础

CPython 运行时将所有 Python 对象都视为类型 PyObject* 的变量,即所有 Python 对象的”基础类型”。 PyObject 结构体本身包含了对象的 reference count 和对象的”类型对象”。 类型对象确定解释器需要调用哪些 (C) 函数,例如一个属性查询一个对象,一个方法调用,或者与另一个对象相乘。 这些 C 函数被称为“类型方法”。

所以,如果你想要定义新的扩展类型,需要创建新的类型对象。

这类事情只能用例子解释,这里用一个最小化但完整的的模块,定义了新的类型叫做 Custom 在C扩展模块 custom 里。

注解

这里展示的方法是定义 static 扩展类型的传统方法。可以适合大部分用途。C API也可以定义在堆上分配的扩展类型,使用 PyType_FromSpec() 函数,但不在本入门里讨论。

#define PY_SSIZE_T_CLEAN
#include <Python.h>
typedef struct {
    PyObject_HEAD
    /* Type-specific fields go here. */
} CustomObject;
static PyTypeObject CustomType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Custom",
    .tp_doc = "Custom objects",
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_new = PyType_GenericNew,
};
static PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};
PyMODINIT_FUNC
PyInit_custom(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;
    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;
    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        Py_DECREF(m);
        return NULL;
    }
    return m;
}

这部分很容易理解,这是为了跟上一章能对接上。这个文件定义了三件事:

  1. Custom 类的对象 object 包含了: CustomObject 结构,这会为每个 Custom 实例分配一次。
  2. Custom type 的行为:这是 CustomType 结构体,其定义了一堆标识和函数指针,会指向解释器里请求的操作。
  3. 初始化 custom 模块: PyInit_custom 函数和对应的 custommodule 结构体。

结构的第一块是

typedef struct {
    PyObject_HEAD
} CustomObject;

This is what a Custom object will contain. PyObject_HEAD is mandatory at the start of each object struct and defines a field called ob_base of type PyObject, containing a pointer to a type object and a reference count (these can be accessed using the macros Py_REFCNT and Py_TYPE respectively). The reason for the macro is to abstract away the layout and to enable additional fields in debug builds.

注解

注意在宏 PyObject_HEAD 后没有分号。意外添加分号会导致编译器提示出错。

当然,对象除了在 PyObject_HEAD 存储数据外,还有额外数据;例如,如下定义了标准的Python浮点数:

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

第二个位是类型对象的定义:

static PyTypeObject CustomType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Custom",
    .tp_doc = "Custom objects",
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_new = PyType_GenericNew,
};

注解

推荐使用如上C99风格的初始化,以避免列出所有的 PyTypeObject 字段,其中很多是你不需要关心的,这样也可以避免关注字段的定义顺序。

object.h 中实际定义的 PyTypeObject 具有比如上定义更多的 字段。 剩余的字段会由 C 编译器用零来填充,通常的做法是不显式地指定它们,除非你确实需要它们。

我们先挑选一部分,每次一个字段:

PyVarObject_HEAD_INIT(NULL, 0)

这一行是强制的样板,用以初始化如上提到的 ob_base 字段:

.tp_name = "custom.Custom",

我们的类型的名称。 这将出现在我们的对象的默认文本表示形式和某些错误消息中,例如:

>>> "" + custom.Custom()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "custom.Custom") to str

Note that the name is a dotted name that includes both the module name and the name of the type within the module. The module in this case is custom and the type is Custom, so we set the type name to custom.Custom. Using the real dotted import path is important to make your type compatible with the pydoc and pickle modules.

.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,

This is so that Python knows how much memory to allocate when creating new Custom instances. tp_itemsize is only used for variable-sized objects and should otherwise be zero.

注解

If you want your type to be subclassable from Python, and your type has the same tp_basicsize as its base type, you may have problems with multiple inheritance. A Python subclass of your type will have to list your type first in its __bases__, or else it will not be able to call your type’s __new__() method without getting an error. You can avoid this problem by ensuring that your type has a larger value for tp_basicsize than its base type does. Most of the time, this will be true anyway, because either your base type will be object, or else you will be adding data members to your base type, and therefore increasing its size.

We set the class flags to Py_TPFLAGS_DEFAULT.

.tp_flags = Py_TPFLAGS_DEFAULT,

All types should include this constant in their flags. It enables all of the members defined until at least Python 3.3. If you need further members, you will need to OR the corresponding flags.

We provide a doc string for the type in tp_doc.

.tp_doc = "Custom objects",

To enable object creation, we have to provide a tp_new handler. This is the equivalent of the Python method __new__(), but has to be specified explicitly. In this case, we can just use the default implementation provided by the API function PyType_GenericNew().

.tp_new = PyType_GenericNew,

Everything else in the file should be familiar, except for some code in PyInit_custom():

if (PyType_Ready(&CustomType) < 0)
    return;

This initializes the Custom type, filling in a number of members to the appropriate default values, including ob_type that we initially set to NULL.

Py_INCREF(&CustomType);
if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
    Py_DECREF(&CustomType);
    Py_DECREF(m);
    return NULL;
}

This adds the type to the module dictionary. This allows us to create Custom instances by calling the Custom class:

>>> import custom
>>> mycustom = custom.Custom()

That’s it! All that remains is to build it; put the above code in a file called custom.c and:

from distutils.core import setup, Extension
setup(name="custom", version="1.0",
      ext_modules=[Extension("custom", ["custom.c"])])

in a file called setup.py; then typing

$ python setup.py build

at a shell should produce a file custom.so in a subdirectory; move to that directory and fire up Python —- you should be able to import custom and play around with Custom objects.

这并不难,对吗?

Of course, the current Custom type is pretty uninteresting. It has no data and doesn’t do anything. It can’t even be subclassed.

注解

While this documentation showcases the standard distutils module for building C extensions, it is recommended in real-world use cases to use the newer and better-maintained setuptools library. Documentation on how to do this is out of scope for this document and can be found in the Python Packaging User’s Guide.

2.2. Adding data and methods to the Basic example

Let’s extend the basic example to add some data and methods. Let’s also make the type usable as a base class. We’ll create a new module, custom2 that adds these capabilities:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "structmember.h"
typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;
static void
Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}
static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}
static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;
    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_XDECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_XDECREF(tmp);
    }
    return 0;
}
static PyMemberDef Custom_members[] = {
    {"first", T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};
static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }
    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}
static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};
static PyTypeObject CustomType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom2.Custom",
    .tp_doc = "Custom objects",
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = Custom_new,
    .tp_init = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
};
static PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom2",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};
PyMODINIT_FUNC
PyInit_custom2(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;
    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;
    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        Py_DECREF(m);
        return NULL;
    }
    return m;
}

This version of the module has a number of changes.

We’ve added an extra include:

#include <structmember.h>

This include provides declarations that we use to handle attributes, as described a bit later.

The Custom type now has three data attributes in its C struct, first, last, and number. The first and last variables are Python strings containing first and last names. The number attribute is a C integer.

The object structure is updated accordingly:

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

Because we now have data to manage, we have to be more careful about object allocation and deallocation. At a minimum, we need a deallocation method:

static void
Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

which is assigned to the tp_dealloc member:

.tp_dealloc = (destructor) Custom_dealloc,

This method first clears the reference counts of the two Python attributes. Py_XDECREF() correctly handles the case where its argument is NULL (which might happen here if tp_new failed midway). It then calls the tp_free member of the object’s type (computed by Py_TYPE(self)) to free the object’s memory. Note that the object’s type might not be CustomType, because the object may be an instance of a subclass.

注解

The explicit cast to destructor above is needed because we defined Custom_dealloc to take a CustomObject * argument, but the tp_dealloc function pointer expects to receive a PyObject * argument. Otherwise, the compiler will emit a warning. This is object-oriented polymorphism, in C!

We want to make sure that the first and last names are initialized to empty strings, so we provide a tp_new implementation:

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

and install it in the tp_new member:

.tp_new = Custom_new,

The tp_new handler is responsible for creating (as opposed to initializing) objects of the type. It is exposed in Python as the __new__() method. It is not required to define a tp_new member, and indeed many extension types will simply reuse PyType_GenericNew() as done in the first version of the Custom type above. In this case, we use the tp_new handler to initialize the first and last attributes to non-NULL default values.

tp_new is passed the type being instantiated (not necessarily CustomType, if a subclass is instantiated) and any arguments passed when the type was called, and is expected to return the instance created. tp_new handlers always accept positional and keyword arguments, but they often ignore the arguments, leaving the argument handling to initializer (a.k.a. tp_init in C or __init__ in Python) methods.

注解

tp_new shouldn’t call tp_init explicitly, as the interpreter will do it itself.

The tp_new implementation calls the tp_alloc slot to allocate memory:

self = (CustomObject *) type->tp_alloc(type, 0);

Since memory allocation may fail, we must check the tp_alloc result against NULL before proceeding.

注解

We didn’t fill the tp_alloc slot ourselves. Rather PyType_Ready() fills it for us by inheriting it from our base class, which is object by default. Most types use the default allocation strategy.

注解

If you are creating a co-operative tp_new (one that calls a base type’s tp_new or __new__()), you must not try to determine what method to call using method resolution order at runtime. Always statically determine what type you are going to call, and call its tp_new directly, or via type->tp_base->tp_new. If you do not do this, Python subclasses of your type that also inherit from other Python-defined classes may not work correctly. (Specifically, you may not be able to create instances of such subclasses without getting a TypeError.)

We also define an initialization function which accepts arguments to provide initial values for our instance:

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;
    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_XDECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_XDECREF(tmp);
    }
    return 0;
}

by filling the tp_init slot.

.tp_init = (initproc) Custom_init,

The tp_init slot is exposed in Python as the __init__() method. It is used to initialize an object after it’s created. Initializers always accept positional and keyword arguments, and they should return either 0 on success or -1 on error.

Unlike the tp_new handler, there is no guarantee that tp_init is called at all (for example, the pickle module by default doesn’t call __init__() on unpickled instances). It can also be called multiple times. Anyone can call the __init__() method on our objects. For this reason, we have to be extra careful when assigning the new attribute values. We might be tempted, for example to assign the first member like this:

if (first) {
    Py_XDECREF(self->first);
    Py_INCREF(first);
    self->first = first;
}

But this would be risky. Our type doesn’t restrict the type of the first member, so it could be any kind of object. It could have a destructor that causes code to be executed that tries to access the first member; or that destructor could release the Global interpreter Lock and let arbitrary code run in other threads that accesses and modifies our object.

To be paranoid and protect ourselves against this possibility, we almost always reassign members before decrementing their reference counts. When don’t we have to do this?

  • when we absolutely know that the reference count is greater than 1;
  • when we know that deallocation of the object will neither release the GIL nor cause any calls back into our type’s code;
  • when decrementing a reference count in a tp_dealloc handler on a type which doesn’t support cyclic garbage collection .

We want to expose our instance variables as attributes. There are a number of ways to do that. The simplest way is to define member definitions:

static PyMemberDef Custom_members[] = {
    {"first", T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

and put the definitions in the tp_members slot:

.tp_members = Custom_members,

Each member definition has a member name, type, offset, access flags and documentation string. See the 泛型属性管理 section below for details.

A disadvantage of this approach is that it doesn’t provide a way to restrict the types of objects that can be assigned to the Python attributes. We expect the first and last names to be strings, but any Python objects can be assigned. Further, the attributes can be deleted, setting the C pointers to NULL. Even though we can make sure the members are initialized to non-NULL values, the members can be set to NULL if the attributes are deleted.

We define a single method, Custom.name(), that outputs the objects name as the concatenation of the first and last names.

static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }
    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

The method is implemented as a C function that takes a Custom (or Custom subclass) instance as the first argument. Methods always take an instance as the first argument. Methods often take positional and keyword arguments as well, but in this case we don’t take any and don’t need to accept a positional argument tuple or keyword argument dictionary. This method is equivalent to the Python method:

def name(self):
    return "%s %s" % (self.first, self.last)

Note that we have to check for the possibility that our first and last members are NULL. This is because they can be deleted, in which case they are set to NULL. It would be better to prevent deletion of these attributes and to restrict the attribute values to be strings. We’ll see how to do that in the next section.

Now that we’ve defined the method, we need to create an array of method definitions:

static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

(note that we used the METH_NOARGS flag to indicate that the method is expecting no arguments other than self)

and assign it to the tp_methods slot:

.tp_methods = Custom_methods,

Finally, we’ll make our type usable as a base class for subclassing. We’ve written our methods carefully so far so that they don’t make any assumptions about the type of the object being created or used, so all we need to do is to add the Py_TPFLAGS_BASETYPE to our class flag definition:

.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,

We rename PyInit_custom() to PyInit_custom2(), update the module name in the PyModuleDef struct, and update the full class name in the PyTypeObject struct.

Finally, we update our setup.py file to build the new module:

from distutils.core import setup, Extension
setup(name="custom", version="1.0",
      ext_modules=[
         Extension("custom", ["custom.c"]),
         Extension("custom2", ["custom2.c"]),
         ])

2.3. Providing finer control over data attributes

In this section, we’ll provide finer control over how the first and last attributes are set in the Custom example. In the previous version of our module, the instance variables first and last could be set to non-string values or even deleted. We want to make sure that these attributes always contain strings.

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "structmember.h"
typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;
static void
Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}
static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}
static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;
    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_DECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_DECREF(tmp);
    }
    return 0;
}
static PyMemberDef Custom_members[] = {
    {"number", T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};
static PyObject *
Custom_getfirst(CustomObject *self, void *closure)
{
    Py_INCREF(self->first);
    return self->first;
}
static int
Custom_setfirst(CustomObject *self, PyObject *value, void *closure)
{
    PyObject *tmp;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    tmp = self->first;
    Py_INCREF(value);
    self->first = value;
    Py_DECREF(tmp);
    return 0;
}
static PyObject *
Custom_getlast(CustomObject *self, void *closure)
{
    Py_INCREF(self->last);
    return self->last;
}
static int
Custom_setlast(CustomObject *self, PyObject *value, void *closure)
{
    PyObject *tmp;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the last attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The last attribute value must be a string");
        return -1;
    }
    tmp = self->last;
    Py_INCREF(value);
    self->last = value;
    Py_DECREF(tmp);
    return 0;
}
static PyGetSetDef Custom_getsetters[] = {
    {"first", (getter) Custom_getfirst, (setter) Custom_setfirst,
     "first name", NULL},
    {"last", (getter) Custom_getlast, (setter) Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};
static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}
static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};
static PyTypeObject CustomType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom3.Custom",
    .tp_doc = "Custom objects",
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = Custom_new,
    .tp_init = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};
static PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom3",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};
PyMODINIT_FUNC
PyInit_custom3(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;
    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;
    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        Py_DECREF(m);
        return NULL;
    }
    return m;
}

To provide greater control, over the first and last attributes, we’ll use custom getter and setter functions. Here are the functions for getting and setting the first attribute:

static PyObject *
Custom_getfirst(CustomObject *self, void *closure)
{
    Py_INCREF(self->first);
    return self->first;
}
static int
Custom_setfirst(CustomObject *self, PyObject *value, void *closure)
{
    PyObject *tmp;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    tmp = self->first;
    Py_INCREF(value);
    self->first = value;
    Py_DECREF(tmp);
    return 0;
}

The getter function is passed a Custom object and a “closure”, which is a void pointer. In this case, the closure is ignored. (The closure supports an advanced usage in which definition data is passed to the getter and setter. This could, for example, be used to allow a single set of getter and setter functions that decide the attribute to get or set based on data in the closure.)

The setter function is passed the Custom object, the new value, and the closure. The new value may be NULL, in which case the attribute is being deleted. In our setter, we raise an error if the attribute is deleted or if its new value is not a string.

We create an array of PyGetSetDef structures:

static PyGetSetDef Custom_getsetters[] = {
    {"first", (getter) Custom_getfirst, (setter) Custom_setfirst,
     "first name", NULL},
    {"last", (getter) Custom_getlast, (setter) Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};

and register it in the tp_getset slot:

.tp_getset = Custom_getsetters,

The last item in a PyGetSetDef structure is the “closure” mentioned above. In this case, we aren’t using a closure, so we just pass NULL.

We also remove the member definitions for these attributes:

static PyMemberDef Custom_members[] = {
    {"number", T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

We also need to update the tp_init handler to only allow strings to be passed:

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;
    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_DECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_DECREF(tmp);
    }
    return 0;
}

With these changes, we can assure that the first and last members are never NULL so we can remove checks for NULL values in almost all cases. This means that most of the Py_XDECREF() calls can be converted to Py_DECREF() calls. The only place we can’t change these calls is in the tp_dealloc implementation, where there is the possibility that the initialization of these members failed in tp_new.

We also rename the module initialization function and module name in the initialization function, as we did before, and we add an extra definition to the setup.py file.

2.4. Supporting cyclic garbage collection

Python has a cyclic garbage collector (GC) that can identify unneeded objects even when their reference counts are not zero. This can happen when objects are involved in cycles. For example, consider:

>>> l = []
>>> l.append(l)
>>> del l

In this example, we create a list that contains itself. When we delete it, it still has a reference from itself. Its reference count doesn’t drop to zero. Fortunately, Python’s cyclic garbage collector will eventually figure out that the list is garbage and free it.

In the second version of the Custom example, we allowed any kind of object to be stored in the first or last attributes. Besides, in the second and third versions, we allowed subclassing Custom, and subclasses may add arbitrary attributes. For any of those two reasons, Custom objects can participate in cycles:

>>> import custom3
>>> class Derived(custom3.Custom): pass
...
>>> n = Derived()
>>> n.some_attribute = n

To allow a Custom instance participating in a reference cycle to be properly detected and collected by the cyclic GC, our Custom type needs to fill two additional slots and to enable a flag that enables these slots:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "structmember.h"
typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;
static int
Custom_traverse(CustomObject *self, visitproc visit, void *arg)
{
    Py_VISIT(self->first);
    Py_VISIT(self->last);
    return 0;
}
static int
Custom_clear(CustomObject *self)
{
    Py_CLEAR(self->first);
    Py_CLEAR(self->last);
    return 0;
}
static void
Custom_dealloc(CustomObject *self)
{
    PyObject_GC_UnTrack(self);
    Custom_clear(self);
    Py_TYPE(self)->tp_free((PyObject *) self);
}
static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}
static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;
    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_DECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_DECREF(tmp);
    }
    return 0;
}
static PyMemberDef Custom_members[] = {
    {"number", T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};
static PyObject *
Custom_getfirst(CustomObject *self, void *closure)
{
    Py_INCREF(self->first);
    return self->first;
}
static int
Custom_setfirst(CustomObject *self, PyObject *value, void *closure)
{
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    Py_INCREF(value);
    Py_CLEAR(self->first);
    self->first = value;
    return 0;
}
static PyObject *
Custom_getlast(CustomObject *self, void *closure)
{
    Py_INCREF(self->last);
    return self->last;
}
static int
Custom_setlast(CustomObject *self, PyObject *value, void *closure)
{
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the last attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The last attribute value must be a string");
        return -1;
    }
    Py_INCREF(value);
    Py_CLEAR(self->last);
    self->last = value;
    return 0;
}
static PyGetSetDef Custom_getsetters[] = {
    {"first", (getter) Custom_getfirst, (setter) Custom_setfirst,
     "first name", NULL},
    {"last", (getter) Custom_getlast, (setter) Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};
static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}
static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};
static PyTypeObject CustomType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom4.Custom",
    .tp_doc = "Custom objects",
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
    .tp_new = Custom_new,
    .tp_init = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_traverse = (traverseproc) Custom_traverse,
    .tp_clear = (inquiry) Custom_clear,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};
static PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom4",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};
PyMODINIT_FUNC
PyInit_custom4(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;
    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;
    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        Py_DECREF(m);
        return NULL;
    }
    return m;
}

First, the traversal method lets the cyclic GC know about subobjects that could participate in cycles:

static int
Custom_traverse(CustomObject *self, visitproc visit, void *arg)
{
    int vret;
    if (self->first) {
        vret = visit(self->first, arg);
        if (vret != 0)
            return vret;
    }
    if (self->last) {
        vret = visit(self->last, arg);
        if (vret != 0)
            return vret;
    }
    return 0;
}

For each subobject that can participate in cycles, we need to call the visit() function, which is passed to the traversal method. The visit() function takes as arguments the subobject and the extra argument arg passed to the traversal method. It returns an integer value that must be returned if it is non-zero.

Python provides a Py_VISIT() macro that automates calling visit functions. With Py_VISIT(), we can minimize the amount of boilerplate in Custom_traverse:

static int
Custom_traverse(CustomObject *self, visitproc visit, void *arg)
{
    Py_VISIT(self->first);
    Py_VISIT(self->last);
    return 0;
}

注解

The tp_traverse implementation must name its arguments exactly visit and arg in order to use Py_VISIT().

Second, we need to provide a method for clearing any subobjects that can participate in cycles:

static int
Custom_clear(CustomObject *self)
{
    Py_CLEAR(self->first);
    Py_CLEAR(self->last);
    return 0;
}

Notice the use of the Py_CLEAR() macro. It is the recommended and safe way to clear data attributes of arbitrary types while decrementing their reference counts. If you were to call Py_XDECREF() instead on the attribute before setting it to NULL, there is a possibility that the attribute’s destructor would call back into code that reads the attribute again (especially if there is a reference cycle).

注解

You could emulate Py_CLEAR() by writing:

PyObject *tmp;
tmp = self->first;
self->first = NULL;
Py_XDECREF(tmp);

Nevertheless, it is much easier and less error-prone to always use Py_CLEAR() when deleting an attribute. Don’t try to micro-optimize at the expense of robustness!

The deallocator Custom_dealloc may call arbitrary code when clearing attributes. It means the circular GC can be triggered inside the function. Since the GC assumes reference count is not zero, we need to untrack the object from the GC by calling PyObject_GC_UnTrack() before clearing members. Here is our reimplemented deallocator using PyObject_GC_UnTrack() and Custom_clear:

static void
Custom_dealloc(CustomObject *self)
{
    PyObject_GC_UnTrack(self);
    Custom_clear(self);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

Finally, we add the Py_TPFLAGS_HAVE_GC flag to the class flags:

.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,

That’s pretty much it. If we had written custom tp_alloc or tp_free handlers, we’d need to modify them for cyclic garbage collection. Most extensions will use the versions automatically provided.

2.5. Subclassing other types

It is possible to create new extension types that are derived from existing types. It is easiest to inherit from the built in types, since an extension can easily use the PyTypeObject it needs. It can be difficult to share these PyTypeObject structures between extension modules.

In this example we will create a SubList type that inherits from the built-in list type. The new type will be completely compatible with regular lists, but will have an additional increment() method that increases an internal counter:

>>> import sublist
>>> s = sublist.SubList(range(3))
>>> s.extend(s)
>>> print(len(s))
6
>>> print(s.increment())
1
>>> print(s.increment())
2
#define PY_SSIZE_T_CLEAN
#include <Python.h>
typedef struct {
    PyListObject list;
    int state;
} SubListObject;
static PyObject *
SubList_increment(SubListObject *self, PyObject *unused)
{
    self->state++;
    return PyLong_FromLong(self->state);
}
static PyMethodDef SubList_methods[] = {
    {"increment", (PyCFunction) SubList_increment, METH_NOARGS,
     PyDoc_STR("increment state counter")},
    {NULL},
};
static int
SubList_init(SubListObject *self, PyObject *args, PyObject *kwds)
{
    if (PyList_Type.tp_init((PyObject *) self, args, kwds) < 0)
        return -1;
    self->state = 0;
    return 0;
}
static PyTypeObject SubListType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "sublist.SubList",
    .tp_doc = "SubList objects",
    .tp_basicsize = sizeof(SubListObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_init = (initproc) SubList_init,
    .tp_methods = SubList_methods,
};
static PyModuleDef sublistmodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "sublist",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};
PyMODINIT_FUNC
PyInit_sublist(void)
{
    PyObject *m;
    SubListType.tp_base = &PyList_Type;
    if (PyType_Ready(&SubListType) < 0)
        return NULL;
    m = PyModule_Create(&sublistmodule);
    if (m == NULL)
        return NULL;
    Py_INCREF(&SubListType);
    if (PyModule_AddObject(m, "SubList", (PyObject *) &SubListType) < 0) {
        Py_DECREF(&SubListType);
        Py_DECREF(m);
        return NULL;
    }
    return m;
}

As you can see, the source code closely resembles the Custom examples in previous sections. We will break down the main differences between them.

typedef struct {
    PyListObject list;
    int state;
} SubListObject;

The primary difference for derived type objects is that the base type’s object structure must be the first value. The base type will already include the PyObject_HEAD() at the beginning of its structure.

When a Python object is a SubList instance, its PyObject * pointer can be safely cast to both PyListObject * and SubListObject *:

static int
SubList_init(SubListObject *self, PyObject *args, PyObject *kwds)
{
    if (PyList_Type.tp_init((PyObject *) self, args, kwds) < 0)
        return -1;
    self->state = 0;
    return 0;
}

We see above how to call through to the __init__ method of the base type.

This pattern is important when writing a type with custom tp_new and tp_dealloc members. The tp_new handler should not actually create the memory for the object with its tp_alloc, but let the base class handle it by calling its own tp_new.

The PyTypeObject struct supports a tp_base specifying the type’s concrete base class. Due to cross-platform compiler issues, you can’t fill that field directly with a reference to PyList_Type; it should be done later in the module initialization function:

PyMODINIT_FUNC
PyInit_sublist(void)
{
    PyObject* m;
    SubListType.tp_base = &PyList_Type;
    if (PyType_Ready(&SubListType) < 0)
        return NULL;
    m = PyModule_Create(&sublistmodule);
    if (m == NULL)
        return NULL;
    Py_INCREF(&SubListType);
    if (PyModule_AddObject(m, "SubList", (PyObject *) &SubListType) < 0) {
        Py_DECREF(&SubListType);
        Py_DECREF(m);
        return NULL;
    }
    return m;
}

Before calling PyType_Ready(), the type structure must have the tp_base slot filled in. When we are deriving an existing type, it is not necessary to fill out the tp_alloc slot with PyType_GenericNew() — the allocation function from the base type will be inherited.

After that, calling PyType_Ready() and adding the type object to the module is the same as with the basic Custom examples.

3. 定义扩展类型:已分类主题

本章节目标是提供一个各种你可以实现的类型方法及其功能的简短介绍。

这是 C 类型 PyTypeObject 的定义,省略了只用于 调试构建 的字段:

typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */
    /* Methods to implement standard operations */
    destructor tp_dealloc;
    Py_ssize_t tp_vectorcall_offset;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
                                    or tp_reserved (Python 3) */
    reprfunc tp_repr;
    /* Method suites for standard classes */
    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;
    /* More standard operations (here for binary compatibility) */
    hashfunc tp_hash;
    ternaryfunc tp_call;
    reprfunc tp_str;
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;
    /* Functions to access object as input/output buffer */
    PyBufferProcs *tp_as_buffer;
    /* Flags to define presence of optional/expanded features */
    unsigned long tp_flags;
    const char *tp_doc; /* Documentation string */
    /* Assigned meaning in release 2.0 */
    /* call function for all accessible objects */
    traverseproc tp_traverse;
    /* delete references to contained objects */
    inquiry tp_clear;
    /* Assigned meaning in release 2.1 */
    /* rich comparisons */
    richcmpfunc tp_richcompare;
    /* weak reference enabler */
    Py_ssize_t tp_weaklistoffset;
    /* Iterators */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;
    /* Attribute descriptor and subclassing stuff */
    struct PyMethodDef *tp_methods;
    struct PyMemberDef *tp_members;
    struct PyGetSetDef *tp_getset;
    // Strong reference on a heap type, borrowed reference on a static type
    struct _typeobject *tp_base;
    PyObject *tp_dict;
    descrgetfunc tp_descr_get;
    descrsetfunc tp_descr_set;
    Py_ssize_t tp_dictoffset;
    initproc tp_init;
    allocfunc tp_alloc;
    newfunc tp_new;
    freefunc tp_free; /* Low-level free-memory routine */
    inquiry tp_is_gc; /* For PyObject_IS_GC */
    PyObject *tp_bases;
    PyObject *tp_mro; /* method resolution order */
    PyObject *tp_cache;
    PyObject *tp_subclasses;
    PyObject *tp_weaklist;
    destructor tp_del;
    /* Type attribute cache version tag. Added in version 2.6 */
    unsigned int tp_version_tag;
    destructor tp_finalize;
    vectorcallfunc tp_vectorcall;
} PyTypeObject;

这里有 很多 方法。但是不要太担心,如果你要定义一个类型,通常只需要实现少量的方法。

正如你猜到的一样,我们正要一步一步详细介绍各种处理程序。因为有大量的历史包袱影响字段的排序,所以我们不会根据它们在结构体里定义的顺序讲解。通常非常容易找到一个包含你需要的字段的例子,然后改变值去适应你新的类型。

const char *tp_name; /* For printing */

类型的名字 - 上一章提到过的,会出现在很多地方,几乎全部都是为了诊断目的。尝试选择一个好名字,对于诊断很有帮助。

Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

这些字段告诉运行时在创造这个类型的新对象时需要分配多少内存。Python为了可变长度的结构(想下:字符串,元组)有些内置支持,这是 tp_itemsize 字段存在的原由。这部分稍后解释。

const char *tp_doc;

这里你可以放置一段字符串(或者它的地址),当你想在Python脚本引用 obj.__doc__ 时返回这段文档字符串。

现在我们来看一下基本类型方法 - 大多数扩展类型将实现的方法。

3.1. 终结和内存释放

destructor tp_dealloc;

当您的类型实例的引用计数减少为零并且Python解释器想要回收它时,将调用此函数。如果你的类型有内存可供释放或执行其他清理,你可以把它放在这里。 对象本身也需要在这里释放。 以下是此函数的示例:

static void
newdatatype_dealloc(newdatatypeobject *obj)
{
    free(obj->obj_UnderlyingDatatypePtr);
    Py_TYPE(obj)->tp_free(obj);
}

一个重要的释放器函数实现要求是把所有未决异常放着不动。这很重要是因为释放器会被解释器频繁的调用,当栈异常退出时(而非正常返回),不会有任何办法保护释放器看到一个异常尚未被设置。此事释放器的任何行为都会导致额外增加的Python代码来检查异常是否被设置。这可能导致解释器的误导性错误。正确的保护方法是,在任何不安全的操作前,保存未决异常,然后在其完成后恢复。者可以通过 PyErr_Fetch()PyErr_Restore() 函数来实现:

static void
my_dealloc(PyObject *obj)
{
    MyObject *self = (MyObject *) obj;
    PyObject *cbresult;
    if (self->my_callback != NULL) {
        PyObject *err_type, *err_value, *err_traceback;
        /* This saves the current exception state */
        PyErr_Fetch(&err_type, &err_value, &err_traceback);
        cbresult = PyObject_CallNoArgs(self->my_callback);
        if (cbresult == NULL)
            PyErr_WriteUnraisable(self->my_callback);
        else
            Py_DECREF(cbresult);
        /* This restores the saved exception state */
        PyErr_Restore(err_type, err_value, err_traceback);
        Py_DECREF(self->my_callback);
    }
    Py_TYPE(obj)->tp_free((PyObject*)self);
}

注解

There are limitations to what you can safely do in a deallocator function. First, if your type supports garbage collection (using tp_traverse and/or tp_clear), some of the object’s members can have been cleared or finalized by the time tp_dealloc is called. Second, in tp_dealloc, your object is in an unstable state: its reference count is equal to zero. Any call to a non-trivial object or API (as in the example above) might end up calling tp_dealloc again, causing a double free and a crash.

从 Python 3.4 开始,推荐不要在 tp_dealloc 放复杂的终结代码,而是使用新的 tp_finalize 类型方法。

参见

PEP 442 解释了新的终结方案。

3.2. 对象展示

在 Python 中,有两种方式可以生成对象的文本表示: repr() 函数和 str() 函数。 (print() 函数会直接调用 str()。) 这些处理程序都是可选的。

reprfunc tp_repr;
reprfunc tp_str;

tp_repr 处理程序应该返回一个字符串对象,其中包含调用它的实例的表示形式。 下面是一个简单的例子:

static PyObject *
newdatatype_repr(newdatatypeobject * obj)
{
    return PyUnicode_FromFormat("Repr-ified_newdatatype{{size:%d}}",
                                obj->obj_UnderlyingDatatypePtr->size);
}

如果没有指定 tp_repr 处理程序,解释器将提供一个使用 tp_name 的表示形式以及对象的惟一标识值。

The tp_str handler is to str() what the tp_repr handler described above is to repr(); that is, it is called when Python code calls str() on an instance of your object. Its implementation is very similar to the tp_repr function, but the resulting string is intended for human consumption. If tp_str is not specified, the tp_repr handler is used instead.

下面是一个简单的例子:

static PyObject *
newdatatype_str(newdatatypeobject * obj)
{
    return PyUnicode_FromFormat("Stringified_newdatatype{{size:%d}}",
                                obj->obj_UnderlyingDatatypePtr->size);
}

3.3. 属性管理

For every object which can support attributes, the corresponding type must provide the functions that control how the attributes are resolved. There needs to be a function which can retrieve attributes (if any are defined), and another to set attributes (if setting attributes is allowed). Removing an attribute is a special case, for which the new value passed to the handler is NULL.

Python supports two pairs of attribute handlers; a type that supports attributes only needs to implement the functions for one pair. The difference is that one pair takes the name of the attribute as a char, while the other accepts a PyObject. Each type can use whichever pair makes more sense for the implementation’s convenience.

getattrfunc  tp_getattr;        /* char * version */
setattrfunc  tp_setattr;
/* ... */
getattrofunc tp_getattro;       /* PyObject * version */
setattrofunc tp_setattro;

If accessing attributes of an object is always a simple operation (this will be explained shortly), there are generic implementations which can be used to provide the PyObject* version of the attribute management functions. The actual need for type-specific attribute handlers almost completely disappeared starting with Python 2.2, though there are many examples which have not been updated to use some of the new generic mechanism that is available.

3.3.1. 泛型属性管理

大多数扩展类型只使用 简单 属性,那么,是什么让属性变得“简单”呢?只需要满足下面几个条件:

  1. 当调用 PyType_Ready() 时,必须知道属性的名称。
  2. 不需要特殊的处理来记录属性是否被查找或设置,也不需要根据值采取操作。

请注意,此列表不对属性的值、值的计算时间或相关数据的存储方式施加任何限制。

When PyType_Ready() is called, it uses three tables referenced by the type object to create descriptors which are placed in the dictionary of the type object. Each descriptor controls access to one attribute of the instance object. Each of the tables is optional; if all three are NULL, instances of the type will only have attributes that are inherited from their base type, and should leave the tp_getattro and tp_setattro fields NULL as well, allowing the base type to handle attributes.

表被声明为object::类型的三个字段:

struct PyMethodDef *tp_methods;
struct PyMemberDef *tp_members;
struct PyGetSetDef *tp_getset;

If tp_methods is not NULL, it must refer to an array of PyMethodDef structures. Each entry in the table is an instance of this structure:

typedef struct PyMethodDef {
    const char  *ml_name;       /* method name */
    PyCFunction  ml_meth;       /* implementation function */
    int          ml_flags;      /* flags */
    const char  *ml_doc;        /* docstring */
} PyMethodDef;

One entry should be defined for each method provided by the type; no entries are needed for methods inherited from a base type. One additional entry is needed at the end; it is a sentinel that marks the end of the array. The ml_name field of the sentinel must be NULL.

The second table is used to define attributes which map directly to data stored in the instance. A variety of primitive C types are supported, and access may be read-only or read-write. The structures in the table are defined as:

typedef struct PyMemberDef {
    const char *name;
    int         type;
    int         offset;
    int         flags;
    const char *doc;
} PyMemberDef;

For each entry in the table, a descriptor will be constructed and added to the type which will be able to extract a value from the instance structure. The type field should contain one of the type codes defined in the structmember.h header; the value will be used to determine how to convert Python values to and from C values. The flags field is used to store flags which control how the attribute can be accessed.

以下标志常量定义在:file: ‘ structmember.h ‘;它们可以使用bitwise-OR组合。

常量 含意
READONLY 没有可写的
PY*AUDIT\*READ** Emit an object.__getattr audit events before reading.

在 3.10 版更改: RESTRICTED, READ_RESTRICTED and WRITE_RESTRICTED are deprecated. However, READ_RESTRICTED is an alias for PY_AUDIT_READ, so fields that specify either RESTRICTED or READ_RESTRICTED will also raise an audit event.

An interesting advantage of using the tp_members table to build descriptors that are used at runtime is that any attribute defined this way can have an associated doc string simply by providing the text in the table. An application can use the introspection API to retrieve the descriptor from the class object, and get the doc string using its __doc__ attribute.

As with the tp_methods table, a sentinel entry with a name value of NULL is required.

3.3.2. Type-specific Attribute Management

For simplicity, only the char* version will be demonstrated here; the type of the name parameter is the only difference between the char* and PyObject* flavors of the interface. This example effectively does the same thing as the generic example above, but does not use the generic support added in Python 2.2. It explains how the handler functions are called, so that if you do need to extend their functionality, you’ll understand what needs to be done.

The tp_getattr handler is called when the object requires an attribute look-up. It is called in the same situations where the __getattr__() method of a class would be called.

例如:

static PyObject *
newdatatype_getattr(newdatatypeobject *obj, char *name)
{
    if (strcmp(name, "data") == 0)
    {
        return PyLong_FromLong(obj->data);
    }
    PyErr_Format(PyExc_AttributeError,
                 "'%.50s' object has no attribute '%.400s'",
                 tp->tp_name, name);
    return NULL;
}

The tp_setattr handler is called when the __setattr__() or __delattr__() method of a class instance would be called. When an attribute should be deleted, the third parameter will be NULL. Here is an example that simply raises an exception; if this were really all you wanted, the tp_setattr handler should be set to NULL.

static int
newdatatype_setattr(newdatatypeobject *obj, char *name, PyObject *v)
{
    PyErr_Format(PyExc_RuntimeError, "Read-only attribute: %s", name);
    return -1;
}

3.4. Object Comparison

richcmpfunc tp_richcompare;

The tp_richcompare handler is called when comparisons are needed. It is analogous to the rich comparison methods, like __lt__(), and also called by PyObject_RichCompare() and PyObject_RichCompareBool().

This function is called with two Python objects and the operator as arguments, where the operator is one of Py_EQ, Py_NE, Py_LE, Py_GT, Py_LT or Py_GT. It should compare the two objects with respect to the specified operator and return Py_True or Py_False if the comparison is successful, Py_NotImplemented to indicate that comparison is not implemented and the other object’s comparison method should be tried, or NULL if an exception was set.

Here is a sample implementation, for a datatype that is considered equal if the size of an internal pointer is equal:

static PyObject *
newdatatype_richcmp(PyObject *obj1, PyObject *obj2, int op)
{
    PyObject *result;
    int c, size1, size2;
    /* code to make sure that both arguments are of type
       newdatatype omitted */
    size1 = obj1->obj_UnderlyingDatatypePtr->size;
    size2 = obj2->obj_UnderlyingDatatypePtr->size;
    switch (op) {
    case Py_LT: c = size1 <  size2; break;
    case Py_LE: c = size1 <= size2; break;
    case Py_EQ: c = size1 == size2; break;
    case Py_NE: c = size1 != size2; break;
    case Py_GT: c = size1 >  size2; break;
    case Py_GE: c = size1 >= size2; break;
    }
    result = c ? Py_True : Py_False;
    Py_INCREF(result);
    return result;
 }

3.5. Abstract Protocol Support

Python supports a variety of abstract ‘protocols;’ the specific interfaces provided to use these interfaces are documented in 抽象对象层.

A number of these abstract interfaces were defined early in the development of the Python implementation. In particular, the number, mapping, and sequence protocols have been part of Python since the beginning. Other protocols have been added over time. For protocols which depend on several handler routines from the type implementation, the older protocols have been defined as optional blocks of handlers referenced by the type object. For newer protocols there are additional slots in the main type object, with a flag bit being set to indicate that the slots are present and should be checked by the interpreter. (The flag bit does not indicate that the slot values are non-NULL. The flag may be set to indicate the presence of a slot, but a slot may still be unfilled.)

PyNumberMethods   *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods  *tp_as_mapping;

If you wish your object to be able to act like a number, a sequence, or a mapping object, then you place the address of a structure that implements the C type PyNumberMethods, PySequenceMethods, or PyMappingMethods, respectively. It is up to you to fill in this structure with appropriate values. You can find examples of the use of each of these in the Objects directory of the Python source distribution.

hashfunc tp_hash;

This function, if you choose to provide it, should return a hash number for an instance of your data type. Here is a simple example:

static Py_hash_t
newdatatype_hash(newdatatypeobject *obj)
{
    Py_hash_t result;
    result = obj->some_size + 32767 * obj->some_number;
    if (result == -1)
       result = -2;
    return result;
}

Py_hash_t is a signed integer type with a platform-varying width. Returning -1 from tp_hash indicates an error, which is why you should be careful to avoid returning it when hash computation is successful, as seen above.

ternaryfunc tp_call;

This function is called when an instance of your data type is “called”, for example, if obj1 is an instance of your data type and the Python script contains obj1('hello'), the tp_call handler is invoked.

This function takes three arguments:

  1. self is the instance of the data type which is the subject of the call. If the call is obj1('hello'), then self is obj1.
  2. args is a tuple containing the arguments to the call. You can use PyArg_ParseTuple() to extract the arguments.
  3. kwds is a dictionary of keyword arguments that were passed. If this is non-NULL and you support keyword arguments, use PyArg_ParseTupleAndKeywords() to extract the arguments. If you do not want to support keyword arguments and this is non-NULL, raise a TypeError with a message saying that keyword arguments are not supported.

Here is a toy tp_call implementation:

static PyObject *
newdatatype_call(newdatatypeobject *self, PyObject *args, PyObject *kwds)
{
    PyObject *result;
    const char *arg1;
    const char *arg2;
    const char *arg3;
    if (!PyArg_ParseTuple(args, "sss:call", &arg1, &arg2, &arg3)) {
        return NULL;
    }
    result = PyUnicode_FromFormat(
        "Returning -- value: [%d] arg1: [%s] arg2: [%s] arg3: [%s]\n",
        obj->obj_UnderlyingDatatypePtr->size,
        arg1, arg2, arg3);
    return result;
}
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;

These functions provide support for the iterator protocol. Both handlers take exactly one parameter, the instance for which they are being called, and return a new reference. In the case of an error, they should set an exception and return NULL. tp_iter corresponds to the Python __iter__() method, while tp_iternext corresponds to the Python __next__() method.

Any iterable object must implement the tp_iter handler, which must return an iterator object. Here the same guidelines apply as for Python classes:

  • For collections (such as lists and tuples) which can support multiple independent iterators, a new iterator should be created and returned by each call to tp_iter.
  • Objects which can only be iterated over once (usually due to side effects of iteration, such as file objects) can implement tp_iter by returning a new reference to themselves — and should also therefore implement the tp_iternext handler.

Any iterator object should implement both tp_iter and tp_iternext. An iterator’s tp_iter handler should return a new reference to the iterator. Its tp_iternext handler should return a new reference to the next object in the iteration, if there is one. If the iteration has reached the end, tp_iternext may return NULL without setting an exception, or it may set StopIteration in addition to returning NULL; avoiding the exception can yield slightly better performance. If an actual error occurs, tp_iternext should always set an exception and return NULL.

3.6. Weak Reference Support

One of the goals of Python’s weak reference implementation is to allow any type to participate in the weak reference mechanism without incurring the overhead on performance-critical objects (such as numbers).

参见

Documentation for the weakref module.

For an object to be weakly referencable, the extension type must do two things:

  1. Include a PyObject* field in the C object structure dedicated to the weak reference mechanism. The object’s constructor should leave it NULL (which is automatic when using the default tp_alloc).
  2. Set the tp_weaklistoffset type member to the offset of the aforementioned field in the C object structure, so that the interpreter knows how to access and modify that field.

Concretely, here is how a trivial object structure would be augmented with the required field:

typedef struct {
    PyObject_HEAD
    PyObject *weakreflist;  /* List of weak references */
} TrivialObject;

And the corresponding member in the statically-declared type object:

static PyTypeObject TrivialType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    /* ... other members omitted for brevity ... */
    .tp_weaklistoffset = offsetof(TrivialObject, weakreflist),
};

The only further addition is that tp_dealloc needs to clear any weak references (by calling PyObject_ClearWeakRefs()) if the field is non-NULL:

static void
Trivial_dealloc(TrivialObject *self)
{
    /* Clear weakrefs first before calling any destructors */
    if (self->weakreflist != NULL)
        PyObject_ClearWeakRefs((PyObject *) self);
    /* ... remainder of destruction code omitted for brevity ... */
    Py_TYPE(self)->tp_free((PyObject *) self);
}

3.7. 更多建议

In order to learn how to implement any specific method for your new data type, get the CPython source code. Go to the Objects directory, then search the C source files for tp_ plus the function you want (for example, tp_richcompare). You will find examples of the function you want to implement.

When you need to verify that an object is a concrete instance of the type you are implementing, use the PyObject_TypeCheck() function. A sample of its use might be something like the following:

if (!PyObject_TypeCheck(some_object, &MyType)) {
    PyErr_SetString(PyExc_TypeError, "arg #1 not a mything");
    return NULL;
}

参见

下载CPython源代码版本。

https://www.python.org/downloads/source/

GitHub上开发CPython源代码的CPython项目。

https://github.com/python/cpython

4. 构建C/C++扩展

一个CPython的C扩展是一个共享库(例如一个Linux上的 .so ,或者Windows上的 .pyd ),其会导出一个 初始化函数

为了可导入,共享库必须在 PYTHONPATH 中有效,且必须命名遵循模块名字,通过适当的扩展。当使用distutils时,会自动生成正确的文件名。

初始化函数的声明如下:

PyObject * PyInit_modulename(void)

该函数返回完整初始化过的模块,或一个 PyModuleDef 实例。

对于仅有ASCII编码的模块名,函数必须是 PyInit_<modulename> ,将 <modulename> 替换为模块的名字。当使用 Multi-phase initialization 时,允许使用非ASCII编码的模块名。此时初始化函数的名字是 PyInitU_<modulename> ,而 <modulename> 需要用Python的 punycode 编码,连字号需替换为下划线。在Python里:

def initfunc_name(name):
    try:
        suffix = b'_' + name.encode('ascii')
    except UnicodeEncodeError:
        suffix = b'U_' + name.encode('punycode').replace(b'-', b'_')
    return b'PyInit' + suffix

可以在一个动态库里导出多个模块,通过定义多个初始化函数。而导入他们需要符号链接或自定义导入器,因为缺省时只有对应了文件名的函数才会被发现。查看 “一个库里的多模块” 章节,在 PEP 489 了解更多细节。

4.1. 使用distutils构建C和C++扩展

扩展模块可以用distutils来构建,这是Python自带的。distutils也支持创建二进制包,用户无需编译器而distutils就能安装扩展。

一个distutils包包含了一个驱动脚本 setup.py 。这是个纯Python文件,大多数时候也很简单,看起来如下:

from distutils.core import setup, Extension
module1 = Extension('demo',
                    sources = ['demo.c'])
setup (name = 'PackageName',
       version = '1.0',
       description = 'This is a demo package',
       ext_modules = [module1])

通过文件 setup.py ,和文件 demo.c ,运行如下

python setup.py build

这会编译 demo.c ,然后产生一个扩展模块叫做 demo 在目录 build 里。依赖于系统,模块文件会放在某个子目录形如 build/lib.system ,名字可能是 demo.sodemo.pyd

在文件 setup.py 里,所有动作的入口通过 setup 函数。该函数可以接受可变数量个关键字参数,上面的例子只使用了一个子集。特别需要注意的例子指定了构建包的元信息,以及指定了包内容。通常一个包会包括多个模块,就像Python的源码模块、文档、子包等。请参数distutils的文档,在 分发 Python 模块(遗留版本) 来了解更多distutils的特性;本章节只解释构建扩展模块的部分。

通常预计算参数给 setup() ,想要更好的结构化驱动脚本。有如如上例子函数 setup()ext_modules 参数是一列扩展模块,每个是一个 Extension 类的实例。例子中的实例定义了扩展命名为 demo ,从单一源码文件构建 demo.c

更多时候,构建一个扩展会复杂的多,需要额外的预处理器定义和库。如下例子展示了这些。

from distutils.core import setup, Extension
module1 = Extension('demo',
                    define_macros = [('MAJOR_VERSION', '1'),
                                     ('MINOR_VERSION', '0')],
                    include_dirs = ['/usr/local/include'],
                    libraries = ['tcl83'],
                    library_dirs = ['/usr/local/lib'],
                    sources = ['demo.c'])
setup (name = 'PackageName',
       version = '1.0',
       description = 'This is a demo package',
       author = 'Martin v. Loewis',
       author_email = 'martin@v.loewis.de',
       url = 'https://docs.python.org/extending/building',
       long_description = '''
This is really just a demo package.
''',
       ext_modules = [module1])

例子中函数 setup() 在调用时额外传递了元信息,是推荐发布包构建时的内容。对于这个扩展,其指定了预处理器定义,include目录,库目录,库。依赖于编译器,distutils还会用其他方式传递信息给编译器。例如在Unix上,结果是如下编译命令

gcc -DNDEBUG -g -O3 -Wall -Wstrict-prototypes -fPIC -DMAJOR_VERSION=1 -DMINOR_VERSION=0 -I/usr/local/include -I/usr/local/include/python2.2 -c demo.c -o build/temp.linux-i686-2.2/demo.o
gcc -shared build/temp.linux-i686-2.2/demo.o -L/usr/local/lib -ltcl83 -o build/lib.linux-i686-2.2/demo.so

这些行代码仅用于展示目的;distutils用户应该相信distutils能正确调用。

4.2. 发布你的扩展模块

当一个扩展已经成功地被构建时,有三种方式来使用它。

最终用户通常想要安装模块,可以这么运行

python setup.py install

模块维护者应该制作源码包;要实现可以运行

python setup.py sdist

有些情况下,需要在源码发布包里包含额外的文件;这通过 MANIFEST.in 文件实现。

如果源码发行包被成功地构建,维护者还可以创建二进制发行包。 取决于具体平台,以下命令中的一个可以用来完成此任务

python setup.py bdist_rpm
python setup.py bdist_dumb

5. 在 Windows 上构建 C 和 C++ 扩展

这一章简要介绍了如何使用 Microsoft Visual C++ 创建 Python 的 Windows 扩展模块,然后再提供有关其工作机理的详细背景信息。 这些说明材料同时适用于 Windows 程序员学习构建 Python 扩展以及 Unix 程序员学习如何生成在 Unix 和 Windows 上均能成功构建的软件。

鼓励模块作者使用 distutils 方式来构建扩展模块,而不使用本节所描述的方式。 你仍将需要使用 C 编译器来构建 Python;通常为 Microsoft Visual C++。

注解

这一章提及了多个包括已编码 Python 版本号的文件名。 这些文件名以显示为 XY 的版本号来代表;在实践中,'X' 将为你所使用的 Python 发布版的主版本号而 'Y' 将为次版本号。 例如,如果你所使用的是 Python 2.2.1,XY 将为 22

5.1. 菜谱式说明

在 Windows 和 Unix 上构建扩展模块都有两种方式:使用 distutils 包来控制构建过程,或者全手动操作。 distutils 方式适用于大多数扩展;使用 distutils 构建和打包扩展模块的文档见 分发 Python 模块(遗留版本)。 如果你发现你确实需要手动操作,那么研究一下 winsound 标准库模块的项目文件可能会很有帮助。

5.2. Unix 和 Windows 之间的差异

Unix 和 Windows 对于代码的运行时加载使用了完全不同的范式。 在你尝试构建可动态加载的模块之前,要先了解你所用系统是如何工作的。

在 Unix 中,一个共享对象 (.so) 文件中包含将由程序来使用的代码,也包含在程序中可被找到的函数名称和数据。 当文件被合并到程序中时,对在文件代码中这些函数和数据的全部引用都会被改为指向程序中函数和数据在内存中所放置的实际位置。 这基本上是一个链接操作。

在 Windows 中,一个动态链接库 (.dll) 文件中没有悬挂的引用。 而是通过一个查找表执行对函数或数据的访问。 因此在运行时 DLL 代码不必在运行时进行修改;相反地,代码已经使用了 DLL 的查找表,并且在运行时查找表会被修改以指向特定的函数和数据。

在 Unix 中,只存在一种库文件 (.a),它包含来自多个对象文件 (.o) 的代码。 在创建共享对象文件 (.so) 的链接阶段,链接器可能会发现它不知道某个标识符是在哪里定义的。 链接器将在各个库的对象文件中查找它;如果找到了它,链接器将会包括来自该对象文件的所有代码。

在 Windows 中,存在两种库类型,静态库和导入库 (扩展名都是 .lib)。 静态库类似于 Unix 的 .a 文件;它包含在必要时可被包括的代码。 导入库基本上仅用于让链接器能确保特定标识符是合法的,并且将在 DLL 被加载时出现于程序中。 这样链接器可使用来自导入库的信息构建查找表以便使用未包括在 DLL 中的标识符。 当一个应用程序或 DLL 被链接时,可能会生成一个导入库,它将需要被用于应用程序或 DLL 中未来所有依赖于这些符号的 DLL。

假设你正在编译两个动态加载模块 B 和 C,它们应当共享另一个代码块 A。 在 Unix 上,你 不应A.a 传给链接器作为 B.soC.so;那会导致它被包括两次,这样 B 和 C 将分别拥有它们自己的副本。 在 Windows 上,编译 A.dll 将同时编译 A.lib。 你 应当A.lib 传给链接器用于 B 和 C。 A.lib 并不包含代码;它只包含将在运行时被用于访问 A 的代码的信息。

在 Windows 上,使用导入库有点像是使用 import spam;它让你可以访问 spam 中的名称,但并不会创建一个单独副本。 在 Unix 上,链接到一个库更像是 from spam import *;它会创建一个单独副本。

5.3. DLL 的实际使用

Windows 版 Python 是用 Microsoft Visual C++ 编译的;使用其他编译器可能行也可能不行(但 Borland 看来是可以的)。 这一节的剩余部分只适用于 MSVC++。

当在 Windows 中创建 DLL 时,你必须将 pythonXY.lib 传给链接器。 要编译两个 DLL,spam 和 ni (会使用 spam 中找到的 C 函数),你应当使用以下命令:

cl /LD /I/python/include spam.c ../libs/pythonXY.lib
cl /LD /I/python/include ni.c spam.lib ../libs/pythonXY.lib

第一条命令创建了三个文件: spam.obj, spam.dllspam.libSpam.dll 不包含任何 Python 函数 (例如 PyArg_ParseTuple()),但它通过 pythonXY.lib 可以知道如何找到所需的 Python 代码。

第二条命令创建了 ni.dll (以及 .obj.lib),它知道如何从 spam 以及 Python 可执行文件中找到所需的函数。

不是每个标识符都会被导出到查找表。 如果你想要任何其他模块(包括 Python)都能看到你的标识符,你必须写上 _declspec(dllexport),就如在 void _declspec(dllexport) initspam(void)PyObject _declspec(dllexport) *NiGetSpamData(void) 中一样。

Developer Studio 将加入大量你并不真正需要的导入库,使你的可执行文件大小增加 100K。 要摆脱它们,请使用项目设置对话框的链接选项卡指定 忽略默认库。 将正确的 msvcrtxx.lib 添加到库列表中。

6. 在其它应用程序嵌入 Python

The previous chapters discussed how to extend Python, that is, how to extend the functionality of Python by attaching a library of C functions to it. It is also possible to do it the other way around: enrich your C/C++ application by embedding Python in it. Embedding provides your application with the ability to implement some of the functionality of your application in Python rather than C or C++. This can be used for many purposes; one example would be to allow users to tailor the application to their needs by writing some scripts in Python. You can also use it yourself if some of the functionality can be written in Python more easily.

Embedding Python is similar to extending it, but not quite. The difference is that when you extend Python, the main program of the application is still the Python interpreter, while if you embed Python, the main program may have nothing to do with Python —- instead, some parts of the application occasionally call the Python interpreter to run some Python code.

So if you are embedding Python, you are providing your own main program. One of the things this main program has to do is initialize the Python interpreter. At the very least, you have to call the function Py_Initialize(). There are optional calls to pass command line arguments to Python. Then later you can call the interpreter from any part of the application.

There are several different ways to call the interpreter: you can pass a string containing Python statements to PyRun_SimpleString(), or you can pass a stdio file pointer and a file name (for identification in error messages only) to PyRun_SimpleFile(). You can also call the lower-level operations described in the previous chapters to construct and use Python objects.

The details of Python’s C interface are given in this manual. A great deal of necessary information can be found here.

6.1. Very High Level Embedding

The simplest form of embedding Python is the use of the very high level interface. This interface is intended to execute a Python script without needing to interact with the application directly. This can for example be used to perform some operation on a file.

#define PY_SSIZE_T_CLEAN
#include <Python.h>
int
main(int argc, char *argv[])
{
    wchar_t *program = Py_DecodeLocale(argv[0], NULL);
    if (program == NULL) {
        fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
        exit(1);
    }
    Py_SetProgramName(program);  /* optional but recommended */
    Py_Initialize();
    PyRun_SimpleString("from time import time,ctime\n"
                       "print('Today is', ctime(time()))\n");
    if (Py_FinalizeEx() < 0) {
        exit(120);
    }
    PyMem_RawFree(program);
    return 0;
}

The Py_SetProgramName() function should be called before Py_Initialize() to inform the interpreter about paths to Python run-time libraries. Next, the Python interpreter is initialized with Py_Initialize(), followed by the execution of a hard-coded Python script that prints the date and time. Afterwards, the Py_FinalizeEx() call shuts the interpreter down, followed by the end of the program. In a real program, you may want to get the Python script from another source, perhaps a text-editor routine, a file, or a database. Getting the Python code from a file can better be done by using the PyRun_SimpleFile() function, which saves you the trouble of allocating memory space and loading the file contents.

6.2. Beyond Very High Level Embedding: An overview

The high level interface gives you the ability to execute arbitrary pieces of Python code from your application, but exchanging data values is quite cumbersome to say the least. If you want that, you should use lower level calls. At the cost of having to write more C code, you can achieve almost anything.

It should be noted that extending Python and embedding Python is quite the same activity, despite the different intent. Most topics discussed in the previous chapters are still valid. To show this, consider what the extension code from Python to C really does:

  1. 转换 Python 的数据值到 C,
  2. Perform a function call to a C routine using the converted values, and
  3. Convert the data values from the call from C to Python.

When embedding Python, the interface code does:

  1. 转换 C 的数据值到 Python,
  2. Perform a function call to a Python interface routine using the converted values, and
  3. Convert the data values from the call from Python to C.

As you can see, the data conversion steps are simply swapped to accommodate the different direction of the cross-language transfer. The only difference is the routine that you call between both data conversions. When extending, you call a C routine, when embedding, you call a Python routine.

This chapter will not discuss how to convert data from Python to C and vice versa. Also, proper use of references and dealing with errors is assumed to be understood. Since these aspects do not differ from extending the interpreter, you can refer to earlier chapters for the required information.

6.3. 纯嵌入

The first program aims to execute a function in a Python script. Like in the section about the very high level interface, the Python interpreter does not directly interact with the application (but that will change in the next section).

The code to run a function defined in a Python script is:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
int
main(int argc, char *argv[])
{
    PyObject *pName, *pModule, *pFunc;
    PyObject *pArgs, *pValue;
    int i;
    if (argc < 3) {
        fprintf(stderr,"Usage: call pythonfile funcname [args]\n");
        return 1;
    }
    Py_Initialize();
    pName = PyUnicode_DecodeFSDefault(argv[1]);
    /* Error checking of pName left out */
    pModule = PyImport_Import(pName);
    Py_DECREF(pName);
    if (pModule != NULL) {
        pFunc = PyObject_GetAttrString(pModule, argv[2]);
        /* pFunc is a new reference */
        if (pFunc && PyCallable_Check(pFunc)) {
            pArgs = PyTuple_New(argc - 3);
            for (i = 0; i < argc - 3; ++i) {
                pValue = PyLong_FromLong(atoi(argv[i + 3]));
                if (!pValue) {
                    Py_DECREF(pArgs);
                    Py_DECREF(pModule);
                    fprintf(stderr, "Cannot convert argument\n");
                    return 1;
                }
                /* pValue reference stolen here: */
                PyTuple_SetItem(pArgs, i, pValue);
            }
            pValue = PyObject_CallObject(pFunc, pArgs);
            Py_DECREF(pArgs);
            if (pValue != NULL) {
                printf("Result of call: %ld\n", PyLong_AsLong(pValue));
                Py_DECREF(pValue);
            }
            else {
                Py_DECREF(pFunc);
                Py_DECREF(pModule);
                PyErr_Print();
                fprintf(stderr,"Call failed\n");
                return 1;
            }
        }
        else {
            if (PyErr_Occurred())
                PyErr_Print();
            fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]);
        }
        Py_XDECREF(pFunc);
        Py_DECREF(pModule);
    }
    else {
        PyErr_Print();
        fprintf(stderr, "Failed to load \"%s\"\n", argv[1]);
        return 1;
    }
    if (Py_FinalizeEx() < 0) {
        return 120;
    }
    return 0;
}

This code loads a Python script using argv[1], and calls the function named in argv[2]. Its integer arguments are the other values of the argv array. If you compile and link this program (let’s call the finished executable call), and use it to execute a Python script, such as:

def multiply(a,b):
    print("Will compute", a, "times", b)
    c = 0
    for i in range(0, a):
        c = c + b
    return c

然后结果应该是:

$ call multiply multiply 3 2
Will compute 3 times 2
Result of call: 6

Although the program is quite large for its functionality, most of the code is for data conversion between Python and C, and for error reporting. The interesting part with respect to embedding Python starts with

Py_Initialize();
pName = PyUnicode_DecodeFSDefault(argv[1]);
/* Error checking of pName left out */
pModule = PyImport_Import(pName);

After initializing the interpreter, the script is loaded using PyImport_Import(). This routine needs a Python string as its argument, which is constructed using the PyUnicode_FromString() data conversion routine.

pFunc = PyObject_GetAttrString(pModule, argv[2]);
/* pFunc is a new reference */
if (pFunc && PyCallable_Check(pFunc)) {
    ...
}
Py_XDECREF(pFunc);

Once the script is loaded, the name we’re looking for is retrieved using PyObject_GetAttrString(). If the name exists, and the object returned is callable, you can safely assume that it is a function. The program then proceeds by constructing a tuple of arguments as normal. The call to the Python function is then made with:

pValue = PyObject_CallObject(pFunc, pArgs);

Upon return of the function, pValue is either NULL or it contains a reference to the return value of the function. Be sure to release the reference after examining the value.

6.4. Extending Embedded Python

Until now, the embedded Python interpreter had no access to functionality from the application itself. The Python API allows this by extending the embedded interpreter. That is, the embedded interpreter gets extended with routines provided by the application. While it sounds complex, it is not so bad. Simply forget for a while that the application starts the Python interpreter. Instead, consider the application to be a set of subroutines, and write some glue code that gives Python access to those routines, just like you would write a normal Python extension. For example:

static int numargs=0;
/* Return the number of arguments of the application command line */
static PyObject*
emb_numargs(PyObject *self, PyObject *args)
{
    if(!PyArg_ParseTuple(args, ":numargs"))
        return NULL;
    return PyLong_FromLong(numargs);
}
static PyMethodDef EmbMethods[] = {
    {"numargs", emb_numargs, METH_VARARGS,
     "Return the number of arguments received by the process."},
    {NULL, NULL, 0, NULL}
};
static PyModuleDef EmbModule = {
    PyModuleDef_HEAD_INIT, "emb", NULL, -1, EmbMethods,
    NULL, NULL, NULL, NULL
};
static PyObject*
PyInit_emb(void)
{
    return PyModule_Create(&EmbModule);
}

Insert the above code just above the main() function. Also, insert the following two statements before the call to Py_Initialize():

numargs = argc;
PyImport_AppendInittab("emb", &PyInit_emb);

These two lines initialize the numargs variable, and make the emb.numargs() function accessible to the embedded Python interpreter. With these extensions, the Python script can do things like

import emb
print("Number of arguments", emb.numargs())

In a real application, the methods will expose an API of the application to Python.

6.5. 在 C++ 中嵌入 Python

It is also possible to embed Python in a C++ program; precisely how this is done will depend on the details of the C++ system used; in general you will need to write the main program in C++, and use the C++ compiler to compile and link your program. There is no need to recompile Python itself using C++.

6.6. 在类 Unix 系统中编译和链接

It is not necessarily trivial to find the right flags to pass to your compiler (and linker) in order to embed the Python interpreter into your application, particularly because Python needs to load library modules implemented as C dynamic extensions (.so files) linked against it.

To find out the required compiler and linker flags, you can execute the python*X.Y*-config script which is generated as part of the installation process (a python3-config script may also be available). This script has several options, of which the following will be directly useful to you:

  • pythonX.Y-config --cflags will give you the recommended flags when compiling:

    $ /opt/bin/python3.4-config --cflags
    -I/opt/include/python3.4m -I/opt/include/python3.4m -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes
  • pythonX.Y-config --ldflags will give you the recommended flags when linking:

    $ /opt/bin/python3.4-config --ldflags
    -L/opt/lib/python3.4/config-3.4m -lpthread -ldl -lutil -lm -lpython3.4m -Xlinker -export-dynamic

注解

To avoid confusion between several Python installations (and especially between the system Python and your own compiled Python), it is recommended that you use the absolute path to python*X.Y*-config, as in the above example.

If this procedure doesn’t work for you (it is not guaranteed to work for all Unix-like platforms; however, we welcome bug reports) you will have to read your system’s documentation about dynamic linking and/or examine Python’s Makefile (use sysconfig.get_makefile_filename() to find its location) and compilation options. In this case, the sysconfig module is a useful tool to programmatically extract the configuration values that you will want to combine together. For example:

>>> import sysconfig
>>> sysconfig.get_config_var('LIBS')
'-lpthread -ldl  -lutil'
>>> sysconfig.get_config_var('LINKFORSHARED')
'-Xlinker -export-dynamic'

Python 常用指引

将 Python 2 代码迁移到 Python 3

Python 3 是 Python 的未来,但 Python 2 仍处于活跃使用阶段,最好让您的项目在两个主要版本的Python 上都可用。本指南旨在帮助您了解如何最好地同时支持 Python 2 和 3。

简要说明

To make your project be single-source Python 2/3 compatible, the basic steps are:

  1. Only worry about supporting Python 2.7
  2. Make sure you have good test coverage (coverage.py can help; python -m pip install coverage)
  3. 了解Python 2 和 3之间的区别
  4. Use Futurize (or Modernize) to update your code (e.g. python -m pip install future)
  5. Use Pylint to help make sure you don’t regress on your Python 3 support (python -m pip install pylint)
  6. Use caniusepython3 to find out which of your dependencies are blocking your use of Python 3 (python -m pip install caniusepython3)
  7. Once your dependencies are no longer blocking you, use continuous integration to make sure you stay compatible with Python 2 & 3 (tox can help test against multiple versions of Python; python -m pip install tox)
  8. Consider using optional static type checking to make sure your type usage works in both Python 2 & 3 (e.g. use mypy to check your typing under both Python 2 & Python 3; python -m pip install mypy).

注解

Note: Using python -m pip install guarantees that the pip you invoke is the one installed for the Python currently in use, whether it be a system-wide pip or one installed within a virtual environment.

详情

A key point about supporting Python 2 & 3 simultaneously is that you can start today! Even if your dependencies are not supporting Python 3 yet that does not mean you can’t modernize your code now to support Python 3. Most changes required to support Python 3 lead to cleaner code using newer practices even in Python 2 code.

Another key point is that modernizing your Python 2 code to also support Python 3 is largely automated for you. While you might have to make some API decisions thanks to Python 3 clarifying text data versus binary data, the lower-level work is now mostly done for you and thus can at least benefit from the automated changes immediately.

Keep those key points in mind while you read on about the details of porting your code to support Python 2 & 3 simultaneously.

删除对Python 2.6及更早版本的支持

While you can make Python 2.5 work with Python 3, it is much easier if you only have to work with Python 2.7. If dropping Python 2.5 is not an option then the six project can help you support Python 2.5 & 3 simultaneously (python -m pip install six). Do realize, though, that nearly all the projects listed in this HOWTO will not be available to you.

If you are able to skip Python 2.5 and older, then the required changes to your code should continue to look and feel like idiomatic Python code. At worst you will have to use a function instead of a method in some instances or have to import a function instead of using a built-in one, but otherwise the overall transformation should not feel foreign to you.

But you should aim for only supporting Python 2.7. Python 2.6 is no longer freely supported and thus is not receiving bugfixes. This means you will have to work around any issues you come across with Python 2.6. There are also some tools mentioned in this HOWTO which do not support Python 2.6 (e.g., Pylint), and this will become more commonplace as time goes on. It will simply be easier for you if you only support the versions of Python that you have to support.

Make sure you specify the proper version support in your setup.py file

In your setup.py file you should have the proper trove classifier specifying what versions of Python you support. As your project does not support Python 3 yet you should at least have Programming Language :: Python :: 2 :: Only specified. Ideally you should also specify each major/minor version of Python that you do support, e.g. Programming Language :: Python :: 2.7.

良好的测试覆盖率

Once you have your code supporting the oldest version of Python 2 you want it to, you will want to make sure your test suite has good coverage. A good rule of thumb is that if you want to be confident enough in your test suite that any failures that appear after having tools rewrite your code are actual bugs in the tools and not in your code. If you want a number to aim for, try to get over 80% coverage (and don’t feel bad if you find it hard to get better than 90% coverage). If you don’t already have a tool to measure test coverage then coverage.py is recommended.

了解Python 2 和 3之间的区别

Once you have your code well-tested you are ready to begin porting your code to Python 3! But to fully understand how your code is going to change and what you want to look out for while you code, you will want to learn what changes Python 3 makes in terms of Python 2. Typically the two best ways of doing that is reading the “What’s New” doc for each release of Python 3 and the Porting to Python 3 book (which is free online). There is also a handy cheat sheet from the Python-Future project.

更新代码

Once you feel like you know what is different in Python 3 compared to Python 2, it’s time to update your code! You have a choice between two tools in porting your code automatically: Futurize and Modernize. Which tool you choose will depend on how much like Python 3 you want your code to be. Futurize does its best to make Python 3 idioms and practices exist in Python 2, e.g. backporting the bytes type from Python 3 so that you have semantic parity between the major versions of Python. Modernize, on the other hand, is more conservative and targets a Python 2/3 subset of Python, directly relying on six to help provide compatibility. As Python 3 is the future, it might be best to consider Futurize to begin adjusting to any new practices that Python 3 introduces which you are not accustomed to yet.

Regardless of which tool you choose, they will update your code to run under Python 3 while staying compatible with the version of Python 2 you started with. Depending on how conservative you want to be, you may want to run the tool over your test suite first and visually inspect the diff to make sure the transformation is accurate. After you have transformed your test suite and verified that all the tests still pass as expected, then you can transform your application code knowing that any tests which fail is a translation failure.

Unfortunately the tools can’t automate everything to make your code work under Python 3 and so there are a handful of things you will need to update manually to get full Python 3 support (which of these steps are necessary vary between the tools). Read the documentation for the tool you choose to use to see what it fixes by default and what it can do optionally to know what will (not) be fixed for you and what you may have to fix on your own (e.g. using io.open() over the built-in open() function is off by default in Modernize). Luckily, though, there are only a couple of things to watch out for which can be considered large issues that may be hard to debug if not watched for.

除法

In Python 3, 5 / 2 == 2.5 and not 2; all division between int values result in a float. This change has actually been planned since Python 2.2 which was released in 2002. Since then users have been encouraged to add from __future__ import division to any and all files which use the / and // operators or to be running the interpreter with the -Q flag. If you have not been doing this then you will need to go through your code and do two things:

  1. Add from __future__ import division to your files
  2. Update any division operator as necessary to either use // to use floor division or continue using / and expect a float

The reason that / isn’t simply translated to // automatically is that if an object defines a __truediv__ method but not __floordiv__ then your code would begin to fail (e.g. a user-defined class that uses / to signify some operation but not // for the same thing or at all).

文本与二进制数据

In Python 2 you could use the str type for both text and binary data. Unfortunately this confluence of two different concepts could lead to brittle code which sometimes worked for either kind of data, sometimes not. It also could lead to confusing APIs if people didn’t explicitly state that something that accepted str accepted either text or binary data instead of one specific type. This complicated the situation especially for anyone supporting multiple languages as APIs wouldn’t bother explicitly supporting unicode when they claimed text data support.

To make the distinction between text and binary data clearer and more pronounced, Python 3 did what most languages created in the age of the internet have done and made text and binary data distinct types that cannot blindly be mixed together (Python predates widespread access to the internet). For any code that deals only with text or only binary data, this separation doesn’t pose an issue. But for code that has to deal with both, it does mean you might have to now care about when you are using text compared to binary data, which is why this cannot be entirely automated.

To start, you will need to decide which APIs take text and which take binary (it is highly recommended you don’t design APIs that can take both due to the difficulty of keeping the code working; as stated earlier it is difficult to do well). In Python 2 this means making sure the APIs that take text can work with unicode and those that work with binary data work with the bytes type from Python 3 (which is a subset of str in Python 2 and acts as an alias for bytes type in Python 2). Usually the biggest issue is realizing which methods exist on which types in Python 2 & 3 simultaneously (for text that’s unicode in Python 2 and str in Python 3, for binary that’s str/bytes in Python 2 and bytes in Python 3). The following table lists the unique methods of each data type across Python 2 & 3 (e.g., the decode() method is usable on the equivalent binary data type in either Python 2 or 3, but it can’t be used by the textual data type consistently between Python 2 and 3 because str in Python 3 doesn’t have the method). Do note that as of Python 3.5 the __mod__ method was added to the bytes type.

文本数据 二进制数据
decode
encode
format
isdecimal
isnumeric

Making the distinction easier to handle can be accomplished by encoding and decoding between binary data and text at the edge of your code. This means that when you receive text in binary data, you should immediately decode it. And if your code needs to send text as binary data then encode it as late as possible. This allows your code to work with only text internally and thus eliminates having to keep track of what type of data you are working with.

The next issue is making sure you know whether the string literals in your code represent text or binary data. You should add a b prefix to any literal that presents binary data. For text you should add a u prefix to the text literal. (there is a __future__ import to force all unspecified literals to be Unicode, but usage has shown it isn’t as effective as adding a b or u prefix to all literals explicitly)

As part of this dichotomy you also need to be careful about opening files. Unless you have been working on Windows, there is a chance you have not always bothered to add the b mode when opening a binary file (e.g., rb for binary reading). Under Python 3, binary files and text files are clearly distinct and mutually incompatible; see the io module for details. Therefore, you must make a decision of whether a file will be used for binary access (allowing binary data to be read and/or written) or textual access (allowing text data to be read and/or written). You should also use io.open() for opening files instead of the built-in open() function as the io module is consistent from Python 2 to 3 while the built-in open() function is not (in Python 3 it’s actually io.open()). Do not bother with the outdated practice of using codecs.open() as that’s only necessary for keeping compatibility with Python 2.5.

The constructors of both str and bytes have different semantics for the same arguments between Python 2 & 3. Passing an integer to bytes in Python 2 will give you the string representation of the integer: bytes(3) == '3'. But in Python 3, an integer argument to bytes will give you a bytes object as long as the integer specified, filled with null bytes: bytes(3) == b'\x00\x00\x00'. A similar worry is necessary when passing a bytes object to str. In Python 2 you just get the bytes object back: str(b'3') == b'3'. But in Python 3 you get the string representation of the bytes object: str(b'3') == "b'3'".

Finally, the indexing of binary data requires careful handling (slicing does not require any special handling). In Python 2, b'123'[1] == b'2' while in Python 3 b'123'[1] == 50. Because binary data is simply a collection of binary numbers, Python 3 returns the integer value for the byte you index on. But in Python 2 because bytes == str, indexing returns a one-item slice of bytes. The six project has a function named six.indexbytes() which will return an integer like in Python 3: six.indexbytes(b'123', 1).

To summarize:

  1. Decide which of your APIs take text and which take binary data
  2. Make sure that your code that works with text also works with unicode and code for binary data works with bytes in Python 2 (see the table above for what methods you cannot use for each type)
  3. Mark all binary literals with a b prefix, textual literals with a u prefix
  4. Decode binary data to text as soon as possible, encode text as binary data as late as possible
  5. Open files using io.open() and make sure to specify the b mode when appropriate
  6. Be careful when indexing into binary data
Use feature detection instead of version detection

Inevitably you will have code that has to choose what to do based on what version of Python is running. The best way to do this is with feature detection of whether the version of Python you’re running under supports what you need. If for some reason that doesn’t work then you should make the version check be against Python 2 and not Python 3. To help explain this, let’s look at an example.

Let’s pretend that you need access to a feature of importlib that is available in Python’s standard library since Python 3.3 and available for Python 2 through importlib2 on PyPI. You might be tempted to write code to access e.g. the importlib.abc module by doing the following:

import sys
if sys.version_info[0] == 3:
    from importlib import abc
else:
    from importlib2 import abc

The problem with this code is what happens when Python 4 comes out? It would be better to treat Python 2 as the exceptional case instead of Python 3 and assume that future Python versions will be more compatible with Python 3 than Python 2:

import sys
if sys.version_info[0] > 2:
    from importlib import abc
else:
    from importlib2 import abc

The best solution, though, is to do no version detection at all and instead rely on feature detection. That avoids any potential issues of getting the version detection wrong and helps keep you future-compatible:

try:
    from importlib import abc
except ImportError:
    from importlib2 import abc

Prevent compatibility regressions

Once you have fully translated your code to be compatible with Python 3, you will want to make sure your code doesn’t regress and stop working under Python 3. This is especially true if you have a dependency which is blocking you from actually running under Python 3 at the moment.

To help with staying compatible, any new modules you create should have at least the following block of code at the top of it:

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

You can also run Python 2 with the -3 flag to be warned about various compatibility issues your code triggers during execution. If you turn warnings into errors with -Werror then you can make sure that you don’t accidentally miss a warning.

You can also use the Pylint project and its --py3k flag to lint your code to receive warnings when your code begins to deviate from Python 3 compatibility. This also prevents you from having to run Modernize or Futurize over your code regularly to catch compatibility regressions. This does require you only support Python 2.7 and Python 3.4 or newer as that is Pylint’s minimum Python version support.

Check which dependencies block your transition

After you have made your code compatible with Python 3 you should begin to care about whether your dependencies have also been ported. The caniusepython3 project was created to help you determine which projects — directly or indirectly — are blocking you from supporting Python 3. There is both a command-line tool as well as a web interface at https://caniusepython3.com.

The project also provides code which you can integrate into your test suite so that you will have a failing test when you no longer have dependencies blocking you from using Python 3. This allows you to avoid having to manually check your dependencies and to be notified quickly when you can start running on Python 3.

Update your setup.py file to denote Python 3 compatibility

Once your code works under Python 3, you should update the classifiers in your setup.py to contain Programming Language :: Python :: 3 and to not specify sole Python 2 support. This will tell anyone using your code that you support Python 2 and 3. Ideally you will also want to add classifiers for each major/minor version of Python you now support.

Use continuous integration to stay compatible

Once you are able to fully run under Python 3 you will want to make sure your code always works under both Python 2 & 3. Probably the best tool for running your tests under multiple Python interpreters is tox. You can then integrate tox with your continuous integration system so that you never accidentally break Python 2 or 3 support.

You may also want to use the -bb flag with the Python 3 interpreter to trigger an exception when you are comparing bytes to strings or bytes to an int (the latter is available starting in Python 3.5). By default type-differing comparisons simply return False, but if you made a mistake in your separation of text/binary data handling or indexing on bytes you wouldn’t easily find the mistake. This flag will raise an exception when these kinds of comparisons occur, making the mistake much easier to track down.

And that’s mostly it! At this point your code base is compatible with both Python 2 and 3 simultaneously. Your testing will also be set up so that you don’t accidentally break Python 2 or 3 compatibility regardless of which version you typically run your tests under while developing.

考虑使用可选的静态类型检查

Another way to help port your code is to use a static type checker like mypy or pytype on your code. These tools can be used to analyze your code as if it’s being run under Python 2, then you can run the tool a second time as if your code is running under Python 3. By running a static type checker twice like this you can discover if you’re e.g. misusing binary data type in one version of Python compared to another. If you add optional type hints to your code you can also explicitly state whether your APIs use textual or binary data, helping to make sure everything functions as expected in both versions of Python.

将扩展模块移植到 Python 3

对于将扩展模块移植到 Python 3,推荐下列资源:

  • Supporting Python 3: An in-depth guide 中的 Migrating C extensions 这一章,这本书介绍了如何从 Python 2 迁移到 Python 3,包括指导读者如何移植扩展模块。
  • py3c 项目中的 Porting guide 提供了有关支持代码的指导性建议。
  • CythonCFFI 库提供了对于 Python 的 C API 的抽象。 扩展大都需要进行重写以使用两者中的一个,然后就可以通过库来处理各种 Python 版本和实现之间的差异。

用 Python 进行 Curses 编程

curses 是什么?

curses 库为基于文本的终端提供了独立于终端的屏幕绘制和键盘处理功能;这些终端包括 VT100,Linux 控制台以及各种程序提供的模拟终端。显示终端支持各种控制代码以执行常见的操作,例如移动光标,滚动屏幕和擦除区域。不同的终端使用相差很大的代码,并且往往有自己的小怪癖。

在普遍使用图形显示的世界中,人们可能会问“为什么自找要麻烦”?毕竟字符单元显示终端确实是一种过时的技术,但是在某些领域中,能够用它们做花哨的事情仍然很有价值。一个小众市场是在不运行 X server 的小型或嵌入式 Unix 上。另一个是在提供图形支持之前,可能需要运行的工具,例如操作系统安装程序和内核配置程序。

curses 库提供了相当基础的功能,为程序员提供了包含多个非重叠文本窗口的显示的抽象。窗口的内容可以通过多种方式更改—-添加文本,擦除文本,更改其外观—-以及curses库将确定需要向终端发送哪些控制代码以产生正确的输出。 curses并没有提供诸多用户界面概念,例如按钮,复选框或对话框。如果需要这些功能,请考虑用户界面库,例如 Urwid

curses 库最初是为BSD Unix 编写的。 后来 AT&T 的Unix System V 版本加入了许多增强功能和新功能。如今BSD curses已不再维护,被ncurses取代,ncurses是 AT&T 接口的开源实现。如果使用的是 Linux 或 FreeBSD 等开源Unix系统,则几乎肯定会使用ncurses。由于大多数当前的商业Unix版本都基于System V代码,因此这里描述的所有功能可能都可用。但是,某些专有Unix所带来的较早版本的curses可能无法支持所有功能。

Windows 版本的 Python 不包含 curses 模块。提供了一个名为 UniCurses 的移植版本。也可以尝试使用 Fredrik Lundh 编写 the Console module,它使用了与curses不相同的API,但提供了可光标定位的文本输出,完全支持鼠标和键盘输入。

Python 的 curses 模块

此 Python 模块相当简单地封装了 curses 提供的 C 函数;如果你已经熟悉在 C 语言中使用 curses 编程,把这些知识转移的 Python 是非常容易的。最大的差异在于 Python 中的接口通过把不同的 C 函数合并来让事情变得更简单,比如 addstr()mvaddstr()mvwaddstr() 三个 C 函数被并入 addstr() 这一个方法。下文中会描述更多的细节。

本 HOWTO 是关于使用 curses 和 Python 编写文本模式程序的概述。它并不被设计为一个 curses API 的完整指南;如需完整指南,请参见 ncurses 的 Python 库指南章节和 ncurses 的 C 手册页。相对地,本 HOWTO 将会给你一些基本思路。

开始和结束 curses 应用程序

在做任何事情之前,curses 必须先被初始化。可以通过调用函数 initscr() 来实现,它将查明终端的类型,向终端发送任何必须的设置代码,并创建多种内部数据结构。如果此操作成功,initscr() 将会返回一个代表整个屏幕的窗口对象;它通常会遵循对应的 C 变量名被称作 stdscr

import curses
stdscr = curses.initscr()

使用 curses 的应用程序通常会关闭按键自动上屏,目的是读取按键并只在特定情况下展示它们。这需要调用函数 noecho()

curses.noecho()

应用程序也会广泛地需要立即响应按键,而不需要按下回车键;这被称为 “cbreak” 模式,与通常的缓冲输入模式相对:

curses.cbreak()

终端通常会以多字节转义序列的形式返回特殊按键,比如光标键和导航键比如 Page Up 键和 Home 键。尽管你可以编写你的程序来应对这些序列,curses 能够代替你做到这件事,返回一个特殊值比如 curses.KEY_LEFT。为了让 curses 做这项工作,你需要启用 keypad 模式:

stdscr.keypad(True)

终止一个 curses 应用程序比建立一个容易得多,你只需要调用:

curses.nocbreak()
stdscr.keypad(False)
curses.echo()

来还原对终端作出的 curses 友好设置。然后,调用函数 endwin() 来将终端还原到它的原始操作模式:

curses.endwin()

调试一个 curses 应用程序时常会发生,一个应用程序还未能还原终端到原本的状态就意外退出了,这会搅乱你的终端。在 Python 中这常常会发生在你的代码中有 bug 并引发了一个未捕获的异常。当你尝试输入时按键不会上屏,这使得使用终端变得困难。

在 Python 中你可以避免这些复杂问题并让调试变得更简单,只需要导入 curses.wrapper() 函数并像这样使用它:

from curses import wrapper
def main(stdscr):
    # Clear screen
    stdscr.clear()
    # This raises ZeroDivisionError when i == 10.
    for i in range(0, 11):
        v = i-10
        stdscr.addstr(i, 0, '10 divided by {} is {}'.format(v, 10/v))
    stdscr.refresh()
    stdscr.getkey()
wrapper(main)

函数 wrapper() 接受一个可调用对象并首先进行上述初始化过程,在终端支持着色时还会初始化颜色。接着 wrapper() 运行你提供的可调用对象。当该可调用对象返回时,wrapper() 会还原终端到初始状态。该可调用对象会在 tryexcept 这样的结构内被调用,当它捕获到异常时,会先还原终端再重新引发这个异常。所以你的终端不会因为异常而被留在一个搞笑的状态,你也可以正常阅读异常消息和回溯信息。

窗口和面板

窗口是 curses 中的基本抽象。一个窗口对象表示了屏幕上的一个矩形区域,并且提供方法来显示文本、擦除文本、允许用户输入字符串等等。

函数 initscr() 返回的 stdscr 对象覆盖整个屏幕。许多程序可能只需要这一个窗口,但你可能希望把屏幕分割为多个更小的窗口,来分别重绘或者清除它们。函数 newwin() 根据给定的尺寸创建一个新窗口,并返回这个新的窗口对象:

begin_x = 20; begin_y = 7
height = 5; width = 40
win = curses.newwin(height, width, begin_y, begin_x)

注意 curses 使用的坐标系统与寻常的不同。坐标始终是以 y,x 的顺序传递,并且左上角是坐标 (0,0)。这打破了正常的坐标处理约定,即 x 坐标在前。这是一个与其他计算机应用程序糟糕的差异,但这从 curses 最初被编写出来就已是它的一部分,现在想要修改它已为时已晚。

你的应用程序能够查明屏幕的尺寸,curses.LINEScurses.COLS 分别代表了 yx 方向上的尺寸。合理的坐标应位于 (0,0)(curses.LINES - 1, curses.COLS - 1) 范围内。

当你调用一个方法来显示或擦除文本时,效果并不会立即显示。相反,你必须调用窗口对象的 refresh() 方法来更新屏幕。

这是因为 curses 最初是为 300 波特的龟速终端连接编写的;在这些终端上,压制重绘屏幕的时间就非常重要。相对地,当你调用 refresh() 时,curses 会累积屏幕的修改并以效率最高的方式显示它们。打个比方,如果你的程序在一个窗口内显示一些文本然后清楚了这个窗口,那么这些原始文本不需要被发送,因为它们甚至不曾能被看见。

在实践中,显式地告诉 curses 来重绘一个窗口并不会太复杂化 curses 编程。大部分程序会显示一堆内容然后等待按键或者其他某些用户侧动作。你要做的事情就是,保证屏幕在暂停并等待用户输入前被重绘,只需要先调用 stdscr.refresh() 或者其他相关窗口的 refresh() 方法。

一个面板是一种特殊的窗口,它可以比实际的显示屏幕更大,并且能只显示它的一部分。创建面板需要指定面板的高度和宽度,但刷新一个面板需要给出屏幕坐标和面板的需要显示的局部。

pad = curses.newpad(100, 100)
# These loops fill the pad with letters; addch() is
# explained in the next section
for y in range(0, 99):
    for x in range(0, 99):
        pad.addch(y,x, ord('a') + (x*x+y*y) % 26)
# Displays a section of the pad in the middle of the screen.
# (0,0) : coordinate of upper-left corner of pad area to display.
# (5,5) : coordinate of upper-left corner of window area to be filled
#         with pad content.
# (20, 75) : coordinate of lower-right corner of window area to be
#          : filled with pad content.
pad.refresh( 0,0, 5,5, 20,75)

refresh() 调用会在屏幕坐标 (5,5) 到坐标 (20,75) 的矩形范围内显示面板的一个部分,被显示的部分在面板上的坐标是 (0,0)。除了上述差异,面板与常规的窗口相同,也支持相同的方法。

如果你在屏幕上有多个窗口和面板,有一个更有效率的方法来更新窗口,避免每个部分单独更新时烦人的屏幕闪烁。refresh() 实际上做了两件事:

  1. 调用每个窗口的 noutrefresh() 方法来更新一个表达屏幕期望状态的底层的数据结构。
  2. 调用函数 doupdate() 来改变物理屏幕来符合这个数据结构中记录的期望状态。

你可以改为调用在多个窗口上 noutrefresh() 方法来更新该数据结构,然后调用函数 doupdate() 来更新屏幕。

显示文字

从一名 C 语言程序员的视角来看,curses 有时看起来就像是一堆略有差异的函数组成的扭曲迷宫。举个例子,addstr()stdscr 窗口的当前光标位置显示一个字符串,而 mvaddstr() 则是先移动到一个给定的 y,x 坐标再显示字符串。waddstr()addstr() 类似,但允许指定一个窗口而非默认的 stdscrmvwaddstr() 允许同时指定一个窗口和一个坐标。

幸运的是,Python 接口隐藏了所有这些细节。stdscr 和其他任何窗口一样是一个窗口对象,并且诸如 addstr() 之类的方法接受多种参数形式。通常有四种形式。

形式 描述
strch 在当前位置显示字符串 str 或字符 ch
strch, attr 在当前位置使用 attr 属性显示字符串 str 或字符 ch
y, x, strch 移动到窗口内的 y,x 位置,并显示 strch
y, x, strch, attr 移至窗口内的 y,x 位置,并使用 attr 属性显示 strch

属性允许以突出显示形态显示文本,比如加粗、下划线、反相或添加颜色。这些属性将来下一小节细说。

方法 addstr() 接受一个 Python 字符串或字节串作为用于显示的值。字节串的内容会被原样发送到终端。字符串会使用窗口的 encoding 属性值编码为字节,它默认为 locale.getpreferredencoding() 返回的系统默认编码。

方法 addch() 接受一个字符,可以是长度为 1 的字符串,长度为 1 的字节串或者一个整数。

对于特殊扩展字符有一些常量,这些常量是大于 255 的整数。比如,ACS_PLMINUS 是一个 “加减” 符号,ACS_ULCORNER 是一个框的左上角(方便绘制边界)。你也可以使用正确的 Unicode 字符。

窗口会记住上次操作之后光标所在位置,所以如果你忽略 y,x 坐标,字符串和字符会出现在上次操作结束的位置。你也可以通过 move(y,x) 的方法来移动光标。因为一些终端始终会显示一个闪烁的光标,你可能会想要保证光标处于一些不会让人感到分心的位置。在看似随机的位置出现一个闪烁的光标会令人非常迷惑。

如果你的应用程序完全不需要一个闪烁的光标,你可以调用 curs_set(False) 来使它隐形。为与旧版本 curses 的兼容性的关系,有函数 leaveok(bool) 作为 curs_set() 的等价替换。如果 bool 是真值,curses 库会尝试移除闪烁光标,并且你也不必担心它会留在一些奇怪的位置。

属性和颜色

字符可以以不同的方式显示。基于文本的应用程序常常以反相显示状态行,一个文本查看器可能需要突出显示某些单词。为了支持这种用法,curses 允许你为屏幕上的每个单元指定一个属性值。

属性值是一个整数,它的每一个二进制位代表一个不同的属性。你可以尝试以多种不属性位组合来显示文本,但 curses 不保证所有的组合都是有效的,或者看上去有明显不同。这一点取决于用户终端的能力,所以最稳妥的方式是只采用最常见的有效属性,见下表。

属性 描述
A_BLINK 闪烁文本
A_BOLD 超亮或粗体文本
A_DIM 半明亮文本
A_REVERSE 反相显示文本
A_STANDOUT 可用的最佳突出显示模式
A_UNDERLINE 带下划线的文本

所以,为了在屏幕顶部显示一个反相的状态行,你可以这么编写:

stdscr.addstr(0, 0, "Current mode: Typing mode",
              curses.A_REVERSE)
stdscr.refresh()

curses 库还支持在提供了颜色功能的终端上显示颜色的功能。最常见的提供颜色的终端很可能是 Linux 控制台,采用了 xterms 配色方案。

为了使用颜色,你必须在调用完函数 initscr() 后尽快调用函数 start_color(),来初始化默认颜色集 (curses.wrapper() 函数自动完成了这一点)。 当它完成后,如果使用中的终端支持显示颜色, has_colors() 会返回真值。 (注意:curses 使用美式拼写 “color”,而不是英式/加拿大拼写 “colour”。如果你习惯了英式拼写,你需要避免自己在这些函数上拼写错误。)

curses 库维护一个有限数量的颜色对,包括一个前景(文本)色和一个背景色。你可以使用函数 color_pair() 获得一个颜色对对应的属性值。它可以通过按位或运算与其他属性,比如 A_REVERSE 组合。但再说明一遍,这种组合并不保证在所有终端上都有效。

一个样例,用 1 号颜色对显示一行文本:

stdscr.addstr("Pretty text", curses.color_pair(1))
stdscr.refresh()

如前所述, 颜色对由前景色和背景色组成。 init_pair(n, f, b) 函数可改变颜色对 n 的定义 为前景色 f 和背景色 b。 颜色对 0 硬编码为黑底白字,不能改变。

颜色已经被编号,并且当其激活 color 模式时 start_color() 会初始化 8 种基本颜色。 它们是: 0:black, 1:red, 2:green, 3:yellow, 4:blue, 5:magenta, 6:cyan 和 7:white。 curses 模块为这些颜色定义了相应的名称常量: curses.COLOR_BLACK, curses.COLOR_RED 等等。

让我们来做个综合练习。 要将颜色 1 改为红色文本白色背景,你应当调用:

curses.init_pair(1, curses.COLOR_RED, curses.COLOR_WHITE)

当你改变一个颜色对时,任何已经使用该颜色对来显示的文本将会更改为新的颜色。 你还可以这样来显示新颜色的文本:

stdscr.addstr(0,0, "RED ALERT!", curses.color_pair(1))

某些非常花哨的终端可以将实际颜色定义修改为给定的 RGB 值。 这允许你将通常为红色的 1 号颜色改成紫色或蓝色或者任何你喜欢的颜色。 不幸的是,Linux 控制台不支持此特性,所以我无法尝试它,也无法提供任何示例。 想要检查你的终端是否能做到你可以调用 can_change_color(),如果有此功能则它将返回 True。 如果你幸运地拥有一个如此优秀的终端,请查询你的系统的帮助页面来了解详情。

用户输入

C curses 库提供了非常简单的输入机制。 Python 的 curses 模块添加了一个基本的文本输入控件。 (其他的库例如 Urwid 拥有更丰富的控件集。)

有两个方法可以从窗口获取输入:

  • getch() 会刷新屏幕然后等待用户按键,如果之前调用过 echo() 还会显示所按的键。 你还可以选择指定一个坐标以便在暂停之前让光标移动到那里。
  • getkey() 将做同样的事但是会把整数转换为字符串。 每个字符将返回为长度为 1 个字符的字符串,特殊键例如函数键将返回包含键名的较长字符串例如 KEY_UP^G

使用 nodelay() 窗口方法可以做到不等待用户。 在 nodelay(True) 之后,窗口的 getch()getkey() 将成为非阻塞的。 为表明输入未就绪,getch() 会返回 curses.ERR (值为 -1) 而 getkey() 会引发异常。 此外还有 halfdelay() 函数,它可被用来(实际地)在每个 getch() 上设置一个计时器;如果在指定的延迟内没有输入可用(以十分之一秒为单位),curses 将引发异常。

getch() 方法返回一个整数;如果数值在 0 到 255 之间,它代表所按下键的 ASCII 码。 大于 255 的值为特殊键例如 Page Up, Home 或方向键等。 你可以将返回的值与 curses.KEY_PPAGE, curses.KEY_HOMEcurses.KEY_LEFT 等常量做比较。 你的程序主循环看起来可能是这样:

while True:
    c = stdscr.getch()
    if c == ord('p'):
        PrintDocument()
    elif c == ord('q'):
        break  # Exit the while loop
    elif c == curses.KEY_HOME:
        x = y = 0

curses.ascii 模块提供了一些 ASCII 类成员函数,它们接受整数或长度为 1 个字符的字符串参数;这些函数在为这样的循环编写更具可读性的测试时可能会很有用。 它还提供了一些转换函数,它们接受整数或长度为 1 个字符的字符串参数并返回同样的类型。 例如,curses.ascii.ctrl() 返回与其参数相对应的控制字符。

还有一个可以提取整个字符串的方法 getstr()。 它并不经常被使用,因为它的功能相当受限;可用的编辑键只有 Backspace 和 Enter 键,它们会结束字符串。 也可以选择限制为固定数量的字符。

curses.echo()            # Enable echoing of characters
# Get a 15-character string, with the cursor on the top line
s = stdscr.getstr(0,0, 15)

curses.textpad 模块提供了一个文本框,它支持类似 Emacs 的键绑定集。 Textbox 类的各种方法支持带输入验证的编辑及包含或不包含末尾空格地收集编辑结果。 下面是一个例子:

import curses
from curses.textpad import Textbox, rectangle
def main(stdscr):
    stdscr.addstr(0, 0, "Enter IM message: (hit Ctrl-G to send)")
    editwin = curses.newwin(5,30, 2,1)
    rectangle(stdscr, 1,0, 1+5+1, 1+30+1)
    stdscr.refresh()
    box = Textbox(editwin)
    # Let the user edit until Ctrl-G is struck.
    box.edit()
    # Get resulting contents
    message = box.gather()

更多的信息

本 HOWTO 没有涵盖一些进阶主题,例如读取屏幕的内容或从 xterm 实例捕获鼠标事件等,但是 curses 模块的 Python 库文档页面现在已相当完善。 接下来你应当去浏览一下其中的内容。

如果你对 curses 函数的细节行为有疑问,请查看你的 curses 实现版本的说明页面,不论它是 ncurses 还是特定 Unix 厂商的版本。 说明页面将记录任何具体问题,并提供所有函数、属性以及可用 ACS_* 字符的完整列表。

由于 curses API 是如此的庞大,某些函数并不被 Python 接口所支持。 这往往不是因为它们难以实现,而是因为还没有人需要它们。 此外,Python 尚不支持与 ncurses 相关联的菜单库。 欢迎提供添加这些功能的补丁;请参阅 Python 开发者指南 了解有关为 Python 提交补丁的详情。

描述器使用指南

描述器 让对象能够自定义属性查找、存储和删除的操作。

本指南主要分为四个部分:

  1. “入门” 部分从简单的示例着手,逐步添加特性,从而给出基本的概述。如果你是刚接触到描述器,请从这里开始。
  2. 第二部分展示了完整的、实用的描述器示例。如果您已经掌握了基础知识,请从此处开始。
  3. 第三部分提供了更多技术教程,详细介绍了描述器如何工作。大多数人并不需要深入到这种程度。
  4. 最后一部分有对内置描述器(用 C 编写)的纯 Python 等价实现。如果您想了解函数如何变成绑定方法或对 classmethod()staticmethod()property()slots 这类常见工具的实现感兴趣,请阅读此部分。

入门

现在,让我们从最基本的示例开始,然后逐步添加新功能。

简单示例:返回常量的描述器

Ten 类是一个描述器,其 __get__() 方法总是返回常量 10

class Ten:
    def __get__(self, obj, objtype=None):
        return 10

要使用描述器,它必须作为一个类变量存储在另一个类中:

class A:
    x = 5                       # Regular class attribute
    y = Ten()                   # Descriptor instance

用交互式会话查看普通属性查找和描述器查找之间的区别:

>>> a = A()                     # Make an instance of class A
>>> a.x                         # Normal attribute lookup
5
>>> a.y                         # Descriptor lookup
10

a.x 属性查找中,点运算符会找到存储在类字典中的 'x': 5。 在 a.y 查找中,点运算符会根据描述器实例的 __get__ 方法将其识别出来,调用该方法并返回 10

请注意,值 10 既不存储在类字典中也不存储在实例字典中。相反,值 10 是在调用时才取到的。

这个简单的例子展示了一个描述器是如何工作的,但它不是很有用。在查找常量时,用常规属性查找会更好。

在下一节中,我们将创建更有用的东西,即动态查找。

动态查找

有趣的描述器通常运行计算而不是返回常量:

import os
class DirectorySize:
    def __get__(self, obj, objtype=None):
        return len(os.listdir(obj.dirname))
class Directory:
    size = DirectorySize()              # Descriptor instance
    def __init__(self, dirname):
        self.dirname = dirname          # Regular instance attribute

交互式会话显示查找是动态的,每次都会计算不同的,经过更新的返回值:

>>> s = Directory('songs')
>>> g = Directory('games')
>>> s.size                              # The songs directory has twenty files
20
>>> g.size                              # The games directory has three files
3
>>> os.remove('games/chess')            # Delete a game
>>> g.size                              # File count is automatically updated
2

除了说明描述器如何运行计算,这个例子也揭示了 __get__() 参数的目的。形参 self 接收的实参是 size*,即 *DirectorySize 的一个实例。形参 obj 接收的实参是 gs,即 Directory 的一个实例。而正是 obj__get__() 方法获得了作为目标的目录。形参 objtype 接收的实参是 Directory 类。

托管属性

描述器的一种流行用法是托管对实例数据的访问。描述器被分配给类字典中的公开属性,而实际数据作为私有属性存储在实例字典中。当访问公开属性时,会触发描述器的 __get__()__set__() 方法。

在下面的例子中,age 是公开属性,_age 是私有属性。当访问公开属性时,描述器会记录下查找或更新的日志:

import logging
logging.basicConfig(level=logging.INFO)
class LoggedAgeAccess:
    def __get__(self, obj, objtype=None):
        value = obj._age
        logging.info('Accessing %r giving %r', 'age', value)
        return value
    def __set__(self, obj, value):
        logging.info('Updating %r to %r', 'age', value)
        obj._age = value
class Person:
    age = LoggedAgeAccess()             # Descriptor instance
    def __init__(self, name, age):
        self.name = name                # Regular instance attribute
        self.age = age                  # Calls __set__()
    def birthday(self):
        self.age += 1                   # Calls both __get__() and __set__()

交互式会话展示中,对托管属性 age 的所有访问都被记录了下来,但常规属性 name 则未被记录:

>>> mary = Person('Mary M', 30)         # The initial age update is logged
INFO:root:Updating 'age' to 30
>>> dave = Person('David D', 40)
INFO:root:Updating 'age' to 40
>>> vars(mary)                          # The actual data is in a private attribute
{'name': 'Mary M', '_age': 30}
>>> vars(dave)
{'name': 'David D', '_age': 40}
>>> mary.age                            # Access the data and log the lookup
INFO:root:Accessing 'age' giving 30
30
>>> mary.birthday()                     # Updates are logged as well
INFO:root:Accessing 'age' giving 30
INFO:root:Updating 'age' to 31
>>> dave.name                           # Regular attribute lookup isn't logged
'David D'
>>> dave.age                            # Only the managed attribute is logged
INFO:root:Accessing 'age' giving 40
40

此示例的一个主要问题是私有名称 _age 在类 LoggedAgeAccess 中是硬耦合的。这意味着每个实例只能有一个用于记录的属性,并且其名称不可更改。

定制名称

当一个类使用描述器时,它可以告知每个描述器使用了什么变量名。

在此示例中, Person 类具有两个描述器实例 nameage*。当类 Person 被定义的时候,他回调了 *LoggedAccess 中的 __set_name__() 来记录字段名称,让每个描述器拥有自己的 public_nameprivate_name

import logging
logging.basicConfig(level=logging.INFO)
class LoggedAccess:
    def __set_name__(self, owner, name):
        self.public_name = name
        self.private_name = '_' + name
    def __get__(self, obj, objtype=None):
        value = getattr(obj, self.private_name)
        logging.info('Accessing %r giving %r', self.public_name, value)
        return value
    def __set__(self, obj, value):
        logging.info('Updating %r to %r', self.public_name, value)
        setattr(obj, self.private_name, value)
class Person:
    name = LoggedAccess()                # First descriptor instance
    age = LoggedAccess()                 # Second descriptor instance
    def __init__(self, name, age):
        self.name = name                 # Calls the first descriptor
        self.age = age                   # Calls the second descriptor
    def birthday(self):
        self.age += 1

交互交互式会话显示类 Person 调用了 __set_name__() 方法来记录字段的名称。在这里,我们调用 vars() 来查找描述器而不触发它:

>>> vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'}
>>> vars(vars(Person)['age'])
{'public_name': 'age', 'private_name': '_age'}

现在,新类会记录对 nameage 二者的访问:

>>> pete = Person('Peter P', 10)
INFO:root:Updating 'name' to 'Peter P'
INFO:root:Updating 'age' to 10
>>> kate = Person('Catherine C', 20)
INFO:root:Updating 'name' to 'Catherine C'
INFO:root:Updating 'age' to 20

这两个 Person 实例仅包含私有名称:

>>> vars(pete)
{'_name': 'Peter P', '_age': 10}
>>> vars(kate)
{'_name': 'Catherine C', '_age': 20}

结束语

descriptor 就是任何一个定义了 __get__()__set__()__delete__() 的对象。

可选地,描述器可以具有 __set_name__() 方法。这仅在描述器需要知道创建它的类或分配给它的类变量名称时使用。(即使该类不是描述器,只要此方法存在就会调用。)

在属性查找期间,描述器由点运算符调用。如果使用 vars(some_class)[descriptor_name] 间接访问描述器,则返回描述器实例而不调用它。

描述器仅在用作类变量时起作用。放入实例时,它们将失效。

描述器的主要目的是提供一个挂钩,允许存储在类变量中的对象控制在属性查找期间发生的情况。

传统上,调用类控制查找过程中发生的事情。描述器反转了这种关系,并允许正在被查询的数据对此进行干涉。

描述器的使用贯穿了整个语言。就是它让函数变成绑定方法。常见工具诸如 classmethod()staticmethod()property()functools.cached_property() 都作为描述器实现。

完整的实际例子

在此示例中,我们创建了一个实用而强大的工具来查找难以发现的数据损坏错误。

验证器类

验证器是一个用于托管属性访问的描述器。在存储任何数据之前,它会验证新值是否满足各种类型和范围限制。如果不满足这些限制,它将引发异常,从源头上防止数据损坏。

这个 Validator 类既是一个 abstract base class 也是一个托管属性描述器。

from abc import ABC, abstractmethod
class Validator(ABC):
    def __set_name__(self, owner, name):
        self.private_name = '_' + name
    def __get__(self, obj, objtype=None):
        return getattr(obj, self.private_name)
    def __set__(self, obj, value):
        self.validate(value)
        setattr(obj, self.private_name, value)
    @abstractmethod
    def validate(self, value):
        pass

自定义验证器需要从 Validator 继承,并且必须提供 validate() 方法以根据需要测试各种约束。

自定义验证器

这是三个实用的数据验证工具:

  1. OneOf 验证值是一组受约束的选项之一。
  2. Number 验证值是否为 intfloat。根据可选参数,它还可以验证值在给定的最小值或最大值之间。
  3. String 验证值是否为 str。根据可选参数,它可以验证给定的最小或最大长度。它还可以验证用户定义的 predicate。
class OneOf(Validator):
    def __init__(self, *options):
        self.options = set(options)
    def validate(self, value):
        if value not in self.options:
            raise ValueError(f'Expected {value!r} to be one of {self.options!r}')
class Number(Validator):
    def __init__(self, minvalue=None, maxvalue=None):
        self.minvalue = minvalue
        self.maxvalue = maxvalue
    def validate(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError(f'Expected {value!r} to be an int or float')
        if self.minvalue is not None and value < self.minvalue:
            raise ValueError(
                f'Expected {value!r} to be at least {self.minvalue!r}'
            )
        if self.maxvalue is not None and value > self.maxvalue:
            raise ValueError(
                f'Expected {value!r} to be no more than {self.maxvalue!r}'
            )
class String(Validator):
    def __init__(self, minsize=None, maxsize=None, predicate=None):
        self.minsize = minsize
        self.maxsize = maxsize
        self.predicate = predicate
    def validate(self, value):
        if not isinstance(value, str):
            raise TypeError(f'Expected {value!r} to be an str')
        if self.minsize is not None and len(value) < self.minsize:
            raise ValueError(
                f'Expected {value!r} to be no smaller than {self.minsize!r}'
            )
        if self.maxsize is not None and len(value) > self.maxsize:
            raise ValueError(
                f'Expected {value!r} to be no bigger than {self.maxsize!r}'
            )
        if self.predicate is not None and not self.predicate(value):
            raise ValueError(
                f'Expected {self.predicate} to be true for {value!r}'
            )

实际应用

这是在真实类中使用数据验证器的方法:

class Component:
    name = String(minsize=3, maxsize=10, predicate=str.isupper)
    kind = OneOf('wood', 'metal', 'plastic')
    quantity = Number(minvalue=0)
    def __init__(self, name, kind, quantity):
        self.name = name
        self.kind = kind
        self.quantity = quantity

描述器阻止无效实例的创建:

>>> Component('Widget', 'metal', 5)      # Blocked: 'Widget' is not all uppercase
Traceback (most recent call last):
    ...
ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'
>>> Component('WIDGET', 'metle', 5)      # Blocked: 'metle' is misspelled
Traceback (most recent call last):
    ...
ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}
>>> Component('WIDGET', 'metal', -5)     # Blocked: -5 is negative
Traceback (most recent call last):
    ...
ValueError: Expected -5 to be at least 0
>>> Component('WIDGET', 'metal', 'V')    # Blocked: 'V' isn't a number
Traceback (most recent call last):
    ...
TypeError: Expected 'V' to be an int or float
>>> c = Component('WIDGET', 'metal', 5)  # Allowed:  The inputs are valid

技术教程

接下来是专业性更强的技术教程,以及描述器工作原理的详细信息。

摘要

定义描述器,总结协议,并说明如何调用描述器。提供一个展示对象关系映射如何工作的示例。

学习描述器不仅能提供接触到更多工具集的途径,还能更深地理解 Python 工作的原理。

定义与介绍

一般而言,描述器是一个包含了描述器协议中的方法的属性值。 这些方法有 __get__(), __set__()__delete__()。 如果为某个属性定义了这些方法中的任意一个,它就可以被称为 descriptor。

属性访问的默认行为是从一个对象的字典中获取、设置或删除属性。对于实例来说,a.x 的查找顺序会从 a.__dict__['x'] 开始,然后是 type(a).__dict__['x'],接下来依次查找 type(a) 的方法解析顺序(MRO)。 如果找到的值是定义了某个描述器方法的对象,则 Python 可能会重写默认行为并转而发起调用描述器方法。这具体发生在优先级链的哪个环节则要根据所定义的描述器方法及其被调用的方式来决定。

描述器是一个强大而通用的协议。 它们是属性、方法、静态方法、类方法和 super() 背后的实现机制。 它们在 Python 内部被广泛使用。 描述器简化了底层的 C 代码并为 Python 的日常程序提供了一组灵活的新工具。

描述器协议

descr.__get__(self, obj, type=None) -> value
descr.__set__(self, obj, value) -> None
descr.__delete__(self, obj) -> None

描述器的方法就这些。一个对象只要定义了以上方法中的任何一个,就被视为描述器,并在被作为属性时覆盖其默认行为。

如果一个对象定义了 __set__()__delete__(),则它会被视为数据描述器。 仅定义了 __get__() 的描述器称为非数据描述器(它们经常被用于方法,但也可以有其他用途)。

数据和非数据描述器的不同之处在于,如何计算实例字典中条目的替代值。如果实例的字典具有与数据描述器同名的条目,则数据描述器优先。如果实例的字典具有与非数据描述器同名的条目,则该字典条目优先。

为了使数据描述器成为只读的,应该同时定义 __get__()__set__() ,并在 __set__() 中引发 AttributeError 。用引发异常的占位符定义 __set__() 方法使其成为数据描述器。

描述器调用概述

描述器可以通过 d.__get__(obj)desc.__get__(None, cls) 直接调用。

但更常见的是通过属性访问自动调用描述器。

表达式 obj.x 在命名空间的链中查找obj 的属性 x。如果搜索在实例 __dict__ 之外找到描述器,则根据下面列出的优先级规则调用其 __get__() 方法。

调用的细节取决于 obj 是对象、类还是超类的实例。

通过实例调用

实例查找通过命名空间链进行扫描,数据描述器的优先级最高,其次是实例变量、非数据描述器、类变量,最后是 __getattr__() (如果存在的话)。

如果 a.x 找到了一个描述器,那么将通过 desc.__get__(a, type(a)) 调用它。

点运算符的查找逻辑在 object.__getattribute__() 中。这里是一个等价的纯 Python 实现:

def object_getattribute(obj, name):
    "Emulate PyObject_GenericGetAttr() in Objects/object.c"
    null = object()
    objtype = type(obj)
    cls_var = getattr(objtype, name, null)
    descr_get = getattr(type(cls_var), '__get__', null)
    if descr_get is not null:
        if (hasattr(type(cls_var), '__set__')
            or hasattr(type(cls_var), '__delete__')):
            return descr_get(cls_var, obj, objtype)     # data descriptor
    if hasattr(obj, '__dict__') and name in vars(obj):
        return vars(obj)[name]                          # instance variable
    if descr_get is not null:
        return descr_get(cls_var, obj, objtype)         # non-data descriptor
    if cls_var is not null:
        return cls_var                                  # class variable
    raise AttributeError(name)

有趣的是,属性查找不会直接调用 object.__getattribute__() ,点运算符和 getattr() 函数均通过辅助函数执行属性查找:

def getattr_hook(obj, name):
    "Emulate slot_tp_getattr_hook() in Objects/typeobject.c"
    try:
        return obj.__getattribute__(name)
    except AttributeError:
        if not hasattr(type(obj), '__getattr__'):
            raise
    return type(obj).__getattr__(obj, name)             # __getattr__

因此,如果 __getattr__() 存在,则只要 __getattribute__() 引发 AttributeError (直接引发异常或在描述符调用中引发都一样),就会调用它。

同时,如果用户直接调用 object.__getattribute__() ,则 __getattr__() 的钩子将被绕开。

通过类调用

A.x 这样的点操作符查找的逻辑在 type.__getattribute__() 中。步骤与 object.__getattribute__() 相似,但是实例字典查找改为搜索类的 method resolution order。

如果找到了一个描述器,那么将通过 desc.__get__(None, A) 调用它。

完整的 C 实现可在 Objects/typeobject.c 中的 type_getattro()_PyType_Lookup() 找到。

通过 super 调用

super 的点操作符查找的逻辑在 super() 返回的对象的 __getattribute__() 方法中。

类似 super(A, obj).m 形式的点分查找将在 obj.__class__.__mro__ 中搜索紧接在 A 之后的基类 B,然后返回 B.__dict__['m'].__get__(obj, A)。如果 m 不是描述器,则直接返回其值。

完整的 C 实现可以在 Objects/typeobject.c 的 super_getattro() 中找到。纯 Python 等价实现可以在 Guido’s Tutorial 中找到。

调用逻辑总结

描述器的机制嵌入在 objecttypesuper()__getattribute__() 方法中。

要记住的重要点是:

  • 描述器由 __getattribute__() 方法调用。
  • 类从 objecttypesuper() 继承此机制。
  • 由于描述器的逻辑在 __getattribute__() 中,因而重写该方法会阻止描述器的自动调用。
  • object.__getattribute__()type.__getattribute__() 会用不同的方式调用 __get__()。前一个会传入实例,也可以包括类。后一个传入的实例为 None ,并且总是包括类。
  • 数据描述器始终会覆盖实例字典。
  • 非数据描述器会被实例字典覆盖。

自动名称通知

有时,描述器想知道它分配到的具体类变量名。创建新类时,元类 type 将扫描新类的字典。如果有描述器,并且它们定义了 __set_name__(),则使用两个参数调用该方法。owner 是使用描述器的类,name 是分配给描述器的类变量名。

实现的细节在 Objects/typeobject.c 中的 type_new()set_names()

由于更新逻辑在 type.__new__() 中,因此通知仅在创建类时发生。之后如果将描述器添加到类中,则需要手动调用 __set_name__()

ORM (对象关系映射)示例

以下代码展示了如何使用数据描述器来实现简单 object relational mapping 框架。

其核心思路是将数据存储在外部数据库中,Python 实例仅持有数据库表中对应的的键。描述器负责对值进行查找或更新:

class Field:
    def __set_name__(self, owner, name):
        self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
        self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'
    def __get__(self, obj, objtype=None):
        return conn.execute(self.fetch, [obj.key]).fetchone()[0]
    def __set__(self, obj, value):
        conn.execute(self.store, [value, obj.key])
        conn.commit()

我们可以用 Field 类来定义描述了数据库中每张表的模式的 models

class Movie:
    table = 'Movies'                    # Table name
    key = 'title'                       # Primary key
    director = Field()
    year = Field()
    def __init__(self, key):
        self.key = key
class Song:
    table = 'Music'
    key = 'title'
    artist = Field()
    year = Field()
    genre = Field()
    def __init__(self, key):
        self.key = key

要使用模型,首先要连接到数据库:

>>> import sqlite3
>>> conn = sqlite3.connect('entertainment.db')

交互式会话显示了如何从数据库中检索数据及如何对其进行更新:

>>> Movie('Star Wars').director
'George Lucas'
>>> jaws = Movie('Jaws')
>>> f'Released in {jaws.year} by {jaws.director}'
'Released in 1975 by Steven Spielberg'
>>> Song('Country Roads').artist
'John Denver'
>>> Movie('Star Wars').director = 'J.J. Abrams'
>>> Movie('Star Wars').director
'J.J. Abrams'

纯 Python 等价实现

描述器协议很简单,但它提供了令人兴奋的可能性。有几个用例非常通用,以至于它们已预先打包到内置工具中。属性、绑定方法、静态方法、类方法和 slots 均基于描述器协议。

属性

调用 property() 是构建数据描述器的简洁方式,该数据描述器在访问属性时触发函数调用。它的签名是:

property(fget=None, fset=None, fdel=None, doc=None) -> property

该文档显示了定义托管属性 x 的典型用法:

class C:
    def getx(self): return self.__x
    def setx(self, value): self.__x = value
    def delx(self): del self.__x
    x = property(getx, setx, delx, "I'm the 'x' property.")

要了解 property() 如何根据描述器协议实现,这里是一个纯 Python 的等价实现:

class Property:
    "Emulate PyProperty_Type() in Objects/descrobject.c"
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc
        self._name = ''
    def __set_name__(self, owner, name):
        self._name = name
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError(f'unreadable attribute {self._name}')
        return self.fget(obj)
    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError(f"can't set attribute {self._name}")
        self.fset(obj, value)
    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError(f"can't delete attribute {self._name}")
        self.fdel(obj)
    def getter(self, fget):
        prop = type(self)(fget, self.fset, self.fdel, self.__doc__)
        prop._name = self._name
        return prop
    def setter(self, fset):
        prop = type(self)(self.fget, fset, self.fdel, self.__doc__)
        prop._name = self._name
        return prop
    def deleter(self, fdel):
        prop = type(self)(self.fget, self.fset, fdel, self.__doc__)
        prop._name = self._name
        return prop

这个内置的 property() 每当用户访问属性时生效,随后的变化需要一个方法的参与。

例如,一个电子表格类可以通过 Cell('b10').value 授予对单元格值的访问权限。对程序的后续改进要求每次访问都要重新计算单元格;但是,程序员不希望影响直接访问该属性的现有客户端代码。解决方案是将对 value 属性的访问包装在属性数据描述器中:

class Cell:
    ...
    @property
    def value(self):
        "Recalculate the cell before returning value"
        self.recalc()
        return self._value

在此示例中,内置的 property() 或我们实现的的 Property() 均适用。

函数和方法

Python 的面向对象功能是在基于函数的环境构建的。通过使用非数据描述器,这两方面完成了无缝融合。

在调用时,存储在类词典中的函数将被转换为方法。方法与常规函数的不同之处仅在于对象实例被置于其他参数之前。方法与常规函数的不同之处仅在于第一个参数是为对象实例保留的。按照惯例,实例引用称为 self ,但也可以称为 this 或任何其他变量名称。

可以使用 types.MethodType 手动创建方法,其行为基本等价于:

class MethodType:
    "Emulate PyMethod_Type in Objects/classobject.c"
    def __init__(self, func, obj):
        self.__func__ = func
        self.__self__ = obj
    def __call__(self, *args, **kwargs):
        func = self.__func__
        obj = self.__self__
        return func(obj, *args, **kwargs)

为了支持自动创建方法,函数包含 __get__() 方法以便在属性访问时绑定其为方法。这意味着函数其是非数据描述器,它在通过实例进行点查找时返回绑定方法,其运作方式如下:

class Function:
    ...
    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        if obj is None:
            return self
        return MethodType(self, obj)

在解释器中运行以下类,这显示了函数描述器的实际工作方式:

class D:
    def f(self, x):
         return x

该函数具有 qualified name 属性以支持自省:

>>> D.f.__qualname__
'D.f'

通过类字典访问函数不会调用 __get__()。相反,它只返回基础函数对象:

>>> D.__dict__['f']
<function D.f at 0x00C45070>

来自类的点运算符访问会调用 __get__(),直接返回底层的函数。

>>> D.f
<function D.f at 0x00C45070>

有趣的行为发生在从实例进行点访问期间。点运算符查找调用 __get__(),返回绑定的方法对象:

>>> d = D()
>>> d.f
<bound method D.f of <__main__.D object at 0x00B18C90>>

绑定方法在内部存储了底层函数和绑定的实例:

>>> d.f.__func__
<function D.f at 0x00C45070>
>>> d.f.__self__
<__main__.D object at 0x1012e1f98>

如果你曾好奇常规方法中的 self 或类方法中的 cls 是从什么地方来的,就是这里了!

方法的种类

非数据描述器为把函数绑定为方法的通常模式提供了一种简单的机制。

概括地说,函数对象具有 __get__() 方法,以便在作为属性访问时可以将其转换为方法。非数据描述器将 obj.f(*args) 的调用会被转换为 f(obj, *args) 。调用 klass.f(args)因而变成f(args)` 。

下表总结了绑定及其两个最有用的变体:

转换形式 通过对象调用 通过类调用
function — 函数 f(obj, args) f(args)
静态方法 f(args) f(args)
类方法 f(type(obj), args) f(cls, args)

静态方法

静态方法返回底层函数,不做任何更改。调用 c.fC.f 等效于通过 object.__getattribute__(c, "f")object.__getattribute__(C, "f") 查找。这样该函数就可以从对象或类中进行相同的访问。

适合作为静态方法的是那些不引用 self 变量的方法。

例如,一个统计用的包可能包含一个实验数据的容器类。该容器类提供了用于计算数据的平均值,均值,中位数和其他描述性统计信息的常规方法。但是,可能有在概念上相关但不依赖于数据的函数。例如, erf(x) 是在统计中的便捷转换,但并不直接依赖于特定的数据集。可以从对象或类中调用它: s.erf(1.5) --> .9332Sample.erf(1.5) --> .9332

由于静态方法返回的底层函数没有任何变化,因此示例调用也是意料之中:

class E:
    @staticmethod
    def f(x):
        return x * 10

使用非数据描述器,纯 Python 版本的 staticmethod() 如下所示:

>>> E.f(3)
30
>>> E().f(3)
30

类方法

与静态方法不同,类方法在调用函数之前将类引用放在参数列表的最前。无论调用方是对象还是类,此格式相同:

class F:
    @classmethod
    def f(cls, x):
        return cls.__name__, x

>>> F.f(3)
('F', 3)
>>> F().f(3)
('F', 3)

当方法仅需要具有类引用并且确实依赖于存储在特定实例中的数据时,此行为就很有用。类方法的一种用途是创建备用类构造函数。例如,类方法 dict.fromkeys() 从键列表创建一个新字典。纯 Python 的等价实现是:

class Dict(dict):
    @classmethod
    def fromkeys(cls, iterable, value=None):
        "Emulate dict_fromkeys() in Objects/dictobject.c"
        d = cls()
        for key in iterable:
            d[key] = value
        return d

现在可以这样构造一个新的唯一键字典:

>>> d = Dict.fromkeys('abracadabra')
>>> type(d) is Dict
True
>>> d
{'a': None, 'b': None, 'r': None, 'c': None, 'd': None}

使用非数据描述器协议,纯 Python 版本的 classmethod() 如下:

class ClassMethod:
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"
    def __init__(self, f):
        self.f = f
    def __get__(self, obj, cls=None):
        if cls is None:
            cls = type(obj)
        if hasattr(type(self.f), '__get__'):
            return self.f.__get__(cls)
        return MethodType(self.f, cls)

hasattr(type(self.f), '__get__') 的代码路径在 Python 3.9 中被加入并让 classmethod() 可以支持链式装饰器。 例如,一个类方法和特征属性可以被链接在一起:

class G:
    @classmethod
    @property
    def __doc__(cls):
        return f'A doc for {cls.__name__!r}'

>>> G.__doc__
"A doc for 'G'"

成员对象和 slots

当一个类定义了 __slots__,它会用一个固定长度的 slot 值数组来替换实例字典。 从用户的视角看,效果是这样的:

  1. Provides immediate detection of bugs due to misspelled attribute assignments. Only attribute names specified in __slots__ are allowed:
class Vehicle:
    __slots__ = ('id_number', 'make', 'model')

>>> auto = Vehicle()
>>> auto.id_nubmer = 'VYE483814LQEX'
Traceback (most recent call last):
    ...
AttributeError: 'Vehicle' object has no attribute 'id_nubmer'
  1. Helps create immutable objects where descriptors manage access to private attributes stored in __slots__:
class Immutable:
    __slots__ = ('_dept', '_name')          # Replace the instance dictionary
    def __init__(self, dept, name):
        self._dept = dept                   # Store to private attribute
        self._name = name                   # Store to private attribute
    @property                               # Read-only descriptor
    def dept(self):
        return self._dept
    @property
    def name(self):                         # Read-only descriptor
        return self._name

>>> mark = Immutable('Botany', 'Mark Watney')
>>> mark.dept
'Botany'
>>> mark.dept = 'Space Pirate'
Traceback (most recent call last):
    ...
AttributeError: can't set attribute
>>> mark.location = 'Mars'
Traceback (most recent call last):
    ...
AttributeError: 'Immutable' object has no attribute 'location'
  1. Saves memory. On a 64-bit Linux build, an instance with two attributes takes 48 bytes with __slots__ and 152 bytes without. This flyweight design pattern likely only matters when a large number of instances are going to be created.

  2. Improves speed. Reading instance variables is 35% faster with __slots__ (as measured with Python 3.10 on an Apple M1 processor).

  3. Blocks tools like functools.cached_property() which require an instance dictionary to function correctly:

from functools import cached_property
class CP:
    __slots__ = ()                          # Eliminates the instance dict
    @cached_property                        # Requires an instance dict
    def pi(self):
        return 4 * sum((-1.0)**n / (2.0*n + 1.0)
                       for n in reversed(range(100_000)))

>>> CP().pi
Traceback (most recent call last):
  ...
TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.

要创建一个一模一样的纯 Python 版的 __slots__ 是不可能的,因为它需要直接访问 C 结构体并控制对象内存分配。 但是,我们可以构建一个非常相似的模拟版,其中作为 slot 的实际 C 结构体由一个私有的 _slotvalues 列表来模拟。 对该私有结构体的读写操作将由成员描述器来管理:

null = object()
class Member:
    def __init__(self, name, clsname, offset):
        'Emulate PyMemberDef in Include/structmember.h'
        # Also see descr_new() in Objects/descrobject.c
        self.name = name
        self.clsname = clsname
        self.offset = offset
    def __get__(self, obj, objtype=None):
        'Emulate member_get() in Objects/descrobject.c'
        # Also see PyMember_GetOne() in Python/structmember.c
        value = obj._slotvalues[self.offset]
        if value is null:
            raise AttributeError(self.name)
        return value
    def __set__(self, obj, value):
        'Emulate member_set() in Objects/descrobject.c'
        obj._slotvalues[self.offset] = value
    def __delete__(self, obj):
        'Emulate member_delete() in Objects/descrobject.c'
        value = obj._slotvalues[self.offset]
        if value is null:
            raise AttributeError(self.name)
        obj._slotvalues[self.offset] = null
    def __repr__(self):
        'Emulate member_repr() in Objects/descrobject.c'
        return f'<Member {self.name!r} of {self.clsname!r}>'

type.__new__() 方法负责将成员对象添加到类变量:

class Type(type):
    'Simulate how the type metaclass adds member objects for slots'
    def __new__(mcls, clsname, bases, mapping):
        'Emuluate type_new() in Objects/typeobject.c'
        # type_new() calls PyTypeReady() which calls add_methods()
        slot_names = mapping.get('slot_names', [])
        for offset, name in enumerate(slot_names):
            mapping[name] = Member(name, clsname, offset)
        return type.__new__(mcls, clsname, bases, mapping)

object.__new__() 方法负责创建具有 slot 而非实例字典的实例。 以下是一个纯 Python 的粗略模拟版:

class Object:
    'Simulate how object.__new__() allocates memory for __slots__'
    def __new__(cls, *args):
        'Emulate object_new() in Objects/typeobject.c'
        inst = super().__new__(cls)
        if hasattr(cls, 'slot_names'):
            empty_slots = [null] * len(cls.slot_names)
            object.__setattr__(inst, '_slotvalues', empty_slots)
        return inst
    def __setattr__(self, name, value):
        'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
        cls = type(self)
        if hasattr(cls, 'slot_names') and name not in cls.slot_names:
            raise AttributeError(
                f'{type(self).__name__!r} object has no attribute {name!r}'
            )
        super().__setattr__(name, value)
    def __delattr__(self, name):
        'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
        cls = type(self)
        if hasattr(cls, 'slot_names') and name not in cls.slot_names:
            raise AttributeError(
                f'{type(self).__name__!r} object has no attribute {name!r}'
            )
        super().__delattr__(name)

要在真实的类中使用这个模拟版,只需从 Object 继承并将 metaclass 设为 Type

class H(Object, metaclass=Type):
    'Instance variables stored in slots'
    slot_names = ['x', 'y']
    def __init__(self, x, y):
        self.x = x
        self.y = y

这时,metaclass 已经为 xy 加载了成员对象:

>>> from pprint import pp
>>> pp(dict(vars(H)))
{'__module__': '__main__',
 '__doc__': 'Instance variables stored in slots',
 'slot_names': ['x', 'y'],
 '__init__': <function H.__init__ at 0x7fb5d302f9d0>,
 'x': <Member 'x' of 'H'>,
 'y': <Member 'y' of 'H'>}

当实例被创建时,它们将拥有一个用于存放属性的 slot_values 列表:

>>> h = H(10, 20)
>>> vars(h)
{'_slotvalues': [10, 20]}
>>> h.x = 55
>>> vars(h)
{'_slotvalues': [55, 20]}

错误拼写或未赋值的属性将引发一个异常:

>>> h.xz
Traceback (most recent call last):
    ...
AttributeError: 'H' object has no attribute 'xz'

函数式编程指引

提供恰当的 Python 函数式编程范例,在函数式编程简单的介绍之后,将简单介绍Python中关于函数式编程的特性如 iterator 和 generator 以及相关库模块如 itertoolsfunctools 等。

概述

编程语言支持通过以下几种方式来解构具体问题:

  • 大多数的编程语言都是 过程式 的,所谓程序就是一连串告诉计算机怎样处理程序输入的指令。C、Pascal 甚至 Unix shells 都是过程式语言。
  • 声明式 语言中,你编写一个用来描述待解决问题的说明,并且这个语言的具体实现会指明怎样高效的进行计算。 SQL 可能是你最熟悉的声明式语言了。 一个 SQL 查询语句描述了你想要检索的数据集,并且 SQL 引擎会决定是扫描整张表还是使用索引,应该先执行哪些子句等等。
  • 面向对象 程序会操作一组对象。 对象拥有内部状态,并能够以某种方式支持请求和修改这个内部状态的方法。Smalltalk 和 Java 都是面向对象的语言。 C++ 和 Python 支持面向对象编程,但并不强制使用面向对象特性。
  • 函数式 编程则将一个问题分解成一系列函数。 理想情况下,函数只接受输入并输出结果,对一个给定的输入也不会有影响输出的内部状态。 著名的函数式语言有 ML 家族(Standard ML,Ocaml 以及其他变种)和 Haskell。

一些语言的设计者选择强调一种特定的编程方式。 这通常会让以不同的方式来编写程序变得困难。其他多范式语言则支持几种不同的编程方式。Lisp,C++ 和 Python 都是多范式语言;使用这些语言,你可以编写主要为过程式,面向对象或者函数式的程序和函数库。在大型程序中,不同的部分可能会采用不同的方式编写;比如 GUI 可能是面向对象的而处理逻辑则是过程式或者函数式。

在函数式程序里,输入会流经一系列函数。每个函数接受输入并输出结果。函数式风格反对使用带有副作用的函数,这些副作用会修改内部状态,或者引起一些无法体现在函数的返回值中的变化。完全不产生副作用的函数被称作“纯函数”。消除副作用意味着不能使用随程序运行而更新的数据结构;每个函数的输出必须只依赖于输入。

一些语言对纯洁性要求非常严格,以至于没有像 a=3c = a + b 这样的赋值表达式,但是完全消除副作用非常困难。 比如,显示在屏幕上或者写到磁盘文件中都是副作用。举个例子,在 Python 里,调用函数 print() 或者 time.sleep() 并不会返回有用的结果;它们的用途只在于副作用,向屏幕发送一段文字或暂停一秒钟。

函数式风格的 Python 程序并不会极端到消除所有 I/O 或者赋值的程度;相反,他们会提供像函数式一样的接口,但会在内部使用非函数式的特性。比如,函数的实现仍然会使用局部变量,但不会修改全局变量或者有其他副作用。

函数式编程可以被认为是面向对象编程的对立面。对象就像是颗小胶囊,包裹着内部状态和随之而来的能让你修改这个内部状态的一组调用方法,以及由正确的状态变化所构成的程序。函数式编程希望尽可能地消除状态变化,只和流经函数的数据打交道。在 Python 里你可以把两种编程方式结合起来,在你的应用(电子邮件信息,事务处理)中编写接受和返回对象实例的函数。

函数式设计在工作中看起来是个奇怪的约束。为什么你要消除对象和副作用呢?不过函数式风格有其理论和实践上的优点:

  • 形式证明。
  • 模块化。
  • 组合性。
  • 易于调试和测试。

形式证明

一个理论上的优点是,构造数学证明来说明函数式程序是正确的相对更容易些。

很长时间,研究者们对寻找证明程序正确的数学方法都很感兴趣。这和通过大量输入来测试,并得出程序的输出基本正确,或者阅读一个程序的源代码然后得出代码看起来没问题不同;相反,这里的目标是一个严格的证明,证明程序对所有可能的输入都能给出正确的结果。

证明程序正确性所用到的技术是写出 不变量,也就是对于输入数据和程序中的变量永远为真的特性。然后对每行代码,你说明这行代码执行前的不变量 X 和 Y 以及执行后稍有不同的不变量 X’ 和 Y’ 为真。如此一直到程序结束,这时候在程序的输出上,不变量应该会与期望的状态一致。

函数式编程之所以要消除赋值,是因为赋值在这个技术中难以处理;赋值可能会破坏赋值前为真的不变量,却并不产生任何可以传递下去的新的不变量。

不幸的是,证明程序的正确性很大程度上是经验性质的,而且和 Python 软件无关。即使是微不足道的程序都需要几页长的证明;一个中等复杂的程序的正确性证明会非常庞大,而且,极少甚至没有你日常所使用的程序(Python 解释器,XML 解析器,浏览器)的正确性能够被证明。即使你写出或者生成一个证明,验证证明也会是一个问题;里面可能出了差错,而你错误地相信你证明了程序的正确性。

模块化

函数式编程的一个更实用的优点是,它强制你把问题分解成小的方面。因此程序会更加模块化。相对于一个进行了复杂变换的大型函数,一个小的函数更明确,更易于编写, 也更易于阅读和检查错误。

易于调试和测试

测试和调试函数式程序相对来说更容易。

调试很简单是因为函数通常都很小而且清晰明确。当程序无法工作的时候,每个函数都是一个可以检查数据是否正确的接入点。你可以通过查看中间输入和输出迅速找到出错的函数。

测试更容易是因为每个函数都是单元测试的潜在目标。在执行测试前,函数并不依赖于需要重现的系统状态;相反,你只需要给出正确的输入,然后检查输出是否和期望的结果一致。

组合性

当你编写函数式风格的程序时,你会写出很多带有不同输入和输出的函数。其中一些不可避免地会局限于特定的应用,但其他的却可以广泛的用在程序中。举例来说,一个接受文件夹目录返回所有文件夹中的 XML 文件的函数; 或是一个接受文件名,然后返回文件内容的函数,都可以应用在很多不同的场合。

久而久之你会形成一个个人工具库。通常你可以重新组织已有的函数来组成新的程序,然后为当前的工作写一些特殊的函数。

迭代器

我会从 Python 的一个语言特性, 编写函数式风格程序的重要基石开始说起:迭代器。

迭代器是一个表示数据流的对象;这个对象每次只返回一个元素。Python 迭代器必须支持 __next__() 方法;这个方法不接受参数,并总是返回数据流中的下一个元素。如果数据流中没有元素,__next__() 会抛出 StopIteration 异常。迭代器未必是有限的;完全有理由构造一个输出无限数据流的迭代器。

内置的 iter() 函数接受任意对象并试图返回一个迭代器来输出对象的内容或元素,并会在对象不支持迭代的时候抛出 TypeError 异常。Python 有几种内置数据类型支持迭代,最常见的就是列表和字典。如果一个对象能生成迭代器,那么它就会被称作 iterable。

你可以手动试验迭代器的接口。

>>> L = [1, 2, 3]
>>> it = iter(L)
>>> it  
<...iterator object at ...>
>>> it.__next__()  # same as next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

Python 有不少要求使用可迭代的对象的地方,其中最重要的就是 for 表达式。在表达式 for X in Y,Y 要么自身是一个迭代器,要么能够由 iter() 创建一个迭代器。以下两种表达是等价的:

for i in iter(obj):
    print(i)
for i in obj:
    print(i)

可以用 list()tuple() 这样的构造函数把迭代器具体化成列表或元组:

>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> t = tuple(iterator)
>>> t
(1, 2, 3)

序列的解压操作也支持迭代器:如果你知道一个迭代器能够返回 N 个元素,你可以把他们解压到有 N 个元素的元组:

>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> a, b, c = iterator
>>> a, b, c
(1, 2, 3)

max()min() 这样的内置函数可以接受单个迭代器参数,然后返回其中最大或者最小的元素。"in""not in" 操作也支持迭代器:如果能够在迭代器 iterator 返回的数据流中找到 X 的话,则X in iterator 为真。很显然,如果迭代器是无限的,这么做你就会遇到问题;max()min() 永远也不会返回;如果元素 X 也不出现在数据流中,"in""not in" 操作同样也永远不会返回。

注意你只能在迭代器中顺序前进;没有获取前一个元素的方法,除非重置迭代器,或者重新复制一份。迭代器对象可以提供这些额外的功能,但迭代器协议只明确了 __next__() 方法。函数可能因此而耗尽迭代器的输出,如果你要对同样的数据流做不同的操作,你必须重新创建一个迭代器。

支持迭代器的数据类型

我们已经知道列表和元组支持迭代器。实际上,Python 中的任何序列类型,比如字符串,都自动支持创建迭代器。

对字典调用 iter() 会返回一个遍历字典的键的迭代器:

>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
...      'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for key in m:
...     print(key, m[key])
Jan 1
Feb 2
Mar 3
Apr 4
May 5
Jun 6
Jul 7
Aug 8
Sep 9
Oct 10
Nov 11
Dec 12

注意从 Python 3.7 开始,字典的遍历顺序一定和输入顺序一样。先前的版本并没有明确这一点,所以不同的实现可能不一致。

对字典使用 iter() 总是会遍历键,但字典也有返回其他迭代器的方法。如果你只遍历值或者键/值对,你可以明确地调用 values()items() 方法得到合适的迭代器。

dict() 构造函数可以接受一个迭代器,然后返回一个有限的 (key, value) 元组的数据流:

>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}

文件也可以通过调用 readline() 来遍历,直到穷尽文件中所有的行。这意味着你可以像这样读取文件中的每一行:

for line in file:
    # do something for each line
    ...

集合可以从可遍历的对象获取内容,也可以让你遍历集合的元素:

S = {2, 3, 5, 7, 11, 13}
for i in S:
    print(i)

生成器表达式和列表推导式

迭代器的输出有两个很常见的使用方式,1) 对每一个元素执行操作,2) 选择一个符合条件的元素子集。比如,给定一个字符串列表,你可能想去掉每个字符串尾部的空白字符,或是选出所有包含给定子串的字符串。

列表推导式和生成器表达时(简写:”listcomps” 和 “genexps”)让这些操作更加简明,这个形式借鉴自函数式程序语言 Haskell(https://www.haskell.org/)。你可以用以下代码去掉一个字符串流中的所有空白字符:

line_list = ['  line 1\n', 'line 2  \n', ...]
# Generator expression -- returns iterator
stripped_iter = (line.strip() for line in line_list)
# List comprehension -- returns list
stripped_list = [line.strip() for line in line_list]

你可以加上条件语句 "if" 来选取特定的元素:

stripped_list = [line.strip() for line in line_list
                 if line != ""]

通过列表推导式,你会获得一个 Python 列表;stripped_list 就是一个包含所有结果行的列表,并不是迭代器。 生成器表达式会返回一个迭代器,它在必要的时候计算结果,避免一次性生成所有的值。 这意味着,如果迭代器返回一个无限数据流或者大量的数据,列表推导式就不太好用了。 这种情况下生成器表达式会更受青睐。

生成器表达式两边使用圆括号 (“()”) ,而列表推导式则使用方括号 (“[]“)。生成器表达式的形式为:

( expression for expr in sequence1
             if condition1
             for expr2 in sequence2
             if condition2
             for expr3 in sequence3 ...
             if condition3
             for exprN in sequenceN
             if conditionN )

再次说明,列表推导式只有两边的括号不一样(方括号而不是圆括号)。

这些生成用于输出的元素会成为 expression 的后继值。其中 if 语句是可选的;如果给定的话 expression 只会在符合条件时计算并加入到结果中。

生成器表达式总是写在圆括号里面,不过也可以算上调用函数时用的括号。如果你想即时创建一个传递给函数的迭代器,可以这么写:

obj_total = sum(obj.count for obj in list_all_objects())

其中 for...in 语句包含了将要遍历的序列。这些序列并不必须同样长,因为它们会从左往右开始遍历,而 不是 同时执行。对每个 sequence1 中的元素,sequence2 会从头开始遍历。sequence3 会对每个 sequence1sequence2 的元素对开始遍历。

换句话说,列表推导式器是和下面的 Python 代码等价:

for expr1 in sequence1:
    if not (condition1):
        continue   # Skip this element
    for expr2 in sequence2:
        if not (condition2):
            continue   # Skip this element
        ...
        for exprN in sequenceN:
            if not (conditionN):
                continue   # Skip this element
            # Output the value of
            # the expression.

这说明,如果有多个 for...in 语句而没有 if 语句,输出结果的长度就是所有序列长度的乘积。如果你的两个列表长度为3,那么输出的列表长度就是9:

>>> seq1 = 'abc'
>>> seq2 = (1, 2, 3)
>>> [(x, y) for x in seq1 for y in seq2]  
[('a', 1), ('a', 2), ('a', 3),
 ('b', 1), ('b', 2), ('b', 3),
 ('c', 1), ('c', 2), ('c', 3)]

为了不让 Python 语法变得含糊,如果 expression 会生成元组,那这个元组必须要用括号括起来。下面第一个列表推导式语法错误,第二个则是正确的:

# Syntax error
[x, y for x in seq1 for y in seq2]
# Correct
[(x, y) for x in seq1 for y in seq2]

生成器

生成器是一类用来简化编写迭代器工作的特殊函数。普通的函数计算并返回一个值,而生成器返回一个能返回数据流的迭代器。

毫无疑问,你已经对如何在 Python 和 C 中调用普通函数很熟悉了,这时候函数会获得一个创建局部变量的私有命名空间。当函数到达 return 表达式时,局部变量会被销毁然后把返回给调用者。之后调用同样的函数时会创建一个新的私有命名空间和一组全新的局部变量。但是,如果在退出一个函数时不扔掉局部变量会如何呢?如果稍后你能够从退出函数的地方重新恢复又如何呢?这就是生成器所提供的;他们可以被看成可恢复的函数。

这里有简单的生成器函数示例:

>>> def generate_ints(N):
...    for i in range(N):
...        yield i

任何包含了 yield 关键字的函数都是生成器函数;Python 的 bytecode 编译器会在编译的时候检测到并因此而特殊处理。

当你调用一个生成器函数,它并不会返回单独的值,而是返回一个支持生成器协议的生成器对象。当执行 yield 表达式时,生成器会输出 i 的值,就像 return 表达式一样。yieldreturn 最大的区别在于,到达 yield 的时候生成器的执行状态会挂起并保留局部变量。在下一次调用生成器 __next__() 方法的时候,函数会恢复执行。

这里有一个 generate_ints() 生成器的示例:

>>> gen = generate_ints(3)
>>> gen  
<generator object generate_ints at ...>
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
  File "stdin", line 1, in <module>
  File "stdin", line 2, in generate_ints
StopIteration

同样,你可以写出 for i in generate_ints(5),或者 a, b, c = generate_ints(3)

在生成器函数里面,return value 会触发从 __next__() 方法抛出 StopIteration(value) 异常。一旦抛出这个异常,或者函数结束,处理数据的过程就会停止,生成器也不会再生成新的值。

你可以手动编写自己的类来达到生成器的效果,把生成器的所有局部变量作为实例的成员变量存储起来。比如,可以这么返回一个整数列表:把 self.count 设为0,然后通过 count()`。然而,对于一个中等复杂程度的生成器,写出一个相应的类可能会相当繁杂。

包含在 Python 库中的测试套件 Lib/test/test_generators.py 里有很多非常有趣的例子。这里是一个用生成器实现树的递归中序遍历示例。:

# A recursive generator that generates Tree leaves in in-order.
def inorder(t):
    if t:
        for x in inorder(t.left):
            yield x
        yield t.label
        for x in inorder(t.right):
            yield x

另外两个 test_generators.py 中的例子给出了 N 皇后问题(在 NxN 的棋盘上放置 N 个皇后,任何一个都不能吃掉另一个),以及马的遍历路线(在NxN 的棋盘上给马找出一条不重复的走过所有格子的路线)的解。

向生成器传递值

在 Python 2.4 及之前的版本中,生成器只产生输出。一旦调用生成器的代码创建一个迭代器,就没有办法在函数恢复执行的时候向它传递新的信息。你可以设法实现这个功能,让生成器引用一个全局变量或者一个调用者可以修改的可变对象,但是这些方法都很繁杂。

在 Python 2.5 里有一个简单的将值传递给生成器的方法。yield 变成了一个表达式,返回一个可以赋给变量或执行操作的值:

val = (yield i)

我建议你在处理 yield 表达式返回值的时候, 总是 两边写上括号,就像上面的例子一样。括号并不总是必须的,但是比起记住什么时候需要括号,写出来会更容易一点。

PEP 342 解释了具体的规则,也就是 yield 表达式必须括起来,除非是出现在最顶级的赋值表达式的右边。这意味着你可以写 val = yield i,但是必须在操作的时候加上括号,就像val = (yield i) + 12

可以调用 send(value)() <generator.send> 方法向生成器发送值。这个方法会恢复执行生成器的代码,然后 yield 表达式返回特定的值。如果调用普通的 __next__方法,``yield()会返回None`.

这里有一个简单的每次加1的计数器,并允许改变内部计数器的值。

def counter(maximum):
    i = 0
    while i < maximum:
        val = (yield i)
        # If value provided, change counter
        if val is not None:
            i = val
        else:
            i += 1

这是改变计数器的一个示例

>>> it = counter(10)  
>>> next(it)  
0
>>> next(it)  
1
>>> it.send(8)  
8
>>> next(it)  
9
>>> next(it)  
Traceback (most recent call last):
  File "t.py", line 15, in <module>
    it.next()
StopIteration

因为 yield 很多时候会返回 None,所以你应该总是检查这个情况。不要在表达式中使用 yield 的值,除非你确定 send() 是唯一的用来恢复你的生成器函数的方法。

除了 send() 之外,生成器还有两个其他的方法:

  • throw(type, value=None, traceback=None) 用于在生成器内部抛出异常;这个异常会在生成器暂停执行的时候由 yield 表达式抛出。

  • generator.close() 会在生成器内部抛出 GeneratorExit 异常来结束迭代。当接收到这个异常时,生成器的代码会抛出 GeneratorExit 或者 StopIteration;捕捉这个异常作其他处理是非法的,并会出发 RuntimeErrorclose() 也会在 Python 垃圾回收器回收生成器的时候调用。

    如果你要在 GeneratorExit 发生的时候清理代码,我建议使用 try: ... finally: 组合来代替 GeneratorExit

这些改变的累积效应是,让生成器从单向的信息生产者变成了既是生产者,又是消费者。

生成器也可以成为 协程 ,一种更广义的子过程形式。子过程可以从一个地方进入,然后从另一个地方退出(从函数的顶端进入,从 return 语句退出),而协程可以进入,退出,然后在很多不同的地方恢复(yield 语句)。

内置函数

我们可以看看迭代器常常用到的函数的更多细节。

Python 内置的两个函数 map()filter() 复制了生成器表达式的两个特性:

map(f, iterA, iterB, ...) 返回一个遍历序列的迭代器

f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ....

>>> def upper(s):
...     return s.upper()


>>> list(map(upper, ['sentence', 'fragment']))
['SENTENCE', 'FRAGMENT']
>>> [upper(s) for s in ['sentence', 'fragment']]
['SENTENCE', 'FRAGMENT']

你当然也可以用列表推导式达到同样的效果。

filter(predicate, iter) 返回一个遍历序列中满足指定条件的元素的迭代器,和列表推导式的功能相似。 predicate (谓词)是一个在特定条件下返回真值的函数;要使用函数 filter(),谓词函数必须只能接受一个参数。

>>> def is_even(x):
...     return (x % 2) == 0

>>> list(filter(is_even, range(10)))
[0, 2, 4, 6, 8]

这也可以写成列表推导式:

>>> list(x for x in range(10) if is_even(x))
[0, 2, 4, 6, 8]

enumerate(iter, start=0) 计数可迭代对象中的元素,然后返回包含每个计数(从 start 开始)和元素两个值的元组。:

>>> for item in enumerate(['subject', 'verb', 'object']):
...     print(item)
(0, 'subject')
(1, 'verb')
(2, 'object')

enumerate() 常常用于遍历列表并记录达到特定条件时的下标:

f = open('data.txt', 'r')
for i, line in enumerate(f):
    if line.strip() == '':
        print('Blank line at line #%i' % i)

sorted(iterable, key=None, reverse=False) 会将 iterable 中的元素收集到一个列表中,然后排序并返回结果。其中 keyreverse 参数会传递给所创建列表的 sort() 方法。:

>>> import random
>>> # Generate 8 random numbers between [0, 10000)
>>> rand_list = random.sample(range(10000), 8)
>>> rand_list  
[769, 7953, 9828, 6431, 8442, 9878, 6213, 2207]
>>> sorted(rand_list)  
[769, 2207, 6213, 6431, 7953, 8442, 9828, 9878]
>>> sorted(rand_list, reverse=True)  
[9878, 9828, 8442, 7953, 6431, 6213, 2207, 769]

内置函数 any(iter)all(iter) 会查看一个可迭代对象内容的逻辑值。any() 在可迭代对象中任意一个元素为真时返回 True,而 all() 在所有元素为真时返回 True:

>>> any([0, 1, 0])
True
>>> any([0, 0, 0])
False
>>> any([1, 1, 1])
True
>>> all([0, 1, 0])
False
>>> all([0, 0, 0])
False
>>> all([1, 1, 1])
True

zip(iterA, iterB, ...) 从每个可迭代对象中选取单个元素组成列表并返回:

zip(['a', 'b', 'c'], (1, 2, 3)) =>
  ('a', 1), ('b', 2), ('c', 3)

它并不会在内存创建一个列表并因此在返回前而耗尽输入的迭代器;相反,只有在被请求的时候元组才会创建并返回。

这个迭代器设计用于长度相同的可迭代对象。如果可迭代对象的长度不一致,返回的数据流的长度会和最短的可迭代对象相同

zip(['a', 'b'], (1, 2, 3)) =>
  ('a', 1), ('b', 2)

然而,你应该避免这种情况,因为所有从更长的迭代器中取出的元素都会被丢弃。这意味着之后你也无法冒着跳过被丢弃元素的风险来继续使用这个迭代器。

itertools 模块

itertools 模块包含很多常用的迭代器以及用来组合迭代器的函数。本节会用些小的例子来介绍这个模块的内容。

这个模块里的函数大致可以分为几类:

  • 从已有的迭代器创建新的迭代器的函数。
  • 接受迭代器元素作为参数的函数。
  • 选取部分迭代器输出的函数。
  • 给迭代器输出分组的函数。

创建新的迭代器

itertools.count(start, step) 返回一个等分的无限数据流。初始值默认为0,间隔默认为1,你也选择可以指定初始值和间隔:

itertools.count() =>
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
itertools.count(10) =>
  10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.count(10, 5) =>
  10, 15, 20, 25, 30, 35, 40, 45, 50, 55, ...

itertools.cycle(iter) 保存一份所提供的可迭代对象的副本,并返回一个能产生整个可迭代对象序列的新迭代器。新迭代器会无限重复这些元素。:

itertools.cycle([1, 2, 3, 4, 5]) =>
1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...

itertools.repeat(elem, [n\]) 返回 n 次所提供的元素,当 n 不存在时,返回无数次所提供的元素。

itertools.repeat('abc') =>
  abc, abc, abc, abc, abc, abc, abc, abc, abc, abc, ...
itertools.repeat('abc', 5) =>
  abc, abc, abc, abc, abc

itertools.chain(iterA, iterB, ...) 接受任意数量的可迭代对象作为输入,首先返回第一个迭代器的所有元素,然后是第二个的所有元素,如此一直进行下去,直到消耗掉所有输入的可迭代对象。

itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
  a, b, c, 1, 2, 3

itertools.islice(iter, [start\], stop, [step]) 返回一个所输入的迭代器切片的数据流。如果只单独给定 stop 参数的话,它会返回从起始算起 stop 个数量的元素。如果你提供了起始下标 start*,你会得到 *stop-start 个元素;如果你给定了 step 参数,数据流会跳过相应的元素。和 Python 里的字符串和列表切片不同,你不能在 start, stop 或者 step 这些参数中使用负数。:

itertools.islice(range(10), 8) =>
  0, 1, 2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8) =>
  2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8, 2) =>
  2, 4, 6

itertools.tee(iter, [n\]) 可以复制一个迭代器;它返回 n 个能够返回源迭代器内容的独立迭代器。如果你不提供参数 n,默认值为 2。复制迭代器需要保存源迭代器的一部分内容,因此在源迭代器比较大的时候会显著地占用内存;同时,在所有新迭代器中,有一个迭代器会比其他迭代器占用更多的内存。

itertools.tee( itertools.count() ) =>
   iterA, iterB
where iterA ->
   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
and   iterB ->
   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...

对元素使用函数

operator 模块包含一组对应于 Python 操作符的函数。比如 operator.add(a, b) (把两个数加起来),operator.ne(a, b) (和 a != b 相同),以及 operator.attrgetter('id') (返回获取 .id 属性的可调用对象)。

itertools.starmap(func, iter) 假定可迭代对象能够返回一个元组的流,并且利用这些元组作为参数来调用 func:

itertools.starmap(os.path.join,
                  [('/bin', 'python'), ('/usr', 'bin', 'java'),
                   ('/usr', 'bin', 'perl'), ('/usr', 'bin', 'ruby')])
=>
  /bin/python, /usr/bin/java, /usr/bin/perl, /usr/bin/ruby

选择元素

另外一系列函数根据谓词选取一个迭代器中元素的子集。

itertools.filterfalse(predicate, iter)filter() 相反,返回所有让 predicate 返回 false 的元素:

itertools.filterfalse(is_even, itertools.count()) =>
  1, 3, 5, 7, 9, 11, 13, 15, ...

itertools.takewhile(predicate, iter) 返回一直让 predicate 返回 true 的元素。一旦 predicate 返回 false,迭代器就会发出终止结果的信号。:

def less_than_10(x):
    return x < 10
itertools.takewhile(less_than_10, itertools.count()) =>
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9
itertools.takewhile(is_even, itertools.count()) =>
  0

itertools.dropwhile(predicate, iter) 在 predicate 返回 true 的时候丢弃元素,并且返回可迭代对象的剩余结果。:

itertools.dropwhile(less_than_10, itertools.count()) =>
  10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.dropwhile(is_even, itertools.count()) =>
  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...

itertools.compress(data, selectors) 接受两个迭代器,然后返回 data 中使相应地 selector 中的元素为真的元素;它会在任一个迭代器耗尽的时候停止:

itertools.compress([1, 2, 3, 4, 5], [True, True, False, False, True]) =>
   1, 2, 5

组合函数

itertools.combinations(iterable, r) 返回一个迭代器,它能给出输入迭代器中所包含的元素的所有可能的 r 元元组的组合。:

itertools.combinations([1, 2, 3, 4, 5], 2) =>
  (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 3), (2, 4), (2, 5),
  (3, 4), (3, 5),
  (4, 5)
itertools.combinations([1, 2, 3, 4, 5], 3) =>
  (1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 4), (1, 3, 5), (1, 4, 5),
  (2, 3, 4), (2, 3, 5), (2, 4, 5),
  (3, 4, 5)

每个元组中的元素保持着 可迭代对象 返回他们的顺序。例如,在上面的例子中数字 1 总是会在 2, 3, 4 或 5 前面。一个类似的函数,itertools.permutations(iterable, r=None),取消了保持顺序的限制,返回所有可能的长度为 r 的排列:

itertools.permutations([1, 2, 3, 4, 5], 2) =>
  (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 1), (2, 3), (2, 4), (2, 5),
  (3, 1), (3, 2), (3, 4), (3, 5),
  (4, 1), (4, 2), (4, 3), (4, 5),
  (5, 1), (5, 2), (5, 3), (5, 4)
itertools.permutations([1, 2, 3, 4, 5]) =>
  (1, 2, 3, 4, 5), (1, 2, 3, 5, 4), (1, 2, 4, 3, 5),
  ...
  (5, 4, 3, 2, 1)

如果你不提供 r 参数的值,它会使用可迭代对象的长度,也就是说会排列所有的元素。

注意这些函数会输出所有可能的位置组合,并不要求 可迭代对象 的内容不重复:

itertools.permutations('aba', 3) =>
  ('a', 'b', 'a'), ('a', 'a', 'b'), ('b', 'a', 'a'),
  ('b', 'a', 'a'), ('a', 'a', 'b'), ('a', 'b', 'a')

同一个元组 ('a', 'a', 'b') 出现了两次,但是两个 ‘a’ 字符来自不同的位置。

itertools.combinations_with_replacement(iterable, r) 函数放松了一个不同的限制:元组中的元素可以重复。从概念讲,为每个元组第一个位置选取一个元素,然后在选择第二个元素前替换掉它。:

itertools.combinations_with_replacement([1, 2, 3, 4, 5], 2) =>
  (1, 1), (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 2), (2, 3), (2, 4), (2, 5),
  (3, 3), (3, 4), (3, 5),
  (4, 4), (4, 5),
  (5, 5)

为元素分组

我要讨论的最后一个函数,itertools.groupby(iter,key_func=None),是最复杂的函数。 key_func(elem) 是一个可以对迭代器返回的每个元素计算键值的函数。 如果你不提供这个键值函数,它就会简化成每个元素自身。

groupby() 从所依据的可迭代对象中连续地收集具有相同值的元素,然后返回一个长度为2的元组的数据流, 每个元组包含键值以及对应这个键值的元素所组成的迭代器。

city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
             ('Anchorage', 'AK'), ('Nome', 'AK'),
             ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
             ...
            ]
def get_state(city_state):
    return city_state[1]
itertools.groupby(city_list, get_state) =>
  ('AL', iterator-1),
  ('AK', iterator-2),
  ('AZ', iterator-3), ...
where
iterator-1 =>
  ('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
iterator-2 =>
  ('Anchorage', 'AK'), ('Nome', 'AK')
iterator-3 =>
  ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')

groupby() 假定了所依据的可迭代对象的内容已经根据键值排序。注意,返回的迭代器也会使用所依据的可迭代对象,所以在请求迭代器 2和相应的键之前你必须先消耗迭代器 1 的结果。

functools 模块

Python 2.5 中的 functools 模块包含了一些高阶函数。 高阶函数 接受一个或多个函数作为输入,返回新的函数。 这个模块中最有用的工具是 functools.partial() 函数。

对于用函数式风格编写的程序,有时你会希望通过给定部分参数,将已有的函数构变形称新的函数。考虑一个 Python 函数 f(a, b, c);你希望创建一个和 f(1, b, c) 等价的新函数 g(b, c);也就是说你给定了 f() 的一个参数的值。这就是所谓的“部分函数应用”。

partial() 接受参数 (function, arg1, arg2, ..., kwarg1=value1, kwarg2=value2)。它会返回一个可调用的对象,所以你能够直接调用这个结果以使用给定参数的 function

这里有一个很小但很现实的例子:

import functools
def log(message, subsystem):
    """Write the contents of 'message' to the specified subsystem."""
    print('%s: %s' % (subsystem, message))
    ...
server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')

functools.reduce(func, iter, [initial_value\]) 持续地在可迭代对象的所有元素上执行操作,因此它不能够用在无限的可迭代对象上。func 必须是一个接受两个元素并返回一个值的函数。functools.reduce() 接受迭代器返回的前两个元素 A 和 B 并计算 func(A, B) 。然后它会请求第三个元素,C,计算 func(func(A, B), C),然后把这个结果再和第四个元素组合并返回,如此继续下去直到消耗整个可迭代对象。如果输入的可迭代对象完全不返回任何值,TypeError 异常就会抛出。如果提供了初值(initial value),它会被用作起始值,也就是先计算 func(initial_value, A) 。:

>>> import operator, functools
>>> functools.reduce(operator.concat, ['A', 'BB', 'C'])
'ABBC'
>>> functools.reduce(operator.concat, [])
Traceback (most recent call last):
  ...
TypeError: reduce() of empty sequence with no initial value
>>> functools.reduce(operator.mul, [1, 2, 3], 1)
6
>>> functools.reduce(operator.mul, [], 1)
1

如果你在 functools.reduce() 中使用 operator.add(),你就会把可迭代对象中的所有元素加起来.这种情况非常常见, 所以 Python 有一个特殊的内置函数 sum():

>>> import functools, operator
>>> functools.reduce(operator.add, [1, 2, 3, 4], 0)
10
>>> sum([1, 2, 3, 4])
10
>>> sum([])
0

不过, 对于很多使用 functools.reduce() 的情形, 使用明显的 for 循环会更清晰:

import functools
# Instead of:
product = functools.reduce(operator.mul, [1, 2, 3], 1)
# You can write:
product = 1
for i in [1, 2, 3]:
    product *= i

一个相关的函数是 itertools.accumulate(iterable, func=operator.add) 。它执行同样的计算, 不过相对于只返回最终结果,accumulate() 会返回一个迭代器来输出所有中间结果:

itertools.accumulate([1, 2, 3, 4, 5]) =>
  1, 3, 6, 10, 15
itertools.accumulate([1, 2, 3, 4, 5], operator.mul) =>
  1, 2, 6, 24, 120

operator 模块

前面已经提到了 operator 模块。它包含一系列对应于 Python 操作符的函数。在函数式风格的代码中,这些函数通常很有用,可以帮你省下不少时间,避免写一些琐碎的仅仅执行一个简单操作的函数。

这个模块里的一些函数:

  • 数学运算: add()sub()mul()floordiv()abs(), …
  • 逻辑运算: not_()truth()
  • 位运算: and_()or_()invert()
  • 比较: eq()ne()lt()le()gt(),和 ge()
  • 确认对象: is_()is_not()

全部函数列表可以参考 operator 模块的文档。

小函数和 lambda 表达式

编写函数式风格程序时,你会经常需要很小的函数,作为谓词函数或者以某种方式来组合元素。

如果合适的 Python 内置的或者其他模块中的函数,你就一点也不需要定义新的函数:

stripped_lines = [line.strip() for line in lines]
existing_files = filter(os.path.exists, file_list)

如果不存在你需要的函数,你就必须自己编写。一个编写小函数的方式是使用 lambda 表达式。lambda 接受一组参数以及组合这些参数的表达式,它会创建一个返回表达式值的匿名函数:

adder = lambda x, y: x+y
print_assign = lambda name, value: name + '=' + str(value)

另一种替代方案就是通常的使用 def 语句来定义函数:

def adder(x, y):
    return x + y
def print_assign(name, value):
    return name + '=' + str(value)

哪一种更受青睐呢?这是一个风格问题;我通常的做法是避免使用 lambda

我这么偏好的一个原因是,lambda 能够定义的函数非常受限。函数的结果必须能够作为单独的表达式来计算,这意味着你不能使用多路 if... elif... else 比较,或者 try... except 语句。如果你尝试在 lambda 语句中做太多事情,你最终会把表达式过于复杂以至于难以阅读。你能快速的说出下面的代码做了什么事情吗?:

import functools
total = functools.reduce(lambda a, b: (0, a[1] + b[1]), items)[1]

你可以弄明白,不过要花上时间来理清表达式来搞清楚发生了什么。使用一个简短的嵌套的 def 语句可以让情况变得更好:

import functools
def combine(a, b):
    return 0, a[1] + b[1]
total = functools.reduce(combine, items)[1]

如果我仅仅使用一个 for 循环会更好:

total = 0
for a, b in items:
    total += b

或者使用内置的 sum() 和一个生成器表达式:

total = sum(b for a, b in items)

许多使用 functools.reduce() 的情形可以更清晰地写成 for 循环的形式。

Fredrik Lundh 曾经建议以下一组规则来重构 lambda 的使用:

  1. 写一个 lambda 函数。
  2. 写一句注释来说明这个 lambda 究竟干了什么。
  3. 研究一会这个注释,然后想出一个抓住注释本质的名字。
  4. 用这个名字,把这个 lambda 改写成 def 语句。
  5. 把注释去掉。

我非常喜欢这些规则,不过你完全有权利争辩这种消除 lambda 的风格是不是更好。

参考文献

通用文献

Structure and Interpretation of Computer Programs, Harold Abelson, Gerald Jay Sussman 和 Julie Sussman 著。全文可见 https://mitpress.mit.edu/sicp/ 。在这部计算机科学的经典教科书中,第二和第三章讨论了使用序列和流来组织程序内部的数据传递。书中的示例采用 Scheme 语言,但其中这些章节中描述的很多设计方法同样适用于函数式风格的 Python 代码。

http://www.defmacro.org/ramblings/fp.html: 一个使用 Java 示例的函数式编程的总体介绍,有很长的历史说明。

https://en.wikipedia.org/wiki/Functional_programming: 一般性的函数式编程的 Wikipedia 条目。

https://en.wikipedia.org/wiki/Coroutine: 协程条目。

https://en.wikipedia.org/wiki/Currying: 函数柯里化条目。

Python 相关

http://gnosis.cx/TPiP/:David Mertz 书中的第一章 Text Processing in Python,”Utilizing Higher-Order Functions in Text Processing” 标题部分讨论了文本处理的函数式编程。

Mertz 还在 IBM 的 DeveloperWorks 站点上针对函数式编程撰写了一系列共 3 篇文章;参见 part 1, part 2part 3,

日志常用指引

日志基础教程

日志是对软件执行时所发生事件的一种追踪方式。软件开发人员对他们的代码添加日志调用,借此来指示某事件的发生。一个事件通过一些包含变量数据的描述信息来描述(比如:每个事件发生时的数据都是不同的)。开发者还会区分事件的重要性,重要性也被称为 等级严重性

什么时候使用日志

对于简单的日志使用来说日志功能提供了一系列便利的函数。它们是 debug()info()warning()error()critical()。想要决定何时使用日志,请看下表,其中显示了对于每个通用任务集合来说最好的工具。

你想要执行的任务 此任务最好的工具
对于命令行或程序的应用,结果显示在控制台。 print()
在对程序的普通操作发生时提交事件报告(比如:状态监控和错误调查) logging.info() 函数(当有诊断目的需要详细输出信息时使用 logging.debug() 函数)
提出一个警告信息基于一个特殊的运行时事件 warnings.warn() 位于代码库中,该事件是可以避免的,需要修改客户端应用以消除告警logging.warning() 不需要修改客户端应用,但是该事件还是需要引起关注
对一个特殊的运行时事件报告错误 引发异常
报告错误而不引发异常(如在长时间运行中的服务端进程的错误处理) logging.error(), logging.exception()logging.critical() 分别适用于特定的错误及应用领域

日志功能应以所追踪事件级别或严重性而定。各级别适用性如下(以严重性递增):

级别 何时使用
DEBUG 细节信息,仅当诊断问题时适用。
INFO 确认程序按预期运行。
WARNING 表明有已经或即将发生的意外(例如:磁盘空间不足)。程序仍按预期进行。
ERROR 由于严重的问题,程序的某些功能已经不能正常执行
CRITICAL 严重的错误,表明程序已不能继续执行

默认的级别是 WARNING,意味着只会追踪该级别及以上的事件,除非更改日志配置。

所追踪事件可以以不同形式处理。最简单的方式是输出到控制台。另一种常用的方式是写入磁盘文件。

一个简单的例子

一个非常简单的例子:

import logging
logging.warning('Watch out!')  # will print a message to the console
logging.info('I told you so')  # will not print anything

如果你在命令行中输入这些代码并运行,你将会看到:

WARNING:root:Watch out!

输出到命令行。INFO 消息并没有出现,因为默认级别是 WARNING 。打印的信息包含事件的级别以及在日志调用中的对于事件的描述,例如 “Watch out!”。暂时不用担心 “root” 部分:之后会作出解释。输出格式可按需要进行调整,格式化选项同样会在之后作出解释。

记录日志到文件

一种非常常见的情况是将日志事件记录到文件,让我们继续往下看。请确认启动新的Python 解释器,不要在上一个环境中继续操作:

import logging
logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.DEBUG)
logging.debug('This message should go to the log file')
logging.info('So should this')
logging.warning('And this, too')
logging.error('And non-ASCII stuff, too, like Øresund and Malmö')

在 3.9 版更改: 增加了 encoding 参数。在更早的 Python 版本中或没有指定时,编码会用 open() 使用的默认值。尽管在上面的例子中没有展示,但也可以传入一个决定如何处理编码错误的 errors 参数。

现在,如果我们打开日志文件,我们应当能看到日志信息:

DEBUG:root:This message should go to the log file
INFO:root:So should this
WARNING:root:And this, too
ERROR:root:And non-ASCII stuff, too, like Øresund and Malmö

该示例同样展示了如何设置日志追踪级别的阈值。该示例中,由于我们设置的阈值是 DEBUG,所有信息都将被打印。

如果你想从命令行设置日志级别,例如:

--log=INFO

并且在一些 loglevel 变量中你可以获得 --log 命令的参数,你可以使用:

getattr(logging, loglevel.upper())

通过 level 参数获得你将传递给 basicConfig() 的值。你需要对用户输入数据进行错误排查,可如下例:

# assuming loglevel is bound to the string value obtained from the
# command line argument. Convert to upper case to allow the user to
# specify --log=DEBUG or --log=debug
numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int):
    raise ValueError('Invalid log level: %s' % loglevel)
logging.basicConfig(level=numeric_level, ...)

basicConfig() 的调用应该在 debug()info() 等的前面。因为它被设计为一次性的配置,只有第一次调用会进行操作,随后的调用不会产生有效操作。

如果多次运行上述脚本,则连续运行的消息将追加到文件 example.log 。 如果你希望每次运行重新开始,而不是记住先前运行的消息,则可以通过将上例中的调用更改为来指定 filemode 参数:

logging.basicConfig(filename='example.log', filemode='w', level=logging.DEBUG)

输出将与之前相同,但不再追加进日志文件,因此早期运行的消息将丢失。

从多个模块记录日志

如果你的程序包含多个模块,这里有一个如何组织日志记录的示例:

# myapp.py
import logging
import mylib
def main():
    logging.basicConfig(filename='myapp.log', level=logging.INFO)
    logging.info('Started')
    mylib.do_something()
    logging.info('Finished')
if __name__ == '__main__':
    main()
# mylib.py
import logging
def do_something():
    logging.info('Doing something')

如果你运行 myapp.py ,你应该在 myapp.log 中看到:

INFO:root:Started
INFO:root:Doing something
INFO:root:Finished

这是你期待看到的。 你可以使用 mylib.py 中的模式将此概括为多个模块。 请注意,对于这种简单的使用模式,除了查看事件描述之外,你不能通过查看日志文件来了解应用程序中消息的 来源

记录变量数据

要记录变量数据,请使用格式字符串作为事件描述消息,并附加传入变量数据作为参数。 例如:

import logging
logging.warning('%s before you %s', 'Look', 'leap!')

将显示:

WARNING:root:Look before you leap!

如你所见,将可变数据合并到事件描述消息中使用旧的 %-s形式的字符串格式化。 这是为了向后兼容:logging 包的出现时间早于较新的格式化选项例如 str.format()string.Template

更改显示消息的格式

要更改用于显示消息的格式,你需要指定要使用的格式:

import logging
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
logging.debug('This message should appear on the console')
logging.info('So should this')
logging.warning('And this, too')

这将输出:

DEBUG:This message should appear on the console
INFO:So should this
WARNING:And this, too

请注意,前面示例中出现的 “root” 已消失。 对于可以出现在格式字符串中的全部内容,你可以参考以下文档 LogRecord 属性 ,但为了简单使用,你只需要 levelname (严重性), message (事件描述,包括可变数据),也许在事件发生时显示。 这将在下一节中介绍。

在消息中显示日期/时间

要显示事件的日期和时间,你可以在格式字符串中放置 ‘%(asctime)s’

import logging
logging.basicConfig(format='%(asctime)s %(message)s')
logging.warning('is when this event was logged.')

应该打印这样的东西:

2010-12-12 11:41:42,612 is when this event was logged.

日期/时间显示的默认格式(如上所示)类似于 ISO8601 或 RFC 3339 。 如果你需要更多地控制日期/时间的格式,请为 basicConfig 提供 datefmt 参数,如下例所示:

import logging
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
logging.warning('is when this event was logged.')

这会显示如下内容:

12/12/2010 11:46:36 AM is when this event was logged.

datefmt 参数的格式与 time.strftime() 支持的格式相同。

进阶日志教程

日志库采用模块化方法,并提供几类组件:记录器、处理器、过滤器和格式器。

  • 记录器暴露了应用程序代码直接使用的接口。
  • 处理器将日志记录(由记录器创建)发送到适当的目标。
  • 过滤器提供了更细粒度的功能,用于确定要输出的日志记录。
  • 格式器指定最终输出中日志记录的样式。

日志事件信息在 LogRecord 实例中的记录器、处理器、过滤器和格式器之间传递。

通过调用 Logger 类(以下称为 loggers , 记录器)的实例来执行日志记录。 每个实例都有一个名称,它们在概念上以点(句点)作为分隔符排列在命名空间的层次结构中。 例如,名为 ‘scan’ 的记录器是记录器 ‘scan.text’ ,’scan.html’ 和 ‘scan.pdf’ 的父级。 记录器名称可以是你想要的任何名称,并指示记录消息源自的应用程序区域。

在命名记录器时使用的一个好习惯是在每个使用日志记录的模块中使用模块级记录器,命名如下:

logger = logging.getLogger(__name__)

这意味着记录器名称跟踪包或模块的层次结构,并且直观地从记录器名称显示记录事件的位置。

记录器层次结构的根称为根记录器。 这是函数 debug()info()warning()error()critical() 使用的记录器,它们就是调用了根记录器的同名方法。 函数和方法具有相同的签名。 根记录器的名称在输出中打印为 ‘root’ 。

当然,可以将消息记录到不同的地方。 软件包中的支持包含,用于将日志消息写入文件、 HTTP GET/POST 位置、通过 SMTP 发送电子邮件、通用套接字、队列或特定于操作系统的日志记录机制(如 syslog 或 Windows NT 事件日志)。 目标由 handler 类提供。 如果你有任何内置处理器类未满足的特殊要求,则可以创建自己的日志目标类。

默认情况下,没有为任何日志消息设置目标。 你可以使用 basicConfig() 指定目标(例如控制台或文件),如教程示例中所示。 如果你调用函数 debug()info()warning()error()critical() ,它们将检查是否有设置目标;如果没有设置,将在委托给根记录器进行实际的消息输出之前设置目标为控制台( sys.stderr )并设置显示消息的默认格式。

basicConfig() 设置的消息默认格式为:

severity:logger 
name:message

你可以通过使用 format 参数将格式字符串传递给 basicConfig() 来更改此设置。

记录流程

记录器和处理器中的日志事件信息流程如下图所示。

记录器

Logger 对象有三重任务。首先,它们向应用程序代码公开了几种方法,以便应用程序可以在运行时记录消息。其次,记录器对象根据严重性(默认过滤工具)或过滤器对象确定要处理的日志消息。第三,记录器对象将相关的日志消息传递给所有感兴趣的日志处理器。

记录器对象上使用最广泛的方法分为两类:配置和消息发送。

这些是最常见的配置方法:

  • Logger.setLevel() 指定记录器将处理的最低严重性日志消息,其中 debug 是最低内置严重性级别, critical 是最高内置严重性级别。 例如,如果严重性级别为 INFO ,则记录器将仅处理 INFO 、 WARNING 、 ERROR 和 CRITICAL 消息,并将忽略 DEBUG 消息。
  • Logger.addHandler()Logger.removeHandler() 从记录器对象中添加和删除处理器对象。
  • Logger.addFilter()Logger.removeFilter() 可以添加或移除记录器对象中的过滤器。

你不需要总是在你创建的每个记录器上都调用这些方法。 请参阅本节的最后两段。

配置记录器对象后,以下方法将创建日志消息:

  • Logger.debug()Logger.info()Logger.warning()Logger.error()Logger.critical() 都创建日志记录,包含消息和与其各自方法名称对应的级别。该消息实际上是一个格式化字符串,它可能包含标题字符串替换语法 %s%d%f 等等。其余参数是与消息中的替换字段对应的对象列表。关于 **kwargs ,日志记录方法只关注 exc_info 的关键字,并用它来确定是否记录异常信息。
  • Logger.exception() 创建与 Logger.error() 相似的日志信息。 不同之处是, Logger.exception() 同时还记录当前的堆栈追踪。仅从异常处理程序调用此方法。
  • Logger.log() 将日志级别作为显式参数。对于记录消息而言,这比使用上面列出的日志级别便利方法更加冗长,但这是使用自定义日志级别的方法。

getLogger() 返回对具有指定名称的记录器实例的引用(如果已提供),或者如果没有则返回 root 。名称是以句点分隔的层次结构。多次调用 getLogger() 具有相同的名称将返回对同一记录器对象的引用。在分层列表中较低的记录器是列表中较高的记录器的子项。例如,给定一个名为 foo 的记录器,名称为 foo.barfoo.bar.bazfoo.bam 的记录器都是 foo 子项。

记录器具有 有效等级 的概念。如果未在记录器上显式设置级别,则使用其父记录器的级别作为其有效级别。如果父记录器没有明确的级别设置,则检查 父级。依此类推,搜索所有上级元素,直到找到明确设置的级别。根记录器始终具有显式级别集(默认情况下为 WARNING )。在决定是否处理事件时,记录器的有效级别用于确定事件是否传递给记录器相关的处理器。

子记录器将消息传播到与其父级记录器关联的处理器。因此,不必为应用程序使用的所有记录器定义和配置处理器。一般为顶级记录器配置处理器,再根据需要创建子记录器就足够了。(但是,你可以通过将记录器的 propagate 属性设置为 False 来关闭传播。)

处理器

Handler 对象负责将适当的日志消息(基于日志消息的严重性)分派给处理器的指定目标。 Logger 对象可以使用 addHandler() 方法向自己添加零个或多个处理器对象。作为示例场景,应用程序可能希望将所有日志消息发送到日志文件,将错误或更高的所有日志消息发送到标准输出,以及将所有关键消息发送至一个邮件地址。 此方案需要三个单独的处理器,其中每个处理器负责将特定严重性的消息发送到特定位置。

标准库包含很多处理器类型;主要使用 StreamHandlerFileHandler

处理器中很少有方法可供应用程序开发人员使用。使用内置处理器对象(即不创建自定义处理器)的应用程序开发人员能用到的仅有以下配置方法:

  • setLevel() 方法,就像在记录器对象中一样,指定将被分派到适当目标的最低严重性。为什么有两个 setLevel() 方法?记录器中设置的级别确定将传递给其处理器的消息的严重性。每个处理器中设置的级别确定该处理器将发送哪些消息。
  • setFormatter() 选择一个该处理器使用的 Formatter 对象。
  • addFilter()removeFilter() 分别在处理器上配置和取消配置过滤器对象。

应用程序代码不应直接实例化并使用 Handler 的实例。 相反, Handler 类是一个基类,它定义了所有处理器应该具有的接口,并建立了子类可以使用(或覆盖)的一些默认行为。

格式器

格式化器对象配置日志消息的最终顺序、结构和内容。 与 logging.Handler 类不同,应用程序代码可以实例化格式器类,但如果应用程序需要特殊行为,则可能会对格式化器进行子类化定制。构造函数有三个可选参数 —— 消息格式字符串、日期格式字符串和样式指示符。

logging.Formatter.__init__(fmt=None, datefmt=None, style=’%’)

如果没有消息格式字符串,则默认使用原始消息。如果没有日期格式字符串,则默认日期格式为:

%Y-%m-%d %H:%M:%S

最后加上毫秒数。 style 是 %,’{ ‘ 或 ‘$’ 之一。 如果未指定,则将使用 ‘%’。

如果 style 是 ‘%’,则消息格式字符串使用 %(<dictionary key>)s 样式字符串替换;可能的键值在 LogRecord 属性 中。 如果样式为 ‘{‘,则假定消息格式字符串与 str.format() (使用关键字参数)兼容,而如果样式为 ‘$’ ,则消息格式字符串应符合 string.Template.substitute()

在 3.2 版更改: 添加 style 形参。

以下消息格式字符串将以人类可读的格式记录时间、消息的严重性以及消息的内容,按此顺序:

'%(asctime)s - %(levelname)s - %(message)s'

格式器通过用户可配置的函数将记录的创建时间转换为元组。 默认情况下,使用 time.localtime() ;要为特定格式器实例更改此项,请将实例的 converter 属性设置为与 time.localtime()time.gmtime() 具有相同签名的函数。 要为所有格式器更改它,例如,如果你希望所有记录时间都以 GMT 显示,请在格式器类中设置 converter 属性(对于 GMT 显示,设置为 time.gmtime )。

配置日志记录

开发者可以通过三种方式配置日志记录:

  1. 使用调用上面列出的配置方法的 Python 代码显式创建记录器、处理器和格式器。
  2. 创建日志配置文件并使用 fileConfig() 函数读取它。
  3. 创建配置信息字典并将其传递给 dictConfig() 函数。

有关最后两个选项的参考文档。 以下示例使用 Python 代码配置一个非常简单的记录器、一个控制台处理器和一个简单的格式器:

import logging
# create logger
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)
# create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# add formatter to ch
ch.setFormatter(formatter)
# add ch to logger
logger.addHandler(ch)
# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')

从命令行运行此模块将生成以下输出:

$ python simple_logging_module.py
2005-03-19 15:10:26,618 - simple_example - DEBUG - debug message
2005-03-19 15:10:26,620 - simple_example - INFO - info message
2005-03-19 15:10:26,695 - simple_example - WARNING - warn message
2005-03-19 15:10:26,697 - simple_example - ERROR - error message
2005-03-19 15:10:26,773 - simple_example - CRITICAL - critical message

以下 Python 模块创建的记录器、处理器和格式器几乎与上面列出的示例中的相同,唯一的区别是对象的名称:

import logging
import logging.config
logging.config.fileConfig('logging.conf')
# create logger
logger = logging.getLogger('simpleExample')
# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')

这是 logging.conf 文件:

[loggers]
keys=root,simpleExample
[handlers]
keys=consoleHandler
[formatters]
keys=simpleFormatter
[logger_root]
level=DEBUG
handlers=consoleHandler
[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)
[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s

其输出与不基于配置文件的示例几乎相同:

$ python simple_logging_config.py
2005-03-19 15:38:55,977 - simpleExample - DEBUG - debug message
2005-03-19 15:38:55,979 - simpleExample - INFO - info message
2005-03-19 15:38:56,054 - simpleExample - WARNING - warn message
2005-03-19 15:38:56,055 - simpleExample - ERROR - error message
2005-03-19 15:38:56,130 - simpleExample - CRITICAL - critical message

你可以看到配置文件方法相较于 Python 代码方法有一些优势,主要是配置和代码的分离以及非开发者轻松修改日志记录属性的能力。

警告

fileConfig() 函数接受一个默认参数 disable_existing_loggers ,出于向后兼容的原因,默认为 True 。这可能与您的期望不同,因为除非在配置中明确命名它们(或其父级),否则它将导致在 fileConfig() 调用之前存在的任何非 root 记录器被禁用。有关更多信息,请参阅参考文档,如果需要,请将此参数指定为 False

传递给 dictConfig() 的字典也可以用键 disable_existing_loggers 指定一个布尔值,如果没有在字典中明确指定,也默认被解释为 True 。这会导致上面描述的记录器禁用行为,这可能与你的期望不同——在这种情况下,请明确地为其提供 False 值。

请注意,配置文件中引用的类名称需要相对于日志记录模块,或者可以使用常规导入机制解析的绝对值。因此,你可以使用 WatchedFileHandler (相对于日志记录模块)或 mypackage.mymodule.MyHandler (对于在 mypackage 包中定义的类和模块 mymodule ,其中 mypackage 在 Python 导入路径上可用)。

在 Python 3.2 中,引入了一种新的配置日志记录的方法,使用字典来保存配置信息。 这提供了上述基于配置文件方法的功能的超集,并且是新应用程序和部署的推荐配置方法。 因为 Python 字典用于保存配置信息,并且由于你可以使用不同的方式填充该字典,因此你有更多的配置选项。 例如,你可以使用 JSON 格式的配置文件,或者如果你有权访问 YAML 处理功能,则可以使用 YAML 格式的文件来填充配置字典。当然,你可以在 Python 代码中构造字典,通过套接字以 pickle 形式接收它,或者使用对你的应用程序合理的任何方法。

以下是与上述相同配置的示例,采用 YAML 格式,用于新的基于字典的方法:

version: 1
formatters:
  simple:
    format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
  console:
    class: logging.StreamHandler
    level: DEBUG
    formatter: simple
    stream: ext://sys.stdout
loggers:
  simpleExample:
    level: DEBUG
    handlers: [console]
    propagate: no
root:
  level: DEBUG
  handlers: [console]

如果没有提供配置会发生什么

如果未提供日志记录配置,则可能出现需要输出日志记录事件但无法找到输出事件的处理器的情况。 在这些情况下,logging 包的行为取决于 Python 版本。

对于 3.2 之前的 Python 版本,行为如下:

  • 如果 logging.raiseExceptionsFalse (生产模式),则会以静默方式丢弃该事件。
  • 如果 logging.raiseExceptionsTrue (开发模式),则会打印一条消息 ‘No handlers could be found for logger X.Y.Z’。

在 Python 3.2 及更高版本中,行为如下:

  • 事件使用 “最后的处理器” 输出,存储在 logging.lastResort 中。 这个内部处理器与任何记录器都没有关联,它的作用类似于 StreamHandler ,它将事件描述消息写入 sys.stderr 的当前值(因此服从任何可能的重定向影响)。 没有对消息进行格式化——只打印裸事件描述消息。处理器的级别设置为 WARNING,因此将输出此级别和更高级别的所有事件。

要获得 3.2 之前的行为,可以设置 logging.lastResortNone

配置库的日志记录

在开发使用日志记录的库时,你应该注意记录库如何使用日志记录——例如,使用的记录器的名称。还需要考虑其日志记录配置。如果应用程序不使用日志记录,并且库代码进行日志记录调用,那么(如上一节所述)严重性为 WARNING 及更高级别的事件将打印到 sys.stderr 。这被认为是最好的默认行为。

如果由于某种原因,你 希望在没有任何日志记录配置的情况下打印这些消息,则可以将无操作处理器附加到库的顶级记录器。这样可以避免打印消息,因为将始终为库的事件找到处理器:它不会产生任何输出。如果库用户配置应用程序使用的日志记录,可能是配置将添加一些处理器,如果级别已适当配置,则在库代码中进行的日志记录调用将正常地将输出发送给这些处理器。

日志包中包含一个不做任何事情的处理器: NullHandler (自 Python 3.1 起)。可以将此处理器的实例添加到库使用的日志记录命名空间的顶级记录器中( 如果 你希望在没有日志记录配置的情况下阻止库的记录事件输出到 sys.stderr )。如果库 foo 的所有日志记录都是使用名称匹配 ‘foo.x’ , ‘foo.x.y’ 等的记录器完成的,那么代码:

import logging
logging.getLogger('foo').addHandler(logging.NullHandler())

应该有预计的效果。如果一个组织生成了许多库,则指定的记录器名称可以是 “orgname.foo” 而不仅仅是 “foo” 。

注解

强烈建议你 不要将 NullHandler 以外的任何处理器添加到库的记录器中 。这是因为处理器的配置是使用你的库的应用程序开发人员的权利。应用程序开发人员了解他们的目标受众以及哪些处理器最适合他们的应用程序:如果你在“底层”添加处理器,则可能会干扰他们执行单元测试和提供符合其要求的日志的能力。

日志级别

日志记录级别的数值在下表中给出。如果你想要定义自己的级别,并且需要它们具有相对于预定义级别的特定值,那么这你可能对以下内容感兴趣。如果你定义具有相同数值的级别,它将覆盖预定义的值;预定义的名称将失效。

级别 数值
CRITICAL 50
ERROR 40
WARNING 30
INFO 20
DEBUG 10
NOTSET 0

级别也可以与记录器相关联,由开发人员设置或通过加载已保存的日志记录配置。在记录器上调用日志记录方法时,记录器会将其自己的级别与与方法调用关联的级别进行比较。如果记录器的级别高于方法调用的级别,则实际上不会生成任何记录消息。这是控制日志记录输出详细程度的基本机制。

记录消息被编码为 LogRecord 类的实例。当记录器决定实际记录事件时,将用记录消息创建 LogRecord 实例。

记录消息受 handlers 建立的调度机制控制,它们是 Handler 类的子类实例。处理器负责确保记录的消息(以 LogRecord 的形式)最终位于对该消息的目标受众(例如最终用户、 支持服务台员工、系统管理员、开发人员)有用的特定位置(或一组位置)上。处理器传递适用于特定目标的 LogRecord 实例。 每个记录器可以有零个、一个或多个与之关联的处理器(通过 LoggeraddHandler() 方法)。除了与记录器直接关联的所有处理器之外,还调用与记录器的 所有祖先关联的处理器来分派消息(除非记录器的 *propagate 标志设置为 false 值,这将停止传递到上级处理器)。

就像记录器一样,处理器可以具有与它们相关联的级别。处理器的级别作为过滤器,其方式与记录器级别相同。如果处理器决定调度一个事件,则使用 emit() 方法将消息发送到其目标。大多数用户定义的 Handler 子类都需要重载 emit()

自定义级别

定义你自己的级别是可能的,但不一定是必要的,因为现有级别是根据实践经验选择的。但是,如果你确信需要自定义级别,那么在执行此操作时应特别小心,如果你正在开发库,则 定义自定义级别可能是一个非常糟糕的主意 。 这是因为如果多个库作者都定义了他们自己的自定义级别,那么使用开发人员很难控制和解释这些多个库的日志记录输出,因为给定的数值对于不同的库可能意味着不同的东西。

有用的处理器

作为 Handler 基类的补充,提供了很多有用的子类:

  1. StreamHandler 实例发送消息到流(类似文件对象)。
  2. FileHandler 实例将消息发送到硬盘文件。
  3. BaseRotatingHandler 是轮换日志文件的处理器的基类。它并不应该直接实例化。而应该使用 RotatingFileHandlerTimedRotatingFileHandler 代替它。
  4. RotatingFileHandler 实例将消息发送到硬盘文件,支持最大日志文件大小和日志文件轮换。
  5. TimedRotatingFileHandler 实例将消息发送到硬盘文件,以特定的时间间隔轮换日志文件。
  6. SocketHandler 实例将消息发送到 TCP/IP 套接字。从 3.4 开始,也支持 Unix 域套接字。
  7. DatagramHandler 实例将消息发送到 UDP 套接字。从 3.4 开始,也支持 Unix 域套接字。
  8. SMTPHandler 实例将消息发送到指定的电子邮件地址。
  9. SysLogHandler 实例将消息发送到 Unix syslog 守护程序,可能在远程计算机上。
  10. NTEventLogHandler 实例将消息发送到 Windows NT/2000/XP 事件日志。
  11. MemoryHandler 实例将消息发送到内存中的缓冲区,只要满足特定条件,缓冲区就会刷新。
  12. HTTPHandler 实例使用 GETPOST 方法将消息发送到 HTTP 服务器。
  13. WatchedFileHandler 实例会监视他们要写入日志的文件。如果文件发生更改,则会关闭该文件并使用文件名重新打开。此处理器仅在类 Unix 系统上有用; Windows 不支持依赖的基础机制。
  14. QueueHandler 实例将消息发送到队列,例如在 queuemultiprocessing 模块中实现的队列。
  15. NullHandler 实例对错误消息不执行任何操作。它们由想要使用日志记录的库开发人员使用,但是想要避免如果库用户没有配置日志记录,则显示 ‘No handlers could be found for logger XXX’ 消息的情况。

3.2 新版功能: QueueHandler 类。

The NullHandlerStreamHandlerFileHandler 类在核心日志包中定义。其他处理器定义在 logging.handlers 中。(还有另一个子模块 logging.config ,用于配置功能)

记录的消息通过 Formatter 类的实例进行格式化后呈现。 它们使用能与 % 运算符一起使用的格式字符串和字典进行初始化。

要批量格式化多个消息,可以使用 BufferingFormatter 的实例。除了格式字符串(应用于批处理中的每个消息)之外,还提供了标题和尾部格式字符串。

当基于记录器级别和处理器级别的过滤不够时,可以将 Filter 的实例添加到 LoggerHandler 实例(通过它们的 addFilter() 方法)。在决定进一步处理消息之前,记录器和处理器都会查询其所有过滤器以获得许可。如果任何过滤器返回 false 值,则不会进一步处理该消息。

基本 Filter 的功能允许按特定的记录器名称进行过滤。如果使用此功能,则允许通过过滤器发送到指定记录器及其子项的消息,并丢弃其他所有消息。

记录日志时引发的异常

logging 包设计为忽略记录日志生产时发生的异常。这样,处理日志记录事件时发生的错误(例如日志记录错误配置、网络或其他类似错误)不会导致使用日志记录的应用程序过早终止。

SystemExitKeyboardInterrupt 异常永远不会被忽略。 在 Handler 子类的 emit() 方法中发生的其他异常被传递给它的 handleError() 方法。

Handler 中默认实现的 handleError() 检查是否设置了模块级变量 raiseExceptions 。如果有设置,则会将回溯打印到 sys.stderr 。如果未设置,则忽略异常。

注解

raiseExceptions 默认值是 True。 这是因为在开发期间,你通常希望收到任何发生异常的通知。建议你将 raiseExceptions 设置为 False 以供生产环境使用。

使用任意对象作为消息

在前面的部分和示例中,都假设记录事件时传递的消息是字符串。 但是,这不是唯一的可能性。你可以将任意对象作为消息传递,并且当日志记录系统需要将其转换为字符串表示时,将调用其 __ str__() 方法。实际上,如果你愿意,你可以完全避免计算字符串表示。例如, SocketHandler 用 pickle 处理事件后,通过网络发送。

优化

消息参数的格式化将被推迟,直到无法避免。但是,计算传递给日志记录方法的参数也可能很消耗资源,如果记录器只是丢弃你的事件,你可能希望避免这样做。要决定做什么,可以调用 isEnabledFor() 方法,该方法接受一个 level 参数,如果记录器为该级别的调用创建了该事件,则返回 true 。 你可以写这样的代码:

if logger.isEnabledFor(logging.DEBUG):
    logger.debug('Message with %s, %s', expensive_func1(),
                                        expensive_func2())

因此,如果记录器的阈值设置在“DEBUG”以上,则永远不会调用 expensive_func1()expensive_func2()

注解

在某些情况下, isEnabledFor() 本身可能比你想要的更消耗资源(例如,对于深度嵌套的记录器,其中仅在记录器层次结构中设置了显式级别)。在这种情况下(或者如果你想避免在紧密循环中调用方法),你可以在本地或实例变量中将调用的结果缓存到 isEnabledFor() ,并使用它而不是每次调用方法。在日志记录配置在应用程序运行时动态更改(这不常见)时,只需要重新计算这样的缓存值即可。

对于需要对收集的日志信息进行更精确控制的特定应用程序,还可以进行其他优化。以下列出了在日志记录过程中您可以避免的非必须处理操作:

你不想收集的内容 如何避免收集它
有关调用来源的信息 logging._srcfile 设置为 None 。这避免了调用 sys._getframe() ,如果 PyPy 支持 Python 3.x ,这可能有助于加速 PyPy (无法加速使用 sys._getframe() 的代码)等环境中的代码。
线程信息 logging.logThreads 设为 False
当前进程 ID (os.getpid()) logging.logProcesses 设为 False
当使用 multiprocessing 来管理多个进程时的当前进程名称。 logging.logMultiprocessing 设为 False

另请注意,核心日志记录模块仅包含基本处理器。如果你不导入 logging.handlerslogging.config ,它们将不会占用任何内存。

日志操作手册

在多个模块中记录日志

无论对 logging.getLogger('someLogger') 进行多少次调用,都会返回同一个 logger 对象的引用。不仅在同一个模块内如此,只要是在同一个 Python 解释器进程中,跨模块调用也是一样。同样是引用同一个对象,应用程序也可以在一个模块中定义和配置一个父 logger,而在另一个单独的模块中创建(但不配置)子 logger,对于子 logger 的所有调用都会传给父 logger。以下是主模块:

import logging
import auxiliary_module
# create logger with 'spam_application'
logger = logging.getLogger('spam_application')
logger.setLevel(logging.DEBUG)
# create file handler which logs even debug messages
fh = logging.FileHandler('spam.log')
fh.setLevel(logging.DEBUG)
# create console handler with a higher log level
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
# create formatter and add it to the handlers
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# add the handlers to the logger
logger.addHandler(fh)
logger.addHandler(ch)
logger.info('creating an instance of auxiliary_module.Auxiliary')
a = auxiliary_module.Auxiliary()
logger.info('created an instance of auxiliary_module.Auxiliary')
logger.info('calling auxiliary_module.Auxiliary.do_something')
a.do_something()
logger.info('finished auxiliary_module.Auxiliary.do_something')
logger.info('calling auxiliary_module.some_function()')
auxiliary_module.some_function()
logger.info('done with auxiliary_module.some_function()')

以下是辅助模块:

import logging
# create logger
module_logger = logging.getLogger('spam_application.auxiliary')
class Auxiliary:
    def __init__(self):
        self.logger = logging.getLogger('spam_application.auxiliary.Auxiliary')
        self.logger.info('creating an instance of Auxiliary')
    def do_something(self):
        self.logger.info('doing something')
        a = 1 + 1
        self.logger.info('done doing something')
def some_function():
    module_logger.info('received a call to "some_function"')

输出结果会像这样:

2005-03-23 23:47:11,663 - spam_application - INFO -
   creating an instance of auxiliary_module.Auxiliary
2005-03-23 23:47:11,665 - spam_application.auxiliary.Auxiliary - INFO -
   creating an instance of Auxiliary
2005-03-23 23:47:11,665 - spam_application - INFO -
   created an instance of auxiliary_module.Auxiliary
2005-03-23 23:47:11,668 - spam_application - INFO -
   calling auxiliary_module.Auxiliary.do_something
2005-03-23 23:47:11,668 - spam_application.auxiliary.Auxiliary - INFO -
   doing something
2005-03-23 23:47:11,669 - spam_application.auxiliary.Auxiliary - INFO -
   done doing something
2005-03-23 23:47:11,670 - spam_application - INFO -
   finished auxiliary_module.Auxiliary.do_something
2005-03-23 23:47:11,671 - spam_application - INFO -
   calling auxiliary_module.some_function()
2005-03-23 23:47:11,672 - spam_application.auxiliary - INFO -
   received a call to 'some_function'
2005-03-23 23:47:11,673 - spam_application - INFO -
   done with auxiliary_module.some_function()

在多个线程中记录日志

多线程记录日志并不需要特殊处理,以下示例演示了在主线程(起始线程)和其他线程中记录日志的过程:

import logging
import threading
import time
def worker(arg):
    while not arg['stop']:
        logging.debug('Hi from myfunc')
        time.sleep(0.5)
def main():
    logging.basicConfig(level=logging.DEBUG, format='%(relativeCreated)6d %(threadName)s %(message)s')
    info = {'stop': False}
    thread = threading.Thread(target=worker, args=(info,))
    thread.start()
    while True:
        try:
            logging.debug('Hello from main')
            time.sleep(0.75)
        except KeyboardInterrupt:
            info['stop'] = True
            break
    thread.join()
if __name__ == '__main__':
    main()

脚本会运行输出类似下面的内容:

   0 Thread-1 Hi from myfunc
   3 MainThread Hello from main
 505 Thread-1 Hi from myfunc
 755 MainThread Hello from main
1007 Thread-1 Hi from myfunc
1507 MainThread Hello from main
1508 Thread-1 Hi from myfunc
2010 Thread-1 Hi from myfunc
2258 MainThread Hello from main
2512 Thread-1 Hi from myfunc
3009 MainThread Hello from main
3013 Thread-1 Hi from myfunc
3515 Thread-1 Hi from myfunc
3761 MainThread Hello from main
4017 Thread-1 Hi from myfunc
4513 MainThread Hello from main
4518 Thread-1 Hi from myfunc

以上如期显示了不同线程的日志是交替输出的。当然更多的线程也会如此。

多个 handler 和多种 formatter

日志是个普通的 Python 对象。 addHandler() 方法可加入不限数量的日志 handler。有时候,应用程序需把严重错误信息记入文本文件,而将一般错误或其他级别的信息输出到控制台。若要进行这样的设定,只需多配置几个日志 handler 即可,应用程序的日志调用代码可以保持不变。下面对之前的分模块日志示例略做修改:

import logging
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)
# create file handler which logs even debug messages
fh = logging.FileHandler('spam.log')
fh.setLevel(logging.DEBUG)
# create console handler with a higher log level
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
# create formatter and add it to the handlers
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
fh.setFormatter(formatter)
# add the handlers to logger
logger.addHandler(ch)
logger.addHandler(fh)
# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')

需要注意的是,“应用程序”内的代码并不关心是否存在多个日志 handler。示例中所做的改变,只是新加入并配置了一个名为 fh 的 handler。

在编写和测试应用程序时,若能创建日志 handler 对不同严重级别的日志信息进行过滤,这将十分有用。调试时无需用多条 print 语句,而是采用 logger.debug :print 语句以后还得注释或删掉,而 logger.debug 语句可以原样留在源码中保持静默。当需要再次调试时,只要改变日志对象或 handler 的严重级别即可。

在多个地方记录日志

假定要根据不同的情况将日志以不同的格式写入控制台和文件。比如把 DEBUG 以上级别的日志信息写于文件,并且把 INFO 以上的日志信息输出到控制台。再假设日志文件需要包含时间戳,控制台信息则不需要。以下演示了做法:

import logging
# set up logging to file - see previous section for more details
logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
                    datefmt='%m-%d %H:%M',
                    filename='/temp/myapp.log',
                    filemode='w')
# define a Handler which writes INFO messages or higher to the sys.stderr
console = logging.StreamHandler()
console.setLevel(logging.INFO)
# set a format which is simpler for console use
formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
# tell the handler to use this format
console.setFormatter(formatter)
# add the handler to the root logger
logging.getLogger('').addHandler(console)
# Now, we can log to the root logger, or any other logger. First the root...
logging.info('Jackdaws love my big sphinx of quartz.')
# Now, define a couple of other loggers which might represent areas in your
# application:
logger1 = logging.getLogger('myapp.area1')
logger2 = logging.getLogger('myapp.area2')
logger1.debug('Quick zephyrs blow, vexing daft Jim.')
logger1.info('How quickly daft jumping zebras vex.')
logger2.warning('Jail zesty vixen who grabbed pay from quack.')
logger2.error('The five boxing wizards jump quickly.')

当运行后,你会看到控制台如下所示

root        : INFO     Jackdaws love my big sphinx of quartz.
myapp.area1 : INFO     How quickly daft jumping zebras vex.
myapp.area2 : WARNING  Jail zesty vixen who grabbed pay from quack.
myapp.area2 : ERROR    The five boxing wizards jump quickly.

而日志文件将如下所示:

10-22 22:19 root         INFO     Jackdaws love my big sphinx of quartz.
10-22 22:19 myapp.area1  DEBUG    Quick zephyrs blow, vexing daft Jim.
10-22 22:19 myapp.area1  INFO     How quickly daft jumping zebras vex.
10-22 22:19 myapp.area2  WARNING  Jail zesty vixen who grabbed pay from quack.
10-22 22:19 myapp.area2  ERROR    The five boxing wizards jump quickly.

如您所见,DEBUG 级别的日志信息只出现在了文件中,而其他信息则两个地方都会输出。

上述示例只用到了控制台和文件 handler,当然还可以自由组合任意数量的日志 handler。

日志配置服务器示例

以下是一个用到了日志配置服务器的模块示例:

import logging
import logging.config
import time
import os
# read initial config file
logging.config.fileConfig('logging.conf')
# create and start listener on port 9999
t = logging.config.listen(9999)
t.start()
logger = logging.getLogger('simpleExample')
try:
    # loop through logging calls to see the difference
    # new configurations make, until Ctrl+C is pressed
    while True:
        logger.debug('debug message')
        logger.info('info message')
        logger.warning('warn message')
        logger.error('error message')
        logger.critical('critical message')
        time.sleep(5)
except KeyboardInterrupt:
    # cleanup
    logging.config.stopListening()
    t.join()

以下脚本将接受文件名作为参数,然后将此文件发送到服务器,前面加上文件的二进制编码长度,做为新的日志配置:

#!/usr/bin/env python
import socket, sys, struct
with open(sys.argv[1], 'rb') as f:
    data_to_send = f.read()
HOST = 'localhost'
PORT = 9999
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print('connecting...')
s.connect((HOST, PORT))
print('sending config...')
s.send(struct.pack('>L', len(data_to_send)))
s.send(data_to_send)
s.close()
print('complete')

处理日志 handler 的阻塞

有时你必须让日志记录处理程序的运行不会阻塞你要记录日志的线程。 这在 Web 应用程序中是很常见,当然在其他场景中也可能发生。

有一种原因往往会让程序表现迟钝,这就是 SMTPHandler:由于很多因素是开发人员无法控制的(例如邮件或网络基础设施的性能不佳),发送电子邮件可能需要很长时间。不过几乎所有网络 handler 都可能会发生阻塞:即使是 SocketHandler 操作也可能在后台执行 DNS 查询,而这种查询实在太慢了(并且 DNS 查询还可能在很底层的套接字库代码中,位于 Python 层之下,超出了可控范围)。

有一种解决方案是分成两部分实现。第一部分,针对那些对性能有要求的关键线程,只为日志对象连接一个 QueueHandler。日志对象只需简单地写入队列即可,可为队列设置足够大的容量,或者可以在初始化时不设置容量上限。尽管为以防万一,可能需要在代码中捕获 queue.Full 异常,不过队列写入操作通常会很快得以处理。如果要开发库代码,包含性能要求较高的线程,为了让使用该库的开发人员受益,请务必在开发文档中进行标明(包括建议仅连接 QueueHandlers )。

解决方案的另一部分就是 QueueListener,它被设计为 QueueHandler 的对应部分。QueueListener 非常简单:传入一个队列和一些 handler,并启动一个内部线程,用于侦听 QueueHandlers(或其他 LogRecords 源)发送的 LogRecord 队列。LogRecords 会从队列中移除并传给 handler 处理。

QueueListener 作为单独的类,好处就是可以用同一个实例为多个 QueueHandlers 服务。这比把现有 handler 类线程化更加资源友好,后者会每个 handler 会占用一个线程,却没有特别的好处。

以下是这两个类的运用示例(省略了 import 语句):

que = queue.Queue(-1)  # no limit on size
queue_handler = QueueHandler(que)
handler = logging.StreamHandler()
listener = QueueListener(que, handler)
root = logging.getLogger()
root.addHandler(queue_handler)
formatter = logging.Formatter('%(threadName)s: %(message)s')
handler.setFormatter(formatter)
listener.start()
# The log output will display the thread which generated
# the event (the main thread) rather than the internal
# thread which monitors the internal queue. This is what
# you want to happen.
root.warning('Look out!')
listener.stop()

在运行后会产生:

MainThread: Look out!

在 3.5 版更改: 在 Python 3.5 之前,QueueListener 总会把由队列接收到的每条信息都传递给已初始化的每个处理程序。(因为这里假定级别过滤操作已在写入队列时完成了。)从 3.5 版开始,可以修改这种处理方式,只要将关键字参数 respect_handler_level=True 传给侦听器的构造函数即可。这样侦听器将会把每条信息的级别与 handler 的级别进行比较,只在适配时才会将信息传给 handler 。

通过网络收发日志事件

假定现在要通过网络发送日志事件,并在接收端进行处理。有一种简单的方案,就是在发送端的根日志对象连接一个 SocketHandler 实例:

import logging, logging.handlers
rootLogger = logging.getLogger('')
rootLogger.setLevel(logging.DEBUG)
socketHandler = logging.handlers.SocketHandler('localhost',
                    logging.handlers.DEFAULT_TCP_LOGGING_PORT)
# don't bother with a formatter, since a socket handler sends the event as
# an unformatted pickle
rootLogger.addHandler(socketHandler)
# Now, we can log to the root logger, or any other logger. First the root...
logging.info('Jackdaws love my big sphinx of quartz.')
# Now, define a couple of other loggers which might represent areas in your
# application:
logger1 = logging.getLogger('myapp.area1')
logger2 = logging.getLogger('myapp.area2')
logger1.debug('Quick zephyrs blow, vexing daft Jim.')
logger1.info('How quickly daft jumping zebras vex.')
logger2.warning('Jail zesty vixen who grabbed pay from quack.')
logger2.error('The five boxing wizards jump quickly.')

在接收端,可以用 socketserver 模块设置一个接收器。简要示例如下:

import pickle
import logging
import logging.handlers
import socketserver
import struct
class LogRecordStreamHandler(socketserver.StreamRequestHandler):
    """Handler for a streaming logging request.
    This basically logs the record using whatever logging policy is
    configured locally.
    """
    def handle(self):
        """
        Handle multiple requests - each expected to be a 4-byte length,
        followed by the LogRecord in pickle format. Logs the record
        according to whatever policy is configured locally.
        """
        while True:
            chunk = self.connection.recv(4)
            if len(chunk) < 4:
                break
            slen = struct.unpack('>L', chunk)[0]
            chunk = self.connection.recv(slen)
            while len(chunk) < slen:
                chunk = chunk + self.connection.recv(slen - len(chunk))
            obj = self.unPickle(chunk)
            record = logging.makeLogRecord(obj)
            self.handleLogRecord(record)
    def unPickle(self, data):
        return pickle.loads(data)
    def handleLogRecord(self, record):
        # if a name is specified, we use the named logger rather than the one
        # implied by the record.
        if self.server.logname is not None:
            name = self.server.logname
        else:
            name = record.name
        logger = logging.getLogger(name)
        # N.B. EVERY record gets logged. This is because Logger.handle
        # is normally called AFTER logger-level filtering. If you want
        # to do filtering, do it at the client end to save wasting
        # cycles and network bandwidth!
        logger.handle(record)
class LogRecordSocketReceiver(socketserver.ThreadingTCPServer):
    """
    Simple TCP socket-based logging receiver suitable for testing.
    """
    allow_reuse_address = True
    def __init__(self, host='localhost',
                 port=logging.handlers.DEFAULT_TCP_LOGGING_PORT,
                 handler=LogRecordStreamHandler):
        socketserver.ThreadingTCPServer.__init__(self, (host, port), handler)
        self.abort = 0
        self.timeout = 1
        self.logname = None
    def serve_until_stopped(self):
        import select
        abort = 0
        while not abort:
            rd, wr, ex = select.select([self.socket.fileno()],
                                       [], [],
                                       self.timeout)
            if rd:
                self.handle_request()
            abort = self.abort
def main():
    logging.basicConfig(
        format='%(relativeCreated)5d %(name)-15s %(levelname)-8s %(message)s')
    tcpserver = LogRecordSocketReceiver()
    print('About to start TCP server...')
    tcpserver.serve_until_stopped()
if __name__ == '__main__':
    main()

先运行服务端,再运行客户端。客户端控制台不会显示什么信息;在服务端应该会看到如下内容:

About to start TCP server...
   59 root            INFO     Jackdaws love my big sphinx of quartz.
   59 myapp.area1     DEBUG    Quick zephyrs blow, vexing daft Jim.
   69 myapp.area1     INFO     How quickly daft jumping zebras vex.
   69 myapp.area2     WARNING  Jail zesty vixen who grabbed pay from quack.
   69 myapp.area2     ERROR    The five boxing wizards jump quickly.

请注意,某些时候 pickle 会存在一些安全问题。若有问题可换用自己的序列化方案,只要覆盖 makePickle() 方法即可,并调整上述脚本以采用自己的序列化方案。

在自己的输出日志中添加上下文信息

有时,除了调用日志对象时传入的参数之外,还希望日志输出中能包含上下文信息。 比如在网络应用程序中,可能需要在日志中记录某客户端的信息(如远程客户端的用户名或 IP 地址)。 这虽然可以用 extra 参数实现,但传递起来并不总是很方便。 虽然为每个网络连接都创建 Logger 实例貌似不错,但并不是个好主意,因为这些实例不会被垃圾回收。 虽然在实践中不是问题,但当 Logger 实例的数量取决于应用程序要采用的日志粒度时,如果 Logger 实例的数量实际上是无限的,则有可能难以管理。

利用 LoggerAdapter 传递上下文信息

要传递上下文信息和日志事件信息,有一种简单方案是利用 LoggerAdapter 类。这个类设计得类似 Logger,所以可以直接调用 debug()info()warning()error()exception()critical()log()。这些方法的签名与 Logger 对应的方法相同,所以这两类实例可以交换使用。

当你创建一个 LoggerAdapter 的实例时,你会传入一个 Logger 的实例和一个包含了上下文信息的字典对象。当你调用一个 LoggerAdapter 实例的方法时,它会把调用委托给内部的 Logger 的实例,并为其整理相关的上下文信息。这是 LoggerAdapter 的一个代码片段:

def debug(self, msg, /, *args, **kwargs):
    """
    Delegate a debug call to the underlying logger, after adding
    contextual information from this adapter instance.
    """
    msg, kwargs = self.process(msg, kwargs)
    self.logger.debug(msg, *args, **kwargs)

LoggerAdapterprocess() 方法是将上下文信息添加到日志的输出中。 它传入日志消息和日志调用的关键字参数,并传回(隐式的)这些修改后的内容去调用底层的日志记录器。此方法的默认参数只是一个消息字段,但留有一个 ‘extra’ 的字段作为关键字参数传给构造器。当然,如果你在调用适配器时传入了一个 ‘extra’ 字段的参数,它会被静默覆盖。

使用 ‘extra’ 的优点是这些键值对会被传入 LogRecord 实例的 dict 中,让你通过 Formatter 的实例直接使用定制的字符串,实例能找到这个字典类对象的键。 如果你需要一个其他的方法,比如说,想要在消息字符串前后增加上下文信息,你只需要创建一个 LoggerAdapter 的子类,并覆盖它的 process() 方法来做你想做的事情,以下是一个简单的示例:

class CustomAdapter(logging.LoggerAdapter):
    """
    This example adapter expects the passed in dict-like object to have a
    'connid' key, whose value in brackets is prepended to the log message.
    """
    def process(self, msg, kwargs):
        return '[%s] %s' % (self.extra['connid'], msg), kwargs

你可以这样使用:

logger = logging.getLogger(__name__)
adapter = CustomAdapter(logger, {'connid': some_conn_id})

然后,你记录在适配器中的任何事件消息前将添加some_conn_id的值。

使用除字典之外的其它对象传递上下文信息

你不需要将一个实际的字典传递给 LoggerAdapter-你可以传入一个实现了__getitem____iter__的类的实例,这样它就像是一个字典。这对于你想动态生成值(而字典中的值往往是常量)将很有帮助。

使用过滤器传递上下文信息

你也可以使用一个用户定义的类 Filter 在日志输出中添加上下文信息。Filter 的实例是被允许修改传入的 LogRecords,包括添加其他的属性,然后可以使用合适的格式化字符串输出,或者可以使用一个自定义的类 Formatter

例如,在一个web应用程序中,正在处理的请求(或者至少是请求的一部分),可以存储在一个线程本地 (threading.local) 变量中,然后从Filter 中去访问。请求中的信息,如IP地址和用户名将被存储在LogRecord中,使用上例 LoggerAdapter 中的 ‘ip’ 和 ‘user’ 属性名。在这种情况下,可以使用相同的格式化字符串来得到上例中类似的输出结果。这是一段示例代码:

import logging
from random import choice
class ContextFilter(logging.Filter):
    """
    This is a filter which injects contextual information into the log.
    Rather than use actual contextual information, we just use random
    data in this demo.
    """
    USERS = ['jim', 'fred', 'sheila']
    IPS = ['123.231.231.123', '127.0.0.1', '192.168.0.1']
    def filter(self, record):
        record.ip = choice(ContextFilter.IPS)
        record.user = choice(ContextFilter.USERS)
        return True
if __name__ == '__main__':
    levels = (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL)
    logging.basicConfig(level=logging.DEBUG,
                        format='%(asctime)-15s %(name)-5s %(levelname)-8s IP: %(ip)-15s User: %(user)-8s %(message)s')
    a1 = logging.getLogger('a.b.c')
    a2 = logging.getLogger('d.e.f')
    f = ContextFilter()
    a1.addFilter(f)
    a2.addFilter(f)
    a1.debug('A debug message')
    a1.info('An info message with %s', 'some parameters')
    for x in range(10):
        lvl = choice(levels)
        lvlname = logging.getLevelName(lvl)
        a2.log(lvl, 'A message at %s level with %d %s', lvlname, 2, 'parameters')

在运行时,产生如下内容:

2010-09-06 22:38:15,292 a.b.c DEBUG    IP: 123.231.231.123 User: fred     A debug message
2010-09-06 22:38:15,300 a.b.c INFO     IP: 192.168.0.1     User: sheila   An info message with some parameters
2010-09-06 22:38:15,300 d.e.f CRITICAL IP: 127.0.0.1       User: sheila   A message at CRITICAL level with 2 parameters
2010-09-06 22:38:15,300 d.e.f ERROR    IP: 127.0.0.1       User: jim      A message at ERROR level with 2 parameters
2010-09-06 22:38:15,300 d.e.f DEBUG    IP: 127.0.0.1       User: sheila   A message at DEBUG level with 2 parameters
2010-09-06 22:38:15,300 d.e.f ERROR    IP: 123.231.231.123 User: fred     A message at ERROR level with 2 parameters
2010-09-06 22:38:15,300 d.e.f CRITICAL IP: 192.168.0.1     User: jim      A message at CRITICAL level with 2 parameters
2010-09-06 22:38:15,300 d.e.f CRITICAL IP: 127.0.0.1       User: sheila   A message at CRITICAL level with 2 parameters
2010-09-06 22:38:15,300 d.e.f DEBUG    IP: 192.168.0.1     User: jim      A message at DEBUG level with 2 parameters
2010-09-06 22:38:15,301 d.e.f ERROR    IP: 127.0.0.1       User: sheila   A message at ERROR level with 2 parameters
2010-09-06 22:38:15,301 d.e.f DEBUG    IP: 123.231.231.123 User: fred     A message at DEBUG level with 2 parameters
2010-09-06 22:38:15,301 d.e.f INFO     IP: 123.231.231.123 User: fred     A message at INFO level with 2 parameters

从多个进程记录至单个文件

尽管 logging 是线程安全的,将单个进程中的多个线程日志记录至单个文件也 受支持的,但将 多个进程 中的日志记录至单个文件则 不是 受支持的,因为在 Python 中并没有在多个进程中实现对单个文件访问的序列化的标准方案。 如果你需要将多个进程中的日志记录至单个文件,有一个方案是让所有进程都将日志记录至一个 SocketHandler,然后用一个实现了套接字服务器的单独进程一边从套接字中读取一边将日志记录至文件。 (如果愿意的话,你可以在一个现有进程中专门开一个线程来执行此项功能。)

你也可以编写你自己的处理程序,让其使用 multiprocessing 模块中的 Lock 类来顺序访问你的多个进程中的文件。 现有的 FileHandler 及其子类目前并不使用 multiprocessing,尽管它们将来可能会这样做。 请注意在目前,multiprocessing 模块并未在所有平台上都提供可用的锁功能 (参见 https://bugs.python.org/issue3770)。

或者,你也可以使用 QueueQueueHandler 将所有的日志事件发送至你的多进程应用的一个进程中。 以下示例脚本演示了如何执行此操作。 在示例中,一个单独的监听进程负责监听其他进程的日志事件,并根据自己的配置记录。 尽管示例只演示了这种方法(例如你可能希望使用单独的监听线程而非监听进程 —— 它们的实现是类似的),但你也可以在应用程序的监听进程和其他进程使用不同的配置,它可以作为满足你特定需求的一个基础:

# You'll need these imports in your own code
import logging
import logging.handlers
import multiprocessing
# Next two import lines for this demo only
from random import choice, random
import time
#
# Because you'll want to define the logging configurations for listener and workers, the
# listener and worker process functions take a configurer parameter which is a callable
# for configuring logging for that process. These functions are also passed the queue,
# which they use for communication.
#
# In practice, you can configure the listener however you want, but note that in this
# simple example, the listener does not apply level or filter logic to received records.
# In practice, you would probably want to do this logic in the worker processes, to avoid
# sending events which would be filtered out between processes.
#
# The size of the rotated files is made small so you can see the results easily.
def listener_configurer():
    root = logging.getLogger()
    h = logging.handlers.RotatingFileHandler('mptest.log', 'a', 300, 10)
    f = logging.Formatter('%(asctime)s %(processName)-10s %(name)s %(levelname)-8s %(message)s')
    h.setFormatter(f)
    root.addHandler(h)
# This is the listener process top-level loop: wait for logging events
# (LogRecords)on the queue and handle them, quit when you get a None for a
# LogRecord.
def listener_process(queue, configurer):
    configurer()
    while True:
        try:
            record = queue.get()
            if record is None:  # We send this as a sentinel to tell the listener to quit.
                break
            logger = logging.getLogger(record.name)
            logger.handle(record)  # No level or filter logic applied - just do it!
        except Exception:
            import sys, traceback
            print('Whoops! Problem:', file=sys.stderr)
            traceback.print_exc(file=sys.stderr)
# Arrays used for random selections in this demo
LEVELS = [logging.DEBUG, logging.INFO, logging.WARNING,
          logging.ERROR, logging.CRITICAL]
LOGGERS = ['a.b.c', 'd.e.f']
MESSAGES = [
    'Random message #1',
    'Random message #2',
    'Random message #3',
]
# The worker configuration is done at the start of the worker process run.
# Note that on Windows you can't rely on fork semantics, so each process
# will run the logging configuration code when it starts.
def worker_configurer(queue):
    h = logging.handlers.QueueHandler(queue)  # Just the one handler needed
    root = logging.getLogger()
    root.addHandler(h)
    # send all messages, for demo; no other level or filter logic applied.
    root.setLevel(logging.DEBUG)
# This is the worker process top-level loop, which just logs ten events with
# random intervening delays before terminating.
# The print messages are just so you know it's doing something!
def worker_process(queue, configurer):
    configurer(queue)
    name = multiprocessing.current_process().name
    print('Worker started: %s' % name)
    for i in range(10):
        time.sleep(random())
        logger = logging.getLogger(choice(LOGGERS))
        level = choice(LEVELS)
        message = choice(MESSAGES)
        logger.log(level, message)
    print('Worker finished: %s' % name)
# Here's where the demo gets orchestrated. Create the queue, create and start
# the listener, create ten workers and start them, wait for them to finish,
# then send a None to the queue to tell the listener to finish.
def main():
    queue = multiprocessing.Queue(-1)
    listener = multiprocessing.Process(target=listener_process,
                                       args=(queue, listener_configurer))
    listener.start()
    workers = []
    for i in range(10):
        worker = multiprocessing.Process(target=worker_process,
                                         args=(queue, worker_configurer))
        workers.append(worker)
        worker.start()
    for w in workers:
        w.join()
    queue.put_nowait(None)
    listener.join()
if __name__ == '__main__':
    main()

上面脚本的一个变种,仍然在主进程中记录日志,但使用一个单独的线程:

import logging
import logging.config
import logging.handlers
from multiprocessing import Process, Queue
import random
import threading
import time
def logger_thread(q):
    while True:
        record = q.get()
        if record is None:
            break
        logger = logging.getLogger(record.name)
        logger.handle(record)
def worker_process(q):
    qh = logging.handlers.QueueHandler(q)
    root = logging.getLogger()
    root.setLevel(logging.DEBUG)
    root.addHandler(qh)
    levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
              logging.CRITICAL]
    loggers = ['foo', 'foo.bar', 'foo.bar.baz',
               'spam', 'spam.ham', 'spam.ham.eggs']
    for i in range(100):
        lvl = random.choice(levels)
        logger = logging.getLogger(random.choice(loggers))
        logger.log(lvl, 'Message no. %d', i)
if __name__ == '__main__':
    q = Queue()
    d = {
        'version': 1,
        'formatters': {
            'detailed': {
                'class': 'logging.Formatter',
                'format': '%(asctime)s %(name)-15s %(levelname)-8s %(processName)-10s %(message)s'
            }
        },
        'handlers': {
            'console': {
                'class': 'logging.StreamHandler',
                'level': 'INFO',
            },
            'file': {
                'class': 'logging.FileHandler',
                'filename': 'mplog.log',
                'mode': 'w',
                'formatter': 'detailed',
            },
            'foofile': {
                'class': 'logging.FileHandler',
                'filename': 'mplog-foo.log',
                'mode': 'w',
                'formatter': 'detailed',
            },
            'errors': {
                'class': 'logging.FileHandler',
                'filename': 'mplog-errors.log',
                'mode': 'w',
                'level': 'ERROR',
                'formatter': 'detailed',
            },
        },
        'loggers': {
            'foo': {
                'handlers': ['foofile']
            }
        },
        'root': {
            'level': 'DEBUG',
            'handlers': ['console', 'file', 'errors']
        },
    }
    workers = []
    for i in range(5):
        wp = Process(target=worker_process, name='worker %d' % (i + 1), args=(q,))
        workers.append(wp)
        wp.start()
    logging.config.dictConfig(d)
    lp = threading.Thread(target=logger_thread, args=(q,))
    lp.start()
    # At this point, the main process could do some useful work of its own
    # Once it's done that, it can wait for the workers to terminate...
    for wp in workers:
        wp.join()
    # And now tell the logging thread to finish up, too
    q.put(None)
    lp.join()

这段变种的代码展示了如何使用特定的日志记录配置 - 例如foo记录器使用了特殊的处理程序,将 foo 子系统中所有的事件记录至一个文件 mplog-foo.log。在主进程(即使是在工作进程中产生的日志事件)的日志记录机制中将直接使用恰当的配置。

concurrent.futures.ProcessPoolExecutor 的用法

若要利用 concurrent.futures.ProcessPoolExecutor 启动工作进程,创建队列的方式应稍有不同。不能是:

queue = multiprocessing.Queue(-1)

而应是:

queue = multiprocessing.Manager().Queue(-1)  # also works with the examples above

然后就可以将以下工作进程的创建过程:

workers = []
for i in range(10):
    worker = multiprocessing.Process(target=worker_process,
                                     args=(queue, worker_configurer))
    workers.append(worker)
    worker.start()
for w in workers:
    w.join()

改为 (记得要先导入 concurrent.futures):

with concurrent.futures.ProcessPoolExecutor(max_workers=10) as executor:
    for i in range(10):
        executor.submit(worker_process, queue, worker_configurer)

轮换日志文件

有时,你希望当日志文件不断记录增长至一定大小时,打开一个新的文件接着记录。 你可能希望只保留一定数量的日志文件,当不断的创建文件到达该数量时,又覆盖掉最开始的文件形成循环。 对于这种使用场景,日志包提供了 RotatingFileHandler:

import glob
import logging
import logging.handlers
LOG_FILENAME = 'logging_rotatingfile_example.out'
# Set up a specific logger with our desired output level
my_logger = logging.getLogger('MyLogger')
my_logger.setLevel(logging.DEBUG)
# Add the log message handler to the logger
handler = logging.handlers.RotatingFileHandler(
              LOG_FILENAME, maxBytes=20, backupCount=5)
my_logger.addHandler(handler)
# Log some messages
for i in range(20):
    my_logger.debug('i = %d' % i)
# See what files are created
logfiles = glob.glob('%s*' % LOG_FILENAME)
for filename in logfiles:
    print(filename)

结果应该是6个单独的文件,每个文件都包含了应用程序的部分历史日志:

logging_rotatingfile_example.out
logging_rotatingfile_example.out.1
logging_rotatingfile_example.out.2
logging_rotatingfile_example.out.3
logging_rotatingfile_example.out.4
logging_rotatingfile_example.out.5

最新的文件始终是:file:logging_rotatingfile_example.out,每次到达大小限制时,都会使用后缀.1重命名。每个现有的备份文件都会被重命名并增加其后缀(例如.1 变为.2),而.6文件会被删除掉。

显然,这个例子将日志长度设置得太小,这是一个极端的例子。 你可能希望将 maxBytes 设置为一个合适的值。

使用其他日志格式化方式

当日志模块被添加至 Python 标准库时,只有一种格式化消息内容的方法即 %-formatting。 在那之后,Python 又增加了两种格式化方法: string.Template (在 Python 2.4 中新增) 和 str.format() (在 Python 2.6 中新增)。

日志(从 3.2 开始)为这两种格式化方式提供了更多支持。Formatter 类可以添加一个额外的可选关键字参数 style。它的默认值是 '%',其他的值 '{''$' 也支持,对应了其他两种格式化样式。其保持了向后兼容(如您所愿),但通过显示指定样式参数,你可以指定格式化字符串的方式是使用 str.format()string.Template。 这里是一个控制台会话的示例,展示了这些方式:

>>> import logging
>>> root = logging.getLogger()
>>> root.setLevel(logging.DEBUG)
>>> handler = logging.StreamHandler()
>>> bf = logging.Formatter('{asctime} {name} {levelname:8s} {message}',
...                        style='{')
>>> handler.setFormatter(bf)
>>> root.addHandler(handler)
>>> logger = logging.getLogger('foo.bar')
>>> logger.debug('This is a DEBUG message')
2010-10-28 15:11:55,341 foo.bar DEBUG    This is a DEBUG message
>>> logger.critical('This is a CRITICAL message')
2010-10-28 15:12:11,526 foo.bar CRITICAL This is a CRITICAL message
>>> df = logging.Formatter('$asctime $name ${levelname} $message',
...                        style='$')
>>> handler.setFormatter(df)
>>> logger.debug('This is a DEBUG message')
2010-10-28 15:13:06,924 foo.bar DEBUG This is a DEBUG message
>>> logger.critical('This is a CRITICAL message')
2010-10-28 15:13:11,494 foo.bar CRITICAL This is a CRITICAL message
>>>

请注意最终输出到日志的消息格式完全独立于单条日志消息的构造方式。 它仍然可以使用 %-formatting,如下所示:

>>> logger.error('This is an%s %s %s', 'other,', 'ERROR,', 'message')
2010-10-28 15:19:29,833 foo.bar ERROR This is another, ERROR, message
>>>

日志调用(logger.debug()logger.info() 等)接受的位置参数只会用于日志信息本身,而关键字参数仅用于日志调用的可选处理参数(如关键字参数 exc_info 表示应记录跟踪信息, extra 则标识了需要加入日志的额外上下文信息)。所以不能直接用 str.format()string.Template 语法进行日志调用,因为日志包在内部使用 %-f 格式来合并格式串和参数变量。在保持向下兼容性时,这一点不会改变,因为已有代码中的所有日志调用都会使用%-f 格式串。

还有一种方法可以构建自己的日志信息,就是利用 {}- 和 $- 格式。回想一下,任意对象都可用为日志信息的格式串,日志包将会调用该对象的 str() 方法,以获取最终的格式串。不妨看下一下两个类:

class BraceMessage:
    def __init__(self, fmt, /, *args, **kwargs):
        self.fmt = fmt
        self.args = args
        self.kwargs = kwargs
    def __str__(self):
        return self.fmt.format(*self.args, **self.kwargs)
class DollarMessage:
    def __init__(self, fmt, /, **kwargs):
        self.fmt = fmt
        self.kwargs = kwargs
    def __str__(self):
        from string import Template
        return Template(self.fmt).substitute(**self.kwargs)

上述两个类均可代替格式串,使得能用 {}- 或 $-formatting 构建最终的“日志信息”部分,这些信息将出现在格式化后的日志输出中,替换 %(message)s 或“{message}”或“$message”。每次写入日志时都要使用类名,有点不大实用,但如果用上 __ 之类的别名就相当合适了(双下划线 —- 不要与 _ 混淆,单下划线用作 gettext.gettext() 或相关函数的同义词/别名 )。

Python 并没有上述两个类,当然复制粘贴到自己的代码中也很容易。用法可如下所示(假定在名为 wherever 的模块中声明):

>>> from wherever import BraceMessage as __
>>> print(__('Message with {0} {name}', 2, name='placeholders'))
Message with 2 placeholders
>>> class Point: pass
...
>>> p = Point()
>>> p.x = 0.5
>>> p.y = 0.5
>>> print(__('Message with coordinates: ({point.x:.2f}, {point.y:.2f})',
...       point=p))
Message with coordinates: (0.50, 0.50)
>>> from wherever import DollarMessage as __
>>> print(__('Message with $num $what', num=2, what='placeholders'))
Message with 2 placeholders
>>>

上述示例用了 print() 演示格式化输出的过程,实际记录日志时当然会用类似 logger.debug() 的方法来应用。

值得注意的是,上述做法对性能并没什么影响:格式化过程其实不是在日志记录调用时发生的,而是在日志信息即将由 handler 输出到日志时发生。因此,唯一可能让人困惑的稍不寻常的地方,就是包裹在格式串和参数外面的括号,而不是格式串。因为 __ 符号只是对 XXXMessage 类的构造函数调用的语法糖。

只要愿意,上述类似的效果即可用 LoggerAdapter 实现,如下例所示:

import logging
class Message:
    def __init__(self, fmt, args):
        self.fmt = fmt
        self.args = args
    def __str__(self):
        return self.fmt.format(*self.args)
class StyleAdapter(logging.LoggerAdapter):
    def __init__(self, logger, extra=None):
        super().__init__(logger, extra or {})
    def log(self, level, msg, /, *args, **kwargs):
        if self.isEnabledFor(level):
            msg, kwargs = self.process(msg, kwargs)
            self.logger._log(level, Message(msg, args), (), **kwargs)
logger = StyleAdapter(logging.getLogger(__name__))
def main():
    logger.debug('Hello, {}', 'world!')
if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
    main()

在用 Python 3.2 以上版本运行时,上述代码应该会把 Hello, world! 写入日志。

自定义 LogRecord

每条日志事件都由一个 LogRecord 实例表示。当某事件要记入日志并且没有被某级别过滤掉时,就会创建一个 LogRecord 对象,并将有关事件的信息填入,传给该日志对象的 handler(及其祖先,直至对象禁止向上传播为止)。在 Python 3.2 之前,只有两个地方会进行事件的创建:

  • Logger.makeRecord(),在事件正常记入日志的过程中调用。这会直接调用 LogRecord 来创建一个实例。
  • makeLogRecord(),调用时会带上一个字典参数,其中存放着要加入 LogRecord 的属性。这通常在通过网络接收到合适的字典时调用(如通过 SocketHandler 以 pickle 形式,或通过 HTTPHandler 以 JSON 形式)。

于是这意味着若要对 LogRecord 进行定制,必须进行下述某种操作。

  • 创建 Logger 自定义子类,重写 Logger.makeRecord(),并在实例化所需日志对象之前用 setLoggerClass() 进行设置。
  • 为日志对象添加 Filter 或 handler,当其 filter() 方法被调用时,会执行必要的定制操作。

比如说在有多个不同库要完成不同操作的场景下,第一种方式会有点笨拙。 每次都要尝试设置自己的 Logger 子类,而起作用的是最后一次尝试。

第二种方式在多数情况下效果都比较良好,但不允许你使用特殊化的 LogRecord 子类。 库开发者可以为他们的日志记录器设置合适的过滤器,但他们应当要记得每次引入新的日志记录器时都需如此(他们只需通过添加新的包或模块并执行以下操作即可):

logger = logging.getLogger(__name__)

或许这样要顾及太多事情。开发人员还可以将过滤器附加到其顶级日志对象的 NullHandler 中,但如果应用程序开发人员将 handler 附加到较底层库的日志对象,则不会调用该过滤器 —- 所以 handler 输出的内容不会符合库开发人员的预期。

在 Python 3.2 以上版本中,LogRecord 的创建是通过工厂对象完成的,工厂对象可以指定。工厂对象只是一个可调用对象,可以用 setLogRecordFactory() 进行设置,并用 getLogRecordFactory() 进行查询。工厂对象的调用参数与 LogRecord 的构造函数相同,因为 LogRecord 是工厂对象的默认设置。

这种方式可以让自定义工厂对象完全控制 LogRecord 的创建过程。比如可以返回一个子类,或者在创建的日志对象中加入一些额外的属性,使用方式如下所示:

old_factory = logging.getLogRecordFactory()
def record_factory(*args, **kwargs):
    record = old_factory(*args, **kwargs)
    record.custom_attribute = 0xdecafbad
    return record
logging.setLogRecordFactory(record_factory)

这种模式允许不同的库将多个工厂对象链在一起,只要不会覆盖彼此的属性或标准属性,就不会出现意外。但应记住,工厂链中的每个节点都会增加日志操作的运行开销,本技术仅在采用 Filter 无法达到目标时才应使用。

子类化 QueueHandler - ZeroMQ 示例

你可以使用 QueueHandler 子类将消息发送给其他类型的队列 ,比如 ZeroMQ ‘publish’ 套接字。 在以下示例中,套接字将单独创建并传给处理句柄 (作为它的 ‘queue’):

import zmq   # using pyzmq, the Python binding for ZeroMQ
import json  # for serializing records portably
ctx = zmq.Context()
sock = zmq.Socket(ctx, zmq.PUB)  # or zmq.PUSH, or other suitable value
sock.bind('tcp://*:5556')        # or wherever
class ZeroMQSocketHandler(QueueHandler):
    def enqueue(self, record):
        self.queue.send_json(record.__dict__)
handler = ZeroMQSocketHandler(sock)

当然还有其他方案,比如通过 hander 传入所需数据,以创建 socket:

class ZeroMQSocketHandler(QueueHandler):
    def __init__(self, uri, socktype=zmq.PUB, ctx=None):
        self.ctx = ctx or zmq.Context()
        socket = zmq.Socket(self.ctx, socktype)
        socket.bind(uri)
        super().__init__(socket)
    def enqueue(self, record):
        self.queue.send_json(record.__dict__)
    def close(self):
        self.queue.close()

子类化 QueueListener —— ZeroMQ 示例

你还可以子类化 QueueListener 来从其他类型的队列中获取消息,比如从 ZeroMQ ‘subscribe’ 套接字。 下面是一个例子:

class ZeroMQSocketListener(QueueListener):
    def __init__(self, uri, /, *handlers, **kwargs):
        self.ctx = kwargs.get('ctx') or zmq.Context()
        socket = zmq.Socket(self.ctx, zmq.SUB)
        socket.setsockopt_string(zmq.SUBSCRIBE, '')  # subscribe to everything
        socket.connect(uri)
        super().__init__(socket, *handlers, **kwargs)
    def dequeue(self):
        msg = self.queue.recv_json()
        return logging.makeLogRecord(msg)

基于字典进行日志配置的示例

以下是日志配置字典的一个示例——它取自 Django 项目的文档https://docs.djangoproject.com/en/stable/topics/logging/#configuring-logging_。此字典将被传给 dictConfig() 以使配置生效:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': True,
    'formatters': {
        'verbose': {
            'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
        },
        'simple': {
            'format': '%(levelname)s %(message)s'
        },
    },
    'filters': {
        'special': {
            '()': 'project.logging.SpecialFilter',
            'foo': 'bar',
        }
    },
    'handlers': {
        'null': {
            'level':'DEBUG',
            'class':'django.utils.log.NullHandler',
        },
        'console':{
            'level':'DEBUG',
            'class':'logging.StreamHandler',
            'formatter': 'simple'
        },
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
            'filters': ['special']
        }
    },
    'loggers': {
        'django': {
            'handlers':['null'],
            'propagate': True,
            'level':'INFO',
        },
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': False,
        },
        'myproject.custom': {
            'handlers': ['console', 'mail_admins'],
            'level': 'INFO',
            'filters': ['special']
        }
    }
}

有关本配置的更多信息,请参阅 Django 文档的 有关章节

利用 rotator 和 namer 自定义日志轮换操作

以下代码给出了定义 namer 和 rotator 的示例,其中演示了基于 zlib 的日志文件压缩过程:

def namer(name):
    return name + ".gz"
def rotator(source, dest):
    with open(source, "rb") as sf:
        data = sf.read()
        compressed = zlib.compress(data, 9)
        with open(dest, "wb") as df:
            df.write(compressed)
    os.remove(source)
rh = logging.handlers.RotatingFileHandler(...)
rh.rotator = rotator
rh.namer = namer

这些不是“真正的” .gz 文件,因为他们只是纯压缩数据,缺少真正 gzip 文件中的“容器”。此段代码只是用于演示。

更加详细的多道处理示例

以下可运行的示例显示了如何利用配置文件在多进程中应用日志。这些配置相当简单,但足以说明如何在真实的多进程场景中实现较为复杂的配置。

上述示例中,主进程产生一个侦听器进程和一些工作进程。每个主进程、侦听器进程和工作进程都有三种独立的日志配置(工作进程共享同一套配置)。大家可以看到主进程的日志记录过程、工作线程向 QueueHandler 写入日志的过程,以及侦听器实现 QueueListener 和较为复杂的日志配置,如何将由队列接收到的事件分发给配置指定的 handler。请注意,这些配置纯粹用于演示,但应该能调整代码以适用于自己的场景。

以下是代码——但愿文档字符串和注释能有助于理解其工作原理:

import logging
import logging.config
import logging.handlers
from multiprocessing import Process, Queue, Event, current_process
import os
import random
import time
class MyHandler:
    """
    A simple handler for logging events. It runs in the listener process and
    dispatches events to loggers based on the name in the received record,
    which then get dispatched, by the logging system, to the handlers
    configured for those loggers.
    """
    def handle(self, record):
        if record.name == "root":
            logger = logging.getLogger()
        else:
            logger = logging.getLogger(record.name)
        if logger.isEnabledFor(record.levelno):
            # The process name is transformed just to show that it's the listener
            # doing the logging to files and console
            record.processName = '%s (for %s)' % (current_process().name, record.processName)
            logger.handle(record)
def listener_process(q, stop_event, config):
    """
    This could be done in the main process, but is just done in a separate
    process for illustrative purposes.
    This initialises logging according to the specified configuration,
    starts the listener and waits for the main process to signal completion
    via the event. The listener is then stopped, and the process exits.
    """
    logging.config.dictConfig(config)
    listener = logging.handlers.QueueListener(q, MyHandler())
    listener.start()
    if os.name == 'posix':
        # On POSIX, the setup logger will have been configured in the
        # parent process, but should have been disabled following the
        # dictConfig call.
        # On Windows, since fork isn't used, the setup logger won't
        # exist in the child, so it would be created and the message
        # would appear - hence the "if posix" clause.
        logger = logging.getLogger('setup')
        logger.critical('Should not appear, because of disabled logger ...')
    stop_event.wait()
    listener.stop()
def worker_process(config):
    """
    A number of these are spawned for the purpose of illustration. In
    practice, they could be a heterogeneous bunch of processes rather than
    ones which are identical to each other.
    This initialises logging according to the specified configuration,
    and logs a hundred messages with random levels to randomly selected
    loggers.
    A small sleep is added to allow other processes a chance to run. This
    is not strictly needed, but it mixes the output from the different
    processes a bit more than if it's left out.
    """
    logging.config.dictConfig(config)
    levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
              logging.CRITICAL]
    loggers = ['foo', 'foo.bar', 'foo.bar.baz',
               'spam', 'spam.ham', 'spam.ham.eggs']
    if os.name == 'posix':
        # On POSIX, the setup logger will have been configured in the
        # parent process, but should have been disabled following the
        # dictConfig call.
        # On Windows, since fork isn't used, the setup logger won't
        # exist in the child, so it would be created and the message
        # would appear - hence the "if posix" clause.
        logger = logging.getLogger('setup')
        logger.critical('Should not appear, because of disabled logger ...')
    for i in range(100):
        lvl = random.choice(levels)
        logger = logging.getLogger(random.choice(loggers))
        logger.log(lvl, 'Message no. %d', i)
        time.sleep(0.01)
def main():
    q = Queue()
    # The main process gets a simple configuration which prints to the console.
    config_initial = {
        'version': 1,
        'handlers': {
            'console': {
                'class': 'logging.StreamHandler',
                'level': 'INFO'
            }
        },
        'root': {
            'handlers': ['console'],
            'level': 'DEBUG'
        }
    }
    # The worker process configuration is just a QueueHandler attached to the
    # root logger, which allows all messages to be sent to the queue.
    # We disable existing loggers to disable the "setup" logger used in the
    # parent process. This is needed on POSIX because the logger will
    # be there in the child following a fork().
    config_worker = {
        'version': 1,
        'disable_existing_loggers': True,
        'handlers': {
            'queue': {
                'class': 'logging.handlers.QueueHandler',
                'queue': q
            }
        },
        'root': {
            'handlers': ['queue'],
            'level': 'DEBUG'
        }
    }
    # The listener process configuration shows that the full flexibility of
    # logging configuration is available to dispatch events to handlers however
    # you want.
    # We disable existing loggers to disable the "setup" logger used in the
    # parent process. This is needed on POSIX because the logger will
    # be there in the child following a fork().
    config_listener = {
        'version': 1,
        'disable_existing_loggers': True,
        'formatters': {
            'detailed': {
                'class': 'logging.Formatter',
                'format': '%(asctime)s %(name)-15s %(levelname)-8s %(processName)-10s %(message)s'
            },
            'simple': {
                'class': 'logging.Formatter',
                'format': '%(name)-15s %(levelname)-8s %(processName)-10s %(message)s'
            }
        },
        'handlers': {
            'console': {
                'class': 'logging.StreamHandler',
                'formatter': 'simple',
                'level': 'INFO'
            },
            'file': {
                'class': 'logging.FileHandler',
                'filename': 'mplog.log',
                'mode': 'w',
                'formatter': 'detailed'
            },
            'foofile': {
                'class': 'logging.FileHandler',
                'filename': 'mplog-foo.log',
                'mode': 'w',
                'formatter': 'detailed'
            },
            'errors': {
                'class': 'logging.FileHandler',
                'filename': 'mplog-errors.log',
                'mode': 'w',
                'formatter': 'detailed',
                'level': 'ERROR'
            }
        },
        'loggers': {
            'foo': {
                'handlers': ['foofile']
            }
        },
        'root': {
            'handlers': ['console', 'file', 'errors'],
            'level': 'DEBUG'
        }
    }
    # Log some initial events, just to show that logging in the parent works
    # normally.
    logging.config.dictConfig(config_initial)
    logger = logging.getLogger('setup')
    logger.info('About to create workers ...')
    workers = []
    for i in range(5):
        wp = Process(target=worker_process, name='worker %d' % (i + 1),
                     args=(config_worker,))
        workers.append(wp)
        wp.start()
        logger.info('Started worker: %s', wp.name)
    logger.info('About to create listener ...')
    stop_event = Event()
    lp = Process(target=listener_process, name='listener',
                 args=(q, stop_event, config_listener))
    lp.start()
    logger.info('Started listener')
    # We now hang around for the workers to finish their work.
    for wp in workers:
        wp.join()
    # Workers all done, listening can now stop.
    # Logging in the parent still works normally.
    logger.info('Telling listener to stop ...')
    stop_event.set()
    lp.join()
    logger.info('All done.')
if __name__ == '__main__':
    main()

在发送给 SysLogHandler 的信息中插入一个 BOM。

RFC 5424 要求,Unicode 信息应采用字节流形式发送到系统 syslog 守护程序,字节流结构如下所示:可选的纯 ASCII部分,后跟 UTF-8 字节序标记(BOM),然后是采用 UTF-8 编码的 Unicode。(参见 相关规范 。)

在 Python 3.1 的 SysLogHandler 中,已加入了在日志信息中插入 BOM 的代码,但不幸的是,代码并不正确,BOM 出现在了日志信息的开头,因此在它之前就不允许出现纯 ASCII 内容了。

由于无法正常工作, Python 3.2.4 以上版本已删除了出错的插入 BOM 代码。但已有版本的代码不会被替换,若要生成与 RFC 5424 兼容的日志信息,包括一个 BOM 符,前面有可选的纯 ASCII 字节流,后面为 UTF-8 编码的任意 Unicode,那么 需要执行以下操作:

  1. SysLogHandler 实例串上一个 Formatter 实例,格式串可如下:

    'ASCII section\ufeffUnicode section'

    用 UTF-8 编码时,Unicode 码位 U+FEFF 将会编码为 UTF-8 BOM——字节串 b'\xef\xbb\xbf'

  2. 用任意占位符替换 ASCII 部分,但要保证替换之后的数据一定是 ASCII 码(这样在 UTF-8 编码后就会维持不变)。

  3. 用任意占位符替换 Unicode 部分;如果替换后的数据包含超出 ASCII 范围的字符,没问题——他们将用 UTF-8 进行编码。

SysLogHandler 对格式化后的日志信息进行 UTF-8 编码。如果遵循上述规则,应能生成符合 RFC 5424 的日志信息。否则,日志记录过程可能不会有什么反馈,但日志信息将不与 RFC 5424 兼容,syslog 守护程序可能会有出错反应。

结构化日志的实现代码

大多数日志信息是供人阅读的,所以机器解析起来并不容易,但某些时候可能希望以结构化的格式输出,以 能够 被程序解析(无需用到复杂的正则表达式)。这可以直接用 logging 包实现。实现方式有很多,以下是一种比较简单的方案,利用 JSON 以机器可解析的方式对事件信息进行序列化:

import json
import logging
class StructuredMessage:
    def __init__(self, message, /, **kwargs):
        self.message = message
        self.kwargs = kwargs
    def __str__(self):
        return '%s >>> %s' % (self.message, json.dumps(self.kwargs))
_ = StructuredMessage   # optional, to improve readability
logging.basicConfig(level=logging.INFO, format='%(message)s')
logging.info(_('message 1', foo='bar', bar='baz', num=123, fnum=123.456))

上述代码运行后的结果是:

message 1 >>> {"fnum": 123.456, "num": 123, "bar": "baz", "foo": "bar"}

请注意,根据 Python 版本的不同,各项数据的输出顺序可能会不一样。

若需进行更为定制化的处理,可以使用自定义 JSON 编码对象,下面给出完整示例:

from __future__ import unicode_literals
import json
import logging
# This next bit is to ensure the script runs unchanged on 2.x and 3.x
try:
    unicode
except NameError:
    unicode = str
class Encoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, set):
            return tuple(o)
        elif isinstance(o, unicode):
            return o.encode('unicode_escape').decode('ascii')
        return super().default(o)
class StructuredMessage:
    def __init__(self, message, /, **kwargs):
        self.message = message
        self.kwargs = kwargs
    def __str__(self):
        s = Encoder().encode(self.kwargs)
        return '%s >>> %s' % (self.message, s)
_ = StructuredMessage   # optional, to improve readability
def main():
    logging.basicConfig(level=logging.INFO, format='%(message)s')
    logging.info(_('message 1', set_value={1, 2, 3}, snowman='\u2603'))
if __name__ == '__main__':
    main()

上述代码运行后的结果是:

message 1 >>> {"snowman": "\u2603", "set_value": [1, 2, 3]}

请注意,根据 Python 版本的不同,各项数据的输出顺序可能会不一样。

利用 dictConfig() 自定义 handler

有时需要以特定方式自定义日志 handler,如果采用 dictConfig(),可能无需生成子类就可以做到。比如要设置日志文件的所有权。在 POSIX 上,可以利用 shutil.chown() 轻松完成,但 stdlib 中的文件 handler 并不提供内置支持。于是可以用普通函数自定义 handler 的创建,例如:

def owned_file_handler(filename, mode='a', encoding=None, owner=None):
    if owner:
        if not os.path.exists(filename):
            open(filename, 'a').close()
        shutil.chown(filename, *owner)
    return logging.FileHandler(filename, mode, encoding)

然后,你可以在传给 dictConfig() 的日志配置中指定通过调用此函数来创建日志处理程序:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'default': {
            'format': '%(asctime)s %(levelname)s %(name)s %(message)s'
        },
    },
    'handlers': {
        'file':{
            # The values below are popped from this dictionary and
            # used to create the handler, set the handler's level and
            # its formatter.
            '()': owned_file_handler,
            'level':'DEBUG',
            'formatter': 'default',
            # The values below are passed to the handler creator callable
            # as keyword arguments.
            'owner': ['pulse', 'pulse'],
            'filename': 'chowntest.log',
            'mode': 'w',
            'encoding': 'utf-8',
        },
    },
    'root': {
        'handlers': ['file'],
        'level': 'DEBUG',
    },
}

出于演示目的,以下示例设置用户和用户组为 pulse。代码置于一个可运行的脚本文件 chowntest.py 中:

import logging, logging.config, os, shutil
def owned_file_handler(filename, mode='a', encoding=None, owner=None):
    if owner:
        if not os.path.exists(filename):
            open(filename, 'a').close()
        shutil.chown(filename, *owner)
    return logging.FileHandler(filename, mode, encoding)
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'default': {
            'format': '%(asctime)s %(levelname)s %(name)s %(message)s'
        },
    },
    'handlers': {
        'file':{
            # The values below are popped from this dictionary and
            # used to create the handler, set the handler's level and
            # its formatter.
            '()': owned_file_handler,
            'level':'DEBUG',
            'formatter': 'default',
            # The values below are passed to the handler creator callable
            # as keyword arguments.
            'owner': ['pulse', 'pulse'],
            'filename': 'chowntest.log',
            'mode': 'w',
            'encoding': 'utf-8',
        },
    },
    'root': {
        'handlers': ['file'],
        'level': 'DEBUG',
    },
}
logging.config.dictConfig(LOGGING)
logger = logging.getLogger('mylogger')
logger.debug('A debug message')

可能需要 root 权限才能运行:

$ sudo python3.3 chowntest.py
$ cat chowntest.log
2013-11-05 09:34:51,128 DEBUG mylogger A debug message
$ ls -l chowntest.log
-rw-r--r-- 1 pulse pulse 55 2013-11-05 09:34 chowntest.log

请注意此示例用的是 Python 3.3,因为 shutil.chown() 是从此版本开始出现的。 此方式应当适用于任何支持 dictConfig() 的 Python 版本 —— 例如 Python 2.7, 3.2 或更新的版本。 对于 3.3 之前的版本,你应当使用 os.chown() 之类的函数来实现实际的所有权修改。

实际应用中,handler 的创建函数可能位于项目的工具模块中。以下配置:

'()': owned_file_handler,

应使用:

'()': 'ext://project.util.owned_file_handler',

这里的 project.util 可以换成函数所在包的实际名称。 在上述的可用脚本中,应该可以使用 'ext://__main__.owned_file_handler'。 在这里,实际的可调用对象是由 dictConfig()ext:// 说明中解析出来的。

上述示例还指明了其他的文件修改类型的实现方案 —— 比如同样利用 os.chmod() 设置 POSIX 访问权限位。

当然,以上做法也可以扩展到 FileHandler 之外的其他类型的 handler ——比如某个轮换文件 handler,或类型完全不同的其他 handler。

生效于整个应用程序的格式化样式

在 Python 3.2 中,Formatter 增加了一个 style 关键字形参,它默认为 % 以便向下兼容,但是允许采用 {{TX-PL-LABEL}#x60; 来支持 str.format()string.Template 所支持的格式化方式。 请注意此形参控制着用用于最终输出到日志的日志消息格式,并且与单独日志消息的构造方式完全无关。

日志函数(debug(), info() 等)只会读取位置参数获取日志信息本身,而关键字参数仅用于确定日志函数的工作选项(比如关键字参数 exc_info 表示应将跟踪信息记入日志,关键字参数 extra 则给出了需加入日志的额外上下文信息)。所以不能直接使用 str.format()string.Template 这种语法进行日志调用,因为日志包在内部使用 %-f 格式来合并格式串和可变参数。因为尚需保持向下兼容,这一点不会改变,已有代码中的所有日志调用都将采用 %-f 格式串。

有人建议将格式化样式与特定的日志对象进行关联,但其实也会遇到向下兼容的问题,因为已有代码可能用到了某日志对象并采用了 %-f 格式串。

为了让第三方库和自编代码都能够交互使用日志功能,需要决定在单次日志记录调用级别采用什么格式。于是就出现了其他几种格式化样式方案。

LogRecord 工厂的用法

在 Python 3.2 中,伴随着 Formatter 的上述变化,logging 包增加了允许用户使用 setLogRecordFactory() 函数来。设置自己的 LogRecord 子类的功能。 你可以使用此功能来设置自己的 LogRecord 子类,它会通过重载 getMessage() 方法来完成适当的操作。 msg % args 格式化是在此方法的基类实现中进行的,你可以在那里用你自己的格式化操作来替换;但是,你应当注意要支持全部的格式化样式并允许将 %-formatting 作为默认样式,以确保与其他代码进行配合。 还应当注意调用 str(self.msg),正如基类实现所做的一样。

自定义信息对象的使用

另一种方案可能更为简单,可以利用 {}- 和 $- 格式构建自己的日志消息。可以用任意对象作为日志信息的格式串,日志包将调用该对象上 str() 获取实际的格式串。看下以下两个类:

class BraceMessage:
    def __init__(self, fmt, /, *args, **kwargs):
        self.fmt = fmt
        self.args = args
        self.kwargs = kwargs
    def __str__(self):
        return self.fmt.format(*self.args, **self.kwargs)
class DollarMessage:
    def __init__(self, fmt, /, **kwargs):
        self.fmt = fmt
        self.kwargs = kwargs
    def __str__(self):
        from string import Template
        return Template(self.fmt).substitute(**self.kwargs)

以上两个类均都可用于替代格式串,以便用 {}- 或 $-formatting 构建实际的“日志信息”部分,此部分将出现在格式化后的日志输出中,替换 %(message)s 、“{message}”或“$message”。每次要写入日志时都使用类名,如果觉得使用不便,可以采用 M_ 之类的别名(如果将 _ 用于本地化操作,则可用 __)。

下面给出示例。 首先用 str.format() 进行格式化:

>>> __ = BraceMessage
>>> print(__('Message with {0} {1}', 2, 'placeholders'))
Message with 2 placeholders
>>> class Point: pass
...
>>> p = Point()
>>> p.x = 0.5
>>> p.y = 0.5
>>> print(__('Message with coordinates: ({point.x:.2f}, {point.y:.2f})', point=p))
Message with coordinates: (0.50, 0.50)

然后,用 string.Template 格式化:

>>> __ = DollarMessage
>>> print(__('Message with $num $what', num=2, what='placeholders'))
Message with 2 placeholders
>>>

值得注意的是,上述做法对性能并没什么影响:格式化过程其实不是在日志调用时发生的,而是在日志信息即将由 handler 输出到日志时发生。因此,唯一可能让人困惑的稍不寻常的地方,就是包裹在格式串和参数外面的括号,而不是格式串。因为 __ 符号只是对 XXXMessage 类的构造函数调用的语法糖。

利用 dictConfig() 定义过滤器

dictConfig() 可以 对日志过滤器进行设置,尽管乍一看做法并不明显(所以才需要本秘籍)。 由于 Filter 是标准库中唯一的日志过滤器类,不太可能满足众多的要求(它只是作为基类存在),通常需要定义自己的 Filter 子类,并重写 filter() 方法。为此,请在过滤器的配置字典中设置 () 键,指定要用于创建过滤器的可调用对象(最明显可用的就是给出一个类,但也可以提供任何一个可调用对象,只要能返回 Filter 实例即可)。下面是一个完整的例子:

import logging
import logging.config
import sys
class MyFilter(logging.Filter):
    def __init__(self, param=None):
        self.param = param
    def filter(self, record):
        if self.param is None:
            allow = True
        else:
            allow = self.param not in record.msg
        if allow:
            record.msg = 'changed: ' + record.msg
        return allow
LOGGING = {
    'version': 1,
    'filters': {
        'myfilter': {
            '()': MyFilter,
            'param': 'noshow',
        }
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'filters': ['myfilter']
        }
    },
    'root': {
        'level': 'DEBUG',
        'handlers': ['console']
    },
}
if __name__ == '__main__':
    logging.config.dictConfig(LOGGING)
    logging.debug('hello')
    logging.debug('hello - noshow')

以上示例展示了将配置数据传给构造实例的可调用对象,形式是关键字参数。运行后将会输出:

changed: hello

这说明过滤器按照配置的参数生效了。

需要额外注意的地方:

  • 如果在配置中无法直接引用可调用对象(比如位于不同的模块中,并且不能在配置字典所在的位置直接导入),则可以采用 ext://... 的形式。例如,在上述示例中可以使用文本 'ext://__main__.MyFilter' 而不是 MyFilter 对象。
  • 与过滤器一样,上述技术还可用于配置自定义 handler 和格式化对象。

异常信息的自定义格式化

有时可能需要设置自定义的异常信息格式——考虑到会用到参数,假定要让每条日志事件只占一行,即便存在异常信息也一样。这可以用自定义格式化类来实现,如下所示:

import logging
class OneLineExceptionFormatter(logging.Formatter):
    def formatException(self, exc_info):
        """
        Format an exception so that it prints on a single line.
        """
        result = super().formatException(exc_info)
        return repr(result)  # or format into one line however you want to
    def format(self, record):
        s = super().format(record)
        if record.exc_text:
            s = s.replace('\n', '') + '|'
        return s
def configure_logging():
    fh = logging.FileHandler('output.txt', 'w')
    f = OneLineExceptionFormatter('%(asctime)s|%(levelname)s|%(message)s|',
                                  '%d/%m/%Y %H:%M:%S')
    fh.setFormatter(f)
    root = logging.getLogger()
    root.setLevel(logging.DEBUG)
    root.addHandler(fh)
def main():
    configure_logging()
    logging.info('Sample message')
    try:
        x = 1 / 0
    except ZeroDivisionError as e:
        logging.exception('ZeroDivisionError: %s', e)
if __name__ == '__main__':
    main()

运行后将会生成只有两行信息的文件:

28/01/2015 07:21:23|INFO|Sample message|
28/01/2015 07:21:23|ERROR|ZeroDivisionError: integer division or modulo by zero|'Traceback (most recent call last):\n  File "logtest7.py", line 30, in main\n    x = 1 / 0\nZeroDivisionError: integer division or modulo by zero'|

虽然上述处理方式很简单,但也给出了根据喜好对异常信息进行格式化输出的方案。或许 traceback 模块能满足更专门的需求。

语音播报日志信息

有时可能需要以声音的形式呈现日志消息。如果系统自带了文本转语音 (TTS)功能,即便没与 Python 关联也很容易做到。大多数 TTS 系统都有一个可运行的命令行程序,在 handler 中可以用 subprocess 进行调用。这里假定 TTS 命令行程序不会与用户交互,或需要很长时间才会执行完毕,写入日志的信息也不会多到影响用户查看,并且可以接受每次播报一条信息,以下示例实现了等一条信息播完再处理下一条,可能会导致其他 handler 的等待。这个简短示例仅供演示,假定 espeak TTS 包已就绪:

import logging
import subprocess
import sys
class TTSHandler(logging.Handler):
    def emit(self, record):
        msg = self.format(record)
        # Speak slowly in a female English voice
        cmd = ['espeak', '-s150', '-ven+f3', msg]
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                             stderr=subprocess.STDOUT)
        # wait for the program to finish
        p.communicate()
def configure_logging():
    h = TTSHandler()
    root = logging.getLogger()
    root.addHandler(h)
    # the default formatter just returns the message
    root.setLevel(logging.DEBUG)
def main():
    logging.info('Hello')
    logging.debug('Goodbye')
if __name__ == '__main__':
    configure_logging()
    sys.exit(main())

运行后将会以女声播报“Hello”和“Goodbye”。

当然,上述方案也适用于其他 TTS 系统,甚至可以通过利用命令行运行的外部程序来处理消息。

缓冲日志消息并有条件地输出它们

在某些情况下,你可能希望在临时区域中记录日志消息,并且只在发生某种特定的情况下才输出它们。 例如,你可能希望起始在函数中记录调试事件,如果函数执行完成且没有错误,你不希望输出收集的调试信息以避免造成日志混乱,但如果出现错误,那么你希望所有调试以及错误消息被输出。

下面是一个示例,展示如何在你的日志记录函数上使用装饰器以实现这一功能。该示例使用 logging.handlers.MemoryHandler ,它允许缓冲已记录的事件直到某些条件发生,缓冲的事件才会被刷新(flushed) - 传递给另一个处理程序( target handler)进行处理。 默认情况下, MemoryHandler 在其缓冲区被填满时被刷新,或者看到一个级别大于或等于指定阈值的事件。 如果想要自定义刷新行为,你可以通过更专业的 MemoryHandler 子类来使用这个秘诀。

这个示例脚本有一个简单的函数 foo ,它只是在所有的日志级别中循环运行,写到 sys.stderr ,说明它要记录在哪个级别上,然后在这个级别上实际记录一个消息。你可以给 foo 传递一个参数,如果为 true ,它将在ERROR和CRITICAL级别记录,否则,它只在DEBUG、INFO和WARNING级别记录。

脚本只是使用了一个装饰器来装饰 foo,这个装饰器将记录执行所需的条件。装饰器使用一个记录器作为参数,并在调用被装饰的函数期间附加一个内存处理程序。装饰器可以使用目标处理程序、记录级别和缓冲区的容量(缓冲记录的数量)来附加参数。这些参数分别默认为写入sys.stderrStreamHandlerlogging.ERROR100

以下是脚本:

import logging
from logging.handlers import MemoryHandler
import sys
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
def log_if_errors(logger, target_handler=None, flush_level=None, capacity=None):
    if target_handler is None:
        target_handler = logging.StreamHandler()
    if flush_level is None:
        flush_level = logging.ERROR
    if capacity is None:
        capacity = 100
    handler = MemoryHandler(capacity, flushLevel=flush_level, target=target_handler)
    def decorator(fn):
        def wrapper(*args, **kwargs):
            logger.addHandler(handler)
            try:
                return fn(*args, **kwargs)
            except Exception:
                logger.exception('call failed')
                raise
            finally:
                super(MemoryHandler, handler).flush()
                logger.removeHandler(handler)
        return wrapper
    return decorator
def write_line(s):
    sys.stderr.write('%s\n' % s)
def foo(fail=False):
    write_line('about to log at DEBUG ...')
    logger.debug('Actually logged at DEBUG')
    write_line('about to log at INFO ...')
    logger.info('Actually logged at INFO')
    write_line('about to log at WARNING ...')
    logger.warning('Actually logged at WARNING')
    if fail:
        write_line('about to log at ERROR ...')
        logger.error('Actually logged at ERROR')
        write_line('about to log at CRITICAL ...')
        logger.critical('Actually logged at CRITICAL')
    return fail
decorated_foo = log_if_errors(logger)(foo)
if __name__ == '__main__':
    logger.setLevel(logging.DEBUG)
    write_line('Calling undecorated foo with False')
    assert not foo(False)
    write_line('Calling undecorated foo with True')
    assert foo(True)
    write_line('Calling decorated foo with False')
    assert not decorated_foo(False)
    write_line('Calling decorated foo with True')
    assert decorated_foo(True)

运行此脚本时,应看到以下输出:

Calling undecorated foo with False
about to log at DEBUG ...
about to log at INFO ...
about to log at WARNING ...
Calling undecorated foo with True
about to log at DEBUG ...
about to log at INFO ...
about to log at WARNING ...
about to log at ERROR ...
about to log at CRITICAL ...
Calling decorated foo with False
about to log at DEBUG ...
about to log at INFO ...
about to log at WARNING ...
Calling decorated foo with True
about to log at DEBUG ...
about to log at INFO ...
about to log at WARNING ...
about to log at ERROR ...
Actually logged at DEBUG
Actually logged at INFO
Actually logged at WARNING
Actually logged at ERROR
about to log at CRITICAL ...
Actually logged at CRITICAL

如你所见,实际日志记录输出仅在消息等级为ERROR或更高的事件时发生,但在这种情况下,任何之前较低消息等级的事件还会被记录。

你当然可以使用传统的装饰方法:

@log_if_errors(logger)
def foo(fail=False):
    ...

通过配置使用UTC (GMT) 格式化时间

有时候,你希望使用UTC来格式化时间,这可以通过使用一个类来实现,例如UTCFormatter,如下所示:

import logging
import time
class UTCFormatter(logging.Formatter):
    converter = time.gmtime

然后你可以在你的代码中使用 UTCFormatter,而不是 Formatter。 如果你想通过配置来实现这一功能,你可以使用 dictConfig() API 来完成,该方法在以下完整示例中展示:

import logging
import logging.config
import time
class UTCFormatter(logging.Formatter):
    converter = time.gmtime
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'utc': {
            '()': UTCFormatter,
            'format': '%(asctime)s %(message)s',
        },
        'local': {
            'format': '%(asctime)s %(message)s',
        }
    },
    'handlers': {
        'console1': {
            'class': 'logging.StreamHandler',
            'formatter': 'utc',
        },
        'console2': {
            'class': 'logging.StreamHandler',
            'formatter': 'local',
        },
    },
    'root': {
        'handlers': ['console1', 'console2'],
   }
}
if __name__ == '__main__':
    logging.config.dictConfig(LOGGING)
    logging.warning('The local time is %s', time.asctime())

脚本会运行输出类似下面的内容:

2015-10-17 12:53:29,501 The local time is Sat Oct 17 13:53:29 2015
2015-10-17 13:53:29,501 The local time is Sat Oct 17 13:53:29 2015

展示了如何将时间格式化为本地时间和UTC两种形式,其中每种形式对应一个日志处理器 。

使用上下文管理器的可选的日志记录

有时候,我们需要暂时更改日志配置,并在执行某些操作后将其还原。为此,上下文管理器是实现保存和恢复日志上下文的最明显的方式。这是一个关于上下文管理器的简单例子,它允许你在上下文管理器的作用域内更改日志记录等级以及增加日志处理器:

import logging
import sys
class LoggingContext:
    def __init__(self, logger, level=None, handler=None, close=True):
        self.logger = logger
        self.level = level
        self.handler = handler
        self.close = close
    def __enter__(self):
        if self.level is not None:
            self.old_level = self.logger.level
            self.logger.setLevel(self.level)
        if self.handler:
            self.logger.addHandler(self.handler)
    def __exit__(self, et, ev, tb):
        if self.level is not None:
            self.logger.setLevel(self.old_level)
        if self.handler:
            self.logger.removeHandler(self.handler)
        if self.handler and self.close:
            self.handler.close()
        # implicit return of None => don't swallow exceptions

如果指定上下文管理器的日志记录等级属性,则在上下文管理器的with语句所涵盖的代码中,日志记录器的记录等级将临时设置为上下文管理器所配置的日志记录等级。 如果指定上下文管理的日志处理器属性,则该句柄在进入上下文管理器的上下文时添加到记录器中,并在退出时被删除。 如果你再也不需要该日志处理器时,你可以让上下文管理器在退出上下文管理器的上下文时关闭它。

为了说明它是如何工作的,我们可以在上面添加以下代码块:

if __name__ == '__main__':
    logger = logging.getLogger('foo')
    logger.addHandler(logging.StreamHandler())
    logger.setLevel(logging.INFO)
    logger.info('1. This should appear just once on stderr.')
    logger.debug('2. This should not appear.')
    with LoggingContext(logger, level=logging.DEBUG):
        logger.debug('3. This should appear once on stderr.')
    logger.debug('4. This should not appear.')
    h = logging.StreamHandler(sys.stdout)
    with LoggingContext(logger, level=logging.DEBUG, handler=h, close=True):
        logger.debug('5. This should appear twice - once on stderr and once on stdout.')
    logger.info('6. This should appear just once on stderr.')
    logger.debug('7. This should not appear.')

我们最初设置日志记录器的消息等级为 INFO,因此消息#1出现,消息#2没有出现。在接下来的 with``代码块中我们暂时将消息等级变更为 ``DEBUG,从而消息 #3 出现。在这一代码块退出后,日志记录器的消息等级恢复为 INFO,从而消息 #4 没有出现。在下一个 with 代码块中,我们再一次将设置消息等级设置为 DEBUG,同时添加一个将消息写入 sys.stdout 的日志处理器。因此,消息#5在控制台出现两次 (分别通过 stderrstdout)。在 with 语句完成后,状态与之前一样,因此消息 #6 出现(类似消息 #1),而消息 #7 没有出现(类似消息 #2)。

如果我们运行生成的脚本,结果如下:

$ python logctx.py
1. This should appear just once on stderr.
3. This should appear once on stderr.
5. This should appear twice - once on stderr and once on stdout.
5. This should appear twice - once on stderr and once on stdout.
6. This should appear just once on stderr.

我们将stderr标准错误重定向到/dev/null,我再次运行生成的脚步,唯一被写入stdout标准输出的消息,即我们所能看见的消息,如下:

$ python logctx.py 2>/dev/null
5. This should appear twice - once on stderr and once on stdout.

再一次,将 stdout 标准输出重定向到 /dev/null,我获得如下结果:

$ python logctx.py >/dev/null
1. This should appear just once on stderr.
3. This should appear once on stderr.
5. This should appear twice - once on stderr and once on stdout.
6. This should appear just once on stderr.

在这种情况下,与预期一致,打印到 stdout 标准输出的消息#5不会出现。

当然,这里描述的方法可以被推广,例如临时附加日志记录过滤器。 请注意,上面的代码适用于Python 2以及Python 3。

命令行日志应用起步

下面的示例提供了如下功能:

  • 根据命令行参数确定日志级别
  • 在单独的文件中分发多条子命令,同一级别的子命令均以一致的方式记录。
  • 最简单的配置用法

假定有一个命令行应用程序,用于停止、启动或重新启动某些服务。为了便于演示,不妨将 app.py 作为应用程序的主代码文件,并在 start.pystop.py``和 ``restart.py 中实现单独的命令。再假定要通过命令行参数控制应用程序的日志粒度,默认为 logging.INFO 。以下是 app.py 的一个示例:

import argparse
import importlib
import logging
import os
import sys
def main(args=None):
    scriptname = os.path.basename(__file__)
    parser = argparse.ArgumentParser(scriptname)
    levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
    parser.add_argument('--log-level', default='INFO', choices=levels)
    subparsers = parser.add_subparsers(dest='command',
                                       help='Available commands:')
    start_cmd = subparsers.add_parser('start', help='Start a service')
    start_cmd.add_argument('name', metavar='NAME',
                           help='Name of service to start')
    stop_cmd = subparsers.add_parser('stop',
                                     help='Stop one or more services')
    stop_cmd.add_argument('names', metavar='NAME', nargs='+',
                          help='Name of service to stop')
    restart_cmd = subparsers.add_parser('restart',
                                        help='Restart one or more services')
    restart_cmd.add_argument('names', metavar='NAME', nargs='+',
                             help='Name of service to restart')
    options = parser.parse_args()
    # the code to dispatch commands could all be in this file. For the purposes
    # of illustration only, we implement each command in a separate module.
    try:
        mod = importlib.import_module(options.command)
        cmd = getattr(mod, 'command')
    except (ImportError, AttributeError):
        print('Unable to find the code for command \'%s\'' % options.command)
        return 1
    # Could get fancy here and load configuration from file or dictionary
    logging.basicConfig(level=options.log_level,
                        format='%(levelname)s %(name)s %(message)s')
    cmd(options)
if __name__ == '__main__':
    sys.exit(main())

startstoprestart 命令可以在单独的模块中实现,启动命令的代码可如下:

# start.py
import logging
logger = logging.getLogger(__name__)
def command(options):
    logger.debug('About to start %s', options.name)
    # actually do the command processing here ...
    logger.info('Started the \'%s\' service.', options.name)

然后是停止命令的代码:

# stop.py
import logging
logger = logging.getLogger(__name__)
def command(options):
    n = len(options.names)
    if n == 1:
        plural = ''
        services = '\'%s\'' % options.names[0]
    else:
        plural = 's'
        services = ', '.join('\'%s\'' % name for name in options.names)
        i = services.rfind(', ')
        services = services[:i] + ' and ' + services[i + 2:]
    logger.debug('About to stop %s', services)
    # actually do the command processing here ...
    logger.info('Stopped the %s service%s.', services, plural)

重启命令类似:

# restart.py
import logging
logger = logging.getLogger(__name__)
def command(options):
    n = len(options.names)
    if n == 1:
        plural = ''
        services = '\'%s\'' % options.names[0]
    else:
        plural = 's'
        services = ', '.join('\'%s\'' % name for name in options.names)
        i = services.rfind(', ')
        services = services[:i] + ' and ' + services[i + 2:]
    logger.debug('About to restart %s', services)
    # actually do the command processing here ...
    logger.info('Restarted the %s service%s.', services, plural)

如果以默认日志级别运行该程序,会得到以下结果:

$ python app.py start foo
INFO start Started the 'foo' service.
$ python app.py stop foo bar
INFO stop Stopped the 'foo' and 'bar' services.
$ python app.py restart foo bar baz
INFO restart Restarted the 'foo', 'bar' and 'baz' services.

第一个单词是日志级别,第二个单词是日志事件所在的模块或包的名称。

如果修改了日志级别,发送给日志的信息就能得以改变。如要显示更多信息,则可:

$ python app.py --log-level DEBUG start foo
DEBUG start About to start foo
INFO start Started the 'foo' service.
$ python app.py --log-level DEBUG stop foo bar
DEBUG stop About to stop 'foo' and 'bar'
INFO stop Stopped the 'foo' and 'bar' services.
$ python app.py --log-level DEBUG restart foo bar baz
DEBUG restart About to restart 'foo', 'bar' and 'baz'
INFO restart Restarted the 'foo', 'bar' and 'baz' services.

若要显示的信息少一些,则:

$ python app.py --log-level WARNING start foo
$ python app.py --log-level WARNING stop foo bar
$ python app.py --log-level WARNING restart foo bar baz

这里的命令不会向控制台输出任何信息,因为没有记录 WARNING 以上级别的日志。

Qt GUI 日志示例

GUI 应用程序如何记录日志,这是个常见的问题。 Qt 框架是一个流行的跨平台 UI 框架,采用的是 PySide2PyQt5 库。

下面的例子演示了将日志写入 Qt GUI 程序的过程。这里引入了一个简单的 QtHandler 类,参数是一个可调用对象,其应为嵌入主线程某个“槽位”中运行的,因为GUI 的更新由主线程完成。这里还创建了一个工作线程,以便演示由 UI(通过人工点击日志按钮)和后台工作线程(此处只是记录级别和时间间隔均随机生成的日志信息)将日志写入 GUI 的过程。

该工作线程是用 Qt 的 QThread 类实现的,而不是 threading 模块,因为某些情况下只能采用 ``QThread,它与其他Qt` 组件的集成性更好一些。

以下代码应能适用于最新版的 PySide2PyQt5。对于低版本的 Qt 应该也能适用。更多详情,请参阅代码注释。

import datetime
import logging
import random
import sys
import time
# Deal with minor differences between PySide2 and PyQt5
try:
    from PySide2 import QtCore, QtGui, QtWidgets
    Signal = QtCore.Signal
    Slot = QtCore.Slot
except ImportError:
    from PyQt5 import QtCore, QtGui, QtWidgets
    Signal = QtCore.pyqtSignal
    Slot = QtCore.pyqtSlot
logger = logging.getLogger(__name__)
#
# Signals need to be contained in a QObject or subclass in order to be correctly
# initialized.
#
class Signaller(QtCore.QObject):
    signal = Signal(str, logging.LogRecord)
#
# Output to a Qt GUI is only supposed to happen on the main thread. So, this
# handler is designed to take a slot function which is set up to run in the main
# thread. In this example, the function takes a string argument which is a
# formatted log message, and the log record which generated it. The formatted
# string is just a convenience - you could format a string for output any way
# you like in the slot function itself.
#
# You specify the slot function to do whatever GUI updates you want. The handler
# doesn't know or care about specific UI elements.
#
class QtHandler(logging.Handler):
    def __init__(self, slotfunc, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.signaller = Signaller()
        self.signaller.signal.connect(slotfunc)
    def emit(self, record):
        s = self.format(record)
        self.signaller.signal.emit(s, record)
#
# This example uses QThreads, which means that the threads at the Python level
# are named something like "Dummy-1". The function below gets the Qt name of the
# current thread.
#
def ctname():
    return QtCore.QThread.currentThread().objectName()
#
# Used to generate random levels for logging.
#
LEVELS = (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
          logging.CRITICAL)
#
# This worker class represents work that is done in a thread separate to the
# main thread. The way the thread is kicked off to do work is via a button press
# that connects to a slot in the worker.
#
# Because the default threadName value in the LogRecord isn't much use, we add
# a qThreadName which contains the QThread name as computed above, and pass that
# value in an "extra" dictionary which is used to update the LogRecord with the
# QThread name.
#
# This example worker just outputs messages sequentially, interspersed with
# random delays of the order of a few seconds.
#
class Worker(QtCore.QObject):
    @Slot()
    def start(self):
        extra = {'qThreadName': ctname() }
        logger.debug('Started work', extra=extra)
        i = 1
        # Let the thread run until interrupted. This allows reasonably clean
        # thread termination.
        while not QtCore.QThread.currentThread().isInterruptionRequested():
            delay = 0.5 + random.random() * 2
            time.sleep(delay)
            level = random.choice(LEVELS)
            logger.log(level, 'Message after delay of %3.1f: %d', delay, i, extra=extra)
            i += 1
#
# Implement a simple UI for this cookbook example. This contains:
#
# * A read-only text edit window which holds formatted log messages
# * A button to start work and log stuff in a separate thread
# * A button to log something from the main thread
# * A button to clear the log window
#
class Window(QtWidgets.QWidget):
    COLORS = {
        logging.DEBUG: 'black',
        logging.INFO: 'blue',
        logging.WARNING: 'orange',
        logging.ERROR: 'red',
        logging.CRITICAL: 'purple',
    }
    def __init__(self, app):
        super().__init__()
        self.app = app
        self.textedit = te = QtWidgets.QPlainTextEdit(self)
        # Set whatever the default monospace font is for the platform
        f = QtGui.QFont('nosuchfont')
        f.setStyleHint(f.Monospace)
        te.setFont(f)
        te.setReadOnly(True)
        PB = QtWidgets.QPushButton
        self.work_button = PB('Start background work', self)
        self.log_button = PB('Log a message at a random level', self)
        self.clear_button = PB('Clear log window', self)
        self.handler = h = QtHandler(self.update_status)
        # Remember to use qThreadName rather than threadName in the format string.
        fs = '%(asctime)s %(qThreadName)-12s %(levelname)-8s %(message)s'
        formatter = logging.Formatter(fs)
        h.setFormatter(formatter)
        logger.addHandler(h)
        # Set up to terminate the QThread when we exit
        app.aboutToQuit.connect(self.force_quit)
        # Lay out all the widgets
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(te)
        layout.addWidget(self.work_button)
        layout.addWidget(self.log_button)
        layout.addWidget(self.clear_button)
        self.setFixedSize(900, 400)
        # Connect the non-worker slots and signals
        self.log_button.clicked.connect(self.manual_update)
        self.clear_button.clicked.connect(self.clear_display)
        # Start a new worker thread and connect the slots for the worker
        self.start_thread()
        self.work_button.clicked.connect(self.worker.start)
        # Once started, the button should be disabled
        self.work_button.clicked.connect(lambda : self.work_button.setEnabled(False))
    def start_thread(self):
        self.worker = Worker()
        self.worker_thread = QtCore.QThread()
        self.worker.setObjectName('Worker')
        self.worker_thread.setObjectName('WorkerThread')  # for qThreadName
        self.worker.moveToThread(self.worker_thread)
        # This will start an event loop in the worker thread
        self.worker_thread.start()
    def kill_thread(self):
        # Just tell the worker to stop, then tell it to quit and wait for that
        # to happen
        self.worker_thread.requestInterruption()
        if self.worker_thread.isRunning():
            self.worker_thread.quit()
            self.worker_thread.wait()
        else:
            print('worker has already exited.')
    def force_quit(self):
        # For use when the window is closed
        if self.worker_thread.isRunning():
            self.kill_thread()
    # The functions below update the UI and run in the main thread because
    # that's where the slots are set up
    @Slot(str, logging.LogRecord)
    def update_status(self, status, record):
        color = self.COLORS.get(record.levelno, 'black')
        s = '<pre><font color="%s">%s</font></pre>' % (color, status)
        self.textedit.appendHtml(s)
    @Slot()
    def manual_update(self):
        # This function uses the formatted message passed in, but also uses
        # information from the record to format the message in an appropriate
        # color according to its severity (level).
        level = random.choice(LEVELS)
        extra = {'qThreadName': ctname() }
        logger.log(level, 'Manually logged!', extra=extra)
    @Slot()
    def clear_display(self):
        self.textedit.clear()
def main():
    QtCore.QThread.currentThread().setObjectName('MainThread')
    logging.getLogger().setLevel(logging.DEBUG)
    app = QtWidgets.QApplication(sys.argv)
    example = Window(app)
    example.show()
    sys.exit(app.exec_())
if __name__=='__main__':
    main()

理应避免的用法

前几节虽介绍了几种方案,描述了可能需要处理的操作,但值得一提的是,有些用法是 没有好处 的,大多数情况下应该避免使用。下面几节的顺序不分先后。

多次打开同一个日志文件

因会导致 “文件被其他进程占用 “错误,所以在 Windows 中一般无法多次打开同一个文件。但在 POSIX 平台中,多次打开同一个文件不会报任何错误。这种操作可能是意外发生的,比如:

  • 多次添加指向同一文件的 handler(比如通过复制/粘贴,或忘记修改)。
  • 打开两个貌似不同(文件名不一样)的文件,但一个是另一个的符号链接,所以其实是同一个文件。
  • 进程 fork,然后父进程和子进程都有对同一文件的引用。 例如,这可能是通过使用 multiprocessing 模块实现的。

在大多数情况下,多次打开同一个文件 貌似 一切正常,但实际会导致很多问题。

  • 由于多个线程或进程会尝试写入同一个文件,日志输出可能会出现乱码。尽管日志对象可以防止多个线程同时使用同一个 handler 实例,但如果两个不同的线程使用不同的 handler 实例同时写入文件,而这两个 handler 又恰好指向同一个文件,那么就失去了这种防护。
  • 删除文件(例如在轮换日志文件时)会静默失败,因为有另一个引用指向这个文件。这可能导致混乱并浪费调试时间——日志项最后会出现在意想不到的地方,或者干脆丢失。

请用 从多个进程记录至单个文件 中介绍的技术来避免上述问题。

将日志对象用作属性或传递参数

虽然特殊情况下可能有必要如此,但一般来说没有意义,因为日志是单实例对象。代码总是可以通过 logging.getLogger(name) 用名称访问一个已有的日志对象实例,因此将实例作为参数来传递,或作为属性留存,都是毫无意义的。请注意,在其他语言中,如 Java 和 C#,日志对象通常是静态类属性。但在 Python 中是没有意义的,因为软件拆分的单位是模块(而不是类)。

给日志库代码添加 NullHandler 之外的其他 handler

通过添加 handler、formatter 和 filter 来配置日志,这是应用程序开发人员的责任,而不是库开发人员该做的。如果正在维护一个库,请确保不要向任何日志对象添加 NullHandler 实例以外的 handler。

创建大量的日志对象

日志是单实例对象,在代码执行过程中不会被释放,因此创建大量的日志对象会占用很多内存,而这些内存又不能被释放。与其为每个文件或网络连接创建一个日志,还不如利用 已有机制 将上下文信息传给自定义日志对象,并将创建的日志对象限制在应用程序内的指定区域(通常是模块,但偶尔会再精细些)使用。

正则表达式HOWTO

在Python中使用 re 模块使用正则表达式的入门教程。 它提供了比“标准库参考”中相应部分更平和的介绍。

概述

正则表达式(称为RE,或正则,或正则表达式模式)本质上是嵌入在Python中的一种微小的、高度专业化的编程语言,可通过 re 模块获得。 使用这种小语言,你可以为要匹配的可能字符串集指定规则;此集可能包含英语句子,电子邮件地址,TeX命令或你喜欢的任何内容。 然后,您可以询问诸如“此字符串是否与模式匹配?”或“此字符串中的模式是否匹配?”等问题。 你还可以使用正则修改字符串或以各种方式将其拆分。

正则表达式模式被编译成一系列字节码,然后由用 C 编写的匹配引擎执行。对于高级用途,可能需要特别注意引擎如何执行给定的正则,并将正则写入以某种方式生成运行速度更快的字节码。 本文档未涉及优化,因为它要求你充分了解匹配引擎的内部结构。

正则表达式语言相对较小且受限制,因此并非所有可能的字符串处理任务都可以使用正则表达式完成。 还有一些任务 可以 用正则表达式完成,但表达式变得非常复杂。 在这些情况下,你最好编写 Python 代码来进行处理;虽然 Python 代码比精心设计的正则表达式慢,但它也可能更容易理解。

简单模式

我们首先要了解最简单的正则表达式。 由于正则表达式用于对字符串进行操作,因此我们将从最常见的任务开始:匹配字符。

有关正则表达式(确定性和非确定性有限自动机)的计算机科学的详细解释,你可以参考几乎所有有关编写编译器的教科书。

匹配字符

大多数字母和字符只会匹配自己。 例如,正则表达式 test 将完全匹配字符串 test 。 (你可以启用一个不区分大小写的模式,让这个正则匹配 TestTEST,稍后会详细介绍。)

这条规则有例外;一些字符是特殊的 metacharacters ,并且不匹配自己。 相反,它们表示应该匹配一些与众不同的东西,或者通过重复它们或改变它们的含义来影响正则的其他部分。 本文档的大部分内容都致力于讨论各种元字符及其功能。

这是元字符的完整列表;它们的意思将在本HOWTO的其余部分讨论。

. ^ $ * + ? { } [ ] \ | ( )

我们将看到的第一个元字符是 [] 。 它们用于指定字符类,它是你希望匹配的一组字符。 可以单独列出字符,也可以通过给出两个字符并用 '-' 标记将它们分开来表示一系列字符。 例如, [abc] 将匹配任何字符 abc ;这与 [a-c] 相同,它使用一个范围来表示同一组字符。 如果你只想匹配小写字母,你的正则是 [a-z]

字符类中的元字符不生效。 例如,[akm$] 将匹配 'a''k''m''$' 中的任意字符; '$' 通常是一个元字符,但在一个字符类中它被剥夺了特殊性。

你可以通过以下方式匹配 complementing 设置的字符类中未列出的字符。这通过包含一个 '^' 作为该类的第一个字符来表示。 例如,[^5] 将匹配除 '5' 之外的任何字符。 如果插入符出现在字符类的其他位置,则它没有特殊含义。 例如:[5^] 将匹配 '5''^'

也许最重要的元字符是反斜杠,\。 与 Python 字符串文字一样,反斜杠后面可以跟各种字符,以指示各种特殊序列。它也用于转义所有元字符,因此您仍然可以在模式中匹配它们;例如,如果你需要匹配 [\,你可以在它们前面加一个反斜杠来移除它们的特殊含义:\[\\

一些以 '\' 开头的特殊序列表示通常有用的预定义字符集,例如数字集、字母集或任何非空格的集合。

让我们举一个例子:\w 匹配任何字母数字字符。 如果正则表达式模式以字节类表示,这相当于类 [a-zA-Z0-9_]。如果正则表达式是一个字符串,\w 将匹配由 unicodedata 模块提供的 Unicode 数据库中标记为字母的所有字符。 通过在编译正则表达式时提供 re.ASCII 标志,可以在字符串模式中使用更为受限制的 \w 定义。

以下特殊序列列表不完整。 有关 Unicode 字符串模式的序列和扩展类定义的完整列表。通常,Unicode 版本匹配 Unicode 数据库中相应类别中的任何字符。

\d

匹配任何十进制数字;这等价于类 [0-9]

\D

匹配任何非数字字符;这等价于类 [^0-9]

\s

匹配任何空白字符;这等价于类 [ \t\n\r\f\v]

\S

匹配任何非空白字符;这相当于类 [^ \t\n\r\f\v]

\w

匹配任何字母与数字字符;这相当于类 [a-zA-Z0-9_]

\W

匹配任何非字母与数字字符;这相当于类 [^a-zA-Z0-9_]

这些序列可以包含在字符类中。 例如,[\s,.] 是一个匹配任何空格字符的字符类或者 ',' ,或 '.'

本节的最后一个元字符是 . 。 它匹配除换行符之外的任何内容,并且有一个可选模式( re.DOTALL )甚至可以匹配换行符。 . 常用于你想匹配“任何字符”的地方。

重复

能够匹配不同的字符集合是正则表达式可以做的第一件事,这对于字符串可用方法来说是不可能的。 但是,如果这是正则表达式的唯一额外功能,那么它们就不会有太大的优势。 另一个功能是你可以指定正则的某些部分必须重复一定次数。

重复中我们要了解的第一个元字符是 ** 与字面字符 '*' 不匹配;相反,它指定前一个字符可以匹配零次或多次,而不是恰好一次。

例如,ca*t 将匹配 'ct' (0个 'a' 字符),'cat' (1个 'a' ), 'caaat' (3个 'a' 字符),等等。

类似 * 这样的重复是 贪婪的;当重复正则时,匹配引擎将尝试尽可能多地重复它。 如果模式的后续部分不匹配,则匹配引擎将回退并以较少的重复次数再次尝试。

一个逐步的例子将使这更加明显。 让我们考虑表达式 a[bcd]*b。 这个正则匹配字母 'a',类 [bcd] 中的零或多个字母,最后以 'b' 结尾。 现在想象一下这个正则与字符串 'abcbd' 匹配。

步骤 匹配 说明
1 a 正则中的 a 匹配。
2 abcbd 引擎尽可能多地匹配 [bcd] ,直到字符串结束。
3 失败 引擎尝试匹配 b ,但是当前位置位于字符串结束,所以匹配失败。
4 abcb 回退一次,[bcd] 少匹配一个字符。
5 失败 再次尝试匹配 b , 但是当前位置是最后一个字符 ‘d’
6 abc 再次回退,所以 [bcd]* 只匹配 bc
6 abcb 再试一次 b 。 这次当前位置的字符是 ‘b’ ,所以它成功了。

正则现在已经结束了,它已经匹配了 'abcb'。 这演示了匹配引擎最初如何进行,如果没有找到匹配,它将逐步回退并一次又一次地重试正则的其余部分。 它将回退,直到它为 [bcd]* 尝试零匹配,如果随后失败,引擎将断定该字符串与正则完全不匹配。

另一个重复的元字符是 +,它匹配一次或多次。 要特别注意 *+ 之间的区别;* 匹配 零次 或更多次,因此重复的任何东西都可能根本不存在,而 + 至少需要 一次。 使用类似的例子,ca+t 将匹配 'cat' (1 个 'a'),'caaat' (3 个 'a'),但不会匹配 'ct'

还有两个重复限定符。 问号字符 ? 匹配一次或零次;你可以把它想象成是可选的。 例如,home-?brew 匹配 'homebrew''home-brew'

最复杂的重复限定符是 {m,n},其中 mn 是十进制整数。 这个限定符意味着必须至少重复 m 次,最多重复 n 次。 例如,a/{1,3}b 将匹配 'a/b''a//b''a///b' 。 它不匹配没有斜线的 'ab',或者有四个的 'a////b'

你可以省略 mn; 在这种情况下,将假定缺失值的合理值。 省略 m 被解释为 0 下限,而省略 n 则为无穷大的上限。

还原论者的读者可能会注意到其他三个限定符都可以用这种表示法表达。 {0,}* 相同, {1,} 相当于 +{0,1}? 相同。 最好使用 *+? ,只要因为它们更短更容易阅读。

使用正则表达式

现在我们已经看了一些简单的正则表达式,我们如何在 Python 中实际使用它们? re 模块提供了正则表达式引擎的接口,允许你将正则编译为对象,然后用它们进行匹配。

编译正则表达式

正则表达式被编译成模式对象,模式对象具有各种操作的方法,例如搜索模式匹配或执行字符串替换。:

>>> import re
>>> p = re.compile('ab*')
>>> p
re.compile('ab*')

re.compile() 也接受一个可选的 flags 参数,用于启用各种特殊功能和语法变体。 我们稍后将介绍可用的设置,但现在只需一个例子

>>> p = re.compile('ab*', re.IGNORECASE)

正则作为字符串传递给 re.compile() 。 正则被处理为字符串,因为正则表达式不是核心Python语言的一部分,并且没有创建用于表达它们的特殊语法。 (有些应用程序根本不需要正则,因此不需要通过包含它们来扩展语言规范。)相反,re 模块只是Python附带的C扩展模块,就类似于 socketzlib 模块。

将正则放在字符串中可以使 Python 语言更简单,但有一个缺点是下一节的主题。

反斜杠灾难

如前所述,正则表达式使用反斜杠字符 ('\') 来表示特殊形式或允许使用特殊字符而不调用它们的特殊含义。 这与 Python 在字符串文字中用于相同目的的相同字符的使用相冲突。

假设你想要编写一个与字符串 \section 相匹配的正则,它可以在 LaTeX 文件中找到。 要找出在程序代码中写入的内容,请从要匹配的字符串开始。 接下来,您必须通过在反斜杠前面添加反斜杠和其他元字符,从而产生字符串 \\section。 必须传递给 re.compile() 的结果字符串必须是 \\section。 但是,要将其表示为 Python 字符串文字,必须 再次 转义两个反斜杠。

字符 阶段
\section 被匹配的字符串
\section re.compile() 转义的反斜杠
“\\section” 为字符串字面转义的反斜杠

简而言之,要匹配文字反斜杠,必须将 '\\\\' 写为正则字符串,因为正则表达式必须是 \\,并且每个反斜杠必须表示为 \\ 在常规Python字符串字面中。 在反复使用反斜杠的正则中,这会导致大量重复的反斜杠,并使得生成的字符串难以理解。

解决方案是使用 Python 的原始字符串表示法来表示正则表达式;反斜杠不以任何特殊的方式处理前缀为 'r' 的字符串字面,因此 r"\n" 是一个包含 '\''n' 的双字符字符串,而 "\n" 是一个包含换行符的单字符字符串。 正则表达式通常使用这种原始字符串表示法用 Python 代码编写。

此外,在正则表达式中有效但在 Python 字符串文字中无效的特殊转义序列现在导致 DeprecationWarning 并最终变为 SyntaxError。 这意味着如果未使用原始字符串表示法或转义反斜杠,序列将无效。

常规字符串 原始字符串
“ab*“* r”ab“
“\\section” r”\section”
“\w+\s+\1” r”\w+\s+\1”

应用匹配

一旦你有一个表示编译正则表达式的对象,你用它做什么? 模式对象有几种方法和属性。

方法 / 属性 目的
match() 确定正则是否从字符串的开头匹配。
search() 扫描字符串,查找此正则匹配的任何位置。
findall() 找到正则匹配的所有子字符串,并将它们作为列表返回。
finditer() 找到正则匹配的所有子字符串,并将它们返回为一个 iterator。

如果没有找到匹配, match()search() 返回 None 。如果它们成功, 一个 匹配对象 实例将被返回,包含匹配相关的信息:起始和终结位置、匹配的子串以及其它。

你可以通过交互式实验 re 模块来了解这一点。 如果你有 tkinter,你可能还想查看 Tools/demo/redemo.py,这是 Python 发行附带的演示程序。 它允许你输入正则和字符串,并显示RE是匹配还是失败。 redemo.py 在尝试调试复杂的正则时非常有用。

本 HOWTO 使用标准 Python 解释器作为示例。 首先,运行 Python 解释器,导入 re 模块,然后编译一个正则

>>> import re
>>> p = re.compile('[a-z]+')
>>> p
re.compile('[a-z]+')

现在,你可以尝试匹配正则 [a-z]+ 的各种字符串。 空字符串根本不匹配,因为 + 表示“一次或多次重复”。 match() 在这种情况下应返回 None,这将导致解释器不打印输出。 你可以显式打印 match() 的结果,使其清晰。:

>>> p.match("")
>>> print(p.match(""))
None

现在,让我们尝试一下它应该匹配的字符串,例如 tempo。在这个例子中 match() 将返回一个 匹配对象,因此你应该将结果储存到一个变量中以供稍后使用。

>>> m = p.match('tempo')
>>> m
<re.Match object; span=(0, 5), match='tempo'>

现在你可以检查 匹配对象 以获取有关匹配字符串的信息。 匹配对象实例也有几个方法和属性;最重要的是:

方法 / 属性 目的
group() 返回正则匹配的字符串
start() 返回匹配的开始位置
end() 返回匹配的结束位置
span() 返回包含匹配 (start, end) 位置的元组

尝试这些方法很快就会清楚它们的含义:

>>> m.group()
'tempo'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)

group() 返回正则匹配的子字符串。 start()end() 返回匹配的起始和结束索引。 span() 在单个元组中返回开始和结束索引。 由于 match() 方法只检查正则是否在字符串的开头匹配,所以 start() 将始终为零。 但是,模式的 search() 方法会扫描字符串,因此在这种情况下匹配可能不会从零开始。:

>>> print(p.match('::: message'))
None
>>> m = p.search('::: message'); print(m)
<re.Match object; span=(4, 11), match='message'>
>>> m.group()
'message'
>>> m.span()
(4, 11)

在实际程序中,最常见的样式是在变量中存储 匹配对象,然后检查它是否为 None。 这通常看起来像:

p = re.compile( ... )
m = p.match( 'string goes here' )
if m:
    print('Match found: ', m.group())
else:
    print('No match')

两种模式方法返回模式的所有匹配项。 findall() 返回匹配字符串的列表:

>>> p = re.compile(r'\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']

在这个例子中需要 r 前缀,使字面为原始字符串字面,因为普通的“加工”字符串字面中的转义序列不能被 Python 识别为正则表达式,导致 DeprecationWarning 并最终产生 SyntaxError

findall() 必须先创建整个列表才能返回结果。 finditer() 方法将一个 匹配对象 的序列返回为一个 iterator

>>> iterator = p.finditer('12 drummers drumming, 11 ... 10 ...')
>>> iterator  
<callable_iterator object at 0x...>
>>> for match in iterator:
...     print(match.span())
...
(0, 2)
(22, 24)
(29, 31)

模块级函数

你不必创建模式对象并调用其方法;re 模块还提供了顶级函数 match()search()findall()sub() 等等。 这些函数采用与相应模式方法相同的参数,并将正则字符串作为第一个参数添加,并仍然返回 None 或 匹配对象 实例。:

>>> print(re.match(r'From\s+', 'Fromage amk'))
None
>>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')  
<re.Match object; span=(0, 5), match='From '>

本质上,这些函数只是为你创建一个模式对象,并在其上调用适当的方法。 它们还将编译对象存储在缓存中,因此使用相同的未来调用将不需要一次又一次地解析该模式。

你是否应该使用这些模块级函数,还是应该自己获取模式并调用其方法? 如果你正在循环中访问正则表达式,预编译它将节省一些函数调用。 在循环之外,由于有内部缓存,没有太大区别。

编译标志

编译标志允许你修改正则表达式的工作方式。 标志在 re 模块中有两个名称,长名称如 IGNORECASE 和一个简短的单字母形式,例如 I。 (如果你熟悉 Perl 的模式修饰符,则单字母形式使用和其相同的字母;例如, re.VERBOSE 的缩写形式为 re.X。)多个标志可以 通过按位或运算来指定它们;例如,re.I | re.M 设置 IM 标志。

这是一个可用标志表,以及每个标志的更详细说明。

旗标 含意
ASCII, A 使几个转义如 \w\b\s\d 匹配仅与具有相应特征属性的 ASCII 字符匹配。
DOTALL, S 使 . 匹配任何字符,包括换行符。
IGNORECASE, I 进行大小写不敏感匹配。
LOCALE, L 进行区域设置感知匹配。
MULTILINE, M 多行匹配,影响 ^$
VERBOSE, X (为 ‘扩展’) 启用详细的正则,可以更清晰,更容易理解。
I
IGNORECASE

执行不区分大小写的匹配;字符类和字面字符串将通过忽略大小写来匹配字母。 例如,[A-Z] 也匹配小写字母。 除非使用 ASCII 标志来禁用非ASCII匹配,否则完全 Unicode 匹配也有效。 当 Unicode 模式 [a-z][A-Z]IGNORECASE 标志结合使用时,它们将匹配 52 个 ASCII 字母和 4 个额外的非 ASCII 字母:’İ’ (U+0130,拉丁大写字母 I,带上面的点),’ı’ (U+0131,拉丁文小写字母无点 i),’s’ (U+017F,拉丁文小写字母长 s) 和’K’ (U+212A,开尔文符号)。 Spam 将匹配 'Spam''spam''spAM''ſpam' (后者仅在 Unicode 模式下匹配)。 此小写不考虑当前区域设置;如果你还设置了 LOCALE 标志,则将考虑。

L
LOCALE

使 \w\W\b\B 和大小写敏感匹配依赖于当前区域而不是 Unicode 数据库。

区域设置是 C 库的一个功能,旨在帮助编写考虑到语言差异的程序。例如,如果你正在处理编码的法语文本,那么你希望能够编写 \w+ 来匹配单词,但 \w 只匹配字符类 [A-Za-z] 字节模式;它不会匹配对应于 éç 的字节。如果你的系统配置正确并且选择了法语区域设置,某些C函数将告诉程序对应于 é 的字节也应该被视为字母。在编译正则表达式时设置 LOCALE 标志将导致生成的编译对象将这些C函数用于 \w;这比较慢,但也可以使 \w+ 匹配你所期望的法语单词。在 Python 3 中不鼓励使用此标志,因为语言环境机制非常不可靠,它一次只处理一个“文化”,它只适用于 8 位语言环境。默认情况下,Python 3 中已经为 Unicode(str)模式启用了 Unicode 匹配,并且它能够处理不同的区域/语言。

M
MULTILINE

(^$ 还没有解释)

通常 ^ 只匹配字符串的开头,而 $ 只匹配字符串的结尾,紧接在字符串末尾的换行符(如果有的话)之前。 当指定了这个标志时,^ 匹配字符串的开头和字符串中每一行的开头,紧跟在每个换行符之后。 类似地,$ 元字符匹配字符串的结尾和每行的结尾(紧接在每个换行符之前)。

S
DOTALL

使 '.' 特殊字符匹配任何字符,包括换行符;没有这个标志,'.' 将匹配任何字符 除了 换行符。

A
ASCII

使 \w\W\b\B\s\S 执行仅 ASCII 匹配而不是完整匹配 Unicode 匹配。 这仅对 Unicode 模式有意义,并且对于字节模式将被忽略。

X
VERBOSE

此标志允许你编写更易读的正则表达式,方法是为您提供更灵活的格式化方式。 指定此标志后,将忽略正则字符串中的空格,除非空格位于字符类中或前面带有未转义的反斜杠;这使你可以更清楚地组织和缩进正则。 此标志还允许你将注释放在正则中,引擎将忽略该注释;注释标记为 '#' 既不是在字符类中,也不是在未转义的反斜杠之前。

例如,这里的正则使用 re.VERBOSE;看看阅读有多容易?:

charref = re.compile(r"""
 &[#]                # Start of a numeric entity reference
 (
     0[0-7]+         # Octal form
   | [0-9]+          # Decimal form
   | x[0-9a-fA-F]+   # Hexadecimal form
 )
 ;                   # Trailing semicolon
""", re.VERBOSE)

如果没有详细设置,正则将如下所示:

charref = re.compile("&#(0[0-7]+"
                     "|[0-9]+"
                     "|x[0-9a-fA-F]+);")

在上面的例子中,Python的字符串文字的自动连接已被用于将正则分解为更小的部分,但它仍然比以下使用 re.VERBOSE 版本更难理解。

更多模式能力

到目前为止,我们只介绍了正则表达式的一部分功能。 在本节中,我们将介绍一些新的元字符,以及如何使用组来检索匹配的文本部分。

更多元字符

我们还没有涉及到一些元字符。 其中大部分内容将在本节中介绍。

要讨论的其余一些元字符是 零宽度断言 。 它们不会使解析引擎在字符串中前进一个字符;相反,它们根本不占用任何字符,只是成功或失败。例如,\b 是一个断言,指明当前位置位于字边界;这个位置根本不会被 \b 改变。这意味着永远不应重复零宽度断言,因为如果它们在给定位置匹配一次,它们显然可以无限次匹配。

|

或者“or”运算符。 如果 AB 是正则表达式,A|B 将匹配任何与 AB 匹配的字符串。 | 具有非常低的优先级,以便在交替使用多字符字符串时使其合理地工作。 Crow|Servo 将匹配 'Crow''Servo',而不是 'Cro''w''S''ervo'

要匹配字面 '|',请使用 \|,或将其括在字符类中,如 [|]

^

在行的开头匹配。 除非设置了 MULTILINE 标志,否则只会在字符串的开头匹配。 在 MULTILINE 模式下,这也在字符串中的每个换行符后立即匹配。

例如,如果你希望仅在行的开头匹配单词 From,则要使用的正则 ^From。:

>>> print(re.search('^From', 'From Here to Eternity'))  
<re.Match object; span=(0, 4), match='From'>
>>> print(re.search('^From', 'Reciting From Memory'))
None

要匹配字面 '^',使用 \^

$

匹配行的末尾,定义为字符串的结尾,或者后跟换行符的任何位置。:

>>> print(re.search('}$', '{block}'))  
<re.Match object; span=(6, 7), match='}'>
>>> print(re.search('}$', '{block} '))
None
>>> print(re.search('}$', '{block}\n'))  
<re.Match object; span=(6, 7), match='}'>

以匹配字面 '$',使用 \$ 或者将其包裹在一个字符类中,例如 [$]

\A

仅匹配字符串的开头。 当不在 MULTILINE 模式时,\A^ 实际上是相同的。 在 MULTILINE 模式中,它们是不同的: \A 仍然只在字符串的开头匹配,但 ^ 可以匹配在换行符之后的字符串内的任何位置。

\Z

只匹配字符串尾。

\b

字边界。 这是一个零宽度断言,仅在单词的开头或结尾处匹配。 单词被定义为一个字母数字字符序列,因此单词的结尾由空格或非字母数字字符表示。

以下示例仅当它是一个完整的单词时匹配 class;当它包含在另一个单词中时将不会匹配。

>>> p = re.compile(r'\bclass\b')
>>> print(p.search('no class at all'))
<re.Match object; span=(3, 8), match='class'>
>>> print(p.search('the declassified algorithm'))
None
>>> print(p.search('one subclass is'))
None

使用这个特殊序列时,你应该记住两个细微之处。 首先,这是 Python 的字符串文字和正则表达式序列之间最严重的冲突。 在 Python 的字符串文字中,\b 是退格字符,ASCII 值为8。 如果你没有使用原始字符串,那么 Python 会将 \b 转换为退格,你的正则不会按照你的预期匹配。 以下示例与我们之前的正则看起来相同,但省略了正则字符串前面的 'r'。:

>>> p = re.compile('\bclass\b')
>>> print(p.search('no class at all'))
None
>>> print(p.search('\b' + 'class' + '\b'))
<re.Match object; span=(0, 7), match='\x08class\x08'>

其次,在一个字符类中,这个断言没有用处,\b 表示退格字符,以便与 Python 的字符串文字兼容。

\B

另一个零宽度断言,这与 \b 相反,仅在当前位置不在字边界时才匹配。

分组

通常,你需要获取更多信息,而不仅仅是正则是否匹配。 正则表达式通常用于通过将正则分成几个子组来解析字符串,这些子组匹配不同的感兴趣组件。 例如,RFC-822 标题行分为标题名称和值,用 ':' 分隔,如下所示:

From: author@example.com
User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Version: 1.0
To: editor@example.com

这可以通过编写与整个标题行匹配的正则表达式来处理,并且具有与标题名称匹配的一个组,以及与标题的值匹配的另一个组。

组由 '('')' 元字符标记。 '('')' 与数学表达式的含义大致相同;它们将包含在其中的表达式组合在一起,你可以使用重复限定符重复组的内容,例如 *+?{m,n}。 例如,(ab)* 将匹配 ab 的零次或多次重复。:

>>> p = re.compile('(ab)*')
>>> print(p.match('ababababab').span())
(0, 10)

'('')' 表示的组也捕获它们匹配的文本的起始和结束索引;这可以通过将参数传递给 group()start()end() 以及 span()。 组从 0 开始编号。组 0 始终存在;它表示整个正则,所以 匹配对象 方法都将组 0 作为默认参数。 稍后我们将看到如何表达不捕获它们匹配的文本范围的组。:

>>> p = re.compile('(a)b')
>>> m = p.match('ab')
>>> m.group()
'ab'
>>> m.group(0)
'ab'

子组从左到右编号,从 1 向上编号。 组可以嵌套;要确定编号,只需计算从左到右的左括号字符。:

>>> p = re.compile('(a(b)c)d')
>>> m = p.match('abcd')
>>> m.group(0)
'abcd'
>>> m.group(1)
'abc'
>>> m.group(2)
'b'

group() 可以一次传递多个组号,在这种情况下,它将返回一个包含这些组的相应值的元组。:

>>> m.group(2,1,2)
('b', 'abc', 'b')

groups() 方法返回一个元组,其中包含所有子组的字符串,从1到最后一个子组。:

>>> m.groups()
('abc', 'b')

模式中的后向引用允许你指定还必须在字符串中的当前位置找到先前捕获组的内容。 例如,如果可以在当前位置找到组 1 的确切内容,则 \1 将成功,否则将失败。 请记住,Python 的字符串文字也使用反斜杠后跟数字以允许在字符串中包含任意字符,因此正则中引入反向引用时务必使用原始字符串。

例如,以下正则检测字符串中的双字。:

>>> p = re.compile(r'\b(\w+)\s+\1\b')
>>> p.search('Paris in the the spring').group()
'the the'

像这样的后向引用通常不仅仅用于搜索字符串 —— 很少有文本格式以这种方式重复数据 —— 但是你很快就会发现它们在执行字符串替换时 非常 有用。

非捕获和命名组

精心设计的正则可以使用许多组,既可以捕获感兴趣的子串,也可以对正则本身进行分组和构建。 在复杂的正则中,很难跟踪组号。 有两个功能可以帮助解决这个问题。 它们都使用常用语法进行正则表达式扩展,因此我们首先看一下。

Perl 5 以其对标准正则表达式的强大补充而闻名。 对于这些新功能,Perl 开发人员无法选择新的单键击元字符或以 \ 开头的新特殊序列,否则 Perl 的正则表达式与标准正则容易混淆。 例如,如果他们选择 & 作为一个新的元字符,旧的表达式将假设 & 是一个普通字符,并且不会编写 \&[&]

Perl 开发人员选择的解决方案是使用 (?...) 作为扩展语法。 括号后面的 ? 是一个语法错误,因为 ? 没有什么可重复的,所以这并没有引入任何兼容性问题。 紧跟在 ? 之后的字符表示正在使用什么扩展名,所以 (?=foo) 是一个东西(一个正向的先行断言)和 (?:foo) 是其它东西( 包含子表达式 foo 的非捕获组)。

Python 支持一些 Perl 的扩展,并增加了新的扩展语法用于 Perl 的扩展语法。 如果在问号之后的第一个字符为 P,即表明其为 Python 专属的扩展。

现在我们已经了解了一般的扩展语法,我们可以回到简化复杂正则中组处理的功能。

有时你会想要使用组来表示正则表达式的一部分,但是对检索组的内容不感兴趣。 你可以通过使用非捕获组来显式表达这个事实: (?:...),你可以用任何其他正则表达式替换 ...。:

>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()

除了你无法检索组匹配内容的事实外,非捕获组的行为与捕获组完全相同;你可以在里面放任何东西,用重复元字符重复它,比如 *,然后把它嵌入其他组(捕获或不捕获)。 (?:...) 在修改现有模式时特别有用,因为你可以添加新组而不更改所有其他组的编号方式。 值得一提的是,捕获和非捕获组之间的搜索没有性能差异;两种形式没有一种更快。

更重要的功能是命名组:不是通过数字引用它们,而是可以通过名称引用组。

命名组的语法是Python特定的扩展之一: (?P<name>...)name 显然是该组的名称。 命名组的行为与捕获组完全相同,并且还将名称与组关联。 处理捕获组的 匹配对象 方法都接受按编号引用组的整数或包含所需组名的字符串。 命名组仍然是给定的数字,因此你可以通过两种方式检索有关组的信息:

>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'

此外,你可以通过 groupdict() 将命名分组提取为一个字典:

>>> m = re.match(r'(?P<first>\w+) (?P<last>\w+)', 'Jane Doe')
>>> m.groupdict()
{'first': 'Jane', 'last': 'Doe'}

命名组很有用,因为它们允许你使用容易记住的名称,而不必记住数字。 这是来自 imaplib 模块的示例正则

InternalDate = re.compile(r'INTERNALDATE "'
        r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-'
        r'(?P<year>[0-9][0-9][0-9][0-9])'
        r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
        r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
        r'"')

检索 m.group('zonem') 显然要容易得多,而不必记住检索第 9 组。

表达式中的后向引用语法,例如 (...)\1,指的是组的编号。 当然有一种变体使用组名而不是数字。 这是另一个 Python 扩展: (?P=name) 表示在当前点再次匹配名为 name 的组的内容。 用于查找双字的正则表达式,\b(\w+)\s+\1\b 也可以写为 \b(?P<word>\w+)\s+(?P=word)\b:

>>> p = re.compile(r'\b(?P<word>\w+)\s+(?P=word)\b')
>>> p.search('Paris in the the spring').group()
'the the'

前向断言

另一个零宽度断言是前向断言。 前向断言以正面和负面形式提供,如下所示:

(?=…)

正向前向断言。 如果包含的正则表达式,由 ... 表示,在当前位置成功匹配,则成功,否则失败。 但是,一旦尝试了包含的表达式,匹配的引擎就不会前进;模式其余的部分会在在断言开始的地方尝试。

(?!…)

负向前向断言。 这与积正向断言相反;如果包含的表达式在字符串中的当前位置 匹配,则成功。

更具体一些,让我们看看前向是有用的情况。 考虑一个简单的模式来匹配文件名并将其拆分为基本名称和扩展名,用 . 分隔。 例如,在 news.rc 中,news 是基本名称,rc 是文件名的扩展名。

与此匹配的模式非常简单:

.*[.].*$

请注意,. 需要特别处理,因为它是元字符,所以它在字符类中只能匹配特定字符。 还要注意尾随的 $;添加此项以确保扩展名中的所有其余字符串都必须包含在扩展名中。 这个正则表达式匹配 foo.barautoexec.batsendmail.cfprinters.conf

现在,考虑使更复杂一点的问题;如果你想匹配扩展名不是 bat 的文件名怎么办? 一些错误的尝试:

.*[.][^b].*$ 上面的第一次尝试试图通过要求扩展名的第一个字符不是 b 来排除 bat。 这是错误的,因为模式也与 foo.bar 不匹配。

.*[.]([^b]..|.[^a].|..[^t])$

当你尝试通过要求以下一种情况匹配来修补第一个解决方案时,表达式变得更加混乱:扩展的第一个字符不是 b。 第二个字符不 a;或者第三个字符不是 t。 这接受 foo.bar 并拒绝 autoexec.bat,但它需要三个字母的扩展名,并且不接受带有两个字母扩展名的文件名,例如 sendmail.cf。 为了解决这个问题,我们会再次使模式复杂化。

.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$

在第三次尝试中,第二个和第三个字母都是可选的,以便允许匹配的扩展名短于三个字符,例如 sendmail.cf

模式现在变得非常复杂,这使得它难以阅读和理解。 更糟糕的是,如果问题发生变化并且你想要将 batexe 排除为扩展,那么该模式将变得更加复杂和混乱。

负面前向消除了所有这些困扰:

.*[.](?!bat$)[^.]*$ 负向前向意味着:如果表达式 bat 此时不匹配,请尝试其余的模式;如果 bat$ 匹配,整个模式将失败。 尾随的 $ 是必需的,以确保允许像 sample.batch 这样的扩展只以 bat 开头的文件能通过。 [^.]* 确保当文件名中有多个点时,模式有效。

现在很容易排除另一个文件扩展名;只需在断言中添加它作为替代。 以下模块排除以 batexe:

.*[.](?!bat$|exe$)[^.]*$

修改字符串

到目前为止,我们只是针对静态字符串执行搜索。 正则表达式通常也用于以各种方式修改字符串,使用以下模式方法:

方法 / 属性 目的
split() 将字符串拆分为一个列表,在正则匹配的任何地方将其拆分
sub() 找到正则匹配的所有子字符串,并用不同的字符串替换它们
subn() sub() 相同,但返回新字符串和替换次数

分割字符串

模式的 split() 方法在正则匹配的任何地方拆分字符串,返回一个片段列表。 它类似于 split() 字符串方法,但在分隔符的分隔符中提供了更多的通用性;字符串的 split() 仅支持按空格或固定字符串进行拆分。 正如你所期望的那样,还有一个模块级 re.split() 函数。

.split(string[, maxsplit=0])

通过正则表达式的匹配拆分 字符串*。 如果在正则中使用捕获括号,则它们的内容也将作为结果列表的一部分返回。 如果 *maxsplit 非零,则最多执行 maxsplit 次拆分。

你可以通过传递 maxsplit 的值来限制分割的数量。 当 maxsplit 非零时,将最多进行 maxsplit 次拆分,并且字符串的其余部分将作为列表的最后一个元素返回。 在以下示例中,分隔符是任何非字母数字字符序列。:

>>> p = re.compile(r'\W+')
>>> p.split('This is a test, short and sweet, of split().')
['This', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', '']
>>> p.split('This is a test, short and sweet, of split().', 3)
['This', 'is', 'a', 'test, short and sweet, of split().']

有时你不仅对分隔符之间的文本感兴趣,而且还需要知道分隔符是什么。 如果在正则中使用捕获括号,则它们的值也将作为列表的一部分返回。 比较以下调用:

>>> p = re.compile(r'\W+')
>>> p2 = re.compile(r'(\W+)')
>>> p.split('This... is a test.')
['This', 'is', 'a', 'test', '']
>>> p2.split('This... is a test.')
['This', '... ', 'is', ' ', 'a', ' ', 'test', '.', '']

模块级函数 re.split() 添加要正则作为第一个参数,但在其他方面是相同的。:

>>> re.split(r'[\W]+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split(r'([\W]+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split(r'[\W]+', 'Words, words, words.', 1)
['Words', 'words, words.']

搜索和替换

另一个常见任务是找到模式的所有匹配项,并用不同的字符串替换它们。 sub() 方法接受一个替换值,可以是字符串或函数,也可以是要处理的字符串。

.sub(replacement, string[, count=0])

返回通过替换 replacement 替换 string 中正则的最左边非重叠出现而获得的字符串。 如果未找到模式,则 string 将保持不变。

可选参数 count 是要替换的模式最大的出现次数;count 必须是非负整数。 默认值 0 表示替换所有。

这是一个使用 sub() 方法的简单示例。 它用 colour 这个词取代颜色名称:

>>> p = re.compile('(blue|white|red)')
>>> p.sub('colour', 'blue socks and red shoes')
'colour socks and colour shoes'
>>> p.sub('colour', 'blue socks and red shoes', count=1)
'colour socks and red shoes'

subn() 方法完成相同的工作,但返回一个包含新字符串值和已执行的替换次数的 2 元组:

>>> p = re.compile('(blue|white|red)')
>>> p.subn('colour', 'blue socks and red shoes')
('colour socks and colour shoes', 2)
>>> p.subn('colour', 'no colours at all')
('no colours at all', 0)

仅当空匹配与前一个空匹配不相邻时,才会替换空匹配。:

>>> p = re.compile('x*')
>>> p.sub('-', 'abxd')
'-a-b--d-'

如果 replacement 是一个字符串,则处理其中的任何反斜杠转义。 也就是说,\n 被转换为单个换行符,\r 被转换为回车符,依此类推。 诸如 \& 之类的未知转义是孤立的。 后向引用,例如 \6,被替换为正则中相应组匹配的子字符串。 这使你可以在生成的替换字符串中合并原始文本的部分内容。

这个例子匹配单词 section 后跟一个用 {} 括起来的字符串,并将 section 改为 subsection

>>> p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First} section{second}')
'subsection{First} subsection{second}'

还有一种语法用于引用由 (?P<name>...) 语法定义的命名组。 \g<name> 将使用名为 name 的组匹配的子字符串,\g<number> 使用相应的组号。 因此 \g<2> 等同于 \2,但在诸如 \g<2>0 之类的替换字符串中并不模糊。 (\20 将被解释为对组 20 的引用,而不是对组 2 的引用,后跟字面字符 '0'。) 以下替换都是等效的,但使用所有三种变体替换字符串。:

>>> p = re.compile('section{ (?P<name> [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<1>}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<name>}','section{First}')
'subsection{First}'

replacement 也可以是一个函数,它可以为你提供更多控制。 如果 replacement 是一个函数,则为 pattern 的每次非重叠出现将调用该函数。 在每次调用时,函数都会传递一个匹配的 匹配对象 参数,并可以使用此信息计算所需的替换字符串并将其返回。

在以下示例中,替换函数将小数转换为十六进制:

>>> def hexrepl(match):
...     "Return the hex string for a decimal number"
...     value = int(match.group())
...     return hex(value)
...
>>> p = re.compile(r'\d+')
>>> p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.')
'Call 0xffd2 for printing, 0xc000 for user code.'

使用模块级别 re.sub() 函数时,模式作为第一个参数传递。 图案可以作为对象或字符串提供;如果需要指定正则表达式标志,则必须使用模式对象作为第一个参数,或者在模式字符串中使用嵌入式修饰符,例如: sub("(?i)b+", "x", "bbbb BBBB") 返回 'x x'

常见问题

正则表达式对于某些应用程序来说是一个强大的工具,但在某些方面,它们的行为并不直观,有时它们的行为方式与你的预期不同。 本节将指出一些最常见的陷阱。

使用字符串方法

有时使用 re 模块是一个错误。 如果你匹配固定字符串或单个字符类,并且你没有使用任何 re 功能,例如 IGNORECASE 标志,那么正则表达式的全部功能可能不是必需的。 字符串有几种方法可以使用固定字符串执行操作,它们通常要快得多,因为实现是一个针对此目的而优化的单个小 C 循环,而不是大型、更通用的正则表达式引擎。

一个例子可能是用另一个固定字符串替换一个固定字符串;例如,你可以用 deed 替换 wordre.sub() 看起来像是用于此的函数,但请考虑 replace() 方法。 注意 replace() 也会替换单词里面的 word ,把 swordfish 变成 sdeedfish ,但简单的正则 word 也会这样做。 (为了避免对单词的部分进行替换,模式必须是 \bword\b,以便要求 word 在任何一方都有一个单词边界。这使得工作超出了 replace() 的能力。)

另一个常见任务是从字符串中删除单个字符的每个匹配项或将其替换为另一个字符。 你可以用 re.sub('\n', ' ', S) 之类的东西来做这件事,但是 translate() 能够完成这两项任务,并且比任何正则表达式都快。

简而言之,在转向 re 模块之前,请考虑是否可以使用更快更简单的字符串方法解决问题。

The match() function only checks if the RE matches at the beginning of the string while search() will scan forward through the string for a match. It’s important to keep this distinction in mind. Remember, match() will only report a successful match which will start at 0; if the match wouldn’t start at zero, match() will not report it.

>>> print(re.match('super', 'superstition').span())
(0, 5)
>>> print(re.match('super', 'insuperable'))
None

另一方面, search() 将向前扫描字符串,报告它找到的第一个匹配项。:

>>> print(re.search('super', 'superstition').span())
(0, 5)
>>> print(re.search('super', 'insuperable').span())
(2, 7)

有时你会被诱惑继续使用 re.match() ,只需在你的正则前面添加 .* 。抵制这种诱惑并使用 re.search() 代替。 正则表达式编译器对正则进行一些分析,以加快寻找匹配的过程。 其中一个分析可以确定匹配的第一个特征必须是什么;例如,以 Crow 开头的模式必须与 'C' 匹配。 分析让引擎快速扫描字符串,寻找起始字符,只在找到 'C' 时尝试完全匹配。

添加 .* 会使这个优化失效,需要扫描到字符串的末尾,然后回溯以找到正则的其余部分的匹配。 使用 re.search() 代替。

贪婪与非贪婪

当重复一个正则表达式时,就像在 a* 中一样,最终的动作就是消耗尽可能多的模式。 当你尝试匹配一对对称分隔符,例如 HTML 标记周围的尖括号时,这个事实经常会让你感到困惑。因为 .* 的贪婪性质, 用于匹配单个 HTML 标记的简单模式不起作用。

>>> s = '<html><head><title>Title</title>'
>>> len(s)
32
>>> print(re.match('<.*>', s).span())
(0, 32)
>>> print(re.match('<.*>', s).group())
<html><head><title>Title</title>

正则匹配 '<' 中的 '<html>'.* 消耗字符串的其余部分。 正则中还有更多的剩余东西,并且 > 在字符串的末尾不能匹配,所以正则表达式引擎必须逐个字符地回溯,直到它找到匹配 > 。最终匹配从 '<html>' 中的 '<' 扩展到 '</title>' 中的 '>' ,而这并不是你想要的结果。

在这种情况下,解决方案是使用非贪婪的限定符 *?+???{m,n}? ,匹配为尽可能 的文字。 在上面的例子中,在第一次 '<' 匹配后立即尝试 '>' ,当它失败时,引擎一次前进一个字符,每一步都重试 '>' 。 这产生了正确的结果:

>>> print(re.match('<.*?>', s).group())
<html>

(请注意,使用正则表达式解析 HTML 或 XML 很痛苦。快而脏的模式将处理常见情况,但 HTML 和 XML 有特殊情况会破坏明显的正则表达式;当你编写正则表达式处理所有可能的情况时,模式将非常复杂。使用 HTML 或 XML 解析器模块来执行此类任务。)

使用 re.VERBOSE

到目前为止,你可能已经注意到正则表达式是一种非常紧凑的表示法,但它们并不是非常易读。 具有中等复杂度的正则可能会成为反斜杠、括号和元字符的冗长集合,使其难以阅读和理解。

对于这样的正则,在编译正则表达式时指定 re.VERBOSE 标志可能会有所帮助,因为它允许你更清楚地格式化正则表达式。

re.VERBOSE 标志有几种效果。 正则表达式中的 不是 在字符类中的空格将被忽略。 这意味着表达式如 dog | cat 等同于不太可读的 dog|cat ,但 [a b] 仍将匹配字符 'a''b' 或空格。 此外,你还可以在正则中放置注释;注释从 # 字符扩展到下一个换行符。 当与三引号字符串一起使用时,这使正则的格式更加整齐:

pat = re.compile(r"""
 \s*                 # Skip leading whitespace
 (?P<header>[^:]+)   # Header name
 \s* :               # Whitespace, and a colon
 (?P<value>.*?)      # The header's value -- *? used to
                     # lose the following trailing whitespace
 \s*$                # Trailing whitespace to end-of-line
""", re.VERBOSE)

这更具有可读性:

pat = re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$")

套接字编程指南

套接字几乎无处不在,但是它却是被误解最严重的技术之一。这是一篇简单的套接字概述。并不是一篇真正的教程 —— 你需要做更多的事情才能让它工作起来。其中也并没有涵盖细节(细节会有很多),但是我希望它能提供足够的背景知识,让你像模像样的开始使用套接字

套接字

我将只讨论关于 INET(比如:IPv4 地址族)的套接字,但是它将覆盖几乎 99% 的套接字使用场景。并且我将仅讨论 STREAM(比如:TCP)类型的套接字 - 除非你真的知道你在做什么(那么这篇 HOWTO 可能并不适合你),使用 STREAM 类型的套接字将会得到比其它类型更好的表现与性能。我将尝试揭开套接字的神秘面纱,也会讲到一些阻塞与非阻塞套接字的使用。但是我将以阻塞套接字为起点开始讨论。只有你了解它是如何工作的以后才能处理非阻塞套接字。

理解这些东西的难点之一在于「套接字」可以表示很多微妙差异的东西,这取决于上下文。所以首先,让我们先分清楚「客户端」套接字和「服务端」套接字之间的不同,客户端套接字表示对话的一端,服务端套接字更像是总机接线员。客户端程序只能(比如:你的浏览器)使用「客户端」套接字;网络服务器则可以使用「服务端」套接字和「客户端」套接字来会话

历史

目前为止,在各种形式的 IPC 中,套接字是最流行的。在任何指定的平台上,可能会有其它更快的 IPC 形式,但是就跨平台通信来说,套接字大概是唯一的玩法

套接字作为 Unix 的 BSD 分支的一部分诞生于 Berkeley。 它们像野火一样在互联网上传播。 这是有充分理由的 —- 套接字与 INET 的结合让世界各地的任何机器之间的通信变得令人难以置信的简单(至少是与其他方案相比)。

创建套接字

简略地说,当你点击带你来到这个页面的链接时,你的浏览器就已经做了下面这几件事情:

# create an INET, STREAMing socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# now connect to the web server on port 80 - the normal http port
s.connect(("www.python.org", 80))

当连接完成,套接字可以用来发送请求来接收页面上显示的文字。同样是这个套接字也会用来读取响应,最后再被销毁。是的,被销毁了。客户端套接字通常用来做一次交换(或者说一小组序列的交换)。

网络服务器发生了什么这个问题就有点复杂了。首页,服务器创建一个「服务端套接字」:

# create an INET, STREAMing socket
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# bind the socket to a public host, and a well-known port
serversocket.bind((socket.gethostname(), 80))
# become a server socket
serversocket.listen(5)

有几件事需要注意:我们使用了 socket.gethostname(),所以套接字将外网可见。如果我们使用的是 s.bind(('localhost', 80)) 或者 s.bind(('127.0.0.1', 80)),也会得到一个「服务端」套接字,但是后者只在同一机器上可见。s.bind(('', 80)) 则指定套接字可以被机器上的任何地址碰巧连接

第二个需要注点是:低端口号通常被一些「常用的」服务(HTTP, SNMP 等)所保留。如果你想把程序跑起来,最好使用一个高位端口号(通常是4位的数字)。

最后,listen 方法的参数会告诉套接字库,我们希望在队列中累积多达 5 个(通常的最大值)连接请求后再拒绝外部连接。 如果所有其他代码都准确无误,这个队列长度应该是足够的。

现在我们已经有一个「服务端」套接字,监听了 80 端口,我们可以进入网络服务器的主循环了:

while True:
    # accept connections from outside
    (clientsocket, address) = serversocket.accept()
    # now do something with the clientsocket
    # in this case, we'll pretend this is a threaded server
    ct = client_thread(clientsocket)
    ct.run()

事际上,通常有 3 种方法可以让这个循环工作起来 - 调度一个线程来处理 客户端套接字,或者把这个应用改成使用非阻塞模式套接字,亦或是使用 select 库来实现「服务端」套接字与任意活动 客户端套接字 之间的多路复用。稍后会详细介绍。现在最重要的是理解:这就是一个 服务端 套接字做的 所有 事情。它并没有发送任何数据。也没有接收任何数据。它只创建「客户端」套接字。每个 客户端套接字 都是为了响应某些其它客户端套接字 connect() 到我们绑定的主机。一旦创建 客户端套接字 完成,就会返回并监听更多的连接请求。现个客户端可以随意通信 - 它们使用了一些动态分配的端口,会话结束时端口才会被回收

进程间通信

如果你需要在同一台机器上进行两个进程间的快速 IPC 通信,你应该了解管道或者共享内存。如果你决定使用 AF_INET 类型的套接字,绑定「服务端」套接字到 'localhost' 。在大多数平台,这将会使用一个许多网络层间的通用快捷方式(本地回环地址)并且速度会快很多

参见

multiprocessing 模块使跨平台 IPC 通信成为一个高层的 API

使用一个套接字

首先需要注意,浏览器的「客户端」套接字和网络服务器的「客户端」套接字是极为相似的。即这种会话是「点对点」的。或者也可以说 你作为设计师需要自行决定会话的规则和礼节 。通常情况下,连接 套接字通过发送一个请求或者信号来开始一次会话。但这属于设计决定,并不是套接字规则。

现在有两组用于通信的动词。你可以使用 sendrecv ,或者你可以把客户端套接字改成文件类型的形式来使用 readwrite 方法。后者是 Java 语言中表示套接字的方法,我将不会在这儿讨论这个,但是要提醒你需要调用套接字的 flush 方法。这些是“缓冲”的文件,一个经常出现的错误是 write 一些东西,然后就直接开始 read 一个响应。如果不调用 flush ,你可能会一直等待这个响应,因为请求可能还在你的输出缓冲中。

现在我来到了套接字的两个主要的绊脚石 - sendrecv 操作网络缓冲区。它们并不一定可以处理所有你想要(期望)的字节,因为它们主要关注点是处理网络缓冲。通常,它们在关联的网络缓冲区 send 或者清空 recv 时返回。然后告诉你处理了多少个字节。 的责任是一直调用它们直到你所有的消息处理完成。

recv 方法返回 0 字节时,就表示另一端已经关闭(或者它所在的进程关闭)了连接。你再也不能从这个连接上获取到任何数据了。你可以成功的发送数据;我将在后面讨论这一点。

像 HTTP 这样的协议只使用一个套接字进行一次传输。客户端发送一个请求,然后读取响应。就这么简单。套接字会被销毁。 表示客户端可以通过接收 0 字节序列表示检测到响应的结束。

但是如果你打算在随后来的传输中复用套接字的话,你需要明白 套接字里面是不存在 :abbr:EOT (传输结束) 的。重复一下:套接字 send 或者 recv 完 0 字节后返回,连接会中断。如果连接没有被断开,你可能会永远处于等待 recv 的状态,因为(就目前来说)套接字 不会 告诉你不用再读取了。现在如果你细心一点,你可能会意识到套接字基本事实:消息必须要么具有固定长度,要么可以界定,要么指定了长度(比较好的做法),要么以关闭连接为结束。选择完全由你而定(这比让别人定更合理)。

假定你不希望结束连接,那么最简单的解决方案就是使用定长消息:

class MySocket:
    """demonstration class only
      - coded for clarity, not efficiency
    """
    def __init__(self, sock=None):
        if sock is None:
            self.sock = socket.socket(
                            socket.AF_INET, socket.SOCK_STREAM)
        else:
            self.sock = sock
    def connect(self, host, port):
        self.sock.connect((host, port))
    def mysend(self, msg):
        totalsent = 0
        while totalsent < MSGLEN:
            sent = self.sock.send(msg[totalsent:])
            if sent == 0:
                raise RuntimeError("socket connection broken")
            totalsent = totalsent + sent
    def myreceive(self):
        chunks = []
        bytes_recd = 0
        while bytes_recd < MSGLEN:
            chunk = self.sock.recv(min(MSGLEN - bytes_recd, 2048))
            if chunk == b'':
                raise RuntimeError("socket connection broken")
            chunks.append(chunk)
            bytes_recd = bytes_recd + len(chunk)
        return b''.join(chunks)

发送分部代码几乎可用于任何消息传递方案 —— 在 Python 中你发送字符串,可以使用 len() 方法来确定它的长度(即使它嵌入了 \0 字符),主要是接收代码变得更复杂。(在 C 语言中,并没有更糟糕,除非消息嵌入了 \0 字符而且你又无法使用 strlen

最简单的改进是让消息的第一个字符表示消息类型,由类型决定长度。现在你需要两次 recv- 第一次取(至少)第一个字符来知晓长度,第二次在循环中获取剩余所有的消息。如果你决定到分界线,你将收到一些任意大小的块,(4096 或者 8192 通常是比较合适的网络缓冲区大小),扫描你接收到的分界符

一个需要意识到的复杂情况是:如果你的会话协议允许多个消息被发送回来(没有响应),调用 recv 传入任意大小的块,你可能会因为读到后续接收的消息而停止读取。你需要将它放在一边并保存,直到它需要为止。

以其长度(例如,作为5个数字字符)作为消息前缀时会变得更复杂,因为(信不信由你)你可能无法在一个 recv 中获得所有5个字符。在一般使用时,你会侥幸避免该状况;但是在高网络负载中,除非你使用两个 recv 循环,否则你的代码将很快中断 —— 第一个用于确定长度,第二个用于获取消息的数据部分。这很讨厌。当你发现 send 并不总是设法在支持搞定一切时,你也会有这种感觉。 尽管已经阅读过这篇文章,但最终还是会有所了解!

限于篇幅,建立你的角色,(保持与我的竞争位置),这些改进将留给读者做为练习。现在让我们继续。

二进制数据

通过套接字传送二进制数据是可行的。主要问题在于并非所有机器都用同样的二进制数据格式。比如 Motorola 芯片用两个十六进制字节 00 01 来表示一个 16 位整数值 1。而 Intel 和 DEC 则会做字节反转 —— 即用 01 00 来表示 1。套接字库要求转换 16 位和 32 位整数 —— ntohl, htonl, ntohs, htons 其中的「n」表示 network,「h」表示 host,「s」表示 short,「l」表示 long。在网络序列就是主机序列时它们什么都不做,但是如果机器是字节反转的则会适当地交换字节序。

在现今的 32 位机器中,二进制数据的 ascii 表示往往比二进制表示要小。这是因为在非常多的时候所有 long 的值均为 0 或者 1。字符串形式的 “0” 为两个字节,而二进制形式则为四个。当然这不适用于固定长度的信息。自行决定,请自行决定。

断开连接

严格地讲,你应该在 close 它之前将套接字 shutdownshutdown 是发送给套接字另一端的一种建议。调用时参数不同意义也不一样,它可能意味着「我不会再发送了,但我仍然会监听」,或者「我没有监听了,真棒!」。然而,大多数套接字库或者程序员都习惯了忽略使用这种礼节,因为通常情况下 closeshutdown(); close() 是一样的。所以在大多数情况下,不需要显式的 shutdown

高效使用 shutdown 的一种方法是在类似 HTTP 的交换中。客户端发送请求,然后执行 shutdown(1) 。 这告诉服务器“此客户端已完成发送,但仍可以接收”。服务器可以通过接收 0 字节来检测 “EOF” 。它可以假设它有完整的请求。服务器发送回复。如果 send 成功完成,那么客户端仍在接收。

Python 进一步自动关闭,并说当一个套接字被垃圾收集时,如果需要它会自动执行 close 。但依靠这个机制是一个非常坏的习惯。如果你的套接字在没有 close 的情况下就消失了,那么另一端的套接字可能会无限期地挂起,以为你只是慢了一步。完成后 close 你的套接字。

套接字何时销毁

使用阻塞套接字最糟糕的事情可能就是当另一边下线时(没有 close )会发生什么。你的套接字可能会挂起。 TCP 是一种可靠的协议,它会在放弃连接之前等待很长时间。如果你正在使用线程,那么整个线程基本上已经死了。你无能为力。只要你没有做一些愚蠢的事情,比如在进行阻塞读取时持有一个锁,那么线程并没有真正消耗掉资源。 不要 尝试杀死线程 —— 线程比进程更有效的部分原因是它们避免了与自动回收资源相关的开销。换句话说,如果你设法杀死线程,你的整个进程很可能被搞坏。

非阻塞的套接字

如果你已理解上述内容,那么你已经了解了使用套接字的机制所需了解的大部分内容。你仍将以相同的方式使用相同的函数调用。 只是,如果你做得对,你的应用程序几乎是由内到外的。

在 Python 中是使用 socket.setblocking(False) 来设置非阻塞。 在 C 中的做法更为复杂(例如,你需要在 BSD 风格的 O_NONBLOCK 和几乎无区别的 POSIX 风格的 O_NDELAY 之间作出选择,这与 TCP_NODELAY 完全不一样),但其思路实际上是相同的。 你要在创建套接字之后但在使用它之前执行此操作。 (实际上,如果你是疯子的话也可以反复进行切换。)

主要的机制差异是 sendrecvconnectaccept 可以在没有做任何事情的情况下返回。 你(当然)有很多选择。你可以检查返回代码和错误代码,通常会让自己发疯。如果你不相信我,请尝试一下。你的应用程序将变得越来越大、越来越 Bug 、吸干 CPU。因此,让我们跳过脑死亡的解决方案并做正确的事。

使用 select

在 C 中,编码 select 相当复杂。 在 Python 中,它是很简单,但它与 C 版本足够接近,如果你在 Python 中理解 select ,那么在 C 中你会几乎不会遇到麻烦:

ready_to_read, ready_to_write, in_error = \
               select.select(
                  potential_readers,
                  potential_writers,
                  potential_errs,
                  timeout)

你传递给 select 三个列表:第一个包含你可能想要尝试读取的所有套接字;第二个是你可能想要尝试写入的所有套接字,以及要检查错误的最后一个(通常为空)。你应该注意,套接字可以进入多个列表。 select 调用是阻塞的,但你可以给它一个超时。这通常是一件明智的事情 —— 给它一个很长的超时(比如一分钟),除非你有充分的理由不这样做。

作为返回,你将获得三个列表。它们包含实际可读、可写和有错误的套接字。 这些列表中的每一个都是你传入的相应列表的子集(可能为空)。

如果一个套接字在输出可读列表中,那么你可以像我们一样接近这个业务,那个套接字上的 recv 将返回 一些内容 。可写列表的也相同,你将能够发送 一些内容 。 也许不是你想要的全部,但 有些东西 比没有东西更好。 (实际上,任何合理健康的套接字都将以可写方式返回 —— 它只是意味着出站网络缓冲区空间可用。)

如果你有一个“服务器”套接字,请将其放在 potential_readers 列表中。如果它出现在可读列表中,那么你的 accept (几乎肯定)会起作用。如果你已经创建了一个新的套接字 connect 其他人,请将它放在 potential_writers 列表中。如果它出现在可写列表中,那么它有可能已连接。

实际上,即使使用阻塞套接字, select 也很方便。这是确定是否阻塞的一种方法 —— 当缓冲区中存在某些内容时,套接字返回为可读。然而,这仍然无助于确定另一端是否完成或者只是忙于其他事情的问题。

可移植性警告 :在 Unix 上, select 适用于套接字和文件。 不要在 Windows 上尝试。在 Windows 上, select 仅适用于套接字。另请注意,在 C 中,许多更高级的套接字选项在 Windows 上的执行方式不同。事实上,在 Windows 上我通常在使用我的套接字使用线程(非常非常好)。

排序指南

Python 列表有一个内置的 list.sort() 方法可以直接修改列表。还有一个 sorted() 内置函数,它会从一个可迭代对象构建一个新的排序列表。

基本排序

简单的升序排序非常简单:只需调用 sorted() 函数。它返回一个新的排序后列表:

>>> sorted([5, 2, 3, 1, 4])
[1, 2, 3, 4, 5]

你也可以使用 list.sort() 方法,它会直接修改原列表(并返回 None 以避免混淆),通常来说它不如 sorted() 方便 ——— 但如果你不需要原列表,它会更有效率。

>>> a = [5, 2, 3, 1, 4]
>>> a.sort()
>>> a
[1, 2, 3, 4, 5]

另外一个区别是, list.sort() 方法只是为列表定义的,而 sorted() 函数可以接受任何可迭代对象。

>>> sorted({1: 'D', 2: 'B', 3: 'B', 4: 'E', 5: 'A'})
[1, 2, 3, 4, 5]

关键函数

list.sort()sorted() 都有一个 key 形参用来指定在进行比较前要在每个列表元素上调用的函数(或其他可调用对象)。

例如,下面是一个不区分大小写的字符串比较:

>>> sorted("This is a test string from Andrew".split(), key=str.lower)
['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']

key 形参的值应该是个函数(或其他可调用对象),它接受一个参数并返回一个用于排序的键。 这种机制速度很快,因为对于每个输入记录只会调用一次键函数。

一种常见的模式是使用对象的一些索引作为键对复杂对象进行排序。例如:

>>> student_tuples = [
...     ('john', 'A', 15),
...     ('jane', 'B', 12),
...     ('dave', 'B', 10),
... ]
>>> sorted(student_tuples, key=lambda student: student[2])   # sort by age
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

同样的技术也适用于具有命名属性的对象。例如:

>>> class Student:
...     def __init__(self, name, grade, age):
...         self.name = name
...         self.grade = grade
...         self.age = age
...     def __repr__(self):
...         return repr((self.name, self.grade, self.age))
>>> student_objects = [
...     Student('john', 'A', 15),
...     Student('jane', 'B', 12),
...     Student('dave', 'B', 10),
... ]
>>> sorted(student_objects, key=lambda student: student.age)   # sort by age
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

Operator 模块函数

上面显示的键函数模式非常常见,因此 Python 提供了便利功能,使访问器功能更容易,更快捷。 operator 模块有 itemgetter()attrgetter()methodcaller() 函数。

使用这些函数,上述示例变得更简单,更快捷:

>>> from operator import itemgetter, attrgetter
>>> sorted(student_tuples, key=itemgetter(2))
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
>>> sorted(student_objects, key=attrgetter('age'))
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

Operator 模块功能允许多级排序。 例如,按 grade 排序,然后按 age 排序:

>>> sorted(student_tuples, key=itemgetter(1,2))
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
>>> sorted(student_objects, key=attrgetter('grade', 'age'))
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]

升序和降序

list.sort()sorted() 接受布尔值的 reverse 参数。这用于标记降序排序。 例如,要以反向 age 顺序获取学生数据:

>>> sorted(student_tuples, key=itemgetter(2), reverse=True)
[('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]
>>> sorted(student_objects, key=attrgetter('age'), reverse=True)
[('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]

排序稳定性和排序复杂度

排序保证是 稳定 的。 这意味着当多个记录具有相同的键值时,将保留其原始顺序。

>>> data = [('red', 1), ('blue', 1), ('red', 2), ('blue', 2)]
>>> sorted(data, key=itemgetter(0))
[('blue', 1), ('blue', 2), ('red', 1), ('red', 2)]

注意 blue 的两个记录如何保留它们的原始顺序,以便 ('blue', 1) 保证在 ('blue', 2) 之前。

这个美妙的属性允许你在一系列排序步骤中构建复杂的排序。例如,要按 grade 降序然后 age 升序对学生数据进行排序,请先 age 排序,然后再使用 grade 排序:

>>> s = sorted(student_objects, key=attrgetter('age'))     # sort on secondary key
>>> sorted(s, key=attrgetter('grade'), reverse=True)       # now sort on primary key, descending
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

这可以被抽象为一个包装函数,该函数能接受一个列表以及字段和顺序的元组,以对它们进行多重排序。

>>> def multisort(xs, specs):
...     for key, reverse in reversed(specs):
...         xs.sort(key=attrgetter(key), reverse=reverse)
...     return xs
>>> multisort(list(student_objects), (('grade', True), ('age', False)))
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

Python 中使用的 Timsort 算法可以有效地进行多种排序,因为它可以利用数据集中已存在的任何排序。

使用装饰-排序-去装饰的旧方法

这个三个步骤被称为 Decorate-Sort-Undecorate :

  • 首先,初始列表使用控制排序顺序的新值进行修饰。
  • 然后,装饰列表已排序。
  • 最后,删除装饰,创建一个仅包含新排序中初始值的列表。

例如,要使用DSU方法按 grade 对学生数据进行排序:

>>> decorated = [(student.grade, i, student) for i, student in enumerate(student_objects)]
>>> decorated.sort()
>>> [student for grade, i, student in decorated]               # undecorate
[('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]

这方法语有效是因为元组按字典顺序进行比较,先比较第一项;如果它们相同则比较第二个项目,依此类推。

不一定在所有情况下都要在装饰列表中包含索引 i ,但包含它有两个好处:

  • 排序是稳定的——如果两个项具有相同的键,它们的顺序将保留在排序列表中。
  • 原始项目不必具有可比性,因为装饰元组的排序最多由前两项决定。 因此,例如原始列表可能包含无法直接排序的复数。

这个方法的另一个名字是 Randal L. Schwartz 在 Perl 程序员中推广的 Schwartzian transform。

既然 Python 排序提供了键函数,那么通常不需要这种技术。

使用 cmp 参数的旧方法

本 HOWTO 中给出的许多结构都假定为 Python 2.4 或更高版本。在此之前,没有内置 sorted()list.sort() 也没有关键字参数。相反,所有 Py2.x 版本都支持 cmp 参数来处理用户指定的比较函数。

在 Py3.0 中, cmp 参数被完全删除(作为简化和统一语言努力的一部分,消除了丰富的比较与 __cmp__() 魔术方法之间的冲突)。

在 Py2.x 中, sort 允许一个可选函数,可以调用它来进行比较。该函数应该采用两个参数进行比较,然后返回负值为小于,如果它们相等则返回零,或者返回大于大于的正值。例如,我们可以这样做:

>>> def numeric_compare(x, y):
...     return x - y
>>> sorted([5, 2, 4, 1, 3], cmp=numeric_compare) 
[1, 2, 3, 4, 5]

或者你可反转比较的顺序:

>>> def reverse_numeric(x, y):
...     return y - x
>>> sorted([5, 2, 4, 1, 3], cmp=reverse_numeric) 
[5, 4, 3, 2, 1]

在将代码从 Python 2.x 移植到 3.x 时,如果让用户提供比较函数并且需要将其转换为键函数则会出现这种情况。 以下包装器使得做到这点变得很容易。

def cmp_to_key(mycmp):
    'Convert a cmp= function into a key= function'
    class K:
        def __init__(self, obj, *args):
            self.obj = obj
        def __lt__(self, other):
            return mycmp(self.obj, other.obj) < 0
        def __gt__(self, other):
            return mycmp(self.obj, other.obj) > 0
        def __eq__(self, other):
            return mycmp(self.obj, other.obj) == 0
        def __le__(self, other):
            return mycmp(self.obj, other.obj) <= 0
        def __ge__(self, other):
            return mycmp(self.obj, other.obj) >= 0
        def __ne__(self, other):
            return mycmp(self.obj, other.obj) != 0
    return K

要转换为键函数,只需包装旧的比较函数:

>>> sorted([5, 2, 4, 1, 3], key=cmp_to_key(reverse_numeric))
[5, 4, 3, 2, 1]

在 Python 3.2 中, functools.cmp_to_key() 函数被添加到标准库中的 functools 模块中。

其它

  • 对于区域相关的排序,请使用 locale.strxfrm() 作为键函数,或者 locale.strcoll() 作为比较函数。

  • reverse 参数仍然保持排序稳定性(因此具有相等键的记录保留原始顺序)。 有趣的是,通过使用内置的 reversed() 函数两次,可以在没有参数的情况下模拟该效果:

    >>> data = [('red', 1), ('blue', 1), ('red', 2), ('blue', 2)]
    >>> standard_way = sorted(data, key=itemgetter(0), reverse=True)
    >>> double_reversed = list(reversed(sorted(reversed(data), key=itemgetter(0))))
    >>> assert standard_way == double_reversed
    >>> standard_way
    [('red', 1), ('red', 2), ('blue', 1), ('blue', 2)]
  • 当在两个对象之间进行比较时,会确保排序例程使用 __lt__()。 因此,通过定义 __lt__() 方法可以很容易地为类添加一个标准排序:

    >>> Student.__lt__ = lambda self, other: self.age < other.age
    >>> sorted(student_objects)
    [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
  • 键函数不需要直接依赖于被排序的对象。键函数还可以访问外部资源。例如,如果学生成绩存储在字典中,则可以使用它们对单独的学生姓名列表进行排序:

    >>> students = ['dave', 'john', 'jane']
    >>> newgrades = {'john': 'F', 'jane':'A', 'dave': 'C'}
    >>> sorted(students, key=newgrades.__getitem__)
    ['jane', 'dave', 'john']

Unicode 指南

Unicode 概述

定义

如今的程序需要能够处理各种各样的字符。应用程序通常做了国际化处理,用户可以选择不同的语言显示信息和输出数据。同一个程序可能需要以英语、法语、日语、希伯来语或俄语输出错误信息。网页内容可能由这些语言书写,并且可能包含不同的表情符号。Python 的字符串类型采用 Unicode 标准来表示字符,使得 Python 程序能够正常处理所有这些不同的字符。

Unicode 规范(https://www.unicode.org/)旨在罗列人类语言所用到的所有字符,并赋予每个字符唯一的编码。该规范一直在进行修订和更新,不断加入新的语种和符号。

一个 字符 是文本的最小组件。‘A’、‘B’、‘C’ 等都是不同的字符。‘È’ 和 ‘Í’ 也一样。字符会随着语言或者上下文的变化而变化。比如,‘Ⅰ’ 是一个表示 “罗马数字 1” 的字符,它与大写字母 ‘I’ 不同。他们往往看起来相同,但这是两个有着不同含义的字符。

Unicode 标准描述了字符是如何用 码位(code point) 表示的。码位的取值范围是 0 到 0x10FFFF 的整数(大约 110 万个值,实际分配的数字 没有那么多)。在 Unicode 标准和本文中,码位采用 U+265E 的形式,表示值为 0x265e 的字符(十进制为 9822)。

Unicode 标准中包含了许多表格,列出了很多字符及其对应的码位。

0061    'a'; LATIN SMALL LETTER A
0062    'b'; LATIN SMALL LETTER B
0063    'c'; LATIN SMALL LETTER C
...
007B    '{'; LEFT CURLY BRACKET
...
2167    'Ⅷ'; ROMAN NUMERAL EIGHT
2168    'Ⅸ'; ROMAN NUMERAL NINE
...
265E    '♞'; BLACK CHESS KNIGHT
265F    '♟'; BLACK CHESS PAWN
...
1F600   '😀'; GRINNING FACE
1F609   '😉'; WINKING FACE
...

严格地说,上述定义暗示了以下说法是没有意义的:“这是字符 U+265E”。U+265E 只是一个码位,代表某个特定的字符;这里它代表了字符 “国际象棋黑骑士” ‘♞’。在非正式的上下文中,有时会忽略码位和字符的区别。

一个字符在屏幕或纸上被表示为一组图形元素,被称为 字形(glyph) 。比如,大写字母 A 的字形,是两笔斜线和一笔横线,而具体的细节取决于所使用的字体。大部分 Python 代码不必担心字形,找到正确的显示字形通常是交给 GUI 工具包或终端的字体渲染程序来完成。

编码

上一段可以归结为:一个 Unicode 字符串是一系列码位(从 0 到 0x10FFFF 或者说十进制的 1,114,111 的数字)组成的序列。这一序列在内存中需被表示为一组 码元(code unit)码元 会映射成包含八个二进制位的字节。将 Unicode 字符串翻译成字节序列的规则称为 字符编码 ,或者 编码

大家首先会想到的编码可能是用 32 位的整数作为代码位,然后采用 CPU 对 32 位整数的表示法。字符串 “Python” 用这种表示法可能会如下所示:

   P           y           t           h           o           n
0x50 00 00 00 79 00 00 00 74 00 00 00 68 00 00 00 6f 00 00 00 6e 00 00 00
   0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

这种表示法非常直白,但也存在 一些问题。

  1. 不具可移植性;不同的处理器的字节序不同。
  2. 非常浪费空间。 在大多数文本中,大部分码位都小于 127 或 255,因此字节 0x00 占用了大量空间。相较于 ASCII 表示法所需的 6 个字节,以上字符串需要占用 24 个字节。RAM 用量的增加没那么要紧(台式计算机有成 GB 的 RAM,而字符串通常不会有那么大),但要把磁盘和网络带宽的用量增加 4 倍是无法忍受的。
  3. 与现有的 C 函数(如 strlen() )不兼容,因此需要采用一套新的宽字符串函数。

因此这种编码用得不多,人们转而选择其他更高效、更方便的编码,比如 UTF-8。

UTF-8 是最常用的编码之一,Python 往往默认会采用它。UTF 代表“Unicode Transformation Format”,’8’ 表示编码采用 8 位数。(UTF-16 和 UTF-32 编码也是存在的,但其使用频率不如 UTF-8。)UTF-8 的规则如下:

  1. 如果码位 < 128,则直接用对应的字节值表示。
  2. 如果码位 >= 128,则转换为 2、3、4 个字节的序列,每个字节值都位于 128 和 255 之间。

UTF-8 有几个很方便的特性:

  1. 可以处理任何 Unicode 码位。
  2. Unicode 字符串被转换为一个字节序列,仅在表示空(null )字符(U+0000)时才会包含零值的字节。这意味着 strcpy() 之类的C 函数可以处理 UTF-8 字符串,而且用那些不能处理字符串结束符之外的零值字节的协议也能发送。
  3. ASCII 字符串也是也是也是合法的 UTF-8 文本。
  4. UTF-8 相当紧凑;大多数常用字符均可用一两个字节表示。
  5. 如果字节数据被损坏或丢失,则可以找出下一个 UTF-8 码点的开始位置并重新开始同步。随机的 8 位数据也不太可能像是有效的 UTF-8 编码。
  6. UTF-8 是一种面向字节的编码。编码规定了每个字符由一个或多个字节的序列表示。这避免了整数和双字节编码(如 UTF-16 和 UTF-32)可能出现的字节顺序问题,那时的字节序列会因执行编码的硬件而异。

Python对Unicode的支持

现在您已经了解了 Unicode 的基础知识,可以看下 Python 的 Unicode 特性。

字符串类型

从 Python 3.0 开始, str 类型包含了 Unicode 字符,这意味着用“unicode rocks!”`、`‘unicode rocks!’ 或三重引号字符串语法创建的任何字符串都会存储为 Unicode。

Python 源代码的默认编码是 UTF-8,因此可以直接在字符串中包含 Unicode 字符:

try:
    with open('/tmp/input.txt', 'r') as f:
        ...
except OSError:
    # 'File not found' error message.
    print("Fichier non trouvé")

旁注:Python 3 还支持在标识符中使用 Unicode 字符:

répertoire = "/tmp/records.log"
with open(répertoire, "w") as f:
    f.write("test\n")

如果无法在编辑器中输入某个字符,或出于某种原因想只保留 ASCII 编码的源代码,则还可以在字符串中使用转义序列。(根据系统的不同,可能会看到真的大写 Delta 字体而不是 u 转义符。):

>>> "\N{GREEK CAPITAL LETTER DELTA}"  # Using the character name
'\u0394'
>>> "\u0394"                          # Using a 16-bit hex value
'\u0394'
>>> "\U00000394"                      # Using a 32-bit hex value
'\u0394'

此外,可以用 bytesdecode() 方法创建一个字符串。 该方法可以接受 encoding 参数,比如可以为 UTF-8 ,以及可选的 errors 参数。

若无法根据编码规则对输入字符串进行编码,errors 参数指定了响应策略。 该参数的合法值可以是 'strict' (触发 UnicodeDecodeError 异常)、'replace' (用 U+FFFDREPLACEMENT CHARACTER)、'ignore' (只是将字符从 Unicode 结果中去掉),或 'backslashreplace' (插入一个 \xNN 转义序列)。 以下示例演示了这些不同的参数:

>>> b'\x80abc'.decode("utf-8", "strict")  
Traceback (most recent call last):
    ...
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 0:
  invalid start byte
>>> b'\x80abc'.decode("utf-8", "replace")
'\ufffdabc'
>>> b'\x80abc'.decode("utf-8", "backslashreplace")
'\\x80abc'
>>> b'\x80abc'.decode("utf-8", "ignore")
'abc'

编码格式以包含编码格式名称的字符串来指明。 Python 有大约 100 种不同的编码格式。 一些编码格式有多个名称,比如 'latin-1''iso_8859_1''8859 都是指同一种编码。

利用内置函数 chr() 还可以创建单字符的 Unicode 字符串,该函数可接受整数参数,并返回包含对应码位的长度为 1 的 Unicode 字符串。内置函数 ord() 是其逆操作,参数为单个字符的 Unicode 字符串,并返回码位值:

>>> chr(57344)
'\ue000'
>>> ord('\ue000')
57344

转换为字节

bytes.decode() 的逆方法是 str.encode() ,它会返回 Unicode 字符串的 bytes 形式,已按要求的 encoding 进行了编码。

参数 errors 的意义与 decode() 方法相同,但支持更多可能的handler。除了 'strict''ignore''replace' (这时会插入问号替换掉无法编码的字符),还有 'xmlcharrefreplace' (插入一个 XML 字符引用)、 backslashreplace (插入一个 \uNNNN 转义序列)和 namereplace (插入一个 \N{...} 转义序列 )。

以下例子演示了各种不同的结果:

>>> u = chr(40960) + 'abcd' + chr(1972)
>>> u.encode('utf-8')
b'\xea\x80\x80abcd\xde\xb4'
>>> u.encode('ascii')  
Traceback (most recent call last):
    ...
UnicodeEncodeError: 'ascii' codec can't encode character '\ua000' in
  position 0: ordinal not in range(128)
>>> u.encode('ascii', 'ignore')
b'abcd'
>>> u.encode('ascii', 'replace')
b'?abcd?'
>>> u.encode('ascii', 'xmlcharrefreplace')
b'&#40960;abcd&#1972;'
>>> u.encode('ascii', 'backslashreplace')
b'\\ua000abcd\\u07b4'
>>> u.encode('ascii', 'namereplace')
b'\\N{YI SYLLABLE IT}abcd\\u07b4'

用于注册和访问可用编码格式的底层函数,位于 codecs 模块中。 若要实现新的编码格式,则还需要了解 codecs 模块。 不过该模块返回的编码和解码函数通常更为底层一些,不大好用,编写新的编码格式是一项专业的任务,因此本文不会涉及该模块。

Python 源代码中的 Unicode 文字

在 Python 源代码中,可以用 \u 转义序列书写特定的 Unicode 码位,该序列后跟 4 个代表码位的十六进制数字。\U 转义序列用法类似,但要用8 个十六进制数字,而不是 4 个:

>>> s = "a\xac\u1234\u20ac\U00008000"
... #     ^^^^ two-digit hex escape
... #         ^^^^^^ four-digit Unicode escape
... #                     ^^^^^^^^^^ eight-digit Unicode escape
>>> [ord(c) for c in s]
[97, 172, 4660, 8364, 32768]

对大于 127 的码位使用转义序列,数量不多时没什么问题,但如果要用到很多重音字符,这会变得很烦人,类似于程序中的信息是用法语或其他使用重音的语言写的。也可以用内置函数 chr() 拼装字符串,但会更加乏味。

理想情况下,都希望能用母语的编码书写文本。还能用喜好的编辑器编辑 Python 源代码,编辑器要能自然地显示重音符,并在运行时使用正确的字符。

默认情况下,Python 支持以 UTF-8 格式编写源代码,但如果声明要用的编码,则几乎可以使用任何编码。只要在源文件的第一行或第二行包含一个特殊注释即可:

#!/usr/bin/env python
# -*- coding: latin-1 -*-
u = 'abcdé'
print(ord(u[-1]))

上述语法的灵感来自于 Emacs 用于指定文件局部变量的符号。Emacs 支持许多不同的变量,但 Python 仅支持“编码”。 -*- 符号向 Emacs 标明该注释是特殊的;这对 Python 没有什么意义,只是一种约定。Python 会在注释中查找 coding: namecoding=name

如果没有这种注释,则默认编码将会是前面提到的 UTF-8。更多信息请参阅 PEP 263

Unicode属性

Unicode 规范包含了一个码位信息数据库。对于定义的每一个码位,都包含了字符的名称、类别、数值(对于表示数字概念的字符,如罗马数字、分数如三分之一和五分之四等)。还有有关显示的属性,比如如何在双向文本中使用码位。

以下程序显示了几个字符的信息,并打印一个字符的数值:

import unicodedata
u = chr(233) + chr(0x0bf2) + chr(3972) + chr(6000) + chr(13231)
for i, c in enumerate(u):
    print(i, '%04x' % ord(c), unicodedata.category(c), end=" ")
    print(unicodedata.name(c))
# Get numeric value of second character
print(unicodedata.numeric(u[1]))

当运行时,这将打印出:

0 00e9 Ll LATIN SMALL LETTER E WITH ACUTE
1 0bf2 No TAMIL NUMBER ONE THOUSAND
2 0f84 Mn TIBETAN MARK HALANTA
3 1770 Lo TAGBANWA LETTER SA
4 33af So SQUARE RAD OVER S SQUARED
1000.0

类别代码是描述字符性质的一个缩写。分为“字母”、“数字”、“标点符号”或“符号”等类别,而这些类别又分为子类别。就以上输出的代码而言,'Ll' 表示“字母,小写”,'No' 表示“数字,其他”,'Mn' 表示“标记,非空白符” , 'So' 是“符号,其他”。有关类别代码的清单,请参阅 Unicode 字符库文档 https://www.unicode.org/reports/tr44/#General_Category_Values`_ 的“通用类别值”部分。

字符串比较

Unicode 让字符串的比较变得复杂了一些,因为同一组字符可能由不同的码位序列组成。例如,像“ê”这样的字母可以表示为单码位 U+00EA,或是 U+0065 U+0302,即“e”的码位后跟“COMBINING CIRCUMFLEX ACCENT”的码位。虽然在打印时会产生同样的输出,但一个是长度为 1 的字符串,另一个是长度为 2 的字符串。

一种不区分大小写比较的工具是字符串方法 casefold() ,将按照 Unicode 标准描述的算法将字符串转换为不区分大小写的形式。该算法对诸如德语字母“ß”(代码点 U+00DF)之类的字符进行了特殊处理,变为一对小写字母“ss”。

>>> street = 'Gürzenichstraße'
>>> street.casefold()
'gürzenichstrasse'

第二个工具是 unicodedata 模块的 normalize() 函数,将字符串转换为几种规范化形式之一,其中后跟组合字符的字母将被替换为单个字符。 normalize() 可用于执行字符串比较,即便两个字符串采用不同的字符组合,也不会错误地报告两者不相等:

import unicodedata
def compare_strs(s1, s2):
    def NFD(s):
        return unicodedata.normalize('NFD', s)
    return NFD(s1) == NFD(s2)
single_char = 'ê'
multiple_chars = '\N{LATIN SMALL LETTER E}\N{COMBINING CIRCUMFLEX ACCENT}'
print('length of first string=', len(single_char))
print('length of second string=', len(multiple_chars))
print(compare_strs(single_char, multiple_chars))

当运行时,这将输出:

$ python3 compare-strs.py
length of first string= 1
length of second string= 2
True

normalize() 函数的第一个参数是个字符串,给出所需的规范化形式,可以是“NFC”、“NFKC”、“NFD”和“NFKD”之一。

Unicode 标准还设定了如何进行不区分大小写的比较:

import unicodedata
def compare_caseless(s1, s2):
    def NFD(s):
        return unicodedata.normalize('NFD', s)
    return NFD(NFD(s1).casefold()) == NFD(NFD(s2).casefold())
# Example usage
single_char = 'ê'
multiple_chars = '\N{LATIN CAPITAL LETTER E}\N{COMBINING CIRCUMFLEX ACCENT}'
print(compare_caseless(single_char, multiple_chars))

这将打印 True 。(为什么 NFD() 会被调用两次?因为有几个字符让 casefold() 返回非规范化的字符串,所以结果需要再次进行规范化。参见 Unicode 标准的 3.13 节 的一个讨论和示例。)

Unicode 正则表达式

re 模块支持的正则表达式可以用字节串或字符串的形式提供。有一些特殊字符序列,比如 \d\w 具有不同的含义,具体取决于匹配模式是以字节串还是字符串形式提供的。例如,\d 将匹配字节串中的字符 [0-9] ,但对于字符串将会匹配 'Nd' 类别中的任何字符。

上述示例中的字符串包含了泰语和阿拉伯数字书写的数字 57:

import re
p = re.compile(r'\d+')
s = "Over \u0e55\u0e57 57 flavours"
m = p.search(s)
print(repr(m.group()))

执行时,\d+ 将匹配上泰语数字并打印出来。如果向 compile() 提供的是 re.ASCII 标志,\d+ 则会匹配子串 “57”。

类似地,\w 将匹配多种 Unicode 字符,但对于字节串则只会匹配 [a-zA-Z0-9_] ,如果指定 re.ASCII\s 将匹配 Unicode 空白符或 [ \t\n\r\f\v]

Unicode 数据的读写

既然处理 Unicode 数据的代码写好了,下一个问题就是输入/输出了。如何将 Unicode 字符串读入程序,如何将 Unicode 转换为适于存储或传输的形式呢?

根据输入源和输出目标的不同,或许什么都不用干;请检查一下应用程序用到的库是否原生支持 Unicode。例如,XML 解析器往往会返回 Unicode 数据。许多关系数据库的字段也支持 Unicode 值,并且 SQL 查询也能返回 Unicode 值。

在写入磁盘或通过套接字发送之前,Unicode 数据通常要转换为特定的编码。可以自己完成所有工作:打开一个文件,从中读取一个 8 位字节对象,然后用 bytes.decode(encoding) 对字节串进行转换。但是,不推荐采用这种全人工的方案。

编码的多字节特性就是一个难题; 一个 Unicode 字符可以用几个字节表示。 如果要以任意大小的块(例如 1024 或 4096 字节)读取文件,那么在块的末尾可能只读到某个 Unicode 字符的部分字节,这就需要编写错误处理代码。 有一种解决方案是将整个文件读入内存,然后进行解码,但这样就没法处理很大的文件了;若要读取 2 GB 的文件,就需要 2 GB 的 RAM。(其实需要的内存会更多些,因为至少有一段时间需要在内存中同时存放已编码字符串及其 Unicode 版本。)

解决方案是利用底层解码接口去捕获编码序列不完整的情况。这部分代码已经是现成的:内置函数 open() 可以返回一个文件类的对象,该对象认为文件的内容采用指定的编码,read()write() 等方法接受 Unicode 参数。只要用 open()encodingerrors 参数即可,参数释义同 str.encode()bytes.decode()

因此从文件读取 Unicode 就比较简单了:

with open('unicode.txt', encoding='utf-8') as f:
    for line in f:
        print(repr(line))

也可以在更新模式下打开文件,以便同时读取和写入:

with open('test', encoding='utf-8', mode='w+') as f:
    f.write('\u4500 blah blah blah\n')
    f.seek(0)
    print(repr(f.readline()[:1]))

Unicode 字符 U+FEFF 用作字节顺序标记(BOM),通常作为文件的第一个字符写入,以帮助自动检测文件的字节顺序。某些编码(例如 UTF-16)期望在文件开头出现 BOM;当采用这种编码时,BOM 将自动作为第一个字符写入,并在读取文件时会静默删除。这些编码有多种变体,例如用于 little-endian 和 big-endian 编码的 “utf-16-le” 和 “utf-16-be”,会指定一种特定的字节顺序并且不会忽略 BOM。

在某些地区,习惯在 UTF-8 编码文件的开头用上“BOM”;此名称具有误导性,因为 UTF-8 与字节顺序无关。此标记只是声明该文件以 UTF-8 编码。要读取此类文件,请使用“utf-8-sig”编解码器自动忽略此标记。

Unicode 文件名

当今大多数操作系统都支持包含任意 Unicode 字符的文件名。 通常这是通过将 Unicode 字符串转换为某种根据具体系统而定的编码格式来实现的。 如今的 Python 倾向于使用 UTF-8:MacOS 上的 Python 已经在多个版本中使用了 UTF-8,而 Python 3.6 也已在 Windows 上改用了 UTF-8。 在 Unix 系统中,将只有一个 文件系统编码格式。 如果你已设置了 LANGLC_CTYPE 环境变量的话;如果未设置,则默认编码格式还是 UTF-8。

sys.getfilesystemencoding() 函数将返回要在当前系统采用的编码,若想手动进行编码时即可用到,但无需多虑。在打开文件进行读写时,通常只需提供 Unicode 字符串作为文件名,会自动转换为合适的编码格式:

filename = 'filename\u4500abc'
with open(filename, 'w') as f:
    f.write('blah\n')

os 模块中的函数也能接受 Unicode 文件名,如 os.stat()

os.listdir() 函数返回文件名,这引发了一个问题:它应该返回文件名的 Unicode 版本,还是应该返回包含已编码版本的字节串? 这两者 os.listdir() 都能做到,具体取决于你给出的目录路径是字节串还是 Unicode 字符串形式的。 如果你传入一个 Unicode 字符串作为路径,文件名将使用文件系统的编码格式进行解码并返回一个 Unicode 字符串列表,而传入一个字节串形式的路径则将返回字节串形式的文件名。 例如,假定默认 文件系统编码 为 UTF-8,运行以下程序:

fn = 'filename\u4500abc'
f = open(fn, 'w')
f.close()
import os
print(os.listdir(b'.'))
print(os.listdir('.'))

将产生以下输出:

$ python listdir-test.py
[b'filename\xe4\x94\x80abc', ...]
['filename\u4500abc', ...]

第一个列表包含 UTF-8 编码的文件名,第二个列表则包含 Unicode 版本的。

请注意,大多时候应该坚持用这些 API 处理 Unicode。字节串 API 应该仅用于可能存在不可解码文件名的系统;现在几乎仅剩 Unix 系统了。

识别 Unicode 的编程技巧

本节提供了一些关于编写 Unicode 处理软件的建议。

最重要的技巧如下:

程序应只在内部处理 Unicode 字符串,尽快对输入数据进行解码,并只在最后对输出进行编码。

如果尝试编写的处理函数对 Unicode 和字节串形式的字符串都能接受,就会发现组合使用两种不同类型的字符串时,容易产生差错。没办法做到自动编码或解码:如果执行 str + bytes,则会触发 TypeError

当要使用的数据来自 Web 浏览器或其他不受信来源时,常用技术是在用该字符串生成命令行之前,或要存入数据库之前,先检查字符串中是否包含非法字符。请仔细检查解码后的字符串,而不是编码格式的字节串数据;有些编码可能具备一些有趣的特性,例如与 ASCII 不是一一对应或不完全兼容。如果输入数据还指定了编码格式,则尤其如此,因为攻击者可以选择一种巧妙的方式将恶意文本隐藏在经过编码的字节流中。

在文件编码格式之间进行转换

StreamRecoder 类可以在两种编码之间透明地进行转换,参数为编码格式为 #1 的数据流,表现行为则是编码格式为 #2 的数据流。

假设输入文件 f 采用 Latin-1 编码格式,即可用 StreamRecoder 包装后返回 UTF-8 编码的字节串:

new_f = codecs.StreamRecoder(f,
    # en/decoder: used by read() to encode its results and
    # by write() to decode its input.
    codecs.getencoder('utf-8'), codecs.getdecoder('utf-8'),
    # reader/writer: used to read and write to the stream.
    codecs.getreader('latin-1'), codecs.getwriter('latin-1') )
编码格式未知的文件

若需对文件进行修改,但不知道文件的编码,那该怎么办呢?如果已知编码格式与 ASCII 兼容,并且只想查看或修改 ASCII 部分,则可利用 surrogateescape 错误处理 handler 打开文件:

with open(fname, 'r', encoding="ascii", errors="surrogateescape") as f:
    data = f.read()
# make changes to the string 'data'
with open(fname + '.new', 'w',
          encoding="ascii", errors="surrogateescape") as f:
    f.write(data)

surrogateescape 错误处理 handler 会把所有非 ASCII 字节解码为 U+DC80 至 U+DCFF 这一特殊范围的码位。当 surrogateescape 错误处理 handler用于数据编码并回写时,这些码位将转换回原样。

如何利用 urllib 包获取网络资源

概述

Related Articles

关于如何用 Python 获取 web 资源,以下文章或许也很有用:

urllib.request 是用于获取 URL (统一资源定位符)的 Python 模块。它以 urlopen 函数的形式提供了一个非常简单的接口,能用不同的协议获取 URL。同时它还为处理各种常见情形提供了一个稍微复杂一些的接口——比如:基础身份认证、cookies、代理等等。这些功能是由名为 handlers 和 opener 的对象提供的。

urllib.request 支持多种 “URL 方案” (通过 URL中 ":" 之前的字符串加以区分——如 "ftp://python.org/" 中的 ``”ftp”)即为采用其关联网络协议(FTP、HTTP 之类)的 URL 方案 。本教程重点关注最常用的 HTTP 场景。

对于简单场景而言, urlopen 用起来十分容易。但只要在打开 HTTP URL 时遇到错误或非常情况,就需要对超文本传输协议有所了解才行。最全面、最权威的 HTTP 参考是 RFC 2616 。那是一份技术文档,并没有追求可读性。本 文旨在说明 urllib 的用法,为了便于阅读也附带了足够详细的 HTTP 信息。

获取 URL 资源

urllib.request 最简单的使用方式如下所示:

import urllib.request
with urllib.request.urlopen('http://python.org/') as response:
   html = response.read()

如果想通过 URL 获取资源并临时存储一下,可以采用 shutil.copyfileobj()tempfile.NamedTemporaryFile() 函数:

import shutil
import tempfile
import urllib.request
with urllib.request.urlopen('http://python.org/') as response:
    with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
        shutil.copyfileobj(response, tmp_file)
with open(tmp_file.name) as html:
    pass

urllib 的很多用法就是这么简单(注意 URL 不仅可以 http: 开头,还可以是 ftp: 、file: 等)。不过本教程的目的是介绍更加复杂的应用场景,重点还是关注 HTTP。

HTTP 以请求和响应为基础——客户端生成请求,服务器发送响应。urllib.request 用 Request 对象来表示要生成的 HTTP 请求。最简单的形式就是创建一个 Request 对象,指定了想要获取的 URL。用这个 Request 对象作为参数调用urlopen ,将会返回该 URL 的响应对象。响应对象类似于文件对象,就是说可以对其调用 .read() 之类的命令:

import urllib.request
req = urllib.request.Request('http://www.voidspace.org.uk')
with urllib.request.urlopen(req) as response:
   the_page = response.read()

请注意,urllib.request 用同一个 Request 接口处理所有 URL 方案。比如可生成 FTP 请求如下:

req = urllib.request.Request('ftp://example.com/')

就 HTTP 而言,Request 对象能够做两件额外的事情:首先可以把数据传给服务器。其次,可以将 有关 数据或请求本身的额外信息(metadata)传给服务器——这些信息将会作为 HTTP “头部”数据发送。下面依次看下。

数据

有时需要向某个 URL 发送数据,通常此 URL 会指向某个CGI(通用网关接口)脚本或其他 web 应用。对于 HTTP 而言,这通常会用所谓的 POST 请求来完成。当要把 Web 页填写的 HTML 表单提交时,浏览器通常会执行此操作。但并不是所有的 POST 都来自表单:可以用 POST 方式传输任何数据到自己的应用上。对于通常的 HTML 表单,数据需要以标准的方式编码,然后作为 data 参数传给 Request 对象。编码过程是用 urllib.parse 库的函数完成的:

import urllib.parse
import urllib.request
url = 'http://www.someserver.com/cgi-bin/register.cgi'
values = {'name' : 'Michael Foord',
          'location' : 'Northampton',
          'language' : 'Python' }
data = urllib.parse.urlencode(values)
data = data.encode('ascii') # data should be bytes
req = urllib.request.Request(url, data)
with urllib.request.urlopen(req) as response:
   the_page = response.read()

请注意,有时还需要采用其他编码,比如由 HTML 表单上传文件——更多细节请参见 HTML 规范,提交表单

如果不传递 data 参数,urllib 将采用 GET 请求。GET 和 POST 请求有一点不同,POST 请求往往具有“副作用”,他们会以某种方式改变系统的状态。例如,从网站下一个订单,购买一大堆罐装垃圾并运送到家。 尽管 HTTP 标准明确指出 POST 总是 要导致副作用,而 GET 请求 从来不会 导致副作用。但没有什么办法能阻止 GET 和 POST 请求的副作用。数据也可以在 HTTP GET 请求中传递,只要把数据编码到 URL 中即可。

做法如下所示:

>>> import urllib.request
>>> import urllib.parse
>>> data = {}
>>> data['name'] = 'Somebody Here'
>>> data['location'] = 'Northampton'
>>> data['language'] = 'Python'
>>> url_values = urllib.parse.urlencode(data)
>>> print(url_values)  # The order may differ from below.  
name=Somebody+Here&language=Python&location=Northampton
>>> url = 'http://www.example.com/example.cgi'
>>> full_url = url + '?' + url_values
>>> data = urllib.request.urlopen(full_url)

请注意,完整的 URL 是通过在其中添加 ? 创建的,后面跟着经过编码的数据。

HTTP 头部信息

下面介绍一个具体的 HTTP 头部信息,以此说明如何在 HTTP 请求加入头部信息。

有些网站 不愿被程序浏览到,或者要向不同的浏览器发送不同版本 的网页。默认情况下,urllib 将自身标识为“Python-urllib/xy”(其中 xy 是 Python 版本的主、次版本号,例如 Python-urllib/2.5),这可能会让网站不知所措,或者干脆就使其无法正常工作。浏览器是通过头部信息 User-Agent 来标识自己的。在创建 Request 对象时,可以传入字典形式的头部信息。以下示例将生成与之前相同的请求,只是将自身标识为某个版本的 Internet Explorer :

import urllib.parse
import urllib.request
url = 'http://www.someserver.com/cgi-bin/register.cgi'
user_agent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)'
values = {'name': 'Michael Foord',
          'location': 'Northampton',
          'language': 'Python' }
headers = {'User-Agent': user_agent}
data = urllib.parse.urlencode(values)
data = data.encode('ascii')
req = urllib.request.Request(url, data, headers)
with urllib.request.urlopen(req) as response:
   the_page = response.read()

响应对象也有两个很有用的方法。

异常的处理

如果 urlopen 无法处理响应信息,就会触发 URLError 。尽管与通常的 Python API 一样,也可能触发 ValueErrorTypeError 等内置异常。

HTTPErrorURLError 的子类,当 URL 是 HTTP 的情况时将会触发。

上述异常类是从 urllib.error 模块中导出的。

URLError

触发 URLError 的原因,通常是网络不通(或者没有到指定服务器的路由),或者指定的服务器不存在。这时触发的异常会带有一个 reason 属性,是一个包含错误代码和文本错误信息的元组。

例如:

>>> req = urllib.request.Request('http://www.pretend_server.org')
>>> try: urllib.request.urlopen(req)
... except urllib.error.URLError as e:
...     print(e.reason)      
...
(4, 'getaddrinfo failed')

HTTPError

从服务器返回的每个 HTTP 响应都包含一个数字的 “状态码”。有时该状态码表明服务器无法完成该请求。默认的处理函数将会处理这其中的一部分响应。如若响应是“redirection”,这是要求客户端从另一 URL 处获取数据,urllib 将会自行处理。对于那些无法处理的状况,urlopen 将会引发 HTTPError 。典型的错误包括:“404”(页面无法找到)、“403”(请求遭拒绝)和“401”(需要身份认证)。

全部的 HTTP 错误码请参阅 RFC 2616

HTTPError 实例将包含一个整数型的“code”属性,对应于服务器发来的错误。

错误代码

由于默认处理函数会自行处理重定向(300 以内的错误码),而且 100—299 的状态码表示成功,因此通常只会出现 400—599 的错误码。

http.server.BaseHTTPRequestHandler.responses 是很有用的响应码字典,其中给出了 RFC 2616 用到的所有响应代码。为方便起见,将此字典转载如下:

# Table mapping response codes to messages; entries have the
# form {code: (shortmessage, longmessage)}.
responses = {
    100: ('Continue', 'Request received, please continue'),
    101: ('Switching Protocols',
          'Switching to new protocol; obey Upgrade header'),
    200: ('OK', 'Request fulfilled, document follows'),
    201: ('Created', 'Document created, URL follows'),
    202: ('Accepted',
          'Request accepted, processing continues off-line'),
    203: ('Non-Authoritative Information', 'Request fulfilled from cache'),
    204: ('No Content', 'Request fulfilled, nothing follows'),
    205: ('Reset Content', 'Clear input form for further input.'),
    206: ('Partial Content', 'Partial content follows.'),
    300: ('Multiple Choices',
          'Object has several resources -- see URI list'),
    301: ('Moved Permanently', 'Object moved permanently -- see URI list'),
    302: ('Found', 'Object moved temporarily -- see URI list'),
    303: ('See Other', 'Object moved -- see Method and URL list'),
    304: ('Not Modified',
          'Document has not changed since given time'),
    305: ('Use Proxy',
          'You must use proxy specified in Location to access this '
          'resource.'),
    307: ('Temporary Redirect',
          'Object moved temporarily -- see URI list'),
    400: ('Bad Request',
          'Bad request syntax or unsupported method'),
    401: ('Unauthorized',
          'No permission -- see authorization schemes'),
    402: ('Payment Required',
          'No payment -- see charging schemes'),
    403: ('Forbidden',
          'Request forbidden -- authorization will not help'),
    404: ('Not Found', 'Nothing matches the given URI'),
    405: ('Method Not Allowed',
          'Specified method is invalid for this server.'),
    406: ('Not Acceptable', 'URI not available in preferred format.'),
    407: ('Proxy Authentication Required', 'You must authenticate with '
          'this proxy before proceeding.'),
    408: ('Request Timeout', 'Request timed out; try again later.'),
    409: ('Conflict', 'Request conflict.'),
    410: ('Gone',
          'URI no longer exists and has been permanently removed.'),
    411: ('Length Required', 'Client must specify Content-Length.'),
    412: ('Precondition Failed', 'Precondition in headers is false.'),
    413: ('Request Entity Too Large', 'Entity is too large.'),
    414: ('Request-URI Too Long', 'URI is too long.'),
    415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
    416: ('Requested Range Not Satisfiable',
          'Cannot satisfy request range.'),
    417: ('Expectation Failed',
          'Expect condition could not be satisfied.'),
    500: ('Internal Server Error', 'Server got itself in trouble'),
    501: ('Not Implemented',
          'Server does not support this operation'),
    502: ('Bad Gateway', 'Invalid responses from another server/proxy.'),
    503: ('Service Unavailable',
          'The server cannot process the request due to a high load'),
    504: ('Gateway Timeout',
          'The gateway server did not receive a timely response'),
    505: ('HTTP Version Not Supported', 'Cannot fulfill request.'),
    }

当触发错误时,服务器通过返回 HTTP 错误码 错误页面进行响应。可以将 HTTPError 实例用作返回页面的响应。这意味着除了 code 属性之外,错误对象还像 urllib.response 模块返回的那样具有 read、geturl 和 info 方法:

>>> req = urllib.request.Request('http://www.python.org/fish.html')
>>> try:
...     urllib.request.urlopen(req)
... except urllib.error.HTTPError as e:
...     print(e.code)
...     print(e.read())  
...
404
b'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n\n\n<html
  ...
  <title>Page Not Found</title>\n
  ...

总之

若要准备处理 HTTPError URLError ,有两种简单的方案。推荐使用第二种方案。

第一种方案
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
req = Request(someurl)
try:
    response = urlopen(req)
except HTTPError as e:
    print('The server couldn\'t fulfill the request.')
    print('Error code: ', e.code)
except URLError as e:
    print('We failed to reach a server.')
    print('Reason: ', e.reason)
else:
    # everything is fine

注解

except HTTPError 必须 首先处理,否则 except URLError 将会 同时 捕获 HTTPError

第二种方案
from urllib.request import Request, urlopen
from urllib.error import URLError
req = Request(someurl)
try:
    response = urlopen(req)
except URLError as e:
    if hasattr(e, 'reason'):
        print('We failed to reach a server.')
        print('Reason: ', e.reason)
    elif hasattr(e, 'code'):
        print('The server couldn\'t fulfill the request.')
        print('Error code: ', e.code)
else:
    # everything is fine

info 和 geturl 方法

由 urlopen (或者 HTTPError 实例)所返回的响应包含两个有用的方法: info()geturl(),该响应由模块 urllib.response 定义。

geturl ——返回所获取页面的真实 URL。该方法很有用,因为 urlopen (或 opener 对象)可能已经经过了一次重定向。已获取页面的 URL 未必就是所请求的 URL 。

info - 该方法返回一个类似字典的对象,描述了所获取的页面,特别是由服务器送出的头部信息(headers) 。目前它是一个 http.client.HTTPMessage 实例。

典型的 HTTP 头部信息包括“Content-length”、“Content-type”等。有关 HTTP 头部信息的清单,包括含义和用途的简要说明,请参阅 HTTP Header 快速参考

Opener 和 Handler

当获取 URL 时,会用到了一个 opener(一个类名可能经过混淆的 urllib.request.OpenerDirector 的实例)。通常一直会用默认的 opener ——通过 urlopen ——但也可以创建自定义的 opener 。opener 会用到 handler。所有的“繁重工作”都由 handler 完成。每种 handler 知道某种 URL 方案(http、ftp 等)的 URL 的打开方式,或是某方面 URL 的打开方式,例如 HTTP 重定向或 HTTP cookie。

若要用已安装的某个 handler 获取 URL,需要创建一个 opener 对象,例如处理 cookie 的 opener,或对重定向不做处理的 opener。

若要创建 opener,请实例化一个 OpenerDirector ,然后重复调用 .add_handler(some_handler_instance)

或者也可以用 build_opener ,这是个用单次调用创建 opener 对象的便捷函数。build_opener 默认会添加几个 handler,不过还提供了一种快速添加和/或覆盖默认 handler 的方法。

可能还需要其他类型的 handler,以便处理代理、身份认证和其他常见但稍微特殊的情况。

install_opener 可用于让 opener 对象成为(全局)默认 opener。这意味着调用 urlopen 时会采用已安装的 opener。

opener 对象带有一个 ``open方法,可供直接调用以获取 url,方式与urlopen函数相同。除非是为了调用方便,否则没必要去调用install_opener` 。

基本认证

为了说明 handler 的创建和安装过程,会用到 HTTPBasicAuthHandler

如果需要身份认证,服务器会发送一条请求身份认证的头部信息(以及 401 错误代码)。这条信息中指明了身份认证方式和“安全区域(realm)”。格式如下所示:WWW-Authenticate: SCHEME realm="REALM"

例如

WWW-Authenticate: Basic realm="cPanel Users"

然后,客户端应重试发起请求,请求数据中的头部信息应包含安全区域对应的用户名和密码。这就是“基本身份认证”。为了简化此过程,可以创建 HTTPBasicAuthHandler 的一个实例及使用它的 opener。

HTTPBasicAuthHandler 用一个名为密码管理器的对象来管理 URL、安全区域与密码、用户名之间的映射关系。如果知道确切的安全区域(来自服务器发送的身份认证头部信息),那就可以用到 HTTPPasswordMgr 。通常人们并不关心安全区域是什么,这时用HTTPPasswordMgrWithDefaultRealm 就很方便,允许为 URL 指定默认的用户名和密码。当没有为某个安全区域提供用户名和密码时,就会用到默认值。下面用 None 作为 add_password 方法的安全区域参数,表明采用默认用户名和密码。

首先需要身份认证的是顶级 URL。比传给 .add_password() 的 URL 级别“更深”的 URL 也会得以匹配:

# create a password manager
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
# Add the username and password.
# If we knew the realm, we could use it instead of None.
top_level_url = "http://example.com/foo/"
password_mgr.add_password(None, top_level_url, username, password)
handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
# create "opener" (OpenerDirector instance)
opener = urllib.request.build_opener(handler)
# use the opener to fetch a URL
opener.open(a_url)
# Install the opener.
# Now all calls to urllib.request.urlopen use our opener.
urllib.request.install_opener(opener)

注解

在以上例子中,只向 build_opener 给出了 HTTPBasicAuthHandler 。默认情况下,opener 会有用于处理常见状况的 handler ——ProxyHandler (如果设置代理的话,比如设置了环境变量 http_proxy ),UnknownHandlerHTTPHandlerHTTPDefaultErrorHandlerHTTPRedirectHandlerFTPHandlerFileHandlerDataHandlerHTTPErrorProcessor

top_level_url 其实 要么 是一条完整的 URL(包括 “http:” 部分和主机名及可选的端口号),比如 "http://example.com/"要么 是一条“访问权限”(即主机名,及可选的端口号),比如 "example.com""example.com:8080" (后一个示例包含了端口号)。访问权限 不得 包含“用户信息”部分——比如 "joe:password@example.com" 就不正确。

代理

urllib 将自动检测并使用代理设置。 这是通过 ProxyHandler 实现的,当检测到代理设置时,是正常 handler 链中的一部分。通常这是一件好事,但有时也可能会无效 。 一种方案是配置自己的 ProxyHandler ,不要定义代理。 设置的步骤与 Basic Authentication handler 类似:

>>> proxy_support = urllib.request.ProxyHandler({})
>>> opener = urllib.request.build_opener(proxy_support)
>>> urllib.request.install_opener(opener)

注解

目前 urllib.request 尚不 支持通过代理抓取 https 链接地址。 但此功能可以通过扩展 urllib.request 来启用,如以下例程所示 6

注解

如果设置了 REQUEST_METHOD 变量,则会忽略 HTTP_PROXY

套接字与分层

Python 获取 Web 资源的能力是分层的。urllib 用到的是 http.client 库,而后者又用到了套接字库。

从 Python 2.3 开始,可以指定套接字等待响应的超时时间。这对必须要读到网页数据的应用程序会很有用。默认情况下,套接字模块 不会超时 并且可以挂起。目前,套接字超时机制未暴露给 http.client 或 urllib.request 层使用。不过可以为所有套接字应用设置默认的全局超时。

import socket
import urllib.request
# timeout in seconds
timeout = 10
socket.setdefaulttimeout(timeout)
# this call to urllib.request.urlopen now uses the default timeout
# we have set in the socket module
req = urllib.request.Request('http://www.voidspace.org.uk')
response = urllib.request.urlopen(req)

使用 DTrace 和 SystemTap 检测CPython

DTrace和SystemTap是监控工具,它们都提供了一种检查计算机系统上的进程的方法。 它们都使用特定领域的语言,允许用户编写脚本,其中:

  • 进程监视的过滤器
  • 从感兴趣的进程中收集数据
  • 生成有关数据的报告

从Python 3.6开始,CPython可以使用嵌入式“标记”构建,也称为“探测器”,可以通过DTrace或SystemTap脚本观察,从而更容易监视系统上的CPython进程正在做什么。

CPython implementation detail: DTrace标记是CPython解释器的实现细节。 不保证CPython版本之间的探针兼容性。 更改CPython版本时,DTrace脚本可能会停止工作或无法正常工作而不会发出警告。

启用静态标记

macOS内置了对DTrace的支持。 在Linux上,为了使用SystemTap的嵌入式标记构建CPython,必须安装SystemTap开发工具。

在Linux机器上,这可以通过:

$ yum install systemtap-sdt-devel

或者:

$ sudo apt-get install systemtap-sdt-dev

之后 CPython 必须 配置 --with-dtrace 选项:

checking for --with-dtrace... yes

在macOS上,您可以通过在后台运行Python进程列出可用的DTrace探测器,并列出Python程序提供的所有探测器:

$ python3.6 -q &
$ sudo dtrace -l -P python$!  # or: dtrace -l -m python3.6
   ID   PROVIDER            MODULE                          FUNCTION NAME
29564 python18035        python3.6          _PyEval_EvalFrameDefault function-entry
29565 python18035        python3.6             dtrace_function_entry function-entry
29566 python18035        python3.6          _PyEval_EvalFrameDefault function-return
29567 python18035        python3.6            dtrace_function_return function-return
29568 python18035        python3.6                           collect gc-done
29569 python18035        python3.6                           collect gc-start
29570 python18035        python3.6          _PyEval_EvalFrameDefault line
29571 python18035        python3.6                 maybe_dtrace_line line

在Linux上,您可以通过查看是否包含“.note.stapsdt”部分来验证构建的二进制文件中是否存在SystemTap静态标记。

$ readelf -S ./python | grep .note.stapsdt
[30] .note.stapsdt        NOTE         0000000000000000 00308d78

如果你将 Python 编译为共享库(使用 --enable-shared 配置选项),那么你需要改为在共享库内部查看。 例如:

$ readelf -S libpython3.3dm.so.1.0 | grep .note.stapsdt
[29] .note.stapsdt        NOTE         0000000000000000 00365b68

足够现代的readelf命令可以打印元数据:

$ readelf -n ./python
Displaying notes found at file offset 0x00000254 with length 0x00000020:
    Owner                 Data size          Description
    GNU                  0x00000010          NT_GNU_ABI_TAG (ABI version tag)
        OS: Linux, ABI: 2.6.32
Displaying notes found at file offset 0x00000274 with length 0x00000024:
    Owner                 Data size          Description
    GNU                  0x00000014          NT_GNU_BUILD_ID (unique build ID bitstring)
        Build ID: df924a2b08a7e89f6e11251d4602022977af2670
Displaying notes found at file offset 0x002d6c30 with length 0x00000144:
    Owner                 Data size          Description
    stapsdt              0x00000031          NT_STAPSDT (SystemTap probe descriptors)
        Provider: python
        Name: gc__start
        Location: 0x00000000004371c3, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bf6
        Arguments: -4@%ebx
    stapsdt              0x00000030          NT_STAPSDT (SystemTap probe descriptors)
        Provider: python
        Name: gc__done
        Location: 0x00000000004374e1, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bf8
        Arguments: -8@%rax
    stapsdt              0x00000045          NT_STAPSDT (SystemTap probe descriptors)
        Provider: python
        Name: function__entry
        Location: 0x000000000053db6c, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6be8
        Arguments: 8@%rbp 8@%r12 -4@%eax
    stapsdt              0x00000046          NT_STAPSDT (SystemTap probe descriptors)
        Provider: python
        Name: function__return
        Location: 0x000000000053dba8, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bea
        Arguments: 8@%rbp 8@%r12 -4@%eax

上述元数据包含SystemTap的信息,描述如何修补策略性放置的机器代码指令以启用SystemTap脚本使用的跟踪钩子。

静态DTrace探针

下面的 DTrace 脚本示例可以用来显示一个 Python 脚本的调用/返回层次结构,只在调用名为 “start” 的函数内进行跟踪。换句话说,导入时的函数调用不会被列出。

self int indent;
python$target:::function-entry
/copyinstr(arg1) == "start"/
{
        self->trace = 1;
}
python$target:::function-entry
/self->trace/
{
        printf("%d\t%*s:", timestamp, 15, probename);
        printf("%*s", self->indent, "");
        printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2);
        self->indent++;
}
python$target:::function-return
/self->trace/
{
        self->indent--;
        printf("%d\t%*s:", timestamp, 15, probename);
        printf("%*s", self->indent, "");
        printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2);
}
python$target:::function-return
/copyinstr(arg1) == "start"/
{
        self->trace = 0;
}

它可以这样调用:

$ sudo dtrace -q -s call_stack.d -c "python3.6 script.py"

输出结果会像这样:

156641360502280  function-entry:call_stack.py:start:23
156641360518804  function-entry: call_stack.py:function_1:1
156641360532797  function-entry:  call_stack.py:function_3:9
156641360546807 function-return:  call_stack.py:function_3:10
156641360563367 function-return: call_stack.py:function_1:2
156641360578365  function-entry: call_stack.py:function_2:5
156641360591757  function-entry:  call_stack.py:function_1:1
156641360605556  function-entry:   call_stack.py:function_3:9
156641360617482 function-return:   call_stack.py:function_3:10
156641360629814 function-return:  call_stack.py:function_1:2
156641360642285 function-return: call_stack.py:function_2:6
156641360656770  function-entry: call_stack.py:function_3:9
156641360669707 function-return: call_stack.py:function_3:10
156641360687853  function-entry: call_stack.py:function_4:13
156641360700719 function-return: call_stack.py:function_4:14
156641360719640  function-entry: call_stack.py:function_5:18
156641360732567 function-return: call_stack.py:function_5:21
156641360747370 function-return:call_stack.py:start:28

静态SystemTap标记

使用 SystemTap 集成的底层方法是直接使用静态标记。 这需要你显式地说明包含它们的二进制文件。

例如,这个SystemTap脚本可以用来显示Python脚本的调用/返回层次结构:

probe process("python").mark("function__entry") {
     filename = user_string($arg1);
     funcname = user_string($arg2);
     lineno = $arg3;
     printf("%s => %s in %s:%d\\n",
            thread_indent(1), funcname, filename, lineno);
}
probe process("python").mark("function__return") {
    filename = user_string($arg1);
    funcname = user_string($arg2);
    lineno = $arg3;
    printf("%s <= %s in %s:%d\\n",
           thread_indent(-1), funcname, filename, lineno);
}

它可以这样调用:

$ stap \
  show-call-hierarchy.stp \
  -c "./python test.py"

输出结果会像这样:

11408 python(8274):        => __contains__ in Lib/_abcoll.py:362
11414 python(8274):         => __getitem__ in Lib/os.py:425
11418 python(8274):          => encode in Lib/os.py:490
11424 python(8274):          <= encode in Lib/os.py:493
11428 python(8274):         <= __getitem__ in Lib/os.py:426
11433 python(8274):        <= __contains__ in Lib/_abcoll.py:366

其中的列是:

  • 脚本开始后经过的微秒数
  • 可执行文件的名字
  • 进程的PID

其余部分则表示脚本执行时的调用/返回层次结构。

对于 CPython 的 --enable-shared 编译版,这些标记包含在 libpython 共享库内部,并且 probe 的加点路径需要反映这个。 例如,上述示例的这一行:

probe process("python").mark("function__entry") {

应改为:

probe process("python").library("libpython3.6dm.so.1.0").mark("function__entry") {

(假定为 CPython 3.6 的 调试编译版)

可用的静态标记

function__entry(str filename, str funcname, int lineno)

这个标记表示一个Python函数的执行已经开始。它只对纯 Python (字节码)函数触发。

文件名、函数名和行号作为位置参数提供给跟踪脚本,必须使用 $arg1, $arg2, $arg3 访问:

  • $arg1 : (const char *) 文件名,使用 user_string($arg1) 访问
  • $arg2 : (const char *) 函数名,使用 user_string($arg2) 访问
  • $arg3 : int 行号
function__return(str filename, str funcname, int lineno)

这个标记与 function__entry() 相反,表示Python函数的执行已经结束 (通过 return 或者异常)。 它只对纯Python (字节码) 函数触发。

参数和 function__entry() 相同

line(str filename, str funcname, int lineno)

这个标记表示一个 Python 行即将被执行。它相当于用 Python 分析器逐行追踪。它不会在C函数中触发。

参数和 function__entry() 相同

gc__start(int generation)

当Python解释器启动一个垃圾回收循环时被触发。 arg0 是要扫描的生成器,如 gc.collect()

gc__done(long collected)

当Python解释器完成一个垃圾回收循环时被触发。arg0 是收集到的对象的数量。

import__find__load__start(str modulename)

importlib 试图查找并加载模块之前被触发。arg0 是模块名称。

3.7 新版功能.

import__find__load__done(str modulename, int found)

importlib 的 find_and_load 函数被调用后被触发 。arg0 是模块名称, arg1 表示模块是否成功加载。

3.7 新版功能.

audit(str event, void *tuple)

sys.audit()PySys_Audit() 被调用时启动。 arg0 是事件名称的 C 字符串,arg1 是一个指向元组对象的 PyObject 指针。

3.8 新版功能.

SystemTap Tapsets

使用SystemTap集成的更高层次的方法是使用 “tapset” 。SystemTap 的等效库,它隐藏了静态标记的一些底层细节。

这里是一个基于 CPython 的非共享构建的 tapset 文件。

/*
   Provide a higher-level wrapping around the function__entry and
   function__return markers:
 \*/
probe python.function.entry = process("python").mark("function__entry")
{
    filename = user_string($arg1);
    funcname = user_string($arg2);
    lineno = $arg3;
    frameptr = $arg4
}
probe python.function.return = process("python").mark("function__return")
{
    filename = user_string($arg1);
    funcname = user_string($arg2);
    lineno = $arg3;
    frameptr = $arg4
}

如果这个文件安装在 SystemTap 的 tapset 目录下(例如/usr/share/systemtap/tapset ),那么这些额外的探测点就会变得可用。

python.function.entry(str filename, str funcname, int lineno, frameptr)

这个探针点表示一个Python函数的执行已经开始。它只对纯Python (字节码)函数触发。

python.function.return(str filename, str funcname, int lineno, frameptr)

这个探针点是 python.function.return 的反义操作,表示一个 Python 函数的执行已经结束(或是通过 return,或是通过异常)。 它只会针对纯 Python(字节码)函数触发。

例子

这个SystemTap脚本使用上面的tapset来更清晰地实现上面给出的跟踪Python函数调用层次结构的例子,而不需要直接命名静态标记。

probe python.function.entry
{
  printf("%s => %s in %s:%d\n",
         thread_indent(1), funcname, filename, lineno);
}
probe python.function.return
{
  printf("%s <= %s in %s:%d\n",
         thread_indent(-1), funcname, filename, lineno);
}

下面的脚本使用上面的tapset提供了所有运行中的CPython代码的顶部视图,显示了整个系统中每一秒钟最频繁输入的前20个字节码帧。

global fn_calls;
probe python.function.entry
{
    fn_calls[pid(), filename, funcname, lineno] += 1;
}
probe timer.ms(1000) {
    printf("\033[2J\033[1;1H") /* clear screen \*/
    printf("%6s %80s %6s %30s %6s\n",
           "PID", "FILENAME", "LINE", "FUNCTION", "CALLS")
    foreach ([pid, filename, funcname, lineno] in fn_calls- limit 20) {
        printf("%6d %80s %6d %30s %6d\n",
            pid, filename, lineno, funcname,
            fn_calls[pid, filename, funcname, lineno]);
    }
    delete fn_calls;
}

对象注解属性的最佳实践

如果 Python 代码会去查看 Python 对象的 __annotations__ 属性,建议遵循以下准则。

四个部分:在 Python 3.10 以上版本中访问对象注解的最佳实践、在Python 3.9 以上版本中访问对象注解的最佳实践、适用于任何 Python 版本的其他 ``annotations最佳实践、annotations` 的特别之处。

请注意,本文是专门介绍 __annotations__ 的,而不是介绍注解的用法。

在 Python 3.10 以上版本中访问对象的注解字典

Python 3.10 在标准库中加入了一个新函数:inspect.get_annotations()。在 Python 3.10 以上的版本中,调用该函数就是访问对象注解字典的最佳做法。该函数还可以“解析”字符串形式的注解。

有时会因为某些原因看不到 inspect.get_annotations() ,也可以直接访问 __annotations__ 数据成员。这方面的最佳实践在 Python 3.10 中也发生了变化:从 Python 3.10 开始,Python 函数、类和模块的 o.__annotations__ 保证 可用。如果确定是要查看这三种对象,只要利用 o.__annotations__ 读取对象的注释字典即可。

不过其他类型的可调用对象可能就没有定义 __annotations__ 属性,比如由 functools.partial() 创建的可调用对象。当访问某个未知对象的__annotations__ 时,Python 3.10 以上版本的最佳做法是带三个参数去调用 getattr() ,比如 getattr(o, '__annotations__', None)

在 Python 3.9 以上版本中访问对象的注解字典

在 Python 3.9 之前的版本中,访问对象的注解字典要比高版本中复杂得多。这个是 Python 低版本的一个设计缺陷,特别是访问类的注解时。

要访问其他对象——函数、可调用对象和模块——的注释字典,最佳做法与 3.10 版本相同,假定不想调用 inspect.get_annotations():你应该用三个参数调用 getattr() ,以访问对象的 __annotations__ 属性。

不幸的是,对于类而言,这并不是最佳做法。因为 ``annotations是类的可选属性,并且类可以从基类继承属性,访问某个类的annotations` 属性可能会无意间返回 基类 的注解数据。

class Base:
    a: int = 3
    b: str = 'abc'
class Derived(Base):
    pass
print(Derived.__annotations__)

如此会打印出 Base 的注解字典,而非 Derived 的。

若要查看的对象是个类(isinstance(o, type)),代码不得不另辟蹊径。这时的最佳做法依赖于 Python 3.9 以下版本的一处细节:若某个类定义了注解,则会存放于字典 __dict__ 中。由于类不一定会定义注解,最好的做法是在类的 dict 上调用 get 方法。

综上所述,下面给出一些示例代码,可以安全地访问 Python 3.9 以下任意对象的 __annotations__ 属性:

if isinstance(o, type):
    ann = o.__dict__.get('__annotations__', None)
else:
    ann = getattr(o, '__annotations__', None)

运行之后,ann 应为一个字典对象或 None。建议在继续之前,先用 isinstance() 再次检查 ann 的类型。

请注意,有些特殊的或畸形的类型对象可能没有 __dict__ 属性,为了以防万一,可能还需要用 getattr() 来访问dict`。

解析字符串形式的注解

有时注释可能会被“字符串化”,解析这些字符串可以求得其所代表的 Python 值,最好是调用 inspect.get_annotations() 来完成这项工作。

如果是 Python 3.9 之前的版本,或者由于某种原因无法使用 inspect.get_annotations() ,那就需要重现其代码逻辑。建议查看一下当前 Python 版本中 inspect.get_annotations() 的实现代码,并遵照实现。

简而言之,假设要对任一对象解析其字符串化的注释 o

  • 如果 o 是个模块,在调用 eval() 时,o.__dict__ 可视为 globals
  • 如果 o 是一个类,在调用 eval() 时,sys.modules[o.__module__].__dict__ 视作 globalsdict(vars(o)) 视作 locals
  • 如果 o 是一个用 functools.update_wrapper()functools.wraps()functools.partial() 封装的可调用对象,可酌情访问 o.__wrapped__o.func 进行反复解包,直到你找到未经封装的根函数。
  • 如果 o 是个可调用对象(但不是一个类),在调用 eval() 时,o.__dict__ 可视为 globals

但并不是所有注解字符串都可以通过 eval() 成功地转化为 Python 值。理论上,注解字符串中可以包含任何合法字符串,确实有一些类型提示的场合,需要用到特殊的 无法 被解析的字符串来作注解。比如:

  • 在从 Python 3.10 加入支持之前,PEP 604 联合类型用到了 | 。
  • 运行时用不到的定义,只在 typing.TYPE_CHECKING 为 True 时才会导入。

如果 eval() 试图求值,将会失败并触发异常。因此,当要设计一个可采用注解的库 API ,建议只在调用方显式请求的时才对字符串求值。

任何版本 Python 中使用 __annotations__ 的最佳实践

  • 应避免直接给对象的 __annotations__ 成员赋值。请让 Python 来管理__annotations__
  • 如果直接给某对象的 __annotations__ 成员赋值,应该确保设成一个dict 对象。
  • 如果直接访问某个对象的 __annotations__ 成员,在解析其值之前,应先确认其为字典类型。
  • 应避免修改 __annotations__ 字典。
  • 应避免删除对象的 __annotations__ 属性。

__annotations__ 的坑

在 Python 3 的所有版本中,如果对象没有定义注解,函数对象就会直接创建一个注解字典对象。用 del fn.__annotations__ 可删除 __annotations__ 属性,但如果后续再访问 fn.__annotations__,该对象将新建一个空的字典对象,用于存放并返回注解。在函数直接创建注解字典前,删除注解操作会抛出 AttributeError 异常;连续两次调用 del fn.__annotations__ 一定会抛出一次 AttributeError 异常。

以上同样适用于 Python 3.10 以上版本中的类和模块对象。

所有版本的 Python 3 中,均可将函数对象的 __annotations__ 设为 None。但后续用 fn.__annotations__ 访问该对象的注解时,会像本节第一段所述那样,直接创建一个空字典。但在任何 Python 版本中,模块和类均非如此,他们允许将 __annotations__ 设为任意 Python 值,并且会留存所设值。

如果 Python 会对注解作字符串化处理(用 from __future__ import annotations ),并且注解本身就是一个字符串,那么将会为其加上引号。实际效果就是,注解加了 两次 引号。例如:

from __future__ import annotationsdef foo(a: "str"): passprint(foo.__annotations__)

这会打印出 {'a': "'str'"}。这不应算是个“坑”;只是因为可能会让人吃惊,所以才提一下。

Python 常见问题

为什么Python使用缩进来分组语句?

Guido van Rossum 认为使用缩进进行分组非常优雅,并且大大提高了普通Python程序的清晰度。大多数人在一段时间后就学会并喜欢上这个功能。

由于没有开始/结束括号,因此解析器感知的分组与人类读者之间不会存在分歧。偶尔C程序员会遇到像这样的代码片段:

if (x <= y)
        x++;
        y--;
z++;

如果条件为真,则只执行 x++ 语句,但缩进会使你认为情况并非如此。 即使是经验丰富的 C 程序员有时也会长久地盯着它发呆,不明白为什么在 x > yy 也会减少。

因为没有开始/结束花括号,所以 Python 更不容易发生编码风格冲突。 在 C 中有许多不同的放置花括号的方式。 在习惯了阅读和编写某种特定风格的代码之后,当阅读(或被要求编写)另一种风格的代码时通常都会令人感觉有点不舒服)。

许多编码风格将开始/结束括号单独放在一行上。这使得程序相当长,浪费了宝贵的屏幕空间,使得更难以对程序进行全面的了解。理想情况下,函数应该适合一个屏幕(例如,20—30行)。 20行Python可以完成比20行C更多的工作。这不仅仅是由于缺少开始/结束括号 — 缺少声明和高级数据类型也是其中的原因 — 但缩进基于语法肯定有帮助。

为什么浮点计算不准确?

用户经常对这样的结果感到惊讶:

>>> 1.2 - 1.00.19999999999999996

并且认为这是 Python中的一个 bug。其实不是这样。这与 Python 关系不大,而与底层平台如何处理浮点数字关系更大。

CPython 中的 float 类型使用C语言的 double 类型进行存储。 float 对象的值是以固定的精度(通常为 53 位)存储的二进制浮点数,由于 Python 使用 C 操作,而后者依赖于处理器中的硬件实现来执行浮点运算。 这意味着就浮点运算而言,Python 的行为类似于许多流行的语言,包括 C 和 Java。

许多可以轻松地用十进制表示的数字不能用二进制浮点表示。例如,在输入以下语句后:

>>> x = 1.2

x 存储的值是与十进制的值 1.2 (非常接近) 的近似值,但不完全等于它。 在典型的机器上,实际存储的值是:

1.0011001100110011001100110011001100110011001100110011 (binary)

它对应于十进制数值:

1.1999999999999999555910790149937383830547332763671875 (decimal)

典型的 53 位精度为 Python 浮点数提供了 15-16 位小数的精度。

为什么Python字符串是不可变的?

有几个优点。

一个是性能:知道字符串是不可变的,意味着我们可以在创建时为它分配空间,并且存储需求是固定不变的。这也是元组和列表之间区别的原因之一。

另一个优点是,Python 中的字符串被视为与数字一样“基本”。 任何动作都不会将值 8 更改为其他值,在 Python 中,任何动作都不会将字符串 “8” 更改为其他值。

为什么必须在方法定义和调用中显式使用“self”?

这个想法借鉴了 Modula-3 语言。 出于多种原因它被证明是非常有用的。

首先,更明显的显示出,使用的是方法或实例属性而不是局部变量。 阅读 self.xself.meth() 可以清楚地表明,即使您不知道类的定义,也会使用实例变量或方法。在 C++ 中,可以通过缺少局部变量声明来判断(假设全局变量很少见或容易识别) —— 但是在 Python 中没有局部变量声明,所以必须查找类定义才能确定。 一些 C++ 和 Java 编码标准要求实例属性具有 m_ 前缀,因此这种显式性在这些语言中仍然有用。

其次,这意味着如果要显式引用或从特定类调用该方法,不需要特殊语法。 在 C++ 中,如果你想使用在派生类中重写基类中的方法,你必须使用 :: 运算符 — 在 Python 中你可以编写 baseclass.methodname(self, <argument list>)。 这对于 __init__() 方法非常有用,特别是在派生类方法想要扩展同名的基类方法,而必须以某种方式调用基类方法时。

最后,它解决了变量赋值的语法问题:为了 Python 中的局部变量(根据定义!)在函数体中赋值的那些变量(并且没有明确声明为全局)赋值,就必须以某种方式告诉解释器一个赋值是为了分配一个实例变量而不是一个局部变量,它最好是通过语法实现的(出于效率原因)。 C++ 通过声明来做到这一点,但是 Python 没有声明,仅仅为了这个目的而引入它们会很可惜。 使用显式的 self.var 很好地解决了这个问题。 类似地,对于使用实例变量,必须编写 self.var 意味着对方法内部的非限定名称的引用不必搜索实例的目录。 换句话说,局部变量和实例变量存在于两个不同的命名空间中,您需要告诉 Python 使用哪个命名空间。

为什么不能在表达式中赋值?

自 Python 3.8 开始,你能做到的!

赋值表达式使用海象运算符 := 在表达式中为变量赋值:

while chunk := fp.read(200):
   print(chunk)

请参阅 PEP 572 了解详情。

为什么Python对某些功能(例如list.index())使用方法来实现,而其他功能(例如len(List))使用函数实现?

正如Guido所说:

(a) 对于某些操作,前缀表示法比后缀更容易阅读 — 前缀(和中缀!)运算在数学中有着悠久的传统,就像在视觉上帮助数学家思考问题的记法。比较一下我们将 x(a+b) 这样的公式改写为 xa+x*b 的容易程度,以及使用原始OO符号做相同事情的笨拙程度。

(b) 当读到写有len(X)的代码时,就知道它要求的是某件东西的长度。这告诉我们两件事:结果是一个整数,参数是某种容器。相反,当阅读x.len()时,必须已经知道x是某种实现接口的容器,或者是从具有标准len()的类继承的容器。当没有实现映射的类有get()或key()方法,或者不是文件的类有write()方法时,我们偶尔会感到困惑。

为什么 join() 是一个字符串方法而不是列表或元组方法?

从 Python 1.6 开始,字符串变得更像其他标准类型,当添加方法时,这些方法提供的功能与始终使用 String 模块的函数时提供的功能相同。这些新方法中的大多数已被广泛接受,但似乎让一些程序员感到不舒服的一种方法是:

", ".join(['1', '2', '4', '8', '16'])

结果如下:

"1, 2, 4, 8, 16"

反对这种用法有两个常见的论点。

第一条是这样的:“使用字符串文本(String Constant)的方法看起来真的很难看”,答案是也许吧,但是字符串文本只是一个固定值。如果在绑定到字符串的名称上允许使用这些方法,则没有逻辑上的理由使其在文字上不可用。

第二个异议通常是这样的:“我实际上是在告诉序列使用字符串常量将其成员连接在一起”。遗憾的是并非如此。出于某种原因,把 split() 作为一个字符串方法似乎要容易得多,因为在这种情况下,很容易看到:

"1, 2, 4, 8, 16".split(", ")

是对字符串文本的指令,用于返回由给定分隔符分隔的子字符串(或在默认情况下,返回任意空格)。

join() 是字符串方法,因为在使用该方法时,您告诉分隔符字符串去迭代一个字符串序列,并在相邻元素之间插入自身。此方法的参数可以是任何遵循序列规则的对象,包括您自己定义的任何新的类。对于字节和字节数组对象也有类似的方法。

异常有多快?

如果没有引发异常,则try/except块的效率极高。实际上捕获异常是昂贵的。在2.0之前的Python版本中,通常使用这个习惯用法:

try:
    value = mydict[key]
except KeyError:
    mydict[key] = getvalue(key)
    value = mydict[key]

只有当你期望dict在任何时候都有key时,这才有意义。如果不是这样的话,你就是应该这样编码:

if key in mydict:
    value = mydict[key]
else:
    value = mydict[key] = getvalue(key)

对于这种特定的情况,您还可以使用 value = dict.setdefault(key, getvalue(key)),但前提是调用 getvalue() 足够便宜,因为在所有情况下都会对其进行评估。

为什么Python中没有switch或case语句?

你可以足够方便地使用一串 if... elif... elif... else 来做到这一点。 对于字面值或某一命名空间内的常量,你还可以 使用 match ... case 语句。

对于需要从大量可能性中进行选择的情况,可以创建一个字典,将case 值映射到要调用的函数。例如:

def function_1(...):
    ...
functions = {'a': function_1,
             'b': function_2,
             'c': self.method_1, ...}
func = functions[value]
func()

对于对象调用方法,可以通过使用 getattr() 内置检索具有特定名称的方法来进一步简化:

def visit_a(self, ...):
    ...
...
def dispatch(self, value):
    method_name = 'visit_' + str(value)
    method = getattr(self, method_name)
    method()

建议对方法名使用前缀,例如本例中的 visit_ 。如果没有这样的前缀,如果值来自不受信任的源,攻击者将能够调用对象上的任何方法。

难道不能在解释器中模拟线程,而非得依赖特定于操作系统的线程实现吗?

答案1: 不幸的是,解释器为每个Python堆栈帧推送至少一个C堆栈帧。此外,扩展可以随时回调Python。因此,一个完整的线程实现需要对C的线程支持。

答案2: 幸运的是, Stackless Python 有一个完全重新设计的解释器循环,可以避免C堆栈。

为什么lambda表达式不能包含语句?

Python的 lambda表达式不能包含语句,因为Python的语法框架不能处理嵌套在表达式内部的语句。然而,在Python中,这并不是一个严重的问题。与其他语言中添加功能的lambda表单不同,Python的 lambdas只是一种速记符号,如果您懒得定义函数的话。

函数已经是Python中的第一类对象,可以在本地范围内声明。 因此,使用lambda而不是本地定义的函数的唯一优点是你不需要为函数创建一个名称 — 这只是一个分配了函数对象(与lambda表达式生成的对象类型完全相同)的局部变量!

可以将Python编译为机器代码,C或其他语言吗?

Cython 将带有可选注释的Python修改版本编译到C扩展中。 Nuitka 是一个将Python编译成 C++ 代码的新兴编译器,旨在支持完整的Python语言。要编译成Java,可以考虑 VOC

Python如何管理内存?

Python 内存管理的细节取决于实现。 Python 的标准实现 CPython 使用引用计数来检测不可访问的对象,并使用另一种机制来收集引用循环,定期执行循环检测算法来查找不可访问的循环并删除所涉及的对象。 gc 模块提供了执行垃圾回收、获取调试统计信息和优化收集器参数的函数。

但是,其他实现(如 JythonPyPy ),)可以依赖不同的机制,如完全的垃圾回收器 。如果你的Python代码依赖于引用计数实现的行为,则这种差异可能会导致一些微妙的移植问题。

在一些Python实现中,以下代码(在CPython中工作的很好)可能会耗尽文件描述符:

for file in very_long_list_of_files:
    f = open(file)
    c = f.read(1)

实际上,使用CPython的引用计数和析构函数方案, 每个新赋值的 f 都会关闭前一个文件。然而,对于传统的GC,这些文件对象只能以不同的时间间隔(可能很长的时间间隔)被收集(和关闭)。

如果要编写可用于任何python实现的代码,则应显式关闭该文件或使用 with 语句;无论内存管理方案如何,这都有效:

for file in very_long_list_of_files:
    with open(file) as f:
        c = f.read(1)

为什么CPython不使用更传统的垃圾回收方案?

首先,这不是C标准特性,因此不能移植。(是的,我们知道Boehm GC库。它包含了 大多数 常见平台(但不是所有平台)的汇编代码,尽管它基本上是透明的,但也不是完全透明的; 要让Python使用它,需要使用补丁。)

当Python嵌入到其他应用程序中时,传统的GC也成为一个问题。在独立的Python中,可以用GC库提供的版本替换标准的malloc()和free(),嵌入Python的应用程序可能希望用 它自己 替代malloc()和free(),而可能不需要Python的。现在,CPython可以正确地实现malloc()和free()。

CPython退出时为什么不释放所有内存?

当Python退出时,从全局命名空间或Python模块引用的对象并不总是被释放。 如果存在循环引用,则可能发生这种情况 C库分配的某些内存也是不可能释放的(例如像Purify这样的工具会抱怨这些内容)。 但是,Python在退出时清理内存并尝试销毁每个对象。

如果要强制 Python 在释放时删除某些内容,请使用 atexit 模块运行一个函数,强制删除这些内容。

为什么有单独的元组和列表数据类型?

虽然列表和元组在许多方面是相似的,但它们的使用方式通常是完全不同的。可以认为元组类似于Pascal记录或C结构;它们是相关数据的小集合,可以是不同类型的数据,可以作为一个组进行操作。例如,笛卡尔坐标适当地表示为两个或三个数字的元组。

另一方面,列表更像其他语言中的数组。它们倾向于持有不同数量的对象,所有对象都具有相同的类型,并且逐个操作。例如, os.listdir('.') 返回表示当前目录中的文件的字符串列表。如果向目录中添加了一两个文件,对此输出进行操作的函数通常不会中断。

元组是不可变的,这意味着一旦创建了元组,就不能用新值替换它的任何元素。列表是可变的,这意味着您始终可以更改列表的元素。只有不变元素可以用作字典的key,因此只能将元组和非列表用作key。

列表是如何在CPython中实现的?

CPython的列表实际上是可变长度的数组,而不是lisp风格的链表。该实现使用对其他对象的引用的连续数组,并在列表头结构中保留指向该数组和数组长度的指针。

这使得索引列表 a[i] 的操作成本与列表的大小或索引的值无关。

当添加或插入项时,将调整引用数组的大小。并采用了一些巧妙的方法来提高重复添加项的性能; 当数组必须增长时,会分配一些额外的空间,以便在接下来的几次中不需要实际调整大小。

字典是如何在CPython中实现的?

CPython的字典实现为可调整大小的哈希表。与B-树相比,这在大多数情况下为查找(目前最常见的操作)提供了更好的性能,并且实现更简单。

字典的工作方式是使用 hash() 内置函数计算字典中存储的每个键的hash代码。hash代码根据键和每个进程的种子而变化很大;例如,”Python” 的hash值为-539294296,而”python”(一个按位不同的字符串)的hash值为1142331976。然后,hash代码用于计算内部数组中将存储该值的位置。假设您存储的键都具有不同的hash值,这意味着字典需要恒定的时间 — O(1),用Big-O表示法 — 来检索一个键。

为什么字典key必须是不可变的?

字典的哈希表实现使用从键值计算的哈希值来查找键。如果键是可变对象,则其值可能会发生变化,因此其哈希值也会发生变化。但是,由于无论谁更改键对象都无法判断它是否被用作字典键值,因此无法在字典中修改条目。然后,当你尝试在字典中查找相同的对象时,将无法找到它,因为其哈希值不同。如果你尝试查找旧值,也不会找到它,因为在该哈希表中找到的对象的值会有所不同。

如果你想要一个用列表索引的字典,只需先将列表转换为元组;用函数 tuple(L) 创建一个元组,其条目与列表 L 相同。 元组是不可变的,因此可以用作字典键。

已经提出的一些不可接受的解决方案:

  • 哈希按其地址(对象ID)列出。这不起作用,因为如果你构造一个具有相同值的新列表,它将无法找到;例如:

    mydict = {[1, 2]: '12'}
    print(mydict[[1, 2]])

    会引发一个 KeyError 异常,因为第二行中使用的 [1, 2] 的 id 与第一行中的 id 不同。换句话说,应该使用 == 来比较字典键,而不是使用 is

  • 使用列表作为键时进行复制。这没有用的,因为作为可变对象的列表可以包含对自身的引用,然后复制代码将进入无限循环。

  • 允许列表作为键,但告诉用户不要修改它们。当你意外忘记或修改列表时,这将产生程序中的一类难以跟踪的错误。它还使一个重要的字典不变量无效: d.keys() 中的每个值都可用作字典的键。

  • 将列表用作字典键后,应标记为其只读。问题是,它不仅仅是可以改变其值的顶级对象;你可以使用包含列表作为键的元组。将任何内容作为键关联到字典中都需要将从那里可到达的所有对象标记为只读 —— 并且自引用对象可能会导致无限循环。

如果需要,可以使用以下方法来解决这个问题,但使用它需要你自担风险:你可以将一个可变结构包装在一个类实例中,该实例同时具有 __eq__()__hash__() 方法。然后,你必须确保驻留在字典(或其他基于 hash 的结构)中的所有此类包装器对象的哈希值在对象位于字典(或其他结构)中时保持固定。:

class ListWrapper:
    def __init__(self, the_list):
        self.the_list = the_list
    def __eq__(self, other):
        return self.the_list == other.the_list
    def __hash__(self):
        l = self.the_list
        result = 98767 - len(l)*555
        for i, el in enumerate(l):
            try:
                result = result + (hash(el) % 9999999) * 1001 + i
            except Exception:
                result = (result % 7777777) + i * 333
        return result

注意,哈希计算由于列表的某些成员可能不可用以及算术溢出的可能性而变得复杂。

此外,必须始终如此,如果 o1 == o2 (即 o1.__eq__(o2) is True )则 hash(o1) == hash(o2)``(即 ``o1.__hash__() == o2.__hash__() ),无论对象是否在字典中。 如果你不能满足这些限制,字典和其他基于 hash 的结构将会出错。

对于 ListWrapper ,只要包装器对象在字典中,包装列表就不能更改以避免异常。除非你准备好认真考虑需求以及不正确地满足这些需求的后果,否则不要这样做。请留意。

为什么 list.sort() 没有返回排序列表?

在性能很重要的情况下,仅仅为了排序而复制一份列表将是一种浪费。因此, list.sort() 对列表进行了适当的排序。为了提醒您这一事实,它不会返回已排序的列表。这样,当您需要排序的副本,但也需要保留未排序的版本时,就不会意外地覆盖列表。

如果要返回新列表,请使用内置 sorted() 函数。此函数从提供的可迭代列表中创建新列表,对其进行排序并返回。例如,下面是如何迭代遍历字典并按keys排序:

for key in sorted(mydict):
    ...  # do whatever with mydict[key]...

如何在Python中指定和实施接口规范?

由C++和Java等语言提供的模块接口规范描述了模块的方法和函数的原型。许多人认为接口规范的编译时强制执行有助于构建大型程序。

Python 2.6添加了一个 abc 模块,允许定义抽象基类 (ABCs)。然后可以使用 isinstance()issubclass() 来检查实例或类是否实现了特定的ABC。 collections.abc 模块定义了一组有用的ABCs 例如 IterableContainer , 和 MutableMapping

对于 Python,接口规范的许多好处可以通过组件的适当测试规程来获得。

一个好的模块测试套件既可以提供回归测试,也可以作为模块接口规范和一组示例。许多Python模块可以作为脚本运行,以提供简单的“自我测试”。即使是使用复杂外部接口的模块,也常常可以使用外部接口的简单“桩代码(stub)”模拟进行隔离测试。可以使用 doctestunittest 模块或第三方测试框架来构造详尽的测试套件,以运行模块中的每一行代码。

适当的测试规程可以帮助在Python中构建大型的、复杂的应用程序以及接口规范。事实上,它可能会更好,因为接口规范不能测试程序的某些属性。例如, append() 方法将向一些内部列表的末尾添加新元素;接口规范不能测试您的 append() 实现是否能够正确执行此操作,但是在测试套件中检查这个属性是很简单的。

编写测试套件非常有用,并且你可能希望将你的代码设计为易于测试。 一种日益流行的技术是面向测试的开发,它要求在编写任何实际代码之前首先编写测试套件的各个部分。 当然 Python 也允许你采用更粗率的方式,不必编写任何测试用例。

为什么没有goto?

在 1970 年代人们了解到不受限制的 goto 可能导致混乱得像“意大利面”那样难以理解和修改的代码。 在高级语言中,它也是不必要的,只需有实现分支 (在 Python 中是使用 if 语句以及 or, andif-else 表达式) 和循环 (使用 whilefor 语句,并可能包含 continuebreak) 的手段就足够了。

人们还可以使用异常捕获来提供甚至能跨函数调用的“结构化 goto”。 许多人认为异常可以方便地模拟 C, Fortran 和其他语言中所有合理使用的“go”或“goto”构造。 例如:

class label(Exception): pass  # declare a label
try:
    ...
    if condition: raise label()  # goto label
    ...
except label:  # where to goto
    pass
...

但是不允许你跳到循环的中间,这通常被认为是滥用goto。谨慎使用。

为什么原始字符串(r-strings)不能以反斜杠结尾?

更准确地说,它们不能以奇数个反斜杠结束:结尾处的不成对反斜杠会转义结束引号字符,留下未结束的字符串。

原始字符串的设计是为了方便想要执行自己的反斜杠转义处理的处理器(主要是正则表达式引擎)创建输入。此类处理器将不匹配的尾随反斜杠视为错误,因此原始字符串不允许这样做。反过来,允许通过使用引号字符转义反斜杠转义字符串。当r-string用于它们的预期目的时,这些规则工作的很好。

如果您正在尝试构建Windows路径名,请注意所有Windows系统调用都使用正斜杠:

f = open("/mydir/file.txt")  # works fine!

如果您正在尝试为DOS命令构建路径名,请尝试以下示例

dir = r"\this\is\my\dos\dir" "\\"
dir = r"\this\is\my\dos\dir\ "[:-1]
dir = "\\this\\is\\my\\dos\\dir\\"

为什么Python没有属性赋值的“with”语句?

Python 具有 ‘with’ 语句,它能将一个代码块的执行包装起来,在进入和退出代码块时调用特定的代码。 有些语言具有这样的结构:

with obj:
    a = 1               # equivalent to obj.a = 1
    total = total + 1   # obj.total = obj.total + 1

在Python中,这样的结构是不明确的。

其他语言,如ObjectPascal、Delphi和C++ 使用静态类型,因此可以毫不含糊地知道分配给什么成员。这是静态类型的要点 — 编译器 总是 在编译时知道每个变量的作用域。

Python使用动态类型。事先不可能知道在运行时引用哪个属性。可以动态地在对象中添加或删除成员属性。这使得无法通过简单的阅读就知道引用的是什么属性:局部属性、全局属性还是成员属性?

例如,采用以下不完整的代码段:

def foo(a):
    with a:
        print(x)

该代码段假设 “a” 必须有一个名为 “x” 的成员属性。然而,Python中并没有告诉解释器这一点。假设 “a” 是整数,会发生什么?如果有一个名为 “x” 的全局变量,它是否会在with块中使用?如您所见,Python的动态特性使得这样的选择更加困难。

然而,Python 可以通过赋值轻松实现 “with” 和类似语言特性(减少代码量)的主要好处。代替:

function(args).mydict[index][index].a = 21
function(args).mydict[index][index].b = 42
function(args).mydict[index][index].c = 63

写成这样:

ref = function(args).mydict[index][index]
ref.a = 21
ref.b = 42
ref.c = 63

这也具有提高执行速度的副作用,因为Python在运行时解析名称绑定,而第二个版本只需要执行一次解析。

生成器为什么不支持 with 语句?

由于技术原因,直接作为上下文管理器使用的生成器将不能正确工作。 最常见的情况是,当生成器用作迭代器时会一直运行到结束,而不需要手动关闭。 确实需要作为上下文管理器时,请在 ‘with’ 语句中将它包装于 “contextlib.closing(generator)” 中。

为什么 if/while/def/class语句需要冒号?

冒号主要用于增强可读性(ABC语言实验的结果之一)。考虑一下这个:

if a == b    print(a)

if a == b:    print(a)

注意第二种方法稍微容易一些。请进一步注意,在这个FAQ解答的示例中,冒号是如何设置的;这是英语中的标准用法。

另一个次要原因是冒号使带有语法突出显示的编辑器更容易工作;他们可以寻找冒号来决定何时需要增加缩进,而不必对程序文本进行更精细的解析。

为什么Python在列表和元组的末尾允许使用逗号?

Python 允许您在列表,元组和字典的末尾添加一个尾随逗号:

[1, 2, 3,]
('a', 'b', 'c',)
d = {
    "A": [1, 5],
    "B": [6, 7],  # last trailing comma is optional but good style
}

有几个理由允许这样做。

如果列表,元组或字典的字面值分布在多行中,则更容易添加更多元素,因为不必记住在上一行中添加逗号。这些行也可以重新排序,而不会产生语法错误。

不小心省略逗号会导致难以诊断的错误。例如:

x = [
  "fee",
  "fie"
  "foo",
  "fum"
]

这个列表看起来有四个元素,但实际上包含三个 : “fee”, “fiefoo” 和 “fum” 。总是加上逗号可以避免这个错误的来源。

允许尾随逗号也可以使编程代码更容易生成.

术语对照表

>>>

交互式终端中默认的 Python 提示符。往往会显示于能以交互方式在解释器里执行的样例代码之前。

...

具有以下含义:

  • 交互式终端中输入特殊代码行时默认的 Python 提示符,包括:缩进的代码块,成对的分隔符之内(圆括号、方括号、花括号或三重引号),或是指定一个装饰器之后。
  • Ellipsis 内置常量。

2to3

把 Python 2.x 代码转换为 Python 3.x 代码的工具,通过解析源码,遍历解析树,处理绝大多数检测到的不兼容问题。

2to3 包含在标准库中,模块名为 lib2to3;提供了独立入口点 Tools/scripts/2to3

abstract base class — 抽象基类

抽象基类简称 ABC,是对 duck-typing 的补充,它提供了一种定义接口的新方式,相比之下其他技巧例如 hasattr() 显得过于笨拙或有微妙错误(例如使用 魔术方法)。ABC 引入了虚拟子类,这种类并非继承自其他类,但却仍能被 isinstance()issubclass() 所认可;详见 abc 模块文档。Python 自带许多内置的 ABC 用于实现数据结构(在 collections.abc 模块中)、数字(在 numbers 模块中)、流(在 io 模块中)、导入查找器和加载器(在 importlib.abc 模块中)。你可以使用 abc 模块来创建自己的 ABC。

annotation — 标注

关联到某个变量、类属性、函数形参或返回值的标签,被约定作为 type hint 来使用。

局部变量的标注在运行时不可访问,但全局变量、类属性和函数的标注会分别存放模块、类和函数的 __annotations__ 特殊属性中。

参见 variable annotation, function annotation, PEP 484PEP 526,对此功能均有介绍。

argument — 参数

在调用函数时传给 function (或 method )的值。参数分为两种:

  • 关键字参数: 在函数调用中前面带有标识符(例如 name=)或者作为包含在前面带有 ** 的字典里的值传入。举例来说,35 在以下对 complex() 的调用中均属于关键字参数:

    complex(real=3, imag=5)
    complex(**{'real': 3, 'imag': 5})
  • 位置参数: 不属于关键字参数的参数。位置参数可出现于参数列表的开头以及/或者作为前面带有 * 的 iterable 里的元素被传入。举例来说,35 在以下调用中均属于位置参数:

    complex(3, 5)
    complex(*(3, 5))

参数会被赋值给函数体中对应的局部变量。根据语法,任何表达式都可用来表示一个参数;最终算出的值会被赋给对应的局部变量。

asynchronous context manager — 异步上下文管理器

此种对象通过定义 __aenter__()__aexit__() 方法来对 async with 语句中的环境进行控制。由 PEP 492 引入。

asynchronous generator — 异步生成器

返回值为 asynchronous generator iterator 的函数。它与使用 async def 定义的协程函数很相似,不同之处在于它包含 yield 表达式以产生一系列可在 async for 循环中使用的值。

此术语通常是指异步生成器函数,但在某些情况下则可能是指 异步生成器迭代器。如果需要清楚表达具体含义,请使用全称以避免歧义。

一个异步生成器函数可能包含 await 表达式或者 async for 以及 async with 语句。

asynchronous generator iterator — 异步生成器迭代器

asynchronous generator 函数所创建的对象。

此对象属于 asynchronous iterator,当使用 __anext__() 方法调用时会返回一个可等待对象来执行异步生成器函数的代码直到下一个 yield 表达式。

每个 yield 会临时暂停处理,记住当前位置执行状态 (包括局部变量和挂起的 try 语句)。当该 异步生成器迭代器 与其他 __anext__() 返回的可等待对象有效恢复时,它会从离开位置继续执行。参见 PEP 492PEP 525

asynchronous iterable — 异步可迭代对象

可在 async for 语句中被使用的对象。必须通过它的 __aiter__() 方法返回一个 asynchronous iterator。由 PEP 492 引入。

asynchronous iterator — 异步迭代器

实现了 __aiter__()__anext__() 方法的对象。__anext__ 必须返回一个 awaitable 对象。async for 会处理异步迭代器的 __anext__() 方法所返回的可等待对象,直到其引发一个 StopAsyncIteration 异常。由 PEP 492 引入。

attribute — 属性

关联到一个对象的值,可以使用点号表达式通过其名称来引用。例如,如果一个对象 o 具有一个属性 a,就可以用 o.a 来引用它。

awaitable — 可等待对象

能在 await 表达式中使用的对象。可以是 coroutine 或是具有 __await__() 方法的对象。参见 PEP 492

BDFL

“终身仁慈独裁者”的英文缩写,即 Guido van Rossum,Python 的创造者。

binary file — 二进制文件

file object 能够读写 字节类对象。二进制文件的例子包括以二进制模式('rb', 'wb' or 'rb+')打开的文件、sys.stdin.buffersys.stdout.buffer 以及 io.BytesIOgzip.GzipFile 的实例。

另请参见 text file 了解能够读写 str 对象的文件对象。

borrowed reference — 借入引用

在 Python 的 C API 中,借入引用是指一种对象引用。 它不会修改对象引用计数。 如果对象被销毁则它会成为一个无目标指针。 例如,垃圾回收器可以移除对象的最后一个 strong reference 来销毁它。

推荐在 borrowed reference 上调用 Py_INCREF() 以将其原地转换为 strong reference,除非是当该对象无法在借入引用的最后一次使用之前被销毁。 Py_NewRef() 函数可以被用来创建一个新的 strong reference。

bytes-like object — 字节类对象

支持 缓冲协议 并且能导出 C-contiguous 缓冲的对象。这包括所有 bytesbytearrayarray.array 对象,以及许多普通 memoryview 对象。字节类对象可在多种二进制数据操作中使用;这些操作包括压缩、保存为二进制文件以及通过套接字发送等。

某些操作需要可变的二进制数据。这种对象在文档中常被称为“可读写字节类对象”。可变缓冲对象的例子包括 bytearray 以及 bytearraymemoryview。其他操作要求二进制数据存放于不可变对象 (“只读字节类对象”);这种对象的例子包括 bytes 以及 bytes 对象的 memoryview

bytecode — 字节码

Python 源代码会被编译为字节码,即 CPython 解释器中表示 Python 程序的内部代码。字节码还会缓存在 .pyc 文件中,这样第二次执行同一文件时速度更快(可以免去将源码重新编译为字节码)。这种 “中间语言” 运行在根据字节码执行相应机器码的 virtual machine 之上。请注意不同 Python 虚拟机上的字节码不一定通用,也不一定能在不同 Python 版本上兼容。

callback — 回调

一个作为参数被传入以用以在未来的某个时刻被调用的子例程函数。

class — 类

用来创建用户定义对象的模板。类定义通常包含对该类的实例进行操作的方法定义。

class variable — 类变量

在类中定义的变量,并且仅限在类的层级上修改 (而不是在类的实例中修改)。

coercion — 强制类型转换

在包含两个相同类型参数的操作中,一种类型的实例隐式地转换为另一种类型。例如,int(3.15) 是将原浮点数转换为整型数 3,但在 3+4.5 中,参数的类型不一致(一个是 int, 一个是 float),两者必须转换为相同类型才能相加,否则将引发 TypeError。如果没有强制类型转换机制,程序员必须将所有可兼容参数归一化为相同类型,例如要写成 float(3)+4.5 而不是 3+4.5

complex number — 复数

对普通实数系统的扩展,其中所有数字都被表示为一个实部和一个虚部的和。虚数是虚数单位(-1 的平方根)的实倍数,通常在数学中写为 i,在工程学中写为 j。Python 内置了对复数的支持,采用工程学标记方式;虚部带有一个 j 后缀,例如 3+1j。如果需要 math 模块内对象的对应复数版本,请使用 cmath,复数的使用是一个比较高级的数学特性。如果你感觉没有必要,忽略它们也几乎不会有任何问题。

context manager — 上下文管理器

with 语句中使用,通过定义 __enter__()__exit__() 方法来控制环境状态的对象。参见 PEP 343

context variable — 上下文变量

一种根据其所属的上下文可以具有不同的值的变量。 这类似于在线程局部存储中每个执行线程可以具有不同的变量值。 不过,对于上下文变量来说,一个执行线程中可能会有多个上下文,而上下文变量的主要用途是对并发异步任务中变量进行追踪。 参见 contextvars

contiguous — 连续

一个缓冲如果是 C 连续Fortran 连续 就会被认为是连续的。零维缓冲是 C 和 Fortran 连续的。在一维数组中,所有条目必须在内存中彼此相邻地排列,采用从零开始的递增索引顺序。在多维 C-连续数组中,当按内存地址排列时用最后一个索引访问条目时速度最快。但是在 Fortran 连续数组中则是用第一个索引最快。

coroutine — 协程

协程是子例程的更一般形式。 子例程可以在某一点进入并在另一点退出。 协程则可以在许多不同的点上进入、退出和恢复。 它们可通过 async def 语句来实现。 参见 PEP 492

coroutine function — 协程函数

返回一个 coroutine 对象的函数。协程函数可通过 async def 语句来定义,并可能包含 awaitasync forasync with 关键字。这些特性是由 PEP 492 引入的。

CPython

Python 编程语言的规范实现,在 python.org 上发布。”CPython” 一词用于在必要时将此实现与其他实现例如 Jython 或 IronPython 相区别。

decorator — 装饰器

返回值为另一个函数的函数,通常使用 @wrapper 语法形式来进行函数变换。 装饰器的常见例子包括 classmethod()staticmethod()

装饰器语法只是一种语法糖,以下两个函数定义在语义上完全等价:

def f(...):
    ...
f = staticmethod(f)
@staticmethod
def f(...):
    ...

同的样概念也适用于类,但通常较少这样使用。

descriptor — 描述器

任何定义了 __get__(), __set__()__delete__() 方法的对象。当一个类属性为描述器时,它的特殊绑定行为就会在属性查找时被触发。通常情况下,使用 a.b 来获取、设置或删除一个属性时会在 a 的类字典中查找名称为 b 的对象,但如果 b 是一个描述器,则会调用对应的描述器方法。理解描述器的概念是更深层次理解 Python 的关键,因为这是许多重要特性的基础,包括函数、方法、属性、类方法、静态方法以及对超类的引用等等。

dictionary — 字典

一个关联数组,其中的任意键都映射到相应的值。键可以是任何具有 __hash__()__eq__() 方法的对象。在 Perl 语言中称为 hash。

dictionary comprehension — 字典推导式

处理一个可迭代对象中的所有或部分元素并返回结果字典的一种紧凑写法。 results = {n: n ** 2 for n in range(10)} 将生成一个由键 n 到值 n ** 2 的映射构成的字典。

dict.keys(), dict.values()dict.items() 返回的对象被称为字典视图。它们提供了字典条目的一个动态视图,这意味着当字典改变时,视图也会相应改变。要将字典视图强制转换为真正的列表,可使用 list(dictview)

docstring — 文档字符串

作为类、函数或模块之内的第一个表达式出现的字符串字面值。它在代码执行时会被忽略,但会被解释器识别并放入所在类、函数或模块的 __doc__ 属性中。由于它可用于代码内省,因此是对象存放文档的规范位置。

duck-typing — 鸭子类型

指一种编程风格,它并不依靠查找对象类型来确定其是否具有正确的接口,而是直接调用或使用其方法或属性(“看起来像鸭子,叫起来也像鸭子,那么肯定就是鸭子。”)由于强调接口而非特定类型,设计良好的代码可通过允许多态替代来提升灵活性。鸭子类型避免使用 type()isinstance() 检测。(但要注意鸭子类型可以使用 抽象基类 作为补充。) 而往往会采用 hasattr() 检测或是 EAFP 编程。

EAFP

“求原谅比求许可更容易”的英文缩写。这种 Python 常用代码编写风格会假定所需的键或属性存在,并在假定错误时捕获异常。这种简洁快速风格的特点就是大量运用 tryexcept 语句。于其相对的则是所谓 LBYL 风格,常见于 C 等许多其他语言。

expression — 表达式

可以求出某个值的语法单元。 换句话说,一个表达式就是表达元素例如字面值、名称、属性访问、运算符或函数调用的汇总,它们最终都会返回一个值。 与许多其他语言不同,并非所有语言构件都是表达式。 还存在不能被用作表达式的 statement,例如 while。 赋值也是属于语句而非表达式。

extension module — 扩展模块

以 C 或 C++ 编写的模块,使用 Python 的 C API 来与语言核心以及用户代码进行交互。

f-string — f-字符串

带有 'f''F' 前缀的字符串字面值通常被称为“f-字符串”即 格式化字符串字面值 的简写。参见 PEP 498

file object — 文件对象

对外提供面向文件 API 以使用下层资源的对象(带有 read()write() 这样的方法)。根据其创建方式的不同,文件对象可以处理对真实磁盘文件,对其他类型存储,或是对通讯设备的访问(例如标准输入/输出、内存缓冲区、套接字、管道等等)。文件对象也被称为 文件类对象

实际上共有三种类别的文件对象: 原始 二进制文件, 缓冲 二进制文件 以及 文本文件。它们的接口定义均在 io 模块中。创建文件对象的规范方式是使用 open() 函数。

file-like object — 文件类对象

file object 的同义词。

filesystem encoding and error handler — 文件系统编码格式与错误处理句柄

Python 用来从操作系统解码字节串和向操作系统编码 Unicode 的编码格式与错误处理句柄。

文件系统编码格式必须保证能成功解码长度在 128 以下的所有字节串。 如果文件系统编码格式无法提供此保证,则 API 函数可能会引发 UnicodeError

sys.getfilesystemencoding()sys.getfilesystemencodeerrors() 函数可被用来获取文件系统编码格式与错误处理句柄。

filesystem encoding and error handler 是在 Python 启动时通过 PyConfig_Read() 函数来配置的

finder — 查找器

一种会尝试查找被导入模块的 loader 的对象。

从 Python 3.3 起存在两种类型的查找器: 元路径查找器 配合 sys.meta_path 使用,以及 path entry finders 配合 sys.path_hooks 使用。

更多详情可参见 PEP 302, PEP 420PEP 451

floor division — 向下取整除法

向下舍入到最接近的整数的数学除法。向下取整除法的运算符是 // 。例如,表达式 11 // 4 的计算结果是 2 ,而与之相反的是浮点数的真正除法返回 2.75 。注意 (-11) // 4 会返回 -3 因为这是 -2.75 向下 舍入得到的结果。见 PEP 238

function — 函数

可以向调用者返回某个值的一组语句。还可以向其传入零个或多个 参数 并在函数体执行中被使用。

即针对函数形参或返回值的 annotation 。

函数标注通常用于 类型提示:例如以下函数预期接受两个 int 参数并预期返回一个 int 值:

def sum_two_numbers(a: int, b: int) -> int:
   return a + b

PEP 484描述了此功能。

future

future 语句, from __future__ import <feature> 指示编译器使用将在未来的 Python 发布版中成为标准的语法和语义来编译当前模块。 __future__ 模块文档记录了可能 的 feature 取值。 通过导入此模块并对其变量求值,你可以看到每项新特性在何时被首次加入到该语言中以及它将(或已)在何时成为默认:

>>> import __future__
>>> __future__.division
_Feature((2, 2, 0, 'alpha', 2), (3, 0, 0, 'alpha', 0), 8192)

garbage collection — 垃圾回收

释放不再被使用的内存空间的过程。Python 是通过引用计数和一个能够检测和打破循环引用的循环垃圾回收器来执行垃圾回收的。可以使用 gc 模块来控制垃圾回收器。

generator — 生成器

返回一个 generator iterator 的函数。它看起来很像普通函数,不同点在于其包含 yield 表达式以便产生一系列值供给 for-循环使用或是通过 next() 函数逐一获取。

通常是指生成器函数,但在某些情况下也可能是指 生成器迭代器。如果需要清楚表达具体含义,请使用全称以避免歧义。

generator iterator — 生成器迭代器

generator 函数所创建的对象。

每个 yield 会临时暂停处理,记住当前位置执行状态(包括局部变量和挂起的 try 语句)。当该 生成器迭代器 恢复时,它会从离开位置继续执行(这与每次调用都从新开始的普通函数差别很大)。

generator expression — 生成器表达式

返回一个迭代器的表达式。 它看起来很像普通表达式后面带有定义了一个循环变量、范围的 for 子句,以及一个可选的 if 子句。 以下复合表达式会为外层函数生成一系列值:

>>> sum(i*i for i in range(10))         # sum of squares 0, 1, 4, ... 81
285

generic function — 泛型函数

为不同的类型实现相同操作的多个函数所组成的函数。在调用时会由调度算法来确定应该使用哪个实现。

generic type — 泛型类型

可以被形参化的 type;通常为容器类型例如 list。 可用于 类型提示 和 标注。

请参阅 PEP 483 来了解详情。

GIL

参见 global interpreter lock。

global interpreter lock — 全局解释器锁

CPython 解释器所采用的一种机制,它确保同一时刻只有一个线程在执行 Python bytecode。此机制通过设置对象模型(包括 dict 等重要内置类型)针对并发访问的隐式安全简化了 CPython 实现。给整个解释器加锁使得解释器多线程运行更方便,其代价则是牺牲了在多处理器上的并行性。

不过,某些标准库或第三方库的扩展模块被设计为在执行计算密集型任务如压缩或哈希时释放 GIL。此外,在执行 I/O 操作时也总是会释放 GIL。

创建一个(以更精细粒度来锁定共享数据的)“自由线程”解释器的努力从未获得成功,因为这会牺牲在普通单处理器情况下的性能。据信克服这种性能问题的措施将导致实现变得更复杂,从而更难以维护。

hash-based pyc — 基于哈希的 pyc

使用对应源文件的哈希值而非最后修改时间来确定其有效性的字节码缓存文件。

hashable — 可哈希

一个对象的哈希值如果在其生命周期内绝不改变,就被称为 可哈希 (它需要具有 __hash__() 方法),并可以同其他对象进行比较(它需要具有 __eq__() 方法)。可哈希对象必须具有相同的哈希值比较结果才会相同。

可哈希性使得对象能够作为字典键或集合成员使用,因为这些数据结构要在内部使用哈希值。

大多数 Python 中的不可变内置对象都是可哈希的;可变容器(例如列表或字典)都不可哈希;不可变容器(例如元组和 frozenset)仅当它们的元素均为可哈希时才是可哈希的。 用户定义类的实例对象默认是可哈希的。 它们在比较时一定不相同(除非是与自己比较),它们的哈希值的生成是基于它们的 id()

IDLE

Python 的 IDE,“集成开发与学习环境”的英文缩写。是 Python 标准发行版附带的基本编辑器和解释器环境。

immutable — 不可变

具有固定值的对象。不可变对象包括数字、字符串和元组。这样的对象不能被改变。如果必须存储一个不同的值,则必须创建新的对象。它们在需要常量哈希值的地方起着重要作用,例如作为字典中的键。

import path — 导入路径

由多个位置(或 路径条目)组成的列表,会被模块的 path based finder 用来查找导入目标。在导入时,此位置列表通常来自 sys.path,但对次级包来说也可能来自上级包的 __path__ 属性。

importing — 导入

令一个模块中的 Python 代码能为另一个模块中的 Python 代码所使用的过程。

importer — 导入器

查找并加载模块的对象;此对象既属于 finder 又属于 loader。

interactive — 交互

Python 带有一个交互式解释器,即你可以在解释器提示符后输入语句和表达式,立即执行并查看其结果。只需不带参数地启动 python 命令(也可以在你的计算机开始菜单中选择相应菜单项)。在测试新想法或检验模块和包的时候用这种方式会非常方便(请记得使用 help(x))。

interpreted — 解释型

Python 一是种解释型语言,与之相对的是编译型语言,虽然两者的区别由于字节码编译器的存在而会有所模糊。这意味着源文件可以直接运行而不必显式地创建可执行文件再运行。解释型语言通常具有比编译型语言更短的开发/调试周期,但是其程序往往运行得更慢。参见 interactive。

interpreter shutdown — 解释器关闭

当被要求关闭时,Python 解释器将进入一个特殊运行阶段并逐步释放所有已分配资源,例如模块和各种关键内部结构等。它还会多次调用 垃圾回收器。这会触发用户定义析构器或弱引用回调中的代码执行。在关闭阶段执行的代码可能会遇到各种异常,因为其所依赖的资源已不再有效(常见的例子有库模块或警告机制等)。

解释器需要关闭的主要原因有 __main__ 模块或所运行的脚本已完成执行。

iterable — 可迭代对象

能够逐一返回其成员项的对象。 可迭代对象的例子包括所有序列类型 (例如 list, strtuple) 以及某些非序列类型例如 dict, 文件对象 以及定义了 __iter__() 方法或是实现了 序列 语义的 __getitem__() 方法的任意自定义类对象。

可迭代对象被可用于 for 循环以及许多其他需要一个序列的地方(zip()map() …)。当一个可迭代对象作为参数传给内置函数 iter() 时,它会返回该对象的迭代器。这种迭代器适用于对值集合的一次性遍历。在使用可迭代对象时,你通常不需要调用 iter() 或者自己处理迭代器对象。for 语句会为你自动处理那些操作,创建一个临时的未命名变量用来在循环期间保存迭代器。

iterator — 迭代器

用来表示一连串数据流的对象。重复调用迭代器的 __next__() 方法(或将其传给内置函数 next())将逐个返回流中的项。当没有数据可用时则将引发 StopIteration 异常。到这时迭代器对象中的数据项已耗尽,继续调用其 __next__() 方法只会再次引发 StopIteration 异常。迭代器必须具有 __iter__() 方法用来返回该迭代器对象自身,因此迭代器必定也是可迭代对象,可被用于其他可迭代对象适用的大部分场合。一个显著的例外是那些会多次重复访问迭代项的代码。容器对象(例如 list)在你每次向其传入 iter() 函数或是在 for 循环中使用它时都会产生一个新的迭代器。如果在此情况下你尝试用迭代器则会返回在之前迭代过程中被耗尽的同一迭代器对象,使其看起来就像是一个空容器。

key function — 键函数

键函数或称整理函数,是能够返回用于排序或排位的值的可调用对象。例如,locale.strxfrm() 可用于生成一个符合特定区域排序约定的排序键。

Python 中有许多工具都允许用键函数来控制元素的排位或分组方式。其中包括 min(), max(), sorted(), list.sort(), heapq.merge(), heapq.nsmallest(), heapq.nlargest() 以及 itertools.groupby()

要创建一个键函数有多种方式。例如,str.lower() 方法可以用作忽略大小写排序的键函数。另外,键函数也可通过 lambda 表达式来创建,例如 lambda r: (r[0], r[2])。还有 operator 模块提供了三个键函数构造器:attrgetter()itemgetter()methodcaller()

keyword argument — 关键字参数

参见 argument。

lambda

由一个单独 expression 构成的匿名内联函数,表达式会在调用时被求值。创建 lambda 函数的句法为 lambda [parameters]: expression

LBYL

“先查看后跳跃”的英文缩写。这种代码编写风格会在进行调用或查找之前显式地检查前提条件。此风格与 EAFP 方式恰成对比,其特点是大量使用 if 语句。

在多线程环境中,LBYL 方式会导致“查看”和“跳跃”之间发生条件竞争风险。例如,以下代码 if key in mapping: return mapping[key] 可能由于在检查操作之后其他线程从 mapping 中移除了 key 而出错。这种问题可通过加锁或使用 EAFP 方式来解决。

locale encoding — 语言区域编码格式

在 Unix 上,它是 LC_CTYPE 语言区域的编码格式。 它可以通过 locale.setlocale(locale.LC_CTYPE, new_locale) 来设置。

在 Windows 上,它是 ANSI 代码页 (例如: cp1252)。

locale.getpreferredencoding(False) 可被用来获取语言区域编码格式。

Python 使用 filesystem encoding and error handler 在 Unicode 文件名和字节串文件名之间进行转换。

list — 列表

Python 内置的一种 sequence。虽然名为列表,但更类似于其他语言中的数组而非链接列表,因为访问元素的时间复杂度为 O(1)。

list comprehension — 列表推导式

处理一个序列中的所有或部分元素并返回结果列表的一种紧凑写法。result = ['{:#04x}'.format(x) for x in range(256) if x % 2 == 0] 将生成一个 0 到 255 范围内的十六进制偶数对应字符串(0x..)的列表。其中 if 子句是可选的,如果省略则 range(256) 中的所有元素都会被处理。

loader — 加载器

负责加载模块的对象。它必须定义名为 load_module() 的方法。加载器通常由一个 finder 返回。详情参见 PEP 302,对于 abstract base class 可参见 importlib.abc.Loader

magic method — 魔术方法

special method 的非正式同义词 。

mapping — 映射

一种支持任意键查找并实现了 MappingMutableMapping 抽象基类 中所规定方法的容器对象。 此类对象的例子包括 dict, collections.defaultdict, collections.OrderedDict 以及 collections.Counter

meta path finder — 元路径查找器

sys.meta_path 的搜索所返回的 finder。元路径查找器与 path entry finders 存在关联但并不相同。

metaclass — 元类

一种用于创建类的类。类定义包含类名、类字典和基类列表。元类负责接受上述三个参数并创建相应的类。大部分面向对象的编程语言都会提供一个默认实现。Python 的特别之处在于可以创建自定义元类。大部分用户永远不需要这个工具,但当需要出现时,元类可提供强大而优雅的解决方案。它们已被用于记录属性访问日志、添加线程安全性、跟踪对象创建、实现单例,以及其他许多任务。

method — 方法

在类内部定义的函数。如果作为该类的实例的一个属性来调用,方法将会获取实例对象作为其第一个 argument (通常命名为 self)。

method resolution order — 方法解析顺序

方法解析顺序就是在查找成员时搜索全部基类所用的先后顺序。

module — 模块

此对象是 Python 代码的一种组织单位。各模块具有独立的命名空间,可包含任意 Python 对象。模块可通过 importing 操作被加载到 Python 中。

module spec — 模块规格

一个命名空间,其中包含用于加载模块的相关导入信息。是 importlib.machinery.ModuleSpec 的实例。

MRO

参见 method resolution order。

mutable — 可变

可变对象可以在其 id() 保持固定的情况下改变其取值。

named tuple — 具名元组

术语“具名元组”可用于任何继承自元组,并且其中的可索引元素还能使用名称属性来访问的类型或类。 这样的类型或类还可能拥有其他特性。

有些内置类型属于具名元组,包括 time.localtime()os.stat() 的返回值。 另一个例子是 sys.float_info:

>>> sys.float_info[1]                   # indexed access
1024
>>> sys.float_info.max_exp              # named field access
1024
>>> isinstance(sys.float_info, tuple)   # kind of tuple
True

有些具名元组是内置类型(例如上面的例子)。 此外,具名元组还可通过常规类定义从 tuple 继承并定义名称字段的方式来创建。 这样的类可以手工编写,或者使用工厂函数 collections.namedtuple() 创建。 后一种方式还会添加一些手工编写或内置具名元组所没有的额外方法。

namespace — 命名空间

命名空间是存放变量的场所。命名空间有局部、全局和内置的,还有对象中的嵌套命名空间(在方法之内)。命名空间通过防止命名冲突来支持模块化。例如,函数 builtins.openos.open() 可通过各自的命名空间来区分。命名空间还通过明确哪个模块实现那个函数来帮助提高可读性和可维护性。例如,random.seed()itertools.islice() 这种写法明确了这些函数是由 randomitertools 模块分别实现的。

namespace package — 命名空间包

PEP 420 所引入的一种仅被用作子包的容器的 package,命名空间包可以没有实体表示物,其描述方式与 regular package 不同,因为它们没有 __init__.py 文件。

nested scope — 嵌套作用域

在一个定义范围内引用变量的能力。例如,在另一函数之内定义的函数可以引用前者的变量。请注意嵌套作用域默认只对引用有效而对赋值无效。局部变量的读写都受限于最内层作用域。类似的,全局变量的读写则作用于全局命名空间。通过 nonlocal 关键字可允许写入外层作用域。

new-style class — 新式类

对于目前已被应于所有类对象的类形式的旧称谓。在早先的 Python 版本中,只有新式类能够使用 Python 新增的更灵活特性,例如 __slots__、描述符、特征属性、__getattribute__()、类方法和静态方法等。

object — 对象

任何具有状态(属性或值)以及预定义行为(方法)的数据。object 也是任何 new-style class 的最顶层基类名。

package — 包

一种可包含子模块或递归地包含子包的 Python module。从技术上说,包是带有 __path__ 属性的 Python 模块。

parameter — 形参

function (或方法)定义中的命名实体,它指定函数可以接受的一个 argument (或在某些情况下,多个实参)。有五种形参:

  • positional-or-keyword*:位置或关键字,指定一个可以作为 位置参数 传入也可以作为 关键字参数 传入的实参。这是默认的形参类型,例如下面的 *foobar:

    def func(foo, bar=None): ...
  • positional-only*:仅限位置,指定一个只能通过位置传入的参数。 仅限位置形参可通过在函数定义的形参列表中它们之后包含一个 / 字符来定义,例如下面的 *posonly1posonly2:

    def func(posonly1, posonly2, /, positional_or_keyword): ...
  • keyword-only:仅限关键字,指定一个只能通过关键字传入的参数。仅限关键字形参可通过在函数定义的形参列表中包含单个可变位置形参或者在多个可变位置形参之前放一个 `` 来定义,例如下面的 *kw_only1kw_only2:

    def func(arg, *, kw_only1, kw_only2): ...
  • var-positional:可变位置,指定可以提供由一个任意数量的位置参数构成的序列(附加在其他形参已接受的位置参数之后)。这种形参可通过在形参名称前加缀 `` 来定义,例如下面的 *args:

    def func(*args, **kwargs): ...
  • var-keyword:可变关键字,指定可以提供任意数量的关键字参数(附加在其他形参已接受的关键字参数之后)。这种形参可通过在形参名称前加缀 ** 来定义,例如上面的 kwargs

形参可以同时指定可选和必选参数,也可以为某些可选参数指定默认值。

path entry — 路径入口

import path 中的一个单独位置,会被 path based finder 用来查找要导入的模块。

path entry finder — 路径入口查找器

任一可调用对象使用 sys.path_hooks (即 path entry hook) 返回的 finder,此种对象能通过 path entry 来定位模块。

path entry hook — 路径入口钩子

一种可调用对象,在知道如何查找特定 path entry 中的模块的情况下能够使用 sys.path_hook 列表返回一个 path entry finder。

path based finder — 基于路径的查找器

默认的一种 元路径查找器,可在一个 import path 中查找模块。

path-like object — 路径类对象

代表一个文件系统路径的对象。类路径对象可以是一个表示路径的 str 或者 bytes 对象,还可以是一个实现了 os.PathLike 协议的对象。一个支持 os.PathLike 协议的对象可通过调用 os.fspath() 函数转换为 str 或者 bytes 类型的文件系统路径;os.fsdecode()os.fsencode() 可被分别用来确保获得 strbytes 类型的结果。此对象是由 PEP 519 引入的。

PEP

“Python 增强提议”的英文缩写。一个 PEP 就是一份设计文档,用来向 Python 社区提供信息,或描述一个 Python 的新增特性及其进度或环境。PEP 应当提供精确的技术规格和所提议特性的原理说明。

PEP 应被作为提出主要新特性建议、收集社区对特定问题反馈以及为必须加入 Python 的设计决策编写文档的首选机制。PEP 的作者有责任在社区内部建立共识,并应将不同意见也记入文档。

参见 PEP 1

portion — 部分

构成一个命名空间包的单个目录内文件集合(也可能存放于一个 zip 文件内),具体定义见 PEP 420

positional argument — 位置参数

参见 argument。

provisional API — 暂定 API

暂定 API 是指被有意排除在标准库的向后兼容性保证之外的应用编程接口。虽然此类接口通常不会再有重大改变,但只要其被标记为暂定,就可能在核心开发者确定有必要的情况下进行向后不兼容的更改(甚至包括移除该接口)。此种更改并不会随意进行 — 仅在 API 被加入之前未考虑到的严重基础性缺陷被发现时才可能会这样做。

即便是对暂定 API 来说,向后不兼容的更改也会被视为“最后的解决方案” —— 任何问题被确认时都会尽可能先尝试找到一种向后兼容的解决方案。

这种处理过程允许标准库持续不断地演进,不至于被有问题的长期性设计缺陷所困。详情见 PEP 411

provisional package — 暂定包

参见 provisional API。

Python 3000

Python 3.x 发布路线的昵称(这个名字在版本 3 的发布还遥遥无期的时候就已出现了)。有时也被缩写为“Py3k”。

Pythonic

指一个思路或一段代码紧密遵循了 Python 语言最常用的风格和理念,而不是使用其他语言中通用的概念来实现代码。例如,Python 的常用风格是使用 for 语句循环来遍历一个可迭代对象中的所有元素。许多其他语言没有这样的结构,因此不熟悉 Python 的人有时会选择使用一个数字计数器:

for i in range(len(food)):
    print(food[i])

而相应的更简洁更 Pythonic 的方法是这样的:

for piece in food:
    print(piece)

qualified name — 限定名称

一个以点号分隔的名称,显示从模块的全局作用域到该模块中定义的某个类、函数或方法的“路径”,相关定义见 PEP 3155。对于最高层级的函数和类,限定名称与对象名称一致:

>>> class C:
...     class D:
...         def meth(self):
...             pass
...
>>> C.__qualname__
'C'
>>> C.D.__qualname__
'C.D'
>>> C.D.meth.__qualname__
'C.D.meth'

当被用于引用模块时,完整限定名称 意为标示该模块的以点号分隔的整个路径,其中包含其所有的父包,例如 email.mime.text:

>>> import email.mime.text
>>> email.mime.text.__name__
'email.mime.text'

reference count — 引用计数

对特定对象的引用的数量。当一个对象的引用计数降为零时,所分配资源将被释放。引用计数对 Python 代码来说通常是不可见的,但它是 CPython 实现的一个关键元素。sys 模块定义了一个 getrefcount() 函数,程序员可调用它来返回特定对象的引用计数。

regular package — 常规包

传统型的 package,例如包含有一个 __init__.py 文件的目录。

slots

一种写在类内部的声明,通过预先声明实例属性等对象并移除实例字典来节省内存。虽然这种技巧很流行,但想要用好却并不容易,最好是只保留在少数情况下采用,例如极耗内存的应用程序,并且其中包含大量实例。

sequence — 序列

一种 iterable,它支持通过 __getitem__() 特殊方法来使用整数索引进行高效的元素访问,并定义了一个返回序列长度的 __len__() 方法。内置的序列类型有 liststrtuplebytes。注意虽然 dict 也支持 __getitem__()__len__(),但它被认为属于映射而非序列,因为它查找时使用任意的 immutable 键而非整数。

collections.abc.Sequence 抽象基类定义了一个更丰富的接口,它在 __getitem__()__len__() 之外又添加了 count(), index(), __contains__()__reversed__()。 实现此扩展接口的类型可以使用 register() 来显式地注册。

set comprehension — 集合推导式

处理一个可迭代对象中的所有或部分元素并返回结果集合的一种紧凑写法。 results = {c for c in 'abracadabra' if c not in 'abc'} 将生成字符串集合 {'r', 'd'}

single dispatch — 单分派

一种 generic function 分派形式,其实现是基于单个参数的类型来选择的。

slice — 切片

通常只包含了特定 sequence 的一部分的对象。切片是通过使用下标标记来创建的,在 [] 中给出几个以冒号分隔的数字,例如 variable_name[1:3:5]。方括号(下标)标记在内部使用 slice 对象。

special method — 特殊方法

一种由 Python 隐式调用的方法,用来对某个类型执行特定操作例如相加等等。这种方法的名称的首尾都为双下划线。

statement — 语句

语句是程序段(一个代码“块”)的组成单位。一条语句可以是一个 expression 或某个带有关键字的结构,例如 ifwhilefor

strong reference — 强引用

在 Python 的 C API 中,强引用是对象引用的一种,当它被创建时将会增加对象引用计数而当它被删除时则会减少对象引用计数。

Py_NewRef() 函数可被用于创建一个对象的强引用。 通常,必须在退出某个强引用的作用域时在该强引用上调用 Py_DECREF() 函数,以避免引用的泄漏。

text encoding — 文本编码

用于将Unicode字符串编码为字节串的编码器。

text file — 文本文件

一种能够读写 str 对象的 file object。通常一个文本文件实际是访问一个面向字节的数据流并自动处理 text encoding。文本文件的例子包括以文本模式('r''w')打开的文件、sys.stdinsys.stdout 以及 io.StringIO 的实例。

triple-quoted string — 三引号字符串

首尾各带三个连续双引号(”)或者单引号(’)的字符串。它们在功能上与首尾各用一个引号标注的字符串没有什么不同,但是有多种用处。它们允许你在字符串内包含未经转义的单引号和双引号,并且可以跨越多行而无需使用连接符,在编写文档字符串时特别好用。

type — 类型

类型决定一个 Python 对象属于什么种类;每个对象都具有一种类型。要知道对象的类型,可以访问它的 __class__ 属性,或是通过 type(obj) 来获取。

type alias — 类型别名

一个类型的同义词,创建方式是把类型赋值给特定的标识符。

类型别名的作用是简化 类型提示。例如:

def remove_gray_shades(
        colors: list[tuple[int, int, int]]) -> list[tuple[int, int, int]]:
    pass

可以这样提高可读性:

Color = tuple[int, int, int]
def remove_gray_shades(colors: list[Color]) -> list[Color]:
    pass

type hint — 类型提示

annotation 为变量、类属性、函数的形参或返回值指定预期的类型。

类型提示属于可选项,Python 不要求提供,但其可对静态类型分析工具起作用,并可协助 IDE 实现代码补全与重构。

全局变量、类属性和函数的类型提示可以使用 typing.get_type_hints() 来访问,但局部变量则不可以。

universal newlines — 通用换行

一种解读文本流的方式,将以下所有符号都识别为行结束标志:Unix 的行结束约定 '\n'、Windows 的约定 '\r\n' 以及旧版 Macintosh 的约定 '\r'。参见 PEP 278PEP 3116bytes.splitlines() 了解更多用法说明。

variable annotation — 变量标注

对变量或类属性的 annotation。

在标注变量或类属性时,还可选择为其赋值:

class C:    field: 'annotation'

变量标注通常被用作 类型提示:例如以下变量预期接受 int 类型的值:

count: int = 0

参见 function annotation, PEP 484PEP 526,其中描述了此功能。

virtual environment — 虚拟环境

一种采用协作式隔离的运行时环境,允许 Python 用户和应用程序在安装和升级 Python 分发包时不会干扰到同一系统上运行的其他 Python 应用程序的行为。

virtual machine — 虚拟机

一台完全通过软件定义的计算机。Python 虚拟机可执行字节码编译器所生成的 bytecode。

Zen of Python — Python 之禅

列出 Python 设计的原则与哲学,有助于理解与使用这种语言。查看其具体内容可在交互模式提示符中输入 “import this“。

Python/C API 参考手册

Python版本变化


文章作者: 杰克成
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 杰克成 !
评论
  目录