Python 技术手册第三版(五)
原文:
annas-archive.org/md5/9e375b08cb0be52e8b7c2a9eba6f5313译者:飞龙
第十二章:持久性和数据库
Python 支持几种持久化数据的方式。一种方式是序列化,将数据视为 Python 对象的集合。这些对象可以序列化(保存)到字节流中,稍后可以从字节流中反序列化(加载和重新创建)。对象持久性依赖于序列化,添加了诸如对象命名等功能。本章介绍了支持序列化和对象持久性的 Python 模块。
另一种使数据持久化的方法是将其存储在数据库(DB)中。一个简单的 DB 类别是使用键访问的文件,以便选择性地读取和更新数据的部分。本章涵盖了支持几种此类文件格式变体的 Python 标准库模块,称为DBM。
关系型 DB 管理系统(RDBMS),如 PostgreSQL 或 Oracle,提供了一种更强大的方法来存储、搜索和检索持久化数据。关系型 DB 依赖于结构化查询语言(SQL)的方言来创建和更改 DB 的模式,在 DB 中插入和更新数据,并使用搜索条件查询 DB。(本书不提供 SQL 的参考资料;为此,我们推荐 O'Reilly 的SQL in a Nutshell,作者是 Kevin Kline、Regina Obe 和 Leo Hsu。)不幸的是,尽管存在 SQL 标准,但没有两个 RDBMS 实现完全相同的 SQL 方言。
Python 标准库没有提供 RDBMS 接口。然而,许多第三方模块让您的 Python 程序访问特定的 RDBMS。这些模块大多遵循Python 数据库 API 2.0标准,也称为DBAPI。本章介绍了 DBAPI 标准,并提到了一些最受欢迎的实现它的第三方模块。
特别方便的 DBAPI 模块(因为它随着每个 Python 标准安装而提供)是sqlite3,它封装了SQLite。SQLite 是“一个自包含的、无服务器的、零配置的、事务性的 SQL DB 引擎”,是世界上部署最广泛的关系型 DB 引擎。我们在“SQLite”中介绍 sqlite3。
除了关系型 DB 和本章介绍的更简单的方法之外,还存在几种NoSQL DB,如Redis和MongoDB,每种都有 Python 接口。本书不涵盖高级非关系型 DB。
序列化
Python 提供了几个模块来将 Python 对象序列化(保存)到各种字节流中,并从流中反序列化(加载和重新创建)Python 对象。序列化也称为编组,意味着格式化用于数据交换。
序列化方法涵盖了一个广泛的范围,从低级别的、特定于 Python 版本的 marshal 和独立于语言的 JSON(两者都限于基本数据类型),到更丰富但特定于 Python 的 pickle 和跨语言格式,如 XML、YAML、协议缓冲区 和 MessagePack。
在本节中,我们涵盖了 Python 的 csv、json、pickle 和 shelve 模块。我们在 第二十三章 中介绍了 XML。marshal 过于低级,不适合在应用程序中使用;如果你需要维护使用它的旧代码,请参考在线文档。至于协议缓冲区、MessagePack、YAML 和其他数据交换/序列化方法(每种都具有特定的优点和缺点),我们无法在本书中覆盖所有内容;我们建议通过网络上可用的资源来学习它们。
CSV 模块
尽管 CSV(代表逗号分隔值¹)格式通常不被视为一种序列化形式,但它是一种广泛使用且方便的表格数据交换格式。由于许多数据是表格形式的,因此尽管在如何在文件中表示它存在一些争议,但 CSV 数据仍然被广泛使用。为了解决这个问题,csv 模块提供了一些方言(特定来源编码 CSV 数据方式的规范),并允许你定义自己的方言。你可以注册额外的方言,并通过调用 csv.list_dialects 函数列出可用的方言。有关方言的更多信息,请参阅模块文档。
csv 函数和类
csv 模块公开了 表 12-1 中详细介绍的函数和类。它提供了两种读取器和写入器,让你在 Python 中处理 CSV 数据行时可以选择使用列表或字典。
表 12-1. csv 模块的函数和类
| reader | reader(csvfile, dialect='excel', **kw) 创建并返回一个 reader 对象 r。csvfile 可以是任何产生文本行(通常是行列表或使用 newline='' 打开的文件)的可迭代对象,dialect 是已注册方言的名称。要修改方言,请添加命名参数:它们的值将覆盖相同名称的方言字段。对 r 进行迭代将产生一个列表序列,每个列表包含 csvfile 的一行元素。 |
|---|---|
| writer | writer(csvfile, dialect='excel', **kw) 创建并返回一个写入对象 w。 csvfile 是一个带有写入方法的对象(如果是文件,请使用 newline='' 打开); dialect 是一个已注册方言的名称。要修改方言,请添加命名参数:它们的值将覆盖同名的方言字段。 *w.*writerow 接受值序列,并将它们的 CSV 表示作为一行写入 csvfile。 *w.*writerows 接受这样的序列的可迭代对象,并对每个调用 *w.*writerow。您有责任关闭 csvfile。 |
| DictReader | DictReader(csvfile, fieldnames=None, restkey=None, restval=None, dialect='excel', **args,*kw)
创建并返回一个对象 r,该对象迭代 csvfile 以生成一个字典的可迭代对象(-3.8 有序字典),每一行一个字典。当给出 fieldnames 参数时,它用于命名 csvfile 中的字段;否则,字段名来自 csvfile 的第一行。如果一行包含比字段名更多的列,则额外的值保存为带有键 restkey 的列表。如果任何行中的值不足,则将这些列值设置为 restval。 dialect、kw 和 args 传递给底层的读取器对象。 |
| DictWriter | DictWriter(csvfile, fieldnames, restval='', extrasaction='raise', dialect='excel'*, *args, *kwds)
创建并返回一个对象 w,其 writerow 和 writerows 方法接受字典或字典的可迭代对象,并使用 csvfile 的写入方法写入它们。 fieldnames 是一个 strs 序列,字典的键。 restval 是用于填充缺少某些键的字典的值。 extrasaction 指定字典具有未列在 fieldnames 中的额外键时该如何处理:当 'raise' 时,默认时,函数在这些情况下引发 ValueError;当 'ignore' 时,函数忽略此类错误。 dialect、kw 和 args 传递给底层的读取器对象。您有责任关闭 csvfile(通常是使用 newline='' 打开的文件)。 |
| ^(a) 使用 newline='' 打开文件允许 csv 模块使用自己的换行处理,并正确处理文本字段可能包含换行符的方言。 |
|---|
一个 csv 示例
这里有一个简单的示例,使用 csv 从字符串列表中读取颜色数据:
`import` csv
color_data = '''\ color,r,g,b
red,255,0,0
green,0,255,0
blue,0,0,255
cyan,0,255,255
magenta,255,0,255
yellow,255,255,0
'''.splitlines()
colors = {row['color']:
row `for` row `in` csv.DictReader(color_data)}
print(colors['red'])
*`# prints: {'color': 'red', 'r': '255', 'g': '0', 'b': '0'}`*
请注意,整数值被读取为字符串。csv 不执行任何数据转换;这需要通过您的程序代码与从 DictReader 返回的字典来完成。
json 模块
标准库的 json 模块支持 Python 本地数据类型(元组、列表、字典、整数、字符串等)的序列化。要序列化自定义类的实例,应实现继承自 JSONEncoder 和 JSONDecoder 的相应类。
json 函数
json 模块提供了四个关键函数,详见 表 12-2。
表 12-2. json 模块的函数
| dump | dump(value, fileobj, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=JSONEncoder, indent=None, separators=(', ', ': '), default=None, sort_keys=False, **kw) 将对象value的 JSON 序列化写入到文件对象fileobj中,fileobj必须以文本模式打开进行写入,通过调用fileobj.write 来传递文本字符串作为参数。
当 skipkeys 为True(默认为False)时,非标量类型(即不是 bool、float、int、str 或None的键)会引发异常。无论如何,标量类型的键会被转换为字符串(例如,None会变成'null'):JSON 只允许在其映射中使用字符串作为键。
| dump (cont.) | 当 ensure_ascii 为True(默认值)时,输出中的所有非 ASCII 字符都会被转义;当其为False时,它们将原样输出。当 check_circular 为True(默认值)时,value中的容器会检查循环引用,如果发现任何循环引用,则会引发 ValueError 异常;当其为False时,则跳过检查,并可能引发多种不同的异常(甚至可能导致崩溃)。
当 allow_nan 为True(默认值)时,浮点标量 nan、inf 和-inf 会输出为它们相应的 JavaScript 等效项 NaN、Infinity 和-Infinity;当其为False时,存在这些标量会引发 ValueError 异常。
您可以选择传递 cls 来使用 JSONEncoder 的自定义子类(这种高级定制很少需要,在本书中我们不涵盖这部分);在这种情况下,**kw会在实例化 cls 时传递给它的调用中使用。默认情况下,编码使用 JSONEncoder 类直接进行。
当缩进为大于 0 的整数时,dump 函数会在每个数组元素和对象成员前面加上相应数量的空格来实现“美观打印”;当缩进为小于等于 0 的整数时,dump 函数仅插入换行符。当缩进为None(默认值)时,dump 函数使用最紧凑的表示方式。缩进也可以是一个字符串,例如'\t',在这种情况下,dump 函数使用该字符串作为缩进。
separators 必须是一个包含两个元素的元组,分别是用于分隔项的字符串和用于分隔键值对的字符串。您可以显式地传递 separators=(',', ':')来确保 dump 函数不插入任何空白字符。
您可以选择传递 default 以将一些本来不能被序列化的对象转换为可序列化对象。default 是一个函数,接受一个非序列化对象作为参数,并且必须返回一个可序列化对象,或者引发 ValueError 异常(默认情况下,存在非序列化对象会引发 ValueError 异常)。
当 sort_keys 为True(默认为False)时,映射将按其键的排序顺序输出;当False时,它们将按照它们的自然迭代顺序输出(如今,对于大多数映射,是插入顺序)。
| dumps | dumps(value, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=JSONEncoder, indent=None, separators=(', ', ': '), default=None, sort_keys=False, **kw)
返回一个字符串,该字符串是对象value的 JSON 序列化结果,即 dump 将写入其文件对象参数的字符串。dumps 的所有参数与 dump 的参数完全相同。
JSON 仅序列化一个对象到每个文件中
JSON 并非所谓的框架格式:这意味着无法多次调用 dump 来将多个对象序列化到同一个文件中,也不能稍后多次调用 load 来反序列化对象,就像使用 pickle(在下一节讨论)那样。因此,从技术上讲,JSON 仅序列化单个对象到一个文件中。但是,该对象可以是一个列表或字典,其中可以包含任意数量的条目。
|
| load | load(fileobj, encoding='utf-8', cls=JSONDecoder, object_hook=None, parse_float=float, parse_int=int, parse_constant=None, object_pairs_hook=None, **kw) 创建并返回先前序列化为文件类对象fileobj中的对象v,fileobj必须以文本模式打开,通过调用fileobj.read 获取fileobj的内容。调用fileobj.read 必须返回文本(Unicode)字符串。
函数 load 和 dump 是互补的。换句话说,单个调用 load(f)将反序列化在调用 dump(v, f)时序列化的相同值(可能会有一些修改:例如,所有字典键都变成字符串时)的值。
可以选择传递 cls 以使用 JSONDecoder 的自定义子类(这种高级定制很少需要,在本书中我们不涵盖它);在这种情况下,**kw将在调用 cls 时传递,并由其实例化。默认情况下,解码直接使用 JSONDecoder 类。
还可以选择传递 object_hook 或 object_pairs_hook(如果两者都传递,则 object_hook 将被忽略,只使用 object_pairs_hook),这是一个允许您实现自定义解码器的函数。当传递 object_hook 但没有传递 object_pairs_hook 时,每次将对象解码为字典时,load 都会使用 object_hook,并以该字典作为唯一参数调用 object_hook,并使用 object_hook 的返回值而不是该字典。当传递 object_pairs_hook 时,每次解码对象时,load 将使用 object_pairs_hook,并将对象的(key, value)对的列表作为唯一参数传递,顺序与输入中的顺序相同,并使用 object_pairs_hook 的返回值。这使您可以执行依赖于输入中(key, value)对顺序的专门解码。
parse_float、parse_int 和 parse_constant 是使用单个参数调用的函数:表示浮点数、整数或三个特殊常量之一('NaN'、'Infinity' 或 '-Infinity')的 str。每次识别输入中表示数字的 str 时,load 调用适当的函数,并使用函数的返回值。默认情况下,parse_float 是内置的 float 函数,parse_int 是 int,parse_constant 是一个返回特殊浮点数标量 nan、inf 或 -inf 的函数。例如,可以传递 parse_float=decimal.Decimal 以确保结果中的所有数字正常情况下都是小数(如 “decimal 模块” 中所述)。 |
| loads | loads(s, cls=JSONDecoder, object_hook=None, parse_float=float, parse_int=int, parse_constant=None, object_pairs_hook=None, **kw) 创建并返回之前已序列化为字符串 s 的对象 v。loads 的所有参数与 load 的参数完全相同。 |
|---|
一个 JSON 示例
如果需要读取多个文本文件,其文件名作为程序参数给出,并记录每个单词在文件中出现的位置,你需要记录每个单词的 (文件名, 行号) 对列表。以下示例使用 fileinput 模块迭代所有作为程序参数给出的文件,并使用 json 将 (文件名, 行号) 对列表编码为字符串,并存储在类似 DBM 的文件中(如 “DBM 模块” 中所述)。由于这些列表包含元组,每个元组包含字符串和数字,它们可以被 json 序列化:
`import` collections, fileinput, json, dbm
word_pos = collections.defaultdict(list)
`for` line `in` fileinput.input():
pos = fileinput.filename(), fileinput.filelineno()
`for` word `in` line.split():
word_pos[word].append(pos)
`with` dbm.open('indexfilem', 'n') `as` dbm_out:
`for` word, word_positions `in` word_pos.items():
dbm_out[word] = json.dumps(word_positions)
然后,我们可以使用 json 解序列化存储在类似 DBM 文件 indexfilem 中的数据,如以下示例所示:
`import` sys, json, dbm, linecache
`with` dbm.open('indexfilem') `as` dbm_in:
`for` word `in` sys.argv[1:]:
`if` word `not` `in` dbm_in:
print(f'Word {word!r} not found in index file', file=sys.stderr)
`continue`
places = json.loads(dbm_in[word])
`for` fname, lineno `in` places:
print(f'Word {word!r} occurs in line {lineno}'
f' of file {fname!r}:')
print(linecache.getline(fname, lineno), end='')
pickle 模块
pickle 模块提供了名为 Pickler 和 Unpickler 的工厂函数,用于生成对象(不可子类化类型的实例,而不是类),这些对象包装文件并提供 Python 特定的序列化机制。通过这些模块进行序列化和反序列化也称为 pickling 和 unpickling。
序列化与深拷贝具有某些相同的问题,如 “copy 模块” 中所述。pickle 模块处理这些问题的方式与 copy 模块非常相似。序列化,就像深拷贝一样,意味着在引用的有向图上进行递归遍历。pickle 保留了图的形状:当多次遇到相同的对象时,仅第一次序列化该对象,其他出现的相同对象序列化为对该单一值的引用。pickle 还正确地序列化具有引用循环的图。然而,这意味着如果可变对象 o 被序列化多次到同一个 Pickler 实例 p,则在第一次将 o 序列化到 p 后对 o 的任何更改都不会被保存。
在序列化正在进行时不要更改对象
为了清晰、正确和简单起见,在 Pickler 实例序列化过程中不要更改正在序列化的对象。
pickle 可以使用遗留 ASCII 协议或多个紧凑的二进制协议进行序列化。表 12-3 列出了可用的协议。
表 12-3. pickle 协议
| 协议 | 格式 | Python 版本新增 | 描述 |
|---|---|---|---|
| 0 | ASCII | 1.4^(a) | 可读性强,序列化/反序列化速度慢 |
| 1 | 二进制 | 1.5 | 早期二进制格式,被协议 2 取代 |
| 2 | 二进制 | 2.3 | 改进对后期 Python 2 特性的支持 |
| 3 | 二进制 | 3.0 | (-3.8 默认)增加对字节对象的具体支持 |
| 4 | 二进制 | 3.4 | (3.8+ 默认)支持非常大的对象 |
| 5 | 二进制 | 3.8 | 3.8+ 添加了支持作为传输过程中序列化的 pickling 特性,参见 PEP 574 |
| ^(a) 或可能更早。这是可在 Python.org 上找到的最古老版本的文档。 |
始终使用协议 2 或更高版本进行 Pickle
始终使用至少协议 2。尺寸和速度节省可观,并且二进制格式基本没有任何缺点,除了导致生成的 pickle 与真正古老版本的 Python 不兼容之外。
当重新加载对象时,pickle 会透明地识别并使用当前 Python 版本支持的任何协议。
pickle(腌制)通过名称而非数值序列化类和函数²。因此,pickle 只能在反序列化时从与 pickle 序列化时相同模块中导入类或函数。特别地,pickle 通常只能序列化和反序列化类和函数,如果它们是其各自模块的顶级名称(即属性)。考虑以下示例:
`def` adder(augend):
`def` inner(addend, augend=augend):
`return` addend+augend
`return` inner
plus5 = adder(5)
此代码将一个闭包绑定到名称 plus5(如“嵌套函数和嵌套作用域”中所述)——一个内部函数 inner 与适当的外部作用域。因此,尝试对 plus5 进行 pickle 会引发 AttributeError:只有当函数处于顶级时,才能对其进行 pickle,而此代码中其闭包绑定到名称 plus5 的函数 inner 并非顶级,而是嵌套在函数 adder 内部。类似的问题也适用于序列化嵌套函数和嵌套类(即不在顶层的类)。
pickle 函数和类
pickle 模块公开了表 12-4 中列出的函数和类。
表 12-4. pickle 模块的函数和类
| dump, dumps | dump(value, fileobj, protocol=None, bin=None), dumps(value, protocol=None, bin=None)
dumps 返回表示对象 value 的字节串。dump 将相同的字符串写入类似文件的对象 fileobj,该对象必须已打开以供写入。dump(v, f) 就像 f.write(dumps(v))。protocol 参数可以是 0(ASCII 输出,最慢和最庞大的选项),或者更大的整数表示各种类型的二进制输出(参见 Table 12-3)。除非 protocol 是 0,否则传递给 dump 的 fileobj 参数必须已打开以供二进制写入。不要传递 bin 参数,它仅为与旧版本 Python 的兼容性而存在。 |
| load, loads | load(fileobj), loads(s, *, fix_imports=True, encoding="ASCII", errors="strict")
函数 load 和 dump 是互补的。换句话说,对 load(f) 的一系列调用将反序列化与之前通过对 dump(v, f) 进行一系列调用而创建 f 内容时序列化的相同值。load 从类似文件的对象 fileobj 中读取正确数量的字节,并创建并返回由这些字节表示的对象 v。load 和 loads 透明地支持在任何二进制或 ASCII 协议中执行的 pickles。如果数据以任何二进制格式进行 pickle,文件必须对于 dump 和 load 都以二进制方式打开。load(f) 就像 Unpickler(f).load()。 |
| load, loads
(cont.) | loads 创建并返回由字节串 s 表示的对象 v,因此对于任何支持的类型的对象 v,v==loads(dumps(v))。如果 s 比 dumps(v) 长,loads 会忽略额外的字节。提供了可选参数 fix_imports、encoding 和 errors 来处理由 Python 2 代码生成的流;请参阅 pickle.loads 文档 以获取更多信息。
永远不要反序列化不受信任的数据
从不受信任的数据源进行反序列化是一种安全风险;攻击者可能利用此漏洞执行任意代码。
|
| 序列化器 | 序列化器(fileobj, protocol=None, bin=None) 创建并返回一个对象 p,使得调用 p.dump 相当于调用 dump 函数并传递给 Pickler fileobj、protocol 和 bin 参数。为了将多个对象序列化到文件中,Pickler 比重复调用 dump 更方便且更快。你可以子类化 pickle.Pickler 来覆盖 Pickler 方法(尤其是 persistent_id 方法)并创建你自己的持久化框架。然而,这是一个高级主题,在本书中不再进一步讨论。 |
|---|---|
| 反序列化器 | 反序列化器(fileobj) 创建并返回一个对象 u,使得调用 u.load 相当于调用 load 并传递 fileobj 参数给 Unpickler。为了从文件中反序列化多个对象,Unpickler 比重复调用 load 函数更方便且更快。你可以子类化 pickle.Unpickler 来覆盖 Unpickler 方法(尤其是 persistent_load 方法)并创建你自己的持久化框架。然而,这是一个高级主题,在本书中不再进一步讨论。 |
一个序列化的例子
以下示例处理与之前显示的 json 示例相同的任务,但使用 pickle 而不是 json 将(filename, linenumber)对的列表序列化为字符串:
`import` collections, fileinput, pickle, dbm
word_pos = collections.defaultdict(list)
`for` line `in` fileinput.input():
pos = fileinput.filename(), fileinput.filelineno()
`for` word `in` line.split():
word_pos[word].append(pos)
`with` dbm.open('indexfilep', 'n') `as` dbm_out:
`for` word, word_positions `in` word_pos.items():
dbm_out[word] = pickle.dumps(word_positions, protocol=2)
然后,我们可以使用 pickle 从类似 DBM 的文件indexfilep中读回存储的数据,如下例所示:
`import` sys, pickle, dbm, linecache
`with` dbm.open('indexfilep') `as` dbm_in:
`for` word `in` sys.argv[1:]:
`if` word `not` `in` dbm_in:
print(f'Word {word!r} not found in index file',
file=sys.stderr)
`continue`
places = pickle.loads(dbm_in[word])
`for` fname, lineno `in` places:
print(f'Word {word!r} occurs in line {lineno}'
f' of file {fname!r}:')
print(linecache.getline(fname, lineno), end='')
对实例进行 pickle
为了让 pickle 重新加载实例x,pickle 必须能够从 pickle 保存实例时定义类的同一模块中导入x的类。以下是 pickle 如何保存类T的实例对象x的状态,并将保存的状态重新加载到类T的新实例y中(重新加载的第一步始终是创建T的新空实例y,除非另有明确说明):
-
当T提供方法 getstate 时,pickle 保存调用T.getstate(x)的结果d。
-
当T提供方法 setstate 时,d可以是任何类型,并且 pickle 通过调用T.setstate(y, d)重新加载保存的状态。
-
否则,d必须是一个字典,pickle 只需设置y.dict = d。
-
否则,当T提供方法 getnewargs,并且 pickle 使用协议 2 或更高版本进行 pickle 时,pickle 保存调用T.getnewargs(x)的结果t;t必须是一个元组。
-
在这种情况下,pickle 不会从空y开始,而是通过执行y = T.new(T, t)来创建y*,从而完成重新加载。
-
否则,默认情况下,pickle 将x.dict 保存为字典d。
-
当T提供方法 setstate 时,pickle 通过调用T.setstate(y, d)重新加载保存的状态。
-
否则,pickle 只需设置y.dict = d。
pickle 保存和重新加载的d或t对象中的所有项(通常是字典或元组)必须依次是适合进行 pickle 和 unpickle(即pickleable)的类型的实例,并且如有必要,该过程可以递归重复进行,直到 pickle 到达原始的 pickleable 内置类型(如字典、元组、列表、集合、数字、字符串等)。
如“copy 模块”中所述,getnewargs、getstate 和 setstate 特殊方法还控制实例对象的复制和深度复制方式。如果一个类定义了 slots,因此其实例没有 dict 属性,pickle 会尽力保存和恢复等同于 slots 名称和值的字典。然而,这样的类应该定义 getstate 和 setstate;否则,其实例可能无法正确进行 pickle 和复制。
使用 copyreg 模块进行 pickle 定制
通过向 copyreg 模块注册工厂和减少函数,可以控制 pickle 如何序列化和反序列化任意类型的对象。当您在 C 代码的 Python 扩展中定义类型时,这尤为有用。copyreg 模块提供了 表 12-5 中列出的函数。
表 12-5. copyreg 模块的函数
| constructor | constructor(fcon) 将 fcon 添加到构造函数表中,该表列出 pickle 可能调用的所有工厂函数。fcon 必须是可调用的,通常是一个函数。 |
|---|
| pickle | pickle(type, fred, fcon=None) 将函数 fred 注册为类型 type 的减少函数,其中 type 必须是一个类型对象。要保存类型为 type 的对象 o,模块 pickle 调用 fred(o) 并保存结果。fred(o) 必须返回一个元组 (fcon, t) 或 (fcon, t, d),其中 fcon 是一个构造函数,t 是一个元组。要重新加载 o,pickle 使用 o=fcon(*t)。然后,当 fred 还返回 d 时,pickle 使用 d 来恢复 o 的状态(如果 o 提供了 setstate,则 o.setstate(d);否则,o.dict.update(d)),如前一节所述。如果 fcon 不为 None,pickle 还会调用构造函数 (fcon) 来注册 fcon 作为构造函数。
pickle 不支持对代码对象的 pickle 操作,但 marshal 支持。以下是如何通过利用 copyreg 将 pickle 定制以支持代码对象的示例:
>>> `import` pickle, copyreg, marshal
>>> `def` marsh(x):
... `return` marshal.loads, (marshal.dumps(x),)
...
>>> c=compile('2+2','','eval')
>>> copyreg.pickle(type(c), marsh)
>>> s=pickle.dumps(c, 2)
>>> cc=pickle.loads(s)
>>> print(eval(cc))
4
使用 marshal 使你的代码依赖于 Python 版本
在你的代码中使用 marshal 时要小心,就像前面的示例一样。marshal 的序列化不能保证跨版本稳定,因此使用 marshal 意味着其他版本的 Python 编写的程序可能无法加载你的程序序列化的对象。
|
shelve 模块
shelve 模块通过协调 pickle、io 和 dbm(及其底层访问 DBM 类型归档文件的模块,如下一节所述),提供了一个简单、轻量级的持久化机制。
shelve 提供了一个函数 open,其多态性类似于 dbm.open。shelve.open 返回的映射 s 比 dbm.open 返回的映射 a 更为灵活。a 的键和值必须是字符串。³ s 的键也必须是字符串,但 s 的值可以是任何可 pickle 的类型。pickle 定制(copyreg、getnewargs、getstate 和 setstate)同样适用于 shelve,因为 shelve 将序列化工作委托给 pickle。键和值以字节形式存储。使用字符串时,在存储之前会隐式地转换为默认编码。
当你在使用 shelve 与可变对象时要小心一个微妙的陷阱:当你对一个存储在 shelf 中的可变对象进行操作时,除非将更改的对象重新分配回相同的索引,否则更改不会存储回去。例如:
`import` shelve
s = shelve.open('data')
s['akey'] = list(range(4))
print(s['akey']) *`# prints: [0, 1, 2, 3]`*
s['akey'].append(9) *`# trying direct mutation`*
print(s['akey']) *`# doesn't "take"; prints: [0, 1, 2, 3]`*
x = s['akey'] *`# fetch the object`*
x.append(9) *`# perform mutation`*
s['akey'] = x *`# key step: store the object back!`*
print(s['akey']) *`# now it "takes", prints: [0, 1, 2, 3, 9]`*
当调用 shelve.open 时,通过传递命名参数 writeback=True,可以解决这个问题,但这可能严重影响程序的性能。
一个 shelve 示例
下面的示例处理与之前的 json 和 pickle 示例相同的任务,但使用 shelve 来持久化 (filename, linenumber) 对的列表:
`import` collections, fileinput, shelve
word_pos = collections.defaultdict(list)
`for` line `in` fileinput.input():
pos = fileinput.filename(), fileinput.filelineno()
`for` word `in` line.split():
word_pos[word].append(pos)
`with` shelve.open('indexfiles','n') `as` sh_out:
sh_out.update(word_pos)
然后,我们必须使用 shelve 读取存储到类似 DBM 的文件 indexfiles 中的数据,如下例所示:
`import` sys, shelve, linecache
`with` shelve.open('indexfiles') `as` sh_in:
`for` word `in` sys.argv[1:]:
if word `not` `in` sh_in:
print(f'Word {word!r} not found in index file',
file=sys.stderr)
`continue`
places = sh_in[word]
`for` fname, lineno `in` places:
print(f'Word {word!r} occurs in line {lineno}'
f' of file {fname!r}:')
print(linecache.getline(fname, lineno), end='')
这两个示例是本节中显示的各种等效示例中最简单、最直接的示例。这反映了 shelve 比之前示例中使用的模块更高级的事实。
DBM 模块
DBM,长期以来是 Unix 的主要组成部分,是一组支持包含字节串对 (key, data) 的数据文件的库。DBM 提供了根据键快速获取和存储数据的功能,这种使用模式称为 键访问。尽管键访问远不及关系数据库的数据访问功能强大,但它的开销较小,对于某些程序的需求可能足够。如果 DBM 类似的文件对您的目的足够,通过这种方法,您可以得到一个比使用关系数据库更小更快的程序。
DBM 数据库以字节为导向
DBM 数据库要求键和值都是字节值。稍后包含的示例中,您将看到文本输入在存储之前被明确编码为 UTF-8。类似地,在读取值时必须执行逆解码。
Python 标准库中的 DBM 支持以一种清晰而优雅的方式组织:dbm 包公开了两个通用函数,同一包中还有其他模块提供特定的实现。
Berkeley DB 接口
bsddb 模块已从 Python 标准库中移除。如果您需要与 BSD DB 存档交互,我们建议使用出色的第三方包 bsddb3。
DBM 包
dbm 包提供了 表 12-6 中描述的顶层函数。
表 12-6. dbm 包的函数
| open | open(filepath, flag='r', mode=0o666) 打开或创建由 filepath(任何文件的路径)指定的 DBM 文件,并返回与 DBM 文件对应的映射对象。当 DBM 文件已经存在时,open 使用 whichdb 函数来确定哪个 DBM 子模块可以处理文件。当 open 创建新的 DBM 文件时,它会按照以下偏好顺序选择第一个可用的 dbm 子模块:gnu、ndbm、dumb。
flag 是一个告诉 open 如何打开文件以及是否创建文件的单字符字符串,根据 表 12-7 中所示的规则。mode 是一个整数,如果 open 创建文件,则 open 会将其用作文件的权限位,如 “使用 open 创建文件对象” 中所述。
表 12-7. dbm.open 的标志值
| 标志 | 只读? | 如果文件存在: | 如果文件不存在: |
| --- | --- | --- | --- |
| 'r' | 是 | 打开文件 | 报错 |
| 'w' | 否 | 打开文件 | 报错 |
| 'c' | 否 | 打开文件 | 创建文件 |
| 'n' | 否 | 截断文件 | 创建文件 |
dbm.open 返回一个映射对象m,其功能子集类似于字典(详见“Dictionary Operations”)。m只接受字节作为键和值,而且m提供的唯一非特殊映射方法是m.get、m.keys 和m.setdefault。你可以使用与字典相同的索引语法m[键]绑定、重新绑定、访问和解绑m中的项目。如果标志是'r',则m是只读的,因此你只能访问m的项目,而不能绑定、重新绑定或解绑它们。你可以使用通常的表达式s in m检查字符串s是否是m的键;你不能直接在m上进行迭代,但可以等效地在m.keys()上进行迭代。
m提供的一个额外方法是m.close,其语义与文件对象的 close 方法相同。与文件对象一样,当你使用完m后,应确保调用m.close。使用“try/finally”中介绍的try/finally语句是确保最终化的一种方式,但使用“The with Statement and Context Managers”中介绍的with语句更好(因为m是上下文管理器,你可以使用with)。 |
| | whichdb | whichdb(文件路径) 打开并读取指定的文件路径,以确定创建文件的 dbm 子模块。当文件不存在或无法打开和读取时,whichdb 返回None。当文件存在并可以打开和读取,但无法确定创建文件的 dbm 子模块时(通常意味着文件不是 DBM 文件),whichdb 返回''。如果可以找出哪个模块可以读取类似 DBM 的文件,whichdb 返回一个命名 dbm 子模块的字符串,例如'dbm.ndbm'、'dbm.dumb'或'dbm.gdbm'。 |
|---|
除了这两个顶级函数外,dbm 包还包含特定模块,如 ndbm、gnu 和 dumb,提供各种 DBM 功能的实现,通常你只通过这些顶级函数访问。第三方包可以在 dbm 中安装更多的实现模块。
DBM 包唯一保证在所有平台上存在的实现模块是 dumb。dumb 提供了最小的 DBM 功能和一般性能;其唯一优点是您可以在任何地方使用,因为 dumb 不依赖于任何库。通常您不会直接 import dbm.dumb:而是 import dbm,让 dbm.open 在当前 Python 安装中提供最好的可用 DBM 模块,如果没有更好的子模块,则默认使用 dumb。唯一需要直接导入 dumb 的情况是在需要创建一个在任何 Python 安装中都可读的类似 DBM 的文件时。dumb 模块提供了一个 open 函数,与 dbm 的 open 函数多态。
DBM-Like 文件的使用示例
DBM 的键控访问适合在程序需要持久记录类似 Python 字典的等效内容时使用,其中键和值都是字符串。例如,假设您需要分析几个文本文件,这些文件名是作为程序参数给出的,并记录每个单词在这些文件中出现的位置。在这种情况下,键是单词,因此本质上是字符串。您需要记录的每个单词的数据是一个 (filename, linenumber) 对的列表。然而,您可以通过几种方法将数据编码为字符串,例如利用路径分隔符字符串 os.pathsep(在“os 模块的路径字符串属性”中介绍),因为该字符串通常不会出现在文件名中。(关于将数据编码为字符串的更一般方法在本章开头的部分有介绍,使用了相同的例子。)在这种简化情况下,编写一个记录文件中单词位置的程序可能如下所示:
`import` collections, fileinput, os, dbm
word_pos = collections.defaultdict(list)
`for` line `in` fileinput.input():
pos = f'{fileinput.filename()}{os.pathsep}{fileinput.filelineno()}'
`for` word `in` line.split():
word_pos[word].append(pos)
sep2 = os.pathsep * 2
`with` dbm.open('indexfile','n') `as` dbm_out:
`for` word `in` word_pos:
dbm_out[word.encode('utf-8')] = sep2.join(
word_pos[word]
).encode('utf-8')
您可以通过几种方式读取存储在类似 DBM 文件 indexfile 中的数据。下面的示例接受单词作为命令行参数,并打印请求单词出现的行:
`import` sys, os, dbm, linecache
sep = os.pathsep
sep2 = sep * 2
`with` dbm.open('indexfile') `as` dbm_in:
`for` word `in` sys.argv[1:]:
e_word = word.encode('utf-8')
`if` e_word `not` `in` dbm_in:
print(f'Word {word!r} not found in index file',
file=sys.stderr)
`continue`
places = dbm_in[e_word].decode('utf-8').split(sep2)
`for` place `in` places:
fname, lineno = place.split(sep)
print(f'Word {word!r} occurs in line {lineno}'
f' of file {fname!r}:')
print(linecache.getline(fname, int(lineno)), end='')
Python 数据库 API(DBAPI)
正如前面提到的,Python 标准库并不附带关系数据库管理系统接口(除了 sqlite3,在“SQLite”中介绍的,它是一个丰富的实现,而不仅仅是接口)。许多第三方模块允许您的 Python 程序访问特定的数据库。这些模块大多遵循 Python 数据库 API 2.0 标准,也称为 DBAPI,如 PEP 249 中所述。
导入任何符合 DBAPI 标准的模块后,您可以使用特定于 DB 的参数调用模块的 connect 函数。connect 返回 x,Connection 的一个实例,表示与 DB 的连接。x 提供 commit 和 rollback 方法来处理事务,提供一个 close 方法,在您完成 DB 操作后调用,以及一个 cursor 方法来返回 c,Cursor 的一个实例。c 提供了用于 DB 操作的方法和属性。符合 DBAPI 标准的模块还提供了异常类、描述性属性、工厂函数和类型描述属性。
异常类
符合 DBAPI 标准的模块提供了异常类 Warning、Error 和 Error 的几个子类。Warning 指示插入时的数据截断等异常。Error 的子类指示您的程序在处理与 DB 和与之接口的符合 DBAPI 标准的模块时可能遇到的各种错误。通常,您的代码使用以下形式的语句:
`try`:
...
`except` module.Error `as` err:
...
以捕获您需要处理而不终止的所有与 DB 相关的错误。
线程安全
当 DBAPI 兼容的模块具有大于 0 的 threadsafety 属性时,该模块在 DB 接口中断言了某种程度的线程安全性。与依赖于此不同,通常更安全,且始终更可移植,要确保单个线程对任何给定的外部资源(如 DB)具有独占访问权,如 “线程化程序架构” 中所述。
参数样式
符合 DBAPI 标准的模块有一个称为 paramstyle 的属性,用于识别用作参数占位符的标记样式。在传递给 Cursor 实例方法(如 execute 方法)的 SQL 语句字符串中插入这些标记,以使用运行时确定的参数值。举个例子,假设您需要获取字段 AFIELD 等于 Python 变量 x 当前值的 DB 表 ATABLE 的行。假设光标实例命名为 c,您理论上(但非常不建议!)可以使用 Python 的字符串格式化执行此任务:
c.execute(f'SELECT * FROM ATABLE WHERE AFIELD={x!r}')
避免使用 SQL 查询字符串格式化:使用参数替换
字符串格式化不是推荐的方法。它为每个 x 的值生成不同的字符串,每次都需要解析和准备语句;它还存在安全弱点的可能性,如 SQL 注入 漏洞。使用参数替换,您将传递一个具有占位符而不是参数值的单个语句字符串给 execute。这样一来,execute 只需解析和准备语句一次,以获得更好的性能;更重要的是,参数替换提高了稳固性和安全性,阻碍了 SQL 注入攻击。
例如,当模块的 paramstyle 属性(下文描述)为 'qmark' 时,您可以将前面的查询表示为:
c.execute('SELECT * FROM ATABLE WHERE AFIELD=?', (some_value,))
只读字符串属性 paramstyle 告诉您的程序如何使用该模块进行参数替换。paramstyle 的可能值如 表 12-8 中所示。
表 12-8. paramstyle 属性的可能值
| format | 标记是 %s,就像旧式字符串格式化一样(始终使用 s:不要使用其他类型指示符字母,无论数据的类型是什么)。一个查询看起来像:
c.execute('SELECT * FROM ATABLE WHERE AFIELD=%s',
(some_value,))
|
| named | 标记是:name,参数是命名的。一个查询看起来像:
c.execute('SELECT * FROM ATABLE WHERE AFIELD=:x',
{'x':some_value})
|
| numeric | 标记是:n,给出参数的编号,从 1 开始。一个查询看起来像:
c.execute('SELECT * FROM ATABLE WHERE AFIELD=:1',
(some_value,))
|
| pyformat | 标记为 %(name)s,参数带有命名。始终使用 s:永不使用其他类型指示符,无论数据类型如何。查询的样子是:
c.execute('SELECT * FROM ATABLE WHERE AFIELD=%(x)s',
{'x':some_value})
|
| qmark | 标记为 ?。查询的样子是:
c.execute('SELECT * FROM ATABLE WHERE AFIELD=?', (x,))
|
当参数被命名时(即 paramstyle 是 'pyformat' 或 'named'),execute 方法的第二个参数是一个映射。否则,第二个参数是一个序列。
format 和 pyformat 只接受类型指示符 s
格式或 pyformat 的唯一有效类型指示符是 s;不接受任何其他类型指示符——例如,永远不要使用 %d 或 %(name)d。无论数据类型如何,都要使用 %s 或 %(name)s 进行所有参数替换。
工厂函数
通过占位符传递给数据库的参数通常必须是正确的类型:这意味着 Python 数字(整数或浮点数值)、字符串(字节或 Unicode)以及None表示 SQL NULL。没有普遍用于表示日期、时间和二进制大对象(BLOBs)的类型。DBAPI 兼容模块提供工厂函数来构建这些对象。大多数 DBAPI 兼容模块用于此目的的类型由 datetime 模块提供(在第 13 章中详述),并且用于 BLOBs 的类型为字符串或缓冲区类型。DBAPI 指定的工厂函数列在表 12-9 中。(*FromTicks 方法接受整数时间戳 s,表示自模块 time 纪元以来的秒数,在第 13 章中详述。)
表 12-9. DBAPI 工厂函数
| 二进制 | Binary(string) 返回表示给定字节string的对象作为 BLOB。 |
|---|---|
| 日期 | Date(year, month, day) 返回表示指定日期的对象。 |
| DateFromTicks | DateFromTicks(s) 返回表示整数时间戳 s 的日期对象。例如,DateFromTicks(time.time()) 表示“今天”。 |
| 时间 | Time(hour, minute, second) 返回表示指定时间的对象。 |
| TimeFromTicks | TimeFromTicks(s) 返回表示整数时间戳 s 的时间对象。例如,TimeFromTicks(time.time()) 表示“当前时间”。 |
| 时间戳 | Timestamp(year, month, day, hour, minute, second) 返回表示指定日期和时间的对象。 |
| TimestampFromTicks | TimestampFromTicks(s) 返回表示整数时间戳 s 的日期和时间对象。例如,TimestampFromTicks(time.time()) 是当前日期和时间。 |
类型描述属性
游标实例的 description 属性描述了您在该游标上最后执行的每个 SELECT 查询的列的类型和其他特征。每列的类型(描述列的元组的第二项)等于 DBAPI 兼容模块的以下属性之一:
| 二进制 | 描述包含 BLOBs 的列 |
|---|---|
| DATETIME | 描述包含日期、时间或两者的列 |
| NUMBER | 描述包含任何类型数字的列 |
| ROWID | 描述包含行标识号的列 |
| STRING | 描述包含任何类型文本的列 |
一个 cursor 的描述,尤其是每一列的类型,对于了解程序所使用的 DB 是非常有用的。这种内省可以帮助你编写通用的模块,并使用不同模式的表,包括在编写代码时可能不知道的模式。
connect 函数
一个 DBAPI 兼容模块的 connect 函数接受依赖于 DB 类型和具体模块的参数。DBAPI 标准建议 connect 接受命名参数。特别是,connect 至少应接受以下名称的可选参数:
| database | 要连接的具体数据库名称 |
|---|---|
| dsn | 用于连接的数据源名称 |
| host | 数据库运行的主机名 |
| password | 用于连接的密码 |
| user | 用于连接的用户名 |
Connection 对象
一个 DBAPI 兼容模块的 connect 函数返回一个对象 x,它是 Connection 类的一个实例。 x 提供了 Table 12-10 中列出的方法。
Table 12-10. 类 Connection 的实例 x 的方法
| close | x.close() 终止 DB 连接并释放所有相关资源。在完成 DB 操作后立即调用 close。不必要地保持 DB 连接开启可能会严重消耗系统资源。 |
|---|---|
| commit | x.commit() 提交当前的 DB 事务。如果 DB 不支持事务,则 x.commit() 不会有任何操作。 |
| cursor | x.cursor() 返回 Cursor 类的一个新实例(在下一节中介绍)。 |
| rollback | x.rollback() 回滚当前的 DB 事务。如果 DB 不支持事务,则 x.rollback() 会引发异常。DBAPI 建议,对于不支持事务的 DB,Connection 类不应提供回滚方法,因此 x.rollback() 会引发 AttributeError:你可以通过 hasattr(x, 'rollback') 测试是否支持事务。 |
Cursor 对象
连接实例提供了一个 cursor 方法,返回一个名为 c 的 Cursor 类实例对象。SQL 游标表示查询结果集,并允许您按顺序逐个处理该集合中的记录。由 DBAPI 建模的游标是一个更丰富的概念,因为它是程序执行 SQL 查询的唯一方式。另一方面,DBAPI 游标只允许您在结果序列中前进(一些关系型数据库,但不是所有的,还提供更高功能的游标,可以前后移动),并且不支持 SQL 子句 WHERE CURRENT OF CURSOR。DBAPI 游标的这些限制使得 DBAPI 兼容模块能够在根本不提供真正 SQL 游标的 RDBMS 上提供 DBAPI 游标。类 Cursor 的实例 c 提供了许多属性和方法;最常用的属性和方法如 Table 12-11 所示。 |
表 12-11. 类 Cursor 的实例 c 的常用属性和方法
| close | c.close() 关闭游标并释放所有相关资源。 |
|---|
| description | 只读属性,是一个由七项元组组成的序列,每项对应最后执行的查询中的一个列:名称、类型代码、显示大小、内部大小、精度、比例。
可为空
c.description 如果最后对 c 的操作不是 SELECT 查询或返回的列描述不可用,则为 None。游标的描述主要用于关于程序正在使用的数据库的内省。这种内省可以帮助您编写通用模块,能够处理使用不同模式的表,包括编写代码时可能不完全了解的模式。 |
| execute | c.execute(statement, parameters=None) 在给定参数的情况下,在 DB 上执行 SQL statement 字符串。当模块的 paramstyle 为 'format'、'numeric' 或 'qmark' 时,parameters 是一个序列;当 paramstyle 为 'named' 或 'pyformat' 时,parameters 是一个映射。某些 DBAPI 模块要求序列必须明确为元组。 |
|---|
| executemany | c.executemany(statement, *parameters) 对 DB 执行 SQL statement,对给定的 parameters 中的每个项执行一次。当模块的 paramstyle 为 'format'、'numeric' 或 'qmark' 时,parameters 是一个序列的序列;当 paramstyle 为 'named' 或 'pyformat' 时,parameters 是映射的序列。例如,当 paramstyle 为 'qmark' 时,语句:
c.executemany('UPDATE atable SET x=? '
'WHERE y=?',(12,23),(23,34))
等同于但比以下两个语句更快:
c.execute('UPDATE atable SET x=12 WHERE y=23')
c.execute('UPDATE atable SET x=23 WHERE y=34')
|
| fetchall | c.fetchall() 返回最后查询的所有剩余行作为元组序列。如果最后的操作不是 SELECT,则会引发异常。 |
|---|---|
| fetchmany | c.fetchmany(n) 返回最后查询的最多 n 行作为元组序列。如果最后的操作不是 SELECT,则会引发异常。 |
| fetchone | c.fetchone() 将从上次查询中返回下一行作为元组。如果上次操作不是 SELECT,则引发异常。 |
| rowcount | 一个只读属性,指定了最后一个操作获取或影响的行数,如果模块无法确定这个值,则为 -1。 |
DBAPI 兼容模块
无论你想使用哪种关系型数据库,至少有一个(通常是多个)Python DBAPI 兼容模块可以从互联网上下载。有这么多的数据库和模块,可能性的集合如此不断变化,我们不可能列出所有的,也无法长期维护这个列表。因此,我们建议你从社区维护的 wiki 页面 开始,这个页面有可能在任何时候都是完整和最新的。
因此,接下来只是一个非常短暂且特定时间的,与撰写时非常流行且与非常流行的开源数据库接口的非常少量 DBAPI 兼容模块的列表:
ODBC 模块
Open Database Connectivity (ODBC) 是连接许多不同的数据库的标准方法,包括一些其他 DBAPI 兼容模块不支持的数据库。对于具有自由开源许可的 ODBC 兼容 DBAPI 兼容模块,请使用 pyodbc;对于商业支持的模块,请使用 mxODBC。
MySQL 模块
MySQL 是一个流行的开源关系型数据库管理系统,于 2010 年被 Oracle 收购。Oracle 提供的“官方”DBAPI 兼容接口是 mysql-connector-python。MariaDB 项目也提供了一个 DBAPI 兼容接口,mariadb,连接到 MySQL 和 MariaDB(一个 GPL 许可的分支)。
PostgreSQL 模块
PostgreSQL 是另一个流行的开源关系型数据库管理系统。它的一个广泛使用的 DBAPI 兼容接口是 psycopg3,它是受欢迎的 psycopg2 包的合理化重写和扩展。
SQLite
SQLite 是一个用 C 编写的库,实现了一个关系型数据库,可以存储在单个文件中,甚至在内存中用于足够小和短暂的情况。Python 的标准库提供了 sqlite3 包,它是与 SQLite 兼容的 DBAPI 接口。
SQLite 具有丰富的高级功能,有许多选项可供选择;sqlite3 提供了访问这些功能的大部分能力,同时提供更多可能性,使得 Python 代码与底层数据库之间的交互更加平稳和自然。我们无法在本书中涵盖这两个强大软件系统的每一个细节;相反,我们专注于最常用和最有用的函数子集。要获取更详细的信息,包括示例和最佳实践建议,请参阅 SQLite 和 sqlite3 的文档,以及 Jay Kreibich 的 Using SQLite(O’Reilly)。
sqlite3 包还提供了 表 12-12 中的函数,以及其他函数。
表 12-12. sqlite3 模块的一些有用函数
| connect | connect(filepath, timeout=5.0, detect_types=0, isolation_level='', check_same_thread=True, factory=Connection, cached_statements=100, uri=False) 连接到名为 filepath 的 SQLite 数据库文件(如果需要则创建),并返回 Connection 类的实例(或传递的子类)。要创建内存中的数据库,请将 ':memory:' 作为第一个参数传递给 filepath。
如果 True,uri 参数激活 SQLite 的 URI 功能,允许通过 filepath 参数一起传递一些额外的选项。
timeout 是在事务中有另一个连接锁定数据库时等待抛出异常之前的秒数。
sqlite3 直接支持以下 SQLite 原生类型,将其转换为相应的 Python 类型:
-
BLOB:转换为字节
-
INTEGER:转换为整数
-
NULL:转换为 None
-
REAL:转换为浮点数
-
TEXT:取决于 Connection 实例的 text_factory 属性,在 表 12-13 中有所涉及;默认为 str
除此以外的任何类型名称被视为 TEXT,除非经过适当的检测并通过函数 register_converter 注册的转换器传递。为了允许类型名称检测,可以传递 detect_types 参数,该参数可以是 sqlite3 包提供的 PARSE_COLNAMES 或 PARSE_DECLTYPES 常量(或两者都使用,通过 | 位 OR 运算符连接)。
当你传递 detect_types=sqlite3.PARSE_COLNAMES 时,类型名称取自于 SQL SELECT 语句中检索列的列名;例如,检索为 foo AS [foo CHAR(10)] 的列具有类型名称 CHAR。
当你传递 detect_types=sqlite3.PARSE_DECLTYPES 时,类型名称取自于原始 CREATE TABLE 或 ALTER TABLE SQL 语句中添加列的声明;例如,声明为 foo CHAR(10) 的列具有类型名称 CHAR。
当你传递 detect_types=sqlite3.PARSE_COLNAMES | sqlite3.PARSE_DECLTYPES 时,两种机制都会被使用,优先使用列名(当列名至少有两个单词时,第二个单词在这种情况下给出类型名),如果没有则回退到声明时给定的类型(在这种情况下,声明类型的第一个单词给出类型名)。
isolation_level 允许你在 SQLite 处理事务时行使一些控制;它可以是 ''(默认值)、None(使用 autocommit 模式)或三个字符串之一:'DEFERRED'、'EXCLUSIVE' 或 'IMMEDIATE'。SQLite 在线文档详细介绍了 事务类型 及其与 SQLite 固有执行的各种 文件锁定 的关系。
| 连接 (续) | 默认情况下,连接对象只能在创建它的 Python 线程中使用,以避免因程序中的轻微错误(多线程编程中常见的不幸问题)而导致数据库损坏。如果你对线程在使用锁和其他同步机制方面完全自信,并且需要在多个线程之间重用连接对象,你可以传递 check_same_thread=False。sqlite3 将不再执行任何检查,相信你知道自己在做什么,并且你的多线程架构没有任何缺陷——祝你好运!cached_statements 是 sqlite3 缓存的 SQL 语句数量,以解析和准备的状态保存,以避免重复解析它们所带来的开销。你可以传递一个低于默认值 100 的值以节省一些内存,或者如果你的应用程序使用了多种多样的 SQL 语句,可以传递一个更大的值。 |
|---|---|
| register_adapter | register_adapter(type, callable) 将 callable 注册为从任何 Python 类型 type 的对象到 sqlite3 直接处理的几种 Python 类型之一(int、float、str 和 bytes)的相应值的 adapter。callable 必须接受一个参数,即要适配的值,并返回一个 sqlite3 直接处理的类型的值。 |
| register_converter | register_converter(typename, callable) 将 callable 注册为从 SQL 中标识为某种 typename 类型的值(请参阅 connect 函数的 detect_types 参数的描述,了解类型名是如何确定的)到相应 Python 对象的 converter。callable 必须接受一个参数,即从 SQL 获取的值的字符串形式,并返回相应的 Python 对象。typename 的匹配区分大小写。 |
另外,sqlite3 还提供了 Connection、Cursor 和 Row 类。每个类都可以进一步定制为子类;但这是一个我们在本书中不再详细讨论的高级主题。Cursor 类是标准的 DBAPI 游标类,除了一个额外的便利方法 executescript,接受一个参数,即由多条语句以 ; 分隔的字符串(无参数)。其他两个类将在接下来的章节中介绍。
sqlite3.Connection 类
除了所有符合 DBAPI 标准模块 Connection 类通用的方法外,详情请参见 “Connection Objects”,sqlite3.Connection 还提供了 Table 12-13 中的方法和属性。
Table 12-13. sqlite3.Connection 类的附加方法和属性
| create_aggregate | create_aggregate(name, num_params, aggregate_class) aggregate_class 必须是一个类,提供两个实例方法:step,接受确切 num_param 个参数;finalize,不接受参数,并返回聚合的最终结果,即 sqlite3 原生支持的类型的值。这个聚合函数可以通过给定的 name 在 SQL 语句中使用。 |
|---|---|
| create_collation | create_collation(name, callable) callable 必须接受两个字节字符串参数(编码为 'utf-8'),如果第一个参数应被视为“小于”第二个参数,则返回 -1;如果应被视为“大于”,则返回 1;如果应被视为“等于”,则返回 0。此类排序规则可以在 SQL SELECT 语句的 ORDER BY 子句中以 name 命名使用。 |
| create_function | create_function(name, num_params, func) func 必须接受确切 num_params 个参数,并返回 sqlite3 原生支持的类型的值;这样的用户定义函数可以通过给定的 name 在 SQL 语句中使用。 |
| interrupt | interrupt() 可以从任何其他线程调用,以中止在此连接上执行的所有查询(在使用连接的线程中引发异常)。 |
| isolation_level | 一个只读属性,其值为连接函数的 isolation_level 参数提供的值。 |
| iterdump | iterdump() 返回一个迭代器,生成字符串:构建当前数据库的 SQL 语句,包括模式和内容。例如,可用于将内存中的数据库持久化到磁盘以供将来重用。 |
| row_factory | 一个可调用对象,接受游标和原始行作为元组,并返回用作真实结果行的对象。一个常见的惯用法是 x.row_factory=sqlite3.Row,使用在下一节详细介绍的高度优化的 Row 类,提供基于索引和不区分大小写的名称访问列,几乎没有额外开销。 |
| text_factory | 一个接受单一字节字符串参数并返回用于 TEXT 列值的对象的可调用函数——默认为 str,但你可以设置为任何类似的可调用函数。 |
| total_changes | 自连接创建以来已修改、插入或删除的总行数。 |
连接对象还可以作为上下文管理器使用,以自动提交数据库更新或在发生异常时回滚;然而,在这种情况下,你需要显式调用 Connection.close() 来关闭连接。
sqlite3.Row 类
sqlite3 还提供了 Row 类。Row 对象大部分类似于元组,但还提供了 keys 方法,返回列名列表,并支持通过列名而不是列编号进行索引。
一个 sqlite3 示例
下面的示例处理与本章早些时候展示的示例相同的任务,但使用 sqlite3 进行持久化,而不是在内存中创建索引:
`import` fileinput, sqlite3
connect = sqlite3.connect('database.db')
cursor = connect.cursor()
`with` connect:
cursor.execute('CREATE TABLE IF NOT EXISTS Words '
'(Word TEXT, File TEXT, Line INT)')
`for` line `in` fileinput.input():
f, l = fileinput.filename(), fileinput.filelineno()
cursor.executemany('INSERT INTO Words VALUES (:w, :f, :l)',
[{'w':w, 'f':f, 'l':l} `for` w `in` line.split()])
connect.close()
然后我们可以使用 sqlite3 读取存储在 DB 文件 database.db 中的数据,如下例所示:
`import` sys, sqlite3, linecache
connect = sqlite3.connect('database.db')
cursor = connect.cursor()
`for` word `in` sys.argv[1:]:
cursor.execute('SELECT File, Line FROM Words '
'WHERE Word=?', [word])
places = cursor.fetchall()
`if` `not` places:
print(f'Word {word!r} not found in index file',
file=sys.stderr)
`continue`
`for` fname, lineno `in` places:
print(f'Word {word!r} occurs in line {lineno}'
f' of file {fname!r}:')
print(linecache.getline(fname, lineno), end='')
connect.close()
¹ 实际上,“CSV” 有点名不副实,因为一些方言使用制表符或其他字符作为字段分隔符,而不是逗号。更容易将它们视为“分隔符分隔的值”。
² 如果你需要在此及其他方面扩展 pickle,请考虑第三方包 dill。
³ dbm 的键和值必须是字节;shelve 将接受字节或 str,并且会透明地对字符串进行编码。
第十三章:时间操作
Python 程序可以以多种方式处理时间。时间间隔是以秒为单位的浮点数(时间间隔的小数部分是间隔的小数部分):所有标准库函数接受以秒为单位表示时间间隔的参数,接受浮点数作为该参数的值。时间中的瞬间是自某个参考瞬间以来的秒数,称为* epoch *。(尽管每种语言和每个平台的 epoch 有所不同,但在所有平台上,Python 的 epoch 是 UTC 时间,1970 年 1 月 1 日午夜。)时间瞬间通常还需要以多种单位(例如年、月、日、小时、分钟和秒)的混合形式表示,特别是用于输入输出目的。当然,输入输出还需要能够将时间和日期格式化为人类可读的字符串,并从字符串格式解析它们回来。
时间模块
时间模块在某种程度上依赖于底层系统的 C 库,这限定了时间模块可以处理的日期范围。在旧的 Unix 系统中,1970 年和 2038 年是典型的截止点¹(这个限制可以通过使用 datetime 避免,后文将讨论)。时间点通常以 UTC(协调世界时,曾称为 GMT 或格林尼治平均时间)指定。时间模块还支持本地时区和夏令时(DST),但仅限于底层 C 系统库支持的范围²。
作为秒数自纪元以来的一个替代方法,时间点可以用一个包含九个整数的元组表示,称为* timetuple *(在 Table 13-1 中有介绍)。所有项目都是整数:timetuples 不跟踪秒的小数部分。一个 timetuple 是 struct_time 的一个实例。你可以将其用作元组;更有用的是,你可以通过只读属性访问项目,如 x.tm_year,x.tm_mon 等等,属性名称在 Table 13-1 中列出。在任何需要 timetuple 参数的函数中,你都可以传递 struct_time 的实例或任何其他项目是九个整数且范围正确的序列(表中的所有范围都包括下限和上限,都是包容的)。
表 13-1 元组形式的时间表示
| 项目 | 含义 | 字段名 | 范围 | 备注 |
|---|---|---|---|---|
| 0 | Year | tm_year | 1970–2038 | 有些平台支持 0001–9999 |
| 1 | Month | tm_mon | 1–12 | 1 代表一月;12 代表十二月 |
| 2 | Day | tm_mday | 1–31 | |
| 3 | 小时 | tm_hour | 0–23 | 0 表示午夜;12 表示中午 |
| 4 | 分钟 | tm_min | 0–59 | |
| 5 | 秒 | tm_sec | 0–61 | 60 和 61 表示闰秒 |
| 6 | 星期几 | tm_wday | 0–6 | 0 表示星期一;6 表示星期日 |
| 7 | 年内天数 | tm_yday | 1–366 | 年内的日期编号 |
| 8 | 夏令时标志 | tm_isdst | −1–1 | −1 表示库确定夏令时 |
要将“自纪元以来的秒数”浮点值转换为时间元组,请将浮点值传递给函数(例如 localtime),该函数返回所有九个有效项目的时间元组。在反向转换时,mktime 忽略元组的多余项目 6(tm_wday)和 7(tm_yday)。在这种情况下,通常将项目 8(tm_isdst)设置为 −1,以便 mktime 自行确定是否应用 DST。
time 提供了 表 13-2 中列出的函数和属性。
表 13-2. time 模块的函数和属性
| asctime | asctime([tupletime]) 接受时间元组并返回可读的 24 字符串,例如 'Sun Jan 8 14:41:06 2017'。调用 asctime() 无参数相当于调用 asctime(time.localtime())(格式化当前本地时间)。 |
|---|---|
| ctime | ctime([secs]) 类似于 asctime(localtime(secs)),接受以自纪元以来的秒数表示的瞬时,并返回该瞬时的可读的 24 字符串形式,以本地时间显示。调用 ctime() 无参数相当于调用 asctime()(格式化当前本地时间)。 |
| gmtime | gmtime([secs]) 接受以自纪元以来的秒数表示的瞬时,并返回 UTC 时间的时间元组 t(t.tm_isdst 总是 0)。调用 gmtime() 无参数相当于调用 gmtime(time())(返回当前时间瞬时的时间元组)。 |
| localtime | localtime([secs]) 接受自纪元以来经过的秒数的瞬时,并返回本地时间的时间元组 t(t.tm_isdst 根据本地规则应用于瞬时 secs 是 0 或 1)。调用 localtime() 无参数相当于调用 localtime(time())(返回当前时间瞬时的时间元组)。 |
| mktime | mktime(tupletime) 接受作为本地时间的时间元组表示的瞬时,并返回以自纪元以来的秒数表示的浮点值(即使在 64 位系统上,只接受 1970–2038 之间的有限纪元日期,而不是扩展范围)^(a)。tupletime 中的最后一项 DST 标志具有意义:将其设置为 0 以获取标准时间,设置为 1 以获取 DST,或设置为 −1 让 mktime 计算给定瞬时时是否适用 DST。 |
| monotonic | monotonic() 类似于 time(),返回当前时间瞬时的浮点数秒数;然而,保证时间值在调用之间不会后退,即使系统时钟调整(例如由于闰秒或在切换到或从 DST 时刻)。 |
| perf_counter | perf_counter() 用于测量连续调用之间的经过时间(如秒表),perf_counter 返回使用最高分辨率时钟得到的秒数值,以获取短时间内的精确度。它是系统范围的,并且在休眠期间也包括经过的时间。只使用连续调用之间的差异,因为没有定义的参考点。 |
| process_time | process_time() 像 perf_counter 一样;但是,返回的时间值是进程范围的,并且不包括在休眠期间经过的时间。仅使用连续调用之间的差异,因为没有定义的参考点。 |
| sleep | sleep(secs) 暂停调用线程* secs 秒。如果是主线程并且某些信号唤醒了它,则在 secs 秒(当它是唯一准备运行的当前线程时)之前或更长时间的暂停之后,调用线程可能会再次开始执行(取决于进程和线程的系统调度)。您可以将 secs *设置为 0 调用 sleep,以便为其他线程提供运行机会,如果当前线程是唯一准备运行的线程,则不会造成显著延迟。 |
| strftime | strftime(fmt[, tupletime]) 接受表示本地时间的时间元组* tupletime ,并返回字符串,该字符串表示按 fmt 指定的即时时间。如果省略 tupletime ,strftime 使用本地时间(time())(格式化当前即时时间)。 fmt 的语法类似于“使用%进行传统字符串格式化”,尽管转换字符不同,如表 13-3 所示。参考 tupletime *指定的时间即时;格式无法指定宽度和精度。
例如,你可以使用 asctime 格式(例如,'Tue Dec 10 18:07:14 2002')获取日期,格式字符串为'%a %b %d %H:%M:%S %Y'。
您可以使用格式字符串'%a, %d %b %Y %H:%M:%S %Z'获取与 RFC 822 兼容的日期(例如'Tue, 10 Dec 2002 18:07:14 EST')。
这些字符串也可用于使用“用户编码类的格式化”中讨论的机制进行日期时间格式化,允许您等效地编写,对于 datetime.datetime 对象 d,可以写为 f'{d:%Y/%m/%d}'或'{:%Y/%m/%d}'.format(d),两者都会给出例如'2022/04/17'的结果。对于 ISO 8601 格式的日期时间,请参阅“日期类”中涵盖的 isoformat()和 fromisoformat()方法。|
| strptime | strptime(str, fmt) 根据格式字符串fmt(例如'%a %b %d %H:%M:%S %Y',详见 strftime 讨论)解析str,并返回时间元组作为即时。如果未提供时间值,默认为午夜。如果未提供日期值,默认为 1900 年 1 月 1 日。例如:
>>> print(time.strptime("Sep 20, 2022", '%b %d, %Y'))
time.struct_time(tm_year=2022, tm_mon=9, tm_mday=20,
tm_hour=0, tm_min=0, tm_sec=0, tm_wday=1,
tm_yday=263, tm_isdst=-1)
|
| time | time() 返回当前时间即时,一个从纪元以来的浮点数秒数。在一些(主要是较旧的)平台上,此时间的精度低至一秒。如果系统时钟在调用之间向后调整(例如由于闰秒),则可能在后续调用中返回较低的值。 |
|---|---|
| timezone | 本地时区(无夏令时)与 UTC 的偏移量(<0 为美洲;>=0 为大部分欧洲、亚洲和非洲)。 |
| tzname | 本地时间区域依赖的一对字符串,即无夏令时和有夏令时的本地时区名称。 |
| ^(a) mktime 的结果小数部分总是 0,因为其 timetuple 参数不考虑秒的小数部分。 |
表 13-3. strftime 的转换字符
| 类型字符 | 含义 | 特殊说明 |
|---|---|---|
| a | 星期几名称(缩写) | 取决于区域设置 |
| A | 星期几名称(完整) | 取决于区域设置 |
| b | 月份名称(缩写) | 取决于区域设置 |
| B | 月份名称(完整) | 取决于区域设置 |
| c | 完整的日期和时间表示 | 取决于区域设置 |
| d | 月份中的第几天 | 从 1 到 31 |
| f | 微秒数以小数形式,零填充到六位数 | 一到六位数字 |
| G | ISO 8601:2000 标准的基于周的年份编号 | |
| H | 小时数(24 小时制钟) | 从 0 到 23 |
| I | 小时数(12 小时制钟) | 从 1 到 12 |
| j | 年份中的第几天 | 从 1 到 366 |
| m | 月份编号 | 从 1 到 12 |
| M | 分钟数 | 从 0 到 59 |
| p | 上午或下午的等价项 | 取决于区域设置 |
| S | 秒数 | 从 0 到 61 |
| u | 星期几 | 星期一为 1,最多为 7 |
| U | 周数(以星期天为第一天) | 从 0 到 53 |
| V | ISO 8601:2000 标准的基于周的周数 | |
| w | 星期几编号 | 0 表示星期天,最大为 6 |
| W | 周数(以星期一为第一天) | 从 0 到 53 |
| x | 完整的日期表示 | 取决于区域设置 |
| X | 完整的时间表示 | 取决于区域设置 |
| y | 世纪内的年份编号 | 从 0 到 99 |
| Y | 年份编号 | 从 1970 到 2038,或更宽 |
| z | UTC 偏移量作为字符串:±HHMM[SS[.ffffff]] | |
| Z | 时区名称 | 如果不存在时区则为空 |
| % | 字面上的 % 字符 | 编码为 %% |
datetime 模块
datetime 提供了用于建模日期和时间对象的类,这些对象可以是有意识的时区或无意识的(默认)。类 tzinfo 的实例用于建模时区,是抽象的:datetime 模块仅提供一个简单的实现 datetime.timezone(更多详细信息,请参阅在线文档)。在下一节中讨论的 zoneinfo 模块提供了 tzinfo 的更丰富的具体实现,它允许您轻松创建时区感知的 datetime 对象。datetime 中的所有类型都有不可变的实例:属性是只读的,实例可以是字典中的键或集合中的项,所有函数和方法都返回新对象,从不改变作为参数传递的对象。
date 类
date 类的实例表示一个日期(特定日期内的无特定时间),满足 date.min <= d <= date.max,始终是无意识的,并假设格里高利历始终有效。date 实例具有三个只读整数属性:year、month 和 day。此类的构造函数签名如下:
| date | class date(year, month, day) 返回给定 year、month 和 day 参数的日期对象,有效范围为 1 <= year <= 9999,1 <= month <= 12,以及 1 <= day <= n,其中 n 是给定月份和年份的天数。如果给出无效值,则引发 ValueError。 |
|---|
日期类还提供了作为替代构造函数可用的三个类方法,列在 表 13-4 中。
表 13-4. 替代日期构造函数
| fromordinal | date.fromordinal(ordinal) 返回一个日期对象,对应于普通格里高利纪元中的 ordinal,其中值为 1 对应于公元 1 年的第一天。 |
|---|---|
| fromtimestamp | date.fromtimestamp(timestamp) 返回一个日期对象,对应于自纪元以来以秒表示的 timestamp。 |
| today | date.today() 返回表示今天日期的日期对象。 |
日期类的实例支持一些算术操作。日期实例之间的差异是一个 timedelta 实例;您可以将 timedelta 添加到日期实例或从日期实例中减去 timedelta 以创建另一个日期实例。您也可以比较日期类的任意两个实例(后面的日期较大)。
类 date 的实例 d 提供了列表中列出的方法,详见 表 13-5。
表 13-5. 类 date 的实例 d 的方法
| ctime | d.ctime() 返回一个字符串,表示日期 d 的格式与 time.ctime 中的 24 字符格式相同(日期设置为 00:00:00,午夜)。 |
|---|---|
| isocalendar | d.isocalendar() 返回一个包含三个整数的元组(ISO 年份、ISO 周数和 ISO 工作日)。更多关于 ISO(国际标准化组织)日历的详细信息,请参见 ISO 8601 标准。 |
| isoformat | d.isoformat() 返回一个字符串,表示日期 d 的格式为 'YYYY-MM-DD';与 str(d) 相同。 |
| isoweekday | d.isoweekday() 返回日期 d 的星期几作为整数,星期一为 1,星期日为 7;类似于 d.weekday() + 1。 |
| replace | d.replace(year=None, month=None, day=None) 返回一个新的日期对象,类似于 d,但显式指定为参数的那些属性被替换。例如:
date(x,y,z).replace(month=m) == date(x,m,z)
|
| strftime | d.strftime(fmt) 返回一个字符串,表示日期 d 按字符串 fmt 指定的格式。例如:
time.strftime(*`fmt`*, *`d`*.timetuple())
|
| timetuple | d.timetuple() 返回一个 timetuple,对应于日期 d 的时间为 00:00:00(午夜)。 |
|---|
| toordinal | d.toordinal() 返回日期 d 的普通格里高利日期。例如:
date(1,1,1).toordinal() == 1
|
| weekday | d.weekday() 返回日期 d 的星期几作为整数,星期一为 0,星期日为 6;类似于 d.isoweekday() - 1。 |
|---|
时间类
时间类的实例表示一天中的时间(没有特定的日期),可以是关于时区的明确或无意识的,并且总是忽略闰秒。它们有五个属性:四个只读整数(小时、分钟、秒和微秒)和一个可选的只读 tzinfo 属性(无意识实例的情况下为 None)。时间类的构造函数的签名为:
| time | class time(hour=0, minute=0, second=0, microsecond=0, tzinfo=None) 类时间的实例不支持算术运算。可以比较两个时间实例(时间较晚的为较大),但仅当它们都是明确或都是无意识的时才行。 |
|---|
类时间的实例 t 提供了 表 13-6 中列出的方法。
表 13-6. 类时间实例 t 的方法
| isoformat | t.isoformat() 返回一个表示时间 t 的字符串,格式为 'HH:MM:SS';与 str(t) 相同。如果 t.microsecond != 0,则结果字符串较长:'HH:MM:SS.mmmmmm'。如果 t 是明确的,则在末尾添加六个字符 '+HH:MM',表示时区与 UTC 的偏移量。换句话说,此格式化操作遵循 ISO 8601 标准。 |
|---|
| replace | t.replace(hour=None, minute=None, second=None, microsecond=None[, tzinfo]) 返回一个新的时间对象,类似于 t,除了那些显式指定为参数的属性将被替换。例如:
time(x,y,z).replace(minute=m) == time(x,m,z)
|
| strftime | t.strftime(fmt) 返回一个字符串,表示按照字符串 fmt 指定的时间 t。 |
|---|
类时间的实例 t 还提供了方法 dst、tzname 和 utcoffset,它们不接受参数并委托给 t.tzinfo,当 t.tzinfo 为 None 时返回 None。
日期时间类
日期时间类的实例表示一个瞬间(一个日期,在该日期内具体的时间),可以是关于时区的明确或无意识的,并且总是忽略闰秒。日期时间扩展了日期并添加了时间的属性;它的实例有只读整数属性年、月、日、小时、分钟、秒和微秒,以及一个可选的 tzinfo 属性(无意识实例的情况下为 None)。此外,日期时间实例有一个只读的 fold 属性,用于在时钟回滚期间区分模糊的时间戳(例如夏令时结束时的“回退”,在凌晨 1 点到 2 点之间创建重复的无意识时间)。fold 取值为 0 或 1,0 对应于回滚前的时间;1 对应于回滚后的时间。
日期时间实例支持一些算术运算:两个日期时间实例之间的差异(均为明确或均为无意识)是一个 timedelta 实例,并且可以将 timedelta 实例添加到或从日期时间实例中减去以构造另一个日期时间实例。可以比较两个日期时间类的实例(较晚的为较大),只要它们都是明确或都是无意识的。此类的构造函数的签名为:
| datetime | class datetime(year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0) 返回一个日期时间对象,遵循与日期类构造函数类似的约束。fold 是一个整数,值为 0 或 1,如前所述。 |
|---|
datetime 还提供一些类方法,可用作替代构造函数,详见 Table 13-7。
Table 13-7. 替代日期时间构造函数
| combine | datetime.combine(date, time) 返回一个日期时间对象,日期属性来自 date,时间属性(包括时区信息)来自 time。datetime.combine(d, t) 的作用类似于: |
datetime(d.year, d.month, d.day,
t.hour, t.minute, t.second,
t.microsecond, t.tzinfo)
|
| fromordinal | datetime.fromordinal(ordinal) 返回一个日期对象,表示普通格里高利历的序数日期 ordinal,其中值为 1 表示公元 1 年的第一天午夜。 |
|---|---|
| fromtimestamp | datetime.fromtimestamp(timestamp, tz=None) 返回一个日期时间对象,表示自纪元以来经过的秒数 timestamp 对应的时刻,以本地时间表示。当 tz 不是 None 时,返回一个带有给定 tzinfo 实例 tz 的带时区信息的日期时间对象。 |
| now | datetime.now(tz=None) 返回一个表示当前本地日期和时间的无时区信息的日期时间对象。当 tz 不是 None 时,返回一个带有给定 tzinfo 实例 tz 的带时区信息的日期时间对象。 |
| strptime | datetime.strptime(str, fmt) 返回一个日期时间对象,表示按照字符串 fmt 指定的格式解析的 str。当 fmt 中包含 %z 时,生成的日期时间对象是带时区信息的。 |
| today | datetime.today() 返回一个表示当前本地日期和时间的无时区信息的日期时间对象;与 now 类方法相同,但不接受可选参数 tz。 |
| utcfromtimestamp | datetime.utcfromtimestamp(timestamp) 返回一个表示自纪元以来经过的秒数 timestamp 对应时刻的无时区信息的日期时间对象,使用的是协调世界时(UTC)。 |
| utcnow | datetime.utcnow() 返回一个表示当前日期和时间的无时区信息的日期时间对象,使用的是协调世界时(UTC)。 |
日期时间实例 d 还提供了 Table 13-8 中列出的方法。
Table 13-8. datetime 实例 d 的方法
| astimezone | d.astimezone(tz) 返回一个新的带时区信息的日期时间对象,类似于 d,但日期和时间与时区 tz 一起转换。^(a) d 必须是带时区信息的,以避免潜在的 bug。传递一个无时区信息的 d 可能导致意外结果。 |
|---|---|
| ctime | d.ctime() 返回一个字符串,表示与 d 的日期时间在与 time.ctime 相同的 24 字符格式中。 |
| date | d.date() 返回一个表示与 d 相同日期的日期对象。 |
| isocalendar | d.isocalendar() 返回一个包含三个整数的元组(ISO 年份、ISO 周号和 ISO 工作日),表示 d 的日期。 |
| isoformat | d.isoformat(sep='T') 返回一个字符串,表示d的格式为'YYYY-MM-DDxHH:MM:SS',其中x是参数 sep 的值(必须是长度为 1 的字符串)。如果d.microsecond != 0,则在字符串的'SS'部分之后添加七个字符'.mmmmmm'。如果t是已知的,则在最后添加六个字符'+HH:MM',以表示时区与 UTC 的偏移量。换句话说,此格式化操作遵循 ISO 8601 标准。str(d)与d.isoformat(sep=' ')相同。 |
| isoweekday | d.isoweekday() 返回d日期的星期几,返回一个整数,星期一为 1,星期日为 7。 |
| replace | d.replace(year=None, month=None, day=None, hour=None, minute=None, second=None, microsecond=None, tzinfo=None,, fold=0) 返回一个新的 datetime 对象,类似于d*,但指定为参数的那些属性被替换(但不进行任何时区转换——如果要转换时间,请使用 astimezone)。您还可以使用 replace 从 naive 创建一个已知的 datetime 对象。例如:
*`# create datetime replacing just month with no`*
*`# other changes (== datetime(x,m,z))`*
datetime(x,y,z).replace(month=m)
*`# create aware datetime from naive datetime.now()`*
*`d`* = datetime.now().replace(tzinfo=ZoneInfo(
"US/Pacific"))
|
| strftime | d.strftime(fmt) 返回一个字符串,表示根据格式字符串fmt指定的格式显示的d。 |
|---|---|
| time | d.time() 返回一个表示与d相同一天中的时间的 naive 时间对象。 |
| timestamp | d.timestamp() 返回自纪元以来的秒数的浮点数。假设 naive 实例处于本地时区。 |
| timetuple | d.timetuple() 返回与时刻d对应的时间元组。 |
| timetz | d.timetz() 返回一个时间对象,表示与d相同的一天中的时间,具有相同的时区信息。 |
| toordinal | d.toordinal() 返回d日期的公历序数。例如:
datetime(1, 1, 1).toordinal() == 1 |
| utctimetuple | d.utctimetuple() 返回一个时间元组,对应于时刻d,如果d是已知的,则规范化为 UTC。 |
|---|---|
| weekday | d.weekday() 返回d日期的星期几,返回一个整数,星期一为 0,星期日为 6。 |
| ^(a) 请注意d.astimezone(tz)与d.replace(tzinfo=tz)非常不同:replace 不进行时区转换,而只是复制了d的所有属性,但d.tzinfo 除外。 |
类 datetime 的实例d还提供了方法 dst、tzname 和 utcoffset,这些方法不接受参数,并委托给d.tzinfo,在d.tzinfo 为None(即d是 naive 时)时返回None。
timedelta 类
timedelta 类的实例表示具有三个只读整数属性的时间间隔:days、seconds 和 microseconds。此类的构造函数的签名为:
| timedelta | timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0) 将所有单位按照明显的因子转换(一周为 7 天,一小时为 3,600 秒等),并将一切标准化为三个整数属性,确保 0 <= seconds < 24 * 60 * 60 且 0 <= microseconds < 1000000。例如:
>>> print(repr(timedelta(minutes=0.5)))
datetime.timedelta(days=0, seconds=30)
>>> print(repr(timedelta(minutes=-0.5)))
datetime.timedelta(days=-1, seconds=86370)
timedelta 的实例支持算术运算:与 timedelta 类型的实例之间的 + 和 -;与整数之间的 *;与整数和 timedelta 实例之间的 /(地板除法、真除法、divmod、%)。它们还支持彼此之间的比较。 |
虽然可以使用此构造函数创建 timedelta 实例,但更常见的是通过两个日期、时间或 datetime 实例相减创建 timedelta,使得结果 timedelta 表示经过的时间段。timedelta 的实例 td 提供了一个方法 td.total_seconds(),返回表示 timedelta 实例的总秒数的浮点数。
tzinfo 抽象类
tzinfo 类定义了在表 13-9 中列出的抽象类方法,用于支持创建和使用带有时区意识的 datetime 和 time 对象。
表 13-9. tzinfo 类的方法
| dst | dst(dt) 返回给定 datetime 的夏令时偏移量,作为 timedelta 对象 |
|---|---|
| tzname | tzname(dt) 返回给定 datetime 的时区缩写 |
| utcoffset | utcoffset(dt) 返回给定 datetime 的与 UTC 的偏移量,作为 timedelta 对象 |
tzinfo 还定义了一个 fromutc 抽象实例方法,主要供 datetime.astimezone 方法内部使用。
timezone 类
timezone 类是 tzinfo 类的具体实现。您可以使用表示与 UTC 时间偏移量的 timedelta 构造一个 timezone 实例。timezone 提供一个类属性 utc,代表 UTC 时区(相当于 timezone(timedelta(0)))。
zoneinfo 模块
3.9+ zoneinfo 模块是 datetime 的 tzinfo 的具体实现,用于时间区域的表示。³ zoneinfo 默认使用系统的时区数据,以 tzdata 作为后备。(在 Windows 上,您可能需要 pip install tzdata;一旦安装完成,您不需要在程序中导入 tzdata—zoneinfo 会自动使用它。)
zoneinfo 提供一个类:ZoneInfo,它是 datetime.tzinfo 抽象类的具体实现。您可以在创建带有时区意识的 datetime 实例时将其赋给 tzinfo,或者在 datetime.replace 或 datetime.astimezone 方法中使用它。要构造 ZoneInfo,请使用定义的 IANA 时区名称之一,例如 "America/Los_Angeles" 或 "Asia/Tokyo"。您可以通过调用 zoneinfo.available_timezones() 获取这些时区名称的列表。更多有关每个时区的详细信息(例如与 UTC 的偏移和夏令时信息)可以在Wikipedia上找到。
这里有一些使用 ZoneInfo 的示例。我们将从获取加州当前本地日期和时间开始:
>>> `from` datetime `import` datetime
>>> `from` zoneinfo `import` ZoneInfo
>>> d=datetime.now(tz=ZoneInfo("America/Los_Angeles"))
>>> d
datetime.datetime(2021,10,21,16,32,23,96782,tzinfo=zoneinfo.ZoneInfo(key
='America/Los_Angeles'))
现在我们可以将时区更新为另一个时区,而不改变其他属性(即不将时间转换为新时区):
>>> dny=d.replace(tzinfo=ZoneInfo("America/New_York"))
>>> dny
datetime.datetime(2021,10,21,16,32,23,96782,tzinfo=zoneinfo.ZoneInfo(key
='America/New_York'))
将 datetime 实例转换为 UTC:
>>> dutc=d.astimezone(tz=ZoneInfo("UTC"))
>>> dutc
datetime.datetime(2021,10,21,23,32,23,96782,tzinfo=zoneinfo.ZoneInfo(key
='UTC'))
获取当前时间的明晰时间戳在 UTC 时区:
>>> daware=datetime.utcnow().replace(tzinfo=ZoneInfo("UTC"))
>>> daware
datetime.datetime(2021,10,*21,23*,32,23,96782,tzinfo=zoneinfo.ZoneInfo(key
='UTC'))
在不同时区显示 datetime 实例:
>>> dutc.astimezone(ZoneInfo("Asia/Katmandu")) *`# offset +5h 45m`*
datetime.datetime(2021,10,*22,5*,17,23,96782,tzinfo=zoneinfo.ZoneInfo(key
='Asia/Katmandu'))
获取本地时区:
>>> tz_local=datetime.now().astimezone().tzinfo
>>> tz_local
datetime.timezone(datetime.timedelta(days=-1, seconds=61200), 'Pacific
Daylight Time')
将 UTC datetime 实例转换回本地时区:
>>> dt_loc=dutc.astimezone(tz_local)
>>> dt_loc
datetime.datetime(2021, 10, 21, 16, 32, 23, 96782, tzinfo=datetime.time
(datetime.timedelta(days=-1, seconds=61200), 'Pacific Daylight Time'))
>>> d==dt_local
True
并获取所有可用时区的排序列表:
>>> tz_list=zoneinfo.available_timezones()
>>> sorted(tz_list)[0],sorted(tz_list)[-1]
('Africa/Abidjan', 'Zulu')
始终在内部使用 UTC 时区
规避时区的陷阱和问题的最佳方法是始终在内部使用 UTC 时区,在输入时从其他时区转换,并仅在显示目的使用 datetime.astimezone。
即使您的应用程序仅在自己的位置运行,并且永远不打算使用其他时区的时间数据,也适用这个技巧。如果您的应用程序连续运行几天或几周,并且为您的系统配置的时区遵循夏令时,如果不在 UTC 内部工作,您将会遇到与时区相关的问题。
dateutil 模块
第三方包dateutil(您可以通过 pip install python-dateutil 安装)提供了许多操作日期的模块。表格 13-10 列出了它提供的主要模块,除了用于时区相关操作(现在最好使用 zoneinfo,在前一节中讨论)的模块。
表格 13-10. dateutil 模块
| easter | easter.easter(year) 返回给定 year 的复活节 datetime.date 对象。例如:
>>> `from` dateutil `import` easter
>>> print(easter.easter(2023))
2023-04-09
|
| parser | parser.parse(s) 返回由字符串 s 表示的 datetime.datetime 对象,具有非常宽松(或“模糊”)的解析规则。例如:
>>> `from` dateutil `import` parser
>>> print(parser.parse('Saturday, January 28,'
' 2006, at 11:15pm'))
2006-01-28 23:15:00
|
| relativedelta | relativedelta.relativedelta(...) 提供了一种简便的方法,用于查找“下个星期一”、“去年”等。dateutil 的文档详细解释了定义 relativedelta 实例行为复杂性规则。 |
|---|---|
| rrule | rrule.rrule(freq, ...) 实现了RFC 2445(也称为 iCalendar RFC),完整呈现其超过 140 页的荣耀。rrule 允许您处理重复事件,提供了诸如 after、before、between 和 count 等方法。 |
详细信息请查看dateutil 模块的文档,了解其丰富功能。
sched 模块
sched 模块实现了事件调度程序,让您可以轻松处理在“真实”或“模拟”时间尺度上安排的事件。这个事件调度程序在单线程和多线程环境中使用都是安全的。sched 提供了一个调度程序类,它接受两个可选参数,timefunc 和 delayfunc。
| scheduler | class scheduler(timefunc=time.monotonic, delayfunc=time.sleep) 可选参数 timefunc 必须是可调用的,没有参数以获取当前时间时刻(以任何度量单位);例如,您可以传递 time.time。可选参数 delayfunc 是可调用的,具有一个参数(时间持续时间,与 timefunc 相同单位),以延迟当前线程的该时间。调度器在每个事件之后调用 delayfunc(0) 给其他线程一个机会;这与 time.sleep 兼容。通过接受函数作为参数,调度器可以让您使用适合应用程序需要的任何“模拟时间”或“伪时间”^(a)。
如果单调时间(即使系统时钟在调用之间向后调整也无法倒退的时间,例如由于闰秒导致)对您的应用程序至关重要,请为您的调度器使用默认的 time.monotonic。
| ^(a) 依赖注入设计模式 的一个很好的示例,用于与测试无关的目的。 |
|---|
调度器实例 s 提供了 表 13-11 中详细描述的方法。
表 13-11. 调度器实例 s 的方法
| cancel | s.cancel(event_token) 从 s 的队列中移除一个事件。event_token 必须是对 s.enter 或 s.enterabs 的先前调用的结果,并且事件尚未发生;否则,cancel 将引发 RuntimeError。 |
|---|---|
| empty | s.empty() 当 s 的队列当前为空时返回 True;否则返回 False。 |
| enter | s.enter(delay, priority, func, argument=(), kwargs={}) 类似于 enterabs,不同之处在于 delay 是相对时间(从当前时刻正向的正差),而 enterabs 的参数 when 是绝对时间(未来时刻)。要为 重复 执行安排事件,请使用一个小的包装函数;例如:
`def` enter_repeat(s, first_delay, period, priority,
func, args):
`def` repeating_wrapper():
s.enter(period, priority,
repeating_wrapper, ())
func(*args)
s.enter(first_delay, priority,
repeating_wrapper, args)
|
| enterabs | s.enterabs(when, priority, func, argument=(), kwargs={}) 在时间 when 安排一个未来事件(回调 func(args, kwargs))。when 使用 s 的时间函数使用的单位。如果为同一时间安排了几个事件,s 将按 priority 的增加顺序执行它们。enterabs 返回一个事件令牌 t,您可以稍后将其传递给 s.cancel 来取消此事件。 |
|---|---|
| run | s.run(blocking=True) 运行已安排的事件。如果 blocking 为True,s.run 会循环直到s.empty 返回True,使用s初始化时传递的 delayfunc 来等待每个已安排的事件。如果 blocking 为False,执行任何即将到期的事件,然后返回下一个事件的截止时间(如果有的话)。当回调函数func引发异常时,s会传播它,但s保持自己的状态,按顺序执行已安排的事件。如果回调函数func运行时间超过下一个已安排事件之前的可用时间,则s会落后但会继续按顺序执行已安排的事件,不会丢弃任何事件。如果不再对某个事件感兴趣,调用s.cancel 显式地丢弃事件。 |
日历模块
日历模块提供与日历相关的函数,包括打印给定月份或年份的文本日历的函数。默认情况下,calendar 将星期一作为一周的第一天,星期日作为最后一天。要更改此设置,请调用 calendar.setfirstweekday。calendar 处理模块时间范围内的年份,通常为 1970 到 2038(至少)。
日历模块提供了表 13-12 中列出的函数。
表 13-12. 日历模块的函数
| calendar | calendar(year, w=2, li=1, c=6) 返回一个多行字符串,其中包含year年的日历,以每个日期间隔 c 个空格分隔成三列。w 是每个日期的字符宽度;每行的长度为 21w+18+2c。li 是每周的行数。 |
|---|---|
| firstweekday | firstweekday() 返回每周起始的当前设置的工作日。默认情况下,当导入 calendar 时,这是 0(表示星期一)。 |
| isleap | isleap(year) 如果year是闰年则返回True;否则返回False。 |
| leapdays | leapdays(y1, y2) 返回范围(y1, y2)内的闰年天数总计(注意,这意味着y2是不包括的)。 |
| month | month(year, month, w=2, li=1) 返回一个多行字符串,其中包含year年month月的日历,每周一行加上两个标题行。w 是每个日期的字符宽度;每行的长度为 7*w+6。li 是每周的行数。 |
| monthcalendar | monthcalendar(year, month) 返回一个整数列表的列表。每个子列表表示一周。年year月month之外的天数设为 0;该月内的天数设为它们的日期,从 1 开始。 |
| monthrange | monthrange(year, month) 返回两个整数。第一个整数是year年month月第一天的工作日代码;第二个整数是该月的天数。工作日代码为 0(星期一)到 6(星期日);月份编号为 1 到 12。 |
| prcal | prcal(year, w=2, li=1, c=6) 类似于 print(calendar.calendar(year, w, li, c))。 |
| prmonth | prmonth(year, month, w=2, li=1) 类似于 print(calendar.month(year, month, w, li))。 |
| setfirstweekday | setfirstweekday(weekday) 设置每周的第一天为星期代码 weekday。星期代码为从 0(星期一)到 6(星期日)。calendar 提供了 MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY 和 SUNDAY 这些属性,它们的值为整数 0 到 6。在代码中表示工作日时(例如,calendar.FRIDAY 而不是 4),使用这些属性可以使您的代码更清晰和更易读。 |
| timegm | timegm(tupletime) 就像 time.mktime 一样:接受时间元组形式的时间点,并将该时间点作为距离纪元的浮点秒数返回。 |
| weekday | weekday(year, month, day) 返回给定日期的星期代码。星期代码为 0(星期一)到 6(星期日);月份编号为 1(一月)到 12(十二月)。 |
python -m calendar 提供了一个有用的命令行界面以访问该模块的功能:运行 python -m calendar -h 可以获取简短的帮助信息。
¹ 在旧的 Unix 系统中,1970-01-01 是纪元的开始,而 2038-01-19 是 32 位时间回到纪元的时间点。大多数现代系统现在使用 64 位时间,许多时间方法可以接受从 0001 到 9999 年的年份,但一些方法或旧系统(特别是嵌入式系统)可能仍然有限制。
² time 和 datetime 不考虑闰秒,因为它们的计划未来不可预知。
³ 在 3.9 之前,请使用第三方模块 pytz。
第十四章:执行定制
Python 公开、支持并记录其许多内部机制。这可能帮助您在高级水平上理解 Python,并允许您将自己的代码连接到这些 Python 机制中,以某种程度上控制它们。例如,“Python 内置函数”介绍了 Python 安排内置函数可见的方式。本章还涵盖了一些其他高级 Python 技术,包括站点定制、终止函数、动态执行、处理内部类型和垃圾回收。我们将在第十五章中讨论使用多线程和进程控制执行的其他问题;第十七章涵盖了与测试、调试和性能分析相关的特定问题。
站点定制
Python 提供了一个特定的“钩子”来让每个站点在每次运行开始时定制 Python 行为的某些方面。Python 在主脚本之前加载标准模块 site。如果使用**-S选项运行 Python,则不加载 site。-S**允许更快的启动,但会为主脚本增加初始化任务。site 的主要任务是将 sys.path 放置在标准形式中(绝对路径,无重复项),包括根据环境变量、虚拟环境和在 sys.path 中找到的每个*.pth*文件的指示。
其次,如果启动的会话是交互式的,则 site 会添加几个方便的内置函数(例如 exit、copyright 等),并且如果启用了 readline,则配置 Tab 键的自动完成功能。
在任何正常的 Python 安装中,安装过程设置了一切以确保 site 的工作足以让 Python 程序和交互式会话“正常”运行,即与安装了该版本 Python 的任何其他系统上的运行方式相同。在特殊情况下,如果作为系统管理员(或在等效角色,例如已将 Python 安装在其主目录以供个人使用的用户)认为绝对需要进行一些定制,则在名为sitecustomize.py的新文件中执行此操作(在与site.py相同的目录中创建它)。
避免修改 site.py
我们强烈建议您不要修改执行基础定制的site.py文件。这样做可能会导致 Python 在您的系统上的行为与其他地方不同。无论如何,site.py文件每次更新 Python 安装时都会被覆盖,您的修改将会丢失。
在sitecustomize.py存在的罕见情况下,它通常的作用是将更多字典添加到 sys.path 中——执行此任务的最佳方法是让sitecustomize.py import site,然后调用 site.addsitedir(path_to_a_dir)。
终止函数
atexit 模块允许你注册终止函数(即,在程序终止时按 LIFO 顺序调用的函数)。终止函数类似于由 try/finally 或 with 建立的清理处理程序。然而,终止函数是全局注册的,在整个程序结束时调用,而清理处理程序是在特定 try 子句或 with 语句结束时调用的。终止函数和清理处理程序在程序正常或异常终止时都会被调用,但不会在通过调用 os._exit 终止程序时调用(所以通常调用 sys.exit)。atexit 模块提供了一个名为 register 的函数,接受 func、args 和 kwds 作为参数,并确保在程序终止时调用 func(args,kwds)。
动态执行和 exec
Python 的内置函数 exec 可以在程序运行时执行你读取、生成或以其他方式获取的代码。exec 动态执行一个语句或一组语句。其语法如下:
exec(*`code`*, *`globals`*=`None`, *`locals`*=`None`, /)
code 可以是 str、bytes、bytearray 或 code 对象。globals 是一个字典,locals 可以是任何映射。
如果同时传递 globals 和 locals,它们分别是代码运行的全局和局部命名空间。如果只传递 globals,exec 将同时使用 globals 作为全局和局部命名空间。如果两者都不传递,code 将在当前作用域中运行。
永远不要在当前作用域中运行 exec
在当前作用域中运行 exec 是一个特别糟糕的主意:它可以绑定、重新绑定或解绑任何全局名称。为了保持控制,请只在特定、显式的字典中使用 exec,如果必须的话。
避免使用 exec
Python 中经常被问到的一个问题是“如何设置一个我刚刚读取或构建的变量的名称?”确实,对于一个 global 变量,exec 允许这样做,但是为此目的使用 exec 是一个非常糟糕的主意。例如,如果变量名是 varname,你可能会考虑使用:
exec(*`varname`* + ' = 23')
不要这样做。在当前作用域中这样的 exec 会使你失去命名空间的控制,导致极难找到的错误,并使你的程序难以理解。将你需要动态设置的“变量”保存为字典条目(例如,mydict)而不是实际变量。然后可以考虑使用:
exec(*`varname`*+'=23', *`mydict`*) *`# Still a bad idea`*
虽然这种方式不如前面的例子那么糟糕,但仍然不是一个好主意。将这些“变量”保存为字典条目意味着你不需要使用 exec 来设置它们!只需编写代码:
mydict[varname] = 23
这样,你的程序会更清晰、直接、优雅且更快。有一些情况下确实可以使用 exec,但这些情况非常罕见:最好是使用显式字典。
努力避免 exec
只有在确实不可或缺时才使用 exec,这种情况极为罕见。通常最好避免 exec,并选择更具体、更受控制的机制:exec 会削弱您对代码命名空间的控制,可能损害程序的性能,并使您面临许多难以发现的错误和巨大的安全风险。
表达式
exec 可以执行表达式,因为任何表达式也都是有效的语句(称为表达式语句)。但是,Python 会忽略表达式语句返回的值。要评估表达式并获取表达式的值,请使用内置函数 eval,如表格 8-2 中所述。(但请注意,exec 的几乎所有安全风险警告同样适用于 eval。)
编译和代码对象
要使一个代码对象用于 exec,调用内置函数 compile 并将最后一个参数设置为'exec'(如表格 8-2 中所述)。
一个代码对象c展示了许多有趣的只读属性,它们的名称都以'co_'开头,比如在表格 14-1 中列出的那些。
表格 14-1. 代码对象的只读属性
| co_argcount | c所代表的函数的参数个数(当c不是函数的代码对象而是直接由 compile 构建时为 0) |
|---|---|
| co_code | 一个字节对象,包含c的字节码 |
| co_consts | c中使用的常量的元组 |
| co_filename | c编译生成的文件名(当c是这种方式构建时为 compile 的第二个参数的字符串) |
| co_firstlineno | 源代码的初始行号(位于由 co_filename 命名的文件中),用于编译生成c,如果c是通过编译文件构建的话 |
| co_name | c所代表的函数的名称(当c不是函数的代码对象而是直接由 compile 构建时为'') |
| co_names | c中使用的所有标识符的元组 |
| co_varnames | c中局部变量标识符的元组,以参数名称开头 |
这些属性大多仅用于调试目的,但有些可能有助于高级内省,正如本节后面所示的例子。
如果您从包含一个或多个语句的字符串开始,首先对字符串使用 compile,然后在生成的代码对象上调用 exec—这比直接将字符串传递给 exec 来编译和执行要好一些。 这种分离允许您单独检查语法错误和执行时错误。 您通常可以安排事务以便字符串只编译一次,而代码对象重复执行,这样可以加快速度。 eval 也可以从这种分离中受益。 此外,编译步骤本质上是安全的(如果在您不完全信任的代码上执行 exec 和 eval 非常危险),您可以在执行代码对象之前检查它,以减少风险(尽管风险永远不会完全为零)。
正如 表 14-1 中所述,代码对象具有只读属性 co_names,该属性是代码中使用的名称的元组。 例如,假设您希望用户输入仅包含文字常量和操作符的表达式—不包含函数调用或其他名称。 在评估表达式之前,您可以检查用户输入的字符串是否满足这些约束:
`def` safer_eval(s):
code = compile(s, '<user-entered string>', 'eval')
`if` code.co_names:
`raise` ValueError(
f'Names {code.co_names!r} not allowed in expression {s!r}')
`return` eval(code)
函数 safer_eval 评估作为参数传递的表达式 s,仅当字符串是语法上有效的表达式(否则,compile 会引发 SyntaxError),且完全不包含任何名称(否则,safer_eval 明确引发 ValueError)。 (这类似于标准库函数 ast.literal_eval,详见 “标准输入”,但更为强大,因为它允许使用操作符。)
了解代码即将访问的名称有时可能有助于优化您需要传递给 exec 或 eval 作为命名空间的字典的准备工作。 由于您只需要为这些名称提供值,因此您可以通过不准备其他条目来节省工作。 例如,假设您的应用程序动态接受来自用户的代码,并且约定以 data_ 开头的变量名引用存储在子目录 data 中的文件,用户编写的代码不需要显式读取。 用户编写的代码反过来可能会计算并将结果留在以 result_ 开头的全局变量中,您的应用程序将这些结果作为文件写回 data 子目录。 由于这种约定,稍后您可以将数据移动到其他位置(例如,到数据库中的 BLOBs,而不是子目录中的文件),用户编写的代码不会受到影响。 这是您可能如何有效实现这些约定的方法:
`def` exec_with_data(user_code_string):
user_code = compile(user_code_string, '<user code>', 'exec')
datadict = {}
`for` name `in` user_code.co_names:
`if` name.startswith('data_'):
`with` open(f'data/{name[5:]}', 'rb') `as` datafile:
datadict[name] = datafile.read()
`elif` name.startswith('result_'):
`pass` *`` # user code assigns to variables named `result_...` ``*
`else`:
`raise` ValueError(f'invalid variable name {name!r}')
exec(user_code, datadict)
`for` name `in` datadict:
`if` name.startswith('result_'):
`with` open(f'data/{name[7:]}', 'wb') `as` datafile:
datafile.write(datadict[name])
永远不要执行或评估不受信任的代码。
早期版本的 Python 提供了旨在减轻使用 exec 和 eval 风险的工具,称为“受限执行”,但这些工具从未完全安全,无法抵御有能力的黑客的狡猾攻击。最近的 Python 版本已经放弃了这些工具,以避免为用户提供不合理的安全感。如果需要防范此类攻击,请利用操作系统提供的保护机制:在一个单独的进程中运行不受信任的代码,并尽可能限制其权限(研究操作系统提供的用于此目的的机制,如 chroot、setuid 和 jail;在 Windows 上,可以尝试第三方商业附加组件 WinJail,或者如果你是容器安全化专家,可以在一个单独且高度受限的虚拟机或容器中运行不受信任的代码)。为了防范服务拒绝攻击,主进程应监控单独的进程,并在资源消耗过多时终止后者。进程在 “运行其他程序” 中有详细描述。
exec 和 eval 在不受信任的代码中是不安全的。
在上一节中定义的 exec_with_data 函数对不受信任的代码根本不安全:如果将作为参数 user_code 传递给它的字符串来自于你不能完全信任的方式,那么它可能会造成无法估量的损害。不幸的是,这几乎适用于任何使用 exec 或 eval 的情况,除非你能对要执行或评估的代码设置极其严格和完全可检查的限制,就像 safer_eval 函数的情况一样。
内部类型
本节描述的一些内部 Python 对象使用起来很困难,事实上并不是大多数情况下你应该使用的。正确地使用这些对象并产生良好效果需要一些研究你的 Python 实现的 C 源码。这种黑魔法很少需要,除非用于构建通用开发工具和类似的高级任务。一旦你深入理解事物,Python 赋予你在必要时施加控制的能力。由于 Python 将许多种类的内部对象暴露给你的 Python 代码,即使需要理解 C 来阅读 Python 的源代码并理解正在发生的事情,你也可以通过在 Python 中编码来施加这种控制。
类型对象
名为type的内置类型充当可调用的工厂,返回类型对象。类型对象除了等价比较和表示为字符串外,不需要支持任何特殊操作。但是,大多数类型对象是可调用的,并在调用时返回该类型的新实例。特别地,内置类型如int、float、list、str、tuple、set和dict都是这样工作的;具体来说,当不带参数调用它们时,它们返回一个新的空实例,或者对于数字,返回等于 0 的实例。类型模块的属性是没有内置名称的内置类型。除了调用以生成实例外,类型对象之所以有用,还因为你可以从中继承,正如“类和实例”中所涵盖的那样。
代码对象类型
除了使用内置函数compile,你还可以通过函数或方法对象的__code__属性获取代码对象。(有关代码对象属性的讨论,请参见“编译和代码对象”。)代码对象本身不能被调用,但是你可以重新绑定函数对象的__code__属性,使用正确数量的参数将代码对象包装成可调用形式。例如:
`def` g(x):
print('g', x)
code_object = g.__code__
`def` f(x):
`pass`
f.__code__ = code_object
f(23) *`# prints: g 23`*
没有参数的代码对象也可以与exec或eval一起使用。直接创建代码对象需要许多参数;请参阅 Stack Overflow 的非官方文档了解如何操作(但请记住,通常最好调用compile而不是直接创建)。
帧类型
模块sys中的函数_getframe从 Python 调用栈返回一个帧对象。帧对象具有属性,提供关于在帧中执行的代码和执行状态的信息。traceback和inspect模块帮助你访问和显示这些信息,特别是在处理异常时。第十七章提供了关于帧和回溯的更多信息,并涵盖了模块inspect,这是执行此类内省的最佳方式。如函数名_getframe中的前导下划线提示的那样,该函数是“有些私有”的;它仅供调试器等工具使用,这些工具不可避免地需要深入内省 Python 的内部。
垃圾回收
Python 的垃圾收集通常是透明且自动进行的,但您可以选择直接进行一些控制。一般原则是,Python 在对象 x 成为不可达时的某个时刻收集 x,即当没有从正在执行的函数实例的本地变量或从已加载模块的全局变量开始的引用链能够到达 x 时。通常,当没有任何引用指向 x 时,对象 x 变得不可达。此外,当一组对象彼此引用但没有全局或局部变量间接引用它们时,它们也可能是不可达的(这种情况称为相互引用循环)。
经典 Python 对每个对象 x 都保留一个称为引用计数的计数,记录了有多少引用指向 x。当 x 的引用计数降至 0 时,CPython 立即收集 x。模块 sys 的函数 getrefcount 接受任何对象并返回其引用计数(至少为 1,因为 getrefcount 本身对要检查的对象有一个引用)。其他版本的 Python(如 Jython 或 PyPy)依赖于由其运行的平台提供的其他垃圾收集机制(例如 JVM 或 LLVM)。因此,模块 gc 和 weakref 仅适用于 CPython。
当 Python 回收 x 并且没有对 x 的引用时,Python 会完成 x 的最终处理(即调用 x.del)并释放 x 占用的内存。如果 x 持有对其他对象的引用,Python 会移除这些引用,从而可能使其他对象因无法访问而可回收。
gc 模块
gc 模块公开了 Python 垃圾收集器的功能。gc 处理了属于相互引用循环的不可达对象。如前所述,在这样的循环中,循环中的每个对象都引用另一个或多个其他对象,保持所有对象的引用计数为正数,但没有外部引用指向这组相互引用的对象集。因此,整个组,也称为循环垃圾,是不可达的,因此可以进行垃圾收集。寻找这样的循环垃圾需要时间,这也是为什么 gc 模块存在的原因:帮助您控制程序是否以及何时花费这些时间。默认情况下,循环垃圾收集功能处于启用状态,并具有一些合理的默认参数;但是,通过导入 gc 模块并调用其函数,您可以选择禁用功能、更改其参数和/或详细了解这方面的情况。
gc 提供了属性和函数来帮助您管理和调整循环垃圾回收,包括 表 14-2 中列出的内容。这些函数可以帮助您追踪内存泄漏 —— 尽管 应该 不再有对它们的引用,但仍然没有被回收的对象 —— 通过帮助您发现确实持有对它们引用的其他对象来发现它们。请注意,gc 实现了计算机科学中称为 分代垃圾回收 的体系结构。
表 14-2. gc 函数和属性
| callbacks | 垃圾收集器将在收集之前和之后调用的回调函数列表。有关详细信息,请参阅 “仪器化垃圾回收”。 |
|---|---|
| collect | collect() 立即强制执行完整的循环垃圾回收运行。 |
| disable | disable() 暂停自动周期性循环垃圾回收。 |
| enable | enable() 重新启用先前使用 disable 暂停的周期性循环垃圾回收。 |
| freeze | freeze() 冻结 gc 跟踪的所有对象:将它们移动到“永久代”,即一组在所有未来收集中被忽略的对象。 |
| garbage | 不可达但不可收集的对象列表(仅读)。当循环垃圾回收环中的任何对象具有 del 特殊方法时,可能不存在明显安全的顺序来终结这些对象。 |
| get_count | get_count() 返回当前收集计数的元组,形式为 (count0, count1, count2)。 |
| get_debug | get_debug() 返回一个整数位串,表示使用 set_debug 设置的垃圾回收调试标志。 |
| get_freeze_count | get_freeze_count() 返回永久代中对象的数量。 |
| get_objects | get_objects(generation=None) 返回被收集器跟踪的对象列表。3.8+ 如果选择的 generation 参数不是 None,则仅列出所选代中的对象。 |
| get_referents | get_referents(*objs) 返回由参数的 C 级 tp_traverse 方法访问的对象列表,这些对象被参数中任何一个引用。 |
| get_referrers | get_referrers(*objs) 返回当前由循环垃圾回收器跟踪的所有容器对象列表,这些对象引用参数中的任意一个或多个对象。 |
| get_stats | get_stats() 返回三个字典的列表,每个字典代表一代,包含收集次数、收集的对象数和不可收集对象数。 |
| get_threshold | get_threshold() 返回当前收集阈值,以三个整数的元组形式返回。 |
| isenabled | isenabled() 当前循环垃圾回收启用时返回 True;否则返回 False。 |
| is_finalized | is_finalized(obj) 3.9+ 当垃圾回收器已经完成对 obj 的终结时返回 True;否则返回 False。 |
| is_tracked | is_tracked(obj) 当 obj 当前被垃圾收集器跟踪时返回 True;否则返回 False。 |
| set_debug | set_debug(flags) 设置在垃圾收集期间的调试行为标志。 flags 是一个整数,被解释为通过按位 OR(位或运算符,|)零个或多个模块 gc 提供的常量来构建的位字符串。 每个位启用一个特定的调试功能:
DEBUG_COLLECTABLE
打印在垃圾收集期间发现的可收集对象的信息。
DEBUG_LEAK
结合 DEBUG_COLLECTABLE、DEBUG_UNCOLLECTABLE 和 DEBUG_SAVEALL 的行为。 这些通常是用于帮助诊断内存泄漏的最常见标志。
DEBUG_SAVEALL
将所有可收集对象保存到列表 gc.garbage 中(其中不可收集对象也始终保存)以帮助您诊断泄漏问题。
DEBUG_STATS
打印在垃圾收集期间收集的统计信息,以帮助您调整阈值。
DEBUG_UNCOLLECTABLE
打印在垃圾收集期间发现的不可收集对象的信息。
|
| set_threshold | set_threshold(thresh0[, thresh1[, thresh2]]) 设置控制循环垃圾收集周期运行频率的阈值。 thresh0 为 0 会禁用垃圾收集。 垃圾收集是一个高级的专题,Python 中使用的分代垃圾收集方法的详细内容(以及因此这些阈值的详细含义)超出了本书的范围; 有关详细信息,请参阅 在线文档。 |
|---|---|
| unfreeze | unfreeze() 解冻永久代中的所有对象,将它们全部移回到最老的代中。 |
当您知道程序中没有循环垃圾环路,或者在某些关键时刻不能承受循环垃圾收集的延迟时,通过调用 gc.disable() 暂时停止自动垃圾收集。 您可以稍后通过调用 gc.enable() 再次启用收集。 您可以通过调用 gc.isenabled() 测试当前是否启用了自动收集,它返回 True 或 False。 要控制收集时机,可以调用 gc.collect() 强制立即执行完整的循环收集运行。 要包装一些时间关键的代码:
`import` gc
gc_was_enabled = gc.isenabled()
`if` gc_was_enabled:
gc.collect()
gc.disable()
*`# insert some time-critical code here`*
`if` gc_was_enabled:
gc.enable()
如果将其实现为上下文管理器,您可能会发现这更容易使用:
`import` gc
`import` contextlib
@contextlib.contextmanager
`def` gc_disabled():
gc_was_enabled = gc.isenabled()
`if` gc_was_enabled:
gc.collect()
gc.disable()
`try`:
`yield`
`finally`:
`if` gc_was_enabled:
gc.enable()
`with` gc_disabled():
*`# ...insert some time-critical code here...`*
该模块 gc 中的其他功能更为高级且很少使用,可以分为两个领域。 函数 get_threshold 和 set_threshold 以及调试标志 DEBUG_STATS 帮助您微调垃圾收集以优化程序的性能。 gc 的其余功能可以帮助您诊断程序中的内存泄漏。 虽然 gc 本身可以自动修复许多泄漏问题(只要避免在类中定义 del,因为存在 del 可以阻止循环垃圾收集),但是如果首先避免创建循环垃圾,程序运行速度将更快。
垃圾收集工具
gc.callbacks 是一个最初为空的列表,您可以向其中添加函数 f(phase, info),Python 将在垃圾回收时调用这些函数。当 Python 调用每个这样的函数时,phase 为 'start' 或 'stop',用于标记收集的开始或结束,info 是一个字典,包含由 CPython 使用的分代收集的信息。您可以向此列表添加函数,例如用于收集有关垃圾回收的统计信息。有关更多详细信息,请参阅文档。
弱引用模块
谨慎的设计通常可以避免引用循环。然而,有时你需要对象彼此知道对方的存在,避免相互引用可能会扭曲和复杂化你的设计。例如,一个容器引用了其项目,但是对象知道容器持有它也常常很有用。结果就是引用循环:由于相互引用,容器和项目保持彼此存活,即使所有其他对象都忘记了它们。弱引用通过允许对象引用其他对象而不保持它们存活来解决了这个问题。
弱引用 是一个特殊对象 w,它引用某个其他对象 x 而不增加 x 的引用计数。当 x 的引用计数降至 0 时,Python 将终止并收集 x,然后通知 w x 的消亡。弱引用 w 现在可以消失或以受控方式标记为无效。在任何时候,给定的 w 要么引用创建 w 时的相同对象 x,要么完全不引用;弱引用永远不会被重新定位。并非所有类型的对象都支持成为弱引用 w 的目标 x,但是类、实例和函数支持。
弱引用模块公开了用于创建和管理弱引用的函数和类型,详见表格 14-3。
表格 14-3. 弱引用模块的函数和类
| getweakrefcount | getweakrefcount(x) 返回 len(getweakrefs(x))。 |
|---|---|
| getweakrefs | getweakrefs(x) 返回所有目标为 x 的弱引用和代理的列表。 |
| proxy | proxy(x[, f]) 返回类型为 ProxyType(当 x 是可调用对象时为 CallableProxyType)的弱代理 p,以 x 为目标。使用 p 就像使用 x 一样,但是当使用 p 时,x 被删除后,Python 将引发 ReferenceError。p 永远不可哈希(您不能将 p 用作字典键)。当 f 存在时,它必须是一个接受一个参数的可调用对象,并且是 p 的最终化回调(即在 x 不再从 p 可达时,Python 调用 f(p))。f 在 x 不再从 p 可达后立即执行。 |
| ref | ref(x[, f]) 返回类型为 ReferenceType 的对象 x 作为目标的弱引用 w。w 可以无参数地调用:调用 w() 在 x 仍然存活时返回 x;否则,调用 w() 返回 None。当 x 可散列时,w 是可散列的。您可以比较弱引用的相等性(==、!=),但不能进行顺序比较(<、>、<=、>=)。当它们的目标存活且相等时,或者 x 等于 y 时,两个弱引用 x 和 y 是相等的。当存在 f 时,它必须是一个带有一个参数的可调用对象,并且是 w 的最终化回调(即,在 x 从 w 不再可达之后,Python 调用 f(w))。f 在 x 不再从 w 可达后立即执行。 |
| 弱键字典 | class WeakKeyDictionary(adict={}) 弱键字典 d 是一个弱引用其键的映射。当 d 中键 k 的引用计数为 0 时,项目 d[k] 消失。adict 用于初始化映射。 |
| 弱引用集 | class WeakSet(elements=[]) 弱引用集 s 是一个弱引用其内容元素的集合,从元素初始化。当 s 中元素 e 的引用计数为 0 时,e 从 s 中消失。 |
| 弱值字典 | class WeakValueDictionary(adict={}) 弱值字典 d 是一个弱引用其值的映射。当 d 中值 v 的引用计数为 0 时,d 中所有 d[k] 为 v 的项目消失。adict 用于初始化映射。 |
WeakKeyDictionary 允许您在一些可散列对象上非侵入式地关联附加数据,而无需更改这些对象。WeakValueDictionary 允许您非侵入式地记录对象之间的瞬时关联,并构建缓存。在每种情况下,使用弱映射而不是字典,以确保其他情况下可被垃圾回收的对象不会仅因在映射中使用而保持活动状态。类似地,WeakSet 在普通集合的位置提供了相同的弱包含功能。
典型示例是一个跟踪其实例但不仅仅是为了跟踪它们而使它们保持活动状态的类:
`import` weakref
`class` Tracking:
_instances_dict = weakref.WeakValueDictionary()
def __init__(self):
Tracking._instances_dict[id(self)] = self
@classmethod
def instances(cls):
`return` cls._instances_dict.values()
当跟踪实例是可散列的时候,可以使用一个实例的 WeakSet 类实现类似的类,或者使用一个 WeakKeyDictionary,其中实例作为键,None作为值。
第十五章:并发:线程和进程
进程是操作系统中彼此保护的运行程序的实例。想要通信的进程必须明确通过进程间通信(IPC)机制,以及/或通过文件(在第十一章讨论)、数据库(在第十二章讨论)、或网络接口(在第十八章讨论)来安排通信。进程之间使用文件和数据库等数据存储机制通信的一般方式是一个进程写入数据,另一个进程稍后读取该数据。本章介绍了处理进程的编程,包括 Python 标准库模块 subprocess 和 multiprocessing;模块 os 中与进程相关的部分,包括通过管道进行简单 IPC;一种称为内存映射文件的跨平台 IPC 机制,在模块 mmap 中可用;以及 3.8+及 multiprocessing.shared_memory 模块。
线程(最初称为“轻量级进程”)是与单个进程内的其他线程共享全局状态(内存)的控制流;所有线程看起来同时执行,尽管它们实际上可能在一个或多个处理器/核心上“轮流”执行。线程远非易于掌握,而多线程程序通常难以测试和调试;然而,如“线程、多进程还是异步编程?”所述,适当使用多线程时,与单线程编程相比性能可能会提高。本章介绍了 Python 提供的处理线程的各种功能,包括线程、队列和 concurrent.futures 模块。
在单个进程内共享控制的另一种机制是所谓的异步(或async)编程。当你阅读 Python 代码时,关键字async和await的存在表明它是异步的。这样的代码依赖于事件循环,它大致相当于进程内部使用的线程切换器。当事件循环是调度器时,每次执行异步函数变成一个任务,与多线程程序中的线程大致对应。
进程调度和线程切换都是抢占式的,这意味着调度器或切换器控制 CPU,并确定何时运行任何特定的代码。然而,异步编程是协作式的:每个任务一旦开始执行,可以在选择放弃控制之前运行多长时间(通常是因为正在等待完成某些其他异步任务,通常是面向 I/O 的任务)。
尽管异步编程提供了优化某些问题类别的灵活性,但这是许多程序员不熟悉的编程范例。由于其协作性质,不慎的异步编程可能导致死锁,而无限循环则可能使其他任务被剥夺处理器时间:弄清楚如何避免死锁为普通程序员增加了显著的认知负担。我们在本卷中不进一步讨论异步编程,包括模块asyncio,认为这是一个足够复杂的主题,值得单独撰写一本书进行探讨。¹
网络机制非常适合 IPC,并且在网络的不同节点上运行的进程之间,以及在同一节点上运行的进程之间同样有效。multiprocessing 模块提供了一些适合在网络上进行 IPC 的机制;第十八章涵盖了提供 IPC 基础的低级网络机制。其他更高级的分布式计算机制(如CORBA、DCOM/COM+、EJB、SOAP、XML-RPC、.NET、gRPC等)可以使 IPC 变得更加容易,无论是本地还是远程;然而,本书不涵盖分布式计算。
当多处理器计算机出现时,操作系统必须处理更复杂的调度问题,而希望获得最大性能的程序员必须编写他们的应用程序,以便代码可以真正并行执行,即在不同的处理器或核心上(从编程角度看,核心只是在同一块硅片上实现的处理器)。这需要知识和纪律。CPython 实现通过实现全局解释器锁(GIL)简化了这些问题。在没有任何 Python 程序员的操作下,在 CPython 中只有持有 GIL 的线程被允许访问处理器,有效地阻止了 CPython 进程充分利用多处理器硬件。诸如NumPy这样的库通常需要进行长时间的计算,使用的是不使用解释器设施的编译代码,这些库在这些计算期间释放 GIL。这允许有效地使用多个处理器,但如果您的所有代码都是纯 Python,则不能使用这种技术。
Python 中的线程
Python 支持在支持线程的平台上进行多线程操作,如 Windows、Linux 和几乎所有 Unix 变体(包括 macOS)。当动作在开始和结束之间保证没有线程切换时,该动作被称为原子。在实践中,在 CPython 中,看起来是原子的操作(例如,简单赋值和访问)大多数情况下确实是原子的,但只适用于内置类型(但增强和多重赋值不是原子的)。尽管如此,依赖于这种“原子性”通常不是一个好主意。您可能正在处理用户编写的类的实例,而不是内置类型的实例,在这种情况下,可能会有隐式调用 Python 代码的情况,这些调用会使原子性假设失效。此外,依赖于实现相关的原子性可能会将您的代码锁定到特定的实现中,从而阻碍未来的更改。建议您在本章的其余部分使用同步设施,而不是依赖于原子性假设。
多线程系统中的关键设计问题是如何最好地协调多个线程。线程模块在下一节中介绍,提供了几种同步对象。队列模块(在“队列模块”中讨论)对于线程同步也非常有用:它提供了同步的、线程安全的队列类型,方便线程间的通信和协调。concurrent 包(在“concurrent.futures 模块”中讨论)提供了一个统一的通信和协调接口,可以由线程池或进程池实现。
线程模块
线程模块提供了多线程功能。线程的方法是将锁和条件建模为单独的对象(例如,在 Java 中,此类功能是每个对象的一部分),并且线程不能直接从外部控制(因此没有优先级、组、销毁或停止)。线程模块提供的所有对象的方法都是原子的。
线程模块提供了以下专注于线程的类,我们将在本节中探讨所有这些类:Thread、Condition、Lock、RLock、Event、Semaphore、BoundedSemaphore、Timer 和 Barrier。
threading 还提供了许多有用的函数,包括在表 15-1 中列出的函数。
表 15-1. 线程模块的函数
| active_count | active_count() 返回一个整数,表示当前存活的线程对象数量(不包括已终止或尚未启动的线程)。 |
|---|---|
| current_thread | current_thread() 返回调用线程的 Thread 对象。如果调用线程不是由 threading 创建的,则 current_thread 创建并返回一个具有有限功能的半虚拟 Thread 对象。 |
| enumerate | enumerate() 返回当前存活的所有 Thread 对象的列表(不包括已终止或尚未启动的线程)。 |
| excepthook | excepthook(args) 3.8+ 重写此函数以确定如何处理线程内的异常;有关详细信息,请参阅在线文档。args 参数具有属性,允许您访问异常和线程详细信息。3.10+ threading.excepthook 保存了模块的原始线程钩子值。 |
| get_ident | get_ident() 返回一个非零整数作为所有当前线程中唯一的标识符。用于管理和跟踪线程数据。线程标识符可能会在线程退出并创建新线程时重复使用。 |
| get_native_id | get_native_id() 3.8+ 返回由操作系统内核分配的当前线程的本机整数 ID。适用于大多数常见操作系统。 |
| stack_size | stack_size([size]) 返回用于新线程的当前堆栈大小(以字节为单位),并(如果提供 size)为新线程设定值。size 的可接受值受到平台特定约束的限制,例如至少为 32768(或某些平台上更高的最小值),并且(在某些平台上)必须是 4096 的倍数。传递值为 0 总是可接受的,表示“使用系统的默认值”。当您传递一个在当前平台上不可接受的 size 值时,stack_size 会引发 ValueError 异常。 |
Thread 对象
线程实例 t 模拟一个线程。在创建 t 时,您可以将一个函数作为 t 的主函数传递给 target 参数,或者您可以子类化 Thread 并重写其 run 方法(您还可以重写 init,但不应重写其他方法)。在创建时,t 还未准备好运行;要使 t 准备就绪(活动),请调用 t.start。一旦 t 处于活动状态,它将在其主函数正常结束或通过传播异常时终止。线程 t 可以是 daemon,这意味着即使 t 仍然活动,Python 也可以终止,而普通(非守护)线程则会一直保持 Python 运行,直到线程终止。Thread 类提供了在 Table 15-2 中详细描述的构造函数、属性和方法。
Table 15-2. Thread 类的构造函数、方法和属性
| Thread | class Thread(name=None, target=None, args=(), kwargs={}, *, daemon=None)
始终使用命名参数调用 Thread:虽然参数的数量和顺序不受规范保证,但参数名是固定的。在构造 Thread 时有两个选项:
-
使用目标函数(t.run 在线程启动时调用 target(*args, **kwargs))来实例化 Thread 类本身。
-
扩展 Thread 类并重写其 run 方法。
在任何情况下,仅当调用 t.start 时执行将开始。name 成为 t 的名称。如果 name 是 None,Thread 为 t 生成一个唯一名称。如果 Thread 的子类 T 重写 init,T.init 必须 在调用任何其他 Thread 方法之前调用 Thread.init(通常通过 super 内置函数)。daemon 可以分配布尔值,或者如果为 None,则将从创建线程的 daemon 属性获取此值。 |
| daemon | daemon 是一个可写的布尔属性,指示 t 是否为守护线程(即使 t 仍然活动,进程也可以终止;这样的终止也会结束 t)。只能在调用 t.start 之前对 t.daemon 赋值;将 true 值赋给 t 将其设置为守护线程。守护线程创建的线程的 t.daemon 默认为 True。 |
|---|---|
| is_alive | t.is_alive() 当 t 处于活动状态时(即 t.start 已执行且 t.run 尚未终止),is_alive 返回 True;否则返回 False。 |
| join | t.join(timeout=None) join 暂停调用线程(不能是 t)直到 t 终止(当 t 已经终止时,调用线程不会暂停)。timeout 在 “超时参数” 中讨论。只能在 t.start 之后调用 t.join。可以多次调用 join。 |
| name | t.name name 是返回 t 名称的属性;分配 name 重新绑定 t 的名称(名称仅用于调试帮助;名称在线程中无需唯一)。如果省略,则线程将接收生成的名称 Thread-n,其中 n 是递增的整数(3.10+ 并且如果指定了 target,则将附加 (target.name))。 |
| run | t.run() 是由 t.start 调用的方法,执行 t 的主函数。Thread 的子类可以重写 run 方法。如果未重写,则 run 方法调用 t 创建时传递的 target 可调用对象。不要直接调用 t.run;调用 t.run 的工作由 t.start 完成! |
| start | t.start() 使 t 变为活动状态,并安排 t.run 在单独的线程中执行。对于任何给定的 Thread 对象 t,只能调用一次 t.start;再次调用会引发异常。 |
线程同步对象
threading 模块提供了几种同步原语(允许线程通信和协调的类型)。每种原语类型都有专门的用途,将在以下部分讨论。
也许你不需要线程同步原语
只要避免有(非队列)全局变量,这些变量会发生变化,多个线程可以访问,队列(在“队列模块”中介绍)通常可以提供所需的所有协调工作,同时并发(在“concurrent.futures 模块”中介绍)也可以。“线程化程序架构”展示了如何使用队列对象为您的多线程程序提供简单而有效的架构,通常无需显式使用同步原语。
超时参数
同步原语 Condition 和 Event 提供接受可选超时参数的 wait 方法。线程对象的 join 方法也接受可选的超时参数(参见表 15-2)。使用默认的超时值 None 会导致正常的阻塞行为(调用线程挂起并等待,直到满足所需条件)。当超时值不是 None 时,超时参数是一个浮点值,表示时间间隔,单位为秒(超时可以有小数部分,因此可以指示任何时间间隔,甚至非常短的间隔)。当超过超时秒数时,调用线程再次准备好,即使所需条件尚未满足;在这种情况下,等待方法返回 False(否则,方法返回 True)。超时让您设计能够处理少数线程偶发异常的系统,从而使系统更加健壮。但是,使用超时可能会降低程序的运行速度:在这种情况下,请务必准确测量您代码的速度。
锁和重入锁对象
锁和重入锁对象提供相同的三种方法,详见表 15-3。
表 15-3. 锁实例 L 的方法
| acquire | L.acquire(blocking=True, timeout=-1) 当 L 未锁定时,或者如果 L 是由同一个线程获取的重入锁,该线程立即锁定它(如果 L 是重入锁,则会增加内部计数器,如后面所述),并返回 True。
当 L 已经被锁定且 blocking 为 False 时,acquire 立即返回 False。当 blocking 为 True 时,调用线程将被挂起,直到以下情况发生之一:
-
另一个线程释放了锁,则该线程锁定它并返回 True。
-
在锁被获取之前操作超时,此时 acquire 返回 False。默认的 -1 值永不超时。
|
| locked | L.locked() 当 L 被锁定时返回 True;否则返回 False。 |
|---|---|
| release | L.release() 解锁 L,必须已锁定(对于 RLock,这意味着减少锁计数,锁计数不得低于零——只有当锁计数为零时才能由新线程获取)。当 L 被锁定时,任何线程都可以调用 L.release,不仅仅是锁定 L 的线程。当多个线程被阻塞在 L 上时(即调用了 L.acquire,发现 L 被锁定,并等待 L 解锁),release 将唤醒其中任意一个等待的线程。调用 release 的线程不会挂起:它仍然准备好并继续执行。 |
下面的控制台会话示例说明了当锁被用作上下文管理器时(以及 Python 在锁的使用过程中维护的其他数据,例如所有者线程 ID 和锁的获取方法被调用的次数)自动获取/释放锁的情况:
>>> lock = threading.RLock()
>>> print(lock)
<unlocked _thread.RLock object owner=0 count=0 at 0x102878e00>
>>> `with` lock:
... print(lock)
...
<locked _thread.RLock object owner=4335175040 count=1 at 0x102878e00>
>>> print(lock)
<unlocked _thread.RLock object owner=0 count=0 at 0x102878e00>
RLock 对象 r 的语义通常更为方便(除非在需要线程能够释放不同线程已获取的锁的特殊体系结构中)。 RLock 是一个可重入锁,意味着当 r 被锁定时,它会跟踪拥有它的线程(即锁定它的线程,对于 RLock 来说也是唯一能够释放它的线程——当任何其他线程尝试释放 RLock 时,会引发 RuntimeError 异常)。拥有它的线程可以再次调用 r.acquire 而不会阻塞;然后 r 只是增加一个内部计数。在涉及 Lock 对象的类似情况下,线程会阻塞直到某个其他线程释放该锁。例如,考虑以下代码片段:
lock = threading.RLock()
global_state = []
`def` recursive_function(some, args):
`with` lock: *`# acquires lock, guarantees release at end`*
*`# ...modify global_state...`*
`if` more_changes_needed(global_state):
recursive_function(other, args)
如果锁是 threading.Lock 的一个实例,当 recursive_function 递归调用自身时,会阻塞其调用线程:with 语句会发现锁已经被获取(尽管是同一个线程获取的),然后会阻塞并等待……等待。而使用 threading.RLock 则不会出现这样的问题:在这种情况下,由于锁已经被同一线程获取,再次获取时只是增加其内部计数然后继续。
一个 RLock 对象 r 只有在释放次数与获取次数相同的情况下才会解锁。当对象的方法相互调用时,RLock 对象非常有用;每个方法在开始时可以获取,并在结束时释放同一个 RLock 实例。
使用 with 语句自动获取和释放同步对象
使用try/finally语句(在“try/finally”中介绍)是确保已获取的锁确实被释放的一种方法。使用with语句,通常更好,因为所有的锁、条件和信号量都是上下文管理器,所以这些类型的实例可以直接在with子句中使用,以获取它(隐式地带有阻塞)并确保在with块的末尾释放它。
条件对象
条件对象c封装了锁或者递归锁对象L。Condition 类公开了在表 15-4 中描述的构造函数和方法。
表 15-4. Condition 类的构造函数和方法
| Condition | class Condition(lock=None) 创建并返回一个新的 Condition 对象c,并使用锁L设置为 lock。如果 lock 为None,则L设置为新创建的 RLock 对象。 |
|---|---|
| acquire, release | c.acquire(blocking=True), c.release() 这些方法只是调用L的相应方法。线程除了持有(即已获取)锁L之外,绝不应调用c的任何其他方法。 |
| notify, notify_all | c.notify(), c.notify_all() notify 唤醒在c上等待的任意一个线程。在调用c.notify 之前,调用线程必须持有L,并且 notify 不会释放L。被唤醒的线程直到再次可以获取L时才变为就绪。因此,通常调用线程在调用 notify 后调用 release。notify_all 类似于 notify,但唤醒所有等待的线程,而不仅仅是一个。 |
| wait | c.wait(timeout=None) wait 释放L,然后挂起调用线程,直到其他线程在c上调用 notify 或 notify_all。在调用c.wait 之前,调用线程必须持有L。timeout 在“超时参数”中有描述。线程通过通知或超时唤醒后,当再次获取L时,线程变为就绪。当 wait 返回True(表示正常退出,而不是超时退出)时,调用线程总是再次持有L。 |
通常,Condition 对象c调节一些在线程间共享的全局状态s的访问。当一个线程必须等待s改变时,线程循环:
`with` *`c`*:
`while` `not` is_ok_state(s):
*`c`*.wait()
do_some_work_using_state(s)
同时,每个修改s的线程在每次s变化时调用 notify(或者如果需要唤醒所有等待的线程而不仅仅是一个,则调用 notify_all):
`with` *`c`*:
do_something_that_modifies_state(*`s`*)
*`c`*.notify() *`# or, c.notify_all()`*
*`# no need to call c.release(), exiting 'with' intrinsically does that`*
您必须始终在每次使用c的方法周围获取和释放c:通过with语句这样做使得使用 Condition 实例更不容易出错。
事件对象
事件对象允许任意数量的线程挂起和等待。当任何其他线程调用 e.set 时,所有等待事件对象 e 的线程都变为就绪状态。e 有一个标志记录事件是否发生;在 e 创建时,该标志最初为 False。因此,事件类似于简化版的条件变量。事件对象适用于一次性的信号传递,但对于更一般的用途而言,可能不够灵活;特别是依赖于调用 e.clear 很容易出错。事件类公开了 表 15-5 中的构造函数和方法。
表 15-5. 事件类的构造函数和方法
| Event | class Event() 创建并返回一个新的事件对象 e,并将 e 的标志设置为 False。 |
|---|---|
| clear | e.clear() 将 e 的标志设置为 False。 |
| is_set | e.is_set() 返回 e 的标志值:True 或 False。 |
| set | e.set() 将 e 的标志设置为 True。所有等待 e 的线程(如果有)都将变为就绪状态。 |
| wait | e.wait(timeout=None) 如果 e 的标志为 True,则立即返回;否则,挂起调用线程,直到其他线程调用 set。timeout 参见 “超时参数”。 |
下面的代码显示了如何显式同步多个线程之间的处理过程使用事件对象:
`import` datetime, random, threading, time
`def` runner():
print('starting')
time.sleep(random.randint(1, 3))
print('waiting')
event.wait()
print(f'running at {datetime.datetime.now()}')
num_threads = 10
event = threading.Event()
threads = [threading.Thread(target=runner) `for` _ `in` range(num_threads)]
`for` t `in` threads:
t.start()
event.set()
`for` t `in` threads:
t.join()
信号量和有界信号量对象
信号量(也称为计数信号量)是锁的一种泛化形式。锁的状态可以看作是 True 或 False;信号量 s 的状态是一个在创建 s 时设置的介于 0 和某个 n 之间的数值(两个边界都包括)。信号量可以用来管理一组固定的资源,例如 4 台打印机或 20 个套接字,尽管对于这种目的,使用队列(本章后面描述)通常更为健壮。有界信号量类与此非常相似,但是如果状态超过初始值,则会引发 ValueError:在许多情况下,这种行为可以作为错误的有用指示器。 表 15-6 显示了信号量和有界信号量类的构造函数以及任何一类对象 s 所暴露的方法。
表 15-6. 信号量和有界信号量类的构造函数和方法
| Semaphore, BoundedSemaphore | class Semaphore(n=1), class BoundedSemaphore(n=1)
信号量使用指定状态 n 创建并返回一个信号量对象 s;有界信号量类似,但如果状态高于 n,则 s.release 会引发 ValueError。 |
| acquire | s.acquire(blocking=True) 当 s 的状态 >0 时,acquire 将状态减 1 并返回 True。当 s 的状态为 0 且 blocking 为 True 时,acquire 暂停调用线程并等待,直到其他线程调用 s.release。当 s 的状态为 0 且 blocking 为 False 时,acquire 立即返回 False。 |
|---|---|
| release | s.release() 当 s 的状态大于 0 时,或者状态为 0 但没有线程在等待 s 时,release 将状态增加 1。当 s 的状态为 0 且有线程在等待 s 时,release 将保持 s 的状态为 0,并唤醒其中一个等待的线程。调用 release 的线程不会挂起;它保持就绪状态并继续正常执行。 |
Timer 对象
Timer 对象在给定延迟后,在新创建的线程中调用指定的可调用对象。Timer 类公开了构造函数和 Table 15-7 中的方法。
Table 15-7. Timer 类的构造函数和方法
| Timer | class Timer(interval, callback, args=None, kwargs=None) 创建一个对象 t,在启动后 interval 秒调用 callback(interval 是一个浮点秒数)。 |
|---|---|
| cancel | t.cancel() 停止定时器并取消其动作的执行,只要在调用 cancel 时 t 仍在等待(尚未调用其回调)。 |
| start | t.start() 启动 t。 |
Timer 继承自 Thread 并添加了属性 function、interval、args 和 kwargs。
Timer 是“一次性”的:t 仅调用其回调一次。要周期性地每隔 interval 秒调用 callback,这里是一个简单的方法——周期性定时器每隔 interval 秒运行 callback,只有在 callback 引发异常时才停止:
`class` Periodic(threading.Timer):
`def` __init__(self, interval, callback, args=`None`, kwargs=`None`):
super().__init__(interval, self._f, args, kwargs)
self.callback = callback
`def` _f(self, *args, **kwargs):
p = type(self)(self.interval, self.callback, args, kwargs)
p.start()
`try`:
self.callback(*args, **kwargs)
`except` Exception:
p.cancel()
Barrier 对象
Barrier 是一种同步原语,允许一定数量的线程等待,直到它们都达到执行中的某一点,然后它们全部恢复。具体来说,当线程调用 b.wait 时,它会阻塞,直到指定数量的线程在 b 上做出相同的调用;此时,所有在 b 上阻塞的线程都被允许恢复。
Barrier 类公开了构造函数、方法和 Table 15-8 中列出的属性。
Table 15-8. Barrier 类的构造函数、方法和属性
| Barrier | class Barrier(num_threads, action=None, timeout=None) 为 num_threads 个线程创建一个 Barrier 对象 b。action 是一个没有参数的可调用对象:如果传递了此参数,则它会在所有阻塞线程中的任何一个被解除阻塞时执行。timeout 在 “Timeout parameters” 中有说明。 |
|---|---|
| abort | b.abort() 将 Barrier b 置于 broken 状态,这意味着当前等待的任何线程都会恢复,并抛出 threading.BrokenBarrierException(在任何后续调用 b.wait 时也会引发相同的异常)。这是一种紧急操作,通常用于当等待线程遭遇异常终止时,以避免整个程序死锁。 |
| broken | b.broken 当 b 处于 broken 状态时为 True;否则为 False。 |
| n_waiting | *b.*n_waiting 当前正在等待 b 的线程数。 |
| parties | parties 在 b 构造函数中作为 num_threads 传递的值。 |
| 重置 | b.reset() 将b重置为初始的空、未损坏状态;但是,当前正在b上等待的任何线程都将恢复,带有 threading.BrokenBarrierException 异常。 |
| 等待 | b.wait() 第一个b.parties-1 个调用b.wait 的线程会阻塞;当阻塞在b上的线程数为b.parties-1 时,再有一个线程调用b.wait,所有阻塞在b上的线程都会恢复。b.wait 向每个恢复的线程返回一个 int,所有返回值都是不同的且在 range(b.parties)范围内,顺序不确定;线程可以使用这个返回值来确定接下来应该做什么(尽管在 Barrier 的构造函数中传递 action 更简单且通常足够)。 |
下面的代码展示了 Barrier 对象如何在多个线程之间同步处理(与之前展示的 Event 对象的示例代码进行对比):
`import` datetime, random, threading, time
`def` runner():
print('starting')
time.sleep(random.randint(1, 3))
print('waiting')
`try`:
my_number = barrier.wait()
`except` threading.BrokenBarrierError:
print('Barrier abort() or reset() called, thread exiting...')
return
print(f'running ({my_number}) at {datetime.datetime.now()}')
`def` announce_release():
print('releasing')
num_threads = 10
barrier = threading.Barrier(num_threads, action=announce_release)
threads = [threading.Thread(target=runner) `for` _ `in` range(num_threads)]
`for` t `in` threads:
t.start()
`for` t `in` threads:
t.join()
线程本地存储
threading 模块提供了类 local,线程可以使用它来获取线程本地存储,也称为每个线程的数据。local 的一个实例L有任意命名的属性,你可以设置和获取,存储在字典L.dict 中,也可以直接访问。L是完全线程安全的,这意味着多个线程同时设置和获取L上的属性没有问题。每个访问L的线程看到的属性集合是独立的:在一个线程中进行的任何更改对其他线程没有影响。例如:
`import` threading
L = threading.local()
print('in main thread, setting zop to 42')
L.zop = 42
`def` targ():
print('in subthread, setting zop to 23')
L.zop = 23
print('in subthread, zop is now', L.zop)
t = threading.Thread(target=targ)
t.start()
t.join()
print('in main thread, zop is now', L.zop)
*`# prints:`*
*`#`* *`in main thread, setting zop to 42`*
*`#`* *`in subthread, setting zop to 23`*
*`#`* *`in subthread, zop is now 23`*
*`#`* *`in main thread, zop is now 42`*
线程本地存储使得编写多线程代码更加容易,因为你可以在多个线程中使用相同的命名空间(即 threading.local 的一个实例),而不会相互干扰。
队列模块
队列模块提供支持多线程访问的队列类型,主要类是 Queue,还有一个简化的类 SimpleQueue,主类的两个子类(LifoQueue 和 PriorityQueue),以及两个异常类(Empty 和 Full),在表 15-9 中描述。主类及其子类实例暴露的方法详见表 15-10。
表 15-9. 队列模块的类
| 队列 | 类 Queue(maxsize=0) Queue 是 queue 模块中的主要类,实现先进先出(FIFO)队列:每次检索的项是最早添加的项。
当 maxsize > 0 时,新的 Queue 实例q在q达到 maxsize 项时被视为已满。当q已满时,插入带有 block=True的项的线程将暂停,直到另一个线程提取一个项。当 maxsize <= 0 时,q永远不会被视为满,仅受可用内存限制,与大多数 Python 容器一样。 |
| SimpleQueue | class SimpleQueue SimpleQueue 是一个简化的 Queue:一个无界的 FIFO 队列,缺少 full、task_done 和 join 方法(请参见 Table 15-10),并且 put 方法忽略其可选参数但保证可重入性(这使其可在 del 方法和 weakref 回调中使用,而 Queue.put 则不能)。 |
|---|---|
| LifoQueue | class LifoQueue(maxsize=0) LifoQueue 是 Queue 的子类;唯一的区别是 LifoQueue 实现了后进先出(LIFO)队列,意味着每次检索到的项是最近添加的项(通常称为 stack)。 |
| PriorityQueue | class PriorityQueue(maxsize=0) PriorityQueue 是 Queue 的子类;唯一的区别是 PriorityQueue 实现了一个 priority 队列,意味着每次检索到的项是当前队列中最小的项。由于没有指定排序的方法,通常会使用 (priority, payload) 对作为项,其中 priority 的值较低表示较早的检索。 |
| Empty | Empty 是当 q 为空时 q.get(block=False) 抛出的异常。 |
| Full | Full 是当 q 满时 q.put(x, block=False) 抛出的异常。 |
实例 q 为 Queue 类(或其子类之一)的一个实例,提供了 Table 15-10 中列出的方法,所有方法都是线程安全的,并且保证是原子操作。有关 SimpleQueue 实例公开的方法的详细信息,请参见 Table 15-9。 |
Table 15-10. 类 Queue、LifoQueue 或 PriorityQueue 的实例 q 的方法
| empty | q.empty() 返回 True 当 q 为空时;否则返回 False。 |
|---|---|
| full | q.full() 返回 True 当 q 满时;否则返回 False。 |
| get, get_nowait | q.get(block=True, timeout=None), q.get_nowait() |
当 block 为 False 时,如果 q 中有可用项,则 get 移除并返回一个项;否则 get 抛出 Empty 异常。当 block 为 True 且 timeout 为 None 时,如果需要,get 移除并返回 q 中的一个项,挂起调用线程,直到有可用项。当 block 为 True 且 timeout 不为 None 时,timeout 必须是 >=0 的数字(可能包括用于指定秒的小数部分),get 等待不超过 timeout 秒(如果到达超时时间仍然没有可用项,则 get 抛出 Empty)。q.get_nowait() 类似于 q.get(False),也类似于 q.get(timeout=0.0)。get 移除并返回项:如果 q 是 Queue 的直接实例,则按照 put 插入它们的顺序(FIFO),如果 q 是 LifoQueue 的实例,则按 LIFO 顺序,如果 q 是 PriorityQueue 的实例,则按最小优先顺序。 |
| put, put_nowait | q.put(item, block=True, timeout=None) q.put_nowait(item) |
当 block 为False时,如果q不满,则 put 将item添加到q中;否则,put 会引发 Full 异常。当 block 为True且 timeout 为None时,如果需要,put 将item添加到q,挂起调用线程,直到q不再满为止。当 block 为True且 timeout 不为None时,timeout 必须是>=0 的数字(可能包括指定秒的小数部分),put 等待不超过 timeout 秒(如果到时q仍然满,则 put 引发 Full 异常)。q.put_nowait(item)类似于q.put(item, False),也类似于q.put(item, timeout=0.0)。
| qsize | q.qsize() 返回当前在q中的项目数。 |
|---|
q维护一个内部的、隐藏的“未完成任务”计数,起始为零。每次调用 put 都会将计数增加一。要将计数减少一,当工作线程完成处理任务时,它调用q.task_done。为了同步“所有任务完成”,调用q.join:当未完成任务的计数非零时,q.join 阻塞调用线程,稍后当计数变为零时解除阻塞;当未完成任务的计数为零时,q.join 继续调用线程。
如果你喜欢以其他方式协调线程,不必使用 join 和 task_done,但是当需要使用队列协调线程系统时,它们提供了一种简单实用的方法。
队列提供了“宁可请求宽恕,不要请求许可”(EAFP)的典型例子,见“错误检查策略”。由于多线程,q的每个非变异方法(empty、full、qsize)只能是建议性的。当其他线程变异q时,线程从非变异方法获取信息的瞬间和线程根据该信息采取行动的下一瞬间之间可能发生变化。因此,依赖“先看再跳”(LBYL)的习惯是徒劳的,而为了修复问题而摆弄锁定则是大量浪费精力。避免脆弱的 LBYL 代码,例如:
`if` *`q`*.empty():
print('no work to perform')
`else`: # Some other thread may now have emptied the queue!
*`x`* = *`q`*.get_nowait()
work_on(*`x`*)
而是采用更简单和更健壮的 EAFP 方法:
`try`:
*`x`* = *`q`*.get_nowait()
`except` queue.Empty: # Guarantees the queue was empty when accessed
print('no work to perform')
`else`:
work_on(*`x`*)
多进程模块
多进程模块提供了函数和类,几乎可以像多线程一样编写代码,但是将工作分布到进程而不是线程中:这些包括类 Process(类似于 threading.Thread)和用于同步原语的类(Lock、RLock、Condition、Event、Semaphore、BoundedSemaphore 和 Barrier,每个类似于线程模块中同名的类,以及 Queue 和 JoinableQueue,这两者类似于 queue.Queue)。这些类使得将用于线程的代码转换为使用多进程的版本变得简单;只需注意我们在下一小节中涵盖的差异即可。
通常最好避免在进程之间共享状态:而是使用队列来显式地在它们之间传递消息。然而,在确实需要共享一些状态的罕见情况下,multiprocessing 提供了访问共享内存的类(Value 和 Array),以及更灵活的(包括在网络上不同计算机之间的协调)但带有更多开销的过程子类 Manager,设计用于保存任意数据并让其他进程通过代理对象操作该数据。我们在“共享状态:类 Value、Array 和 Manager”中介绍状态共享。
在编写新代码时,与其移植最初使用线程编写的代码,通常可以使用 multiprocessing 提供的不同方法。特别是 Pool 类(在“进程池”中介绍)通常可以简化您的代码。进行多进程处理的最简单和最高级的方式是与 ProcessPoolExecutor 一起使用的 concurrent.futures 模块(在“concurrent.futures 模块”中介绍)。
基于由 Pipe 工厂函数构建或包装在 Client 和 Listener 对象中的 Connection 对象的其他高级方法,甚至更加灵活,但也更加复杂;我们在本书中不再进一步讨论它们。有关更详尽的 multiprocessing 覆盖,请参阅在线文档²以及像PyMOTW中的第三方在线教程。
multiprocessing 与线程之间的区别
您可以相对容易地将使用线程编写的代码移植为使用 multiprocessing 的变体,但是您必须考虑几个不同之处。
结构差异
您在进程之间交换的所有对象(例如通过队列或作为进程目标函数参数)都是通过 pickle 序列化的,详见“pickle 模块”。因此,您只能交换可以这样序列化的对象。此外,序列化的字节串不能超过约 32 MB(取决于平台),否则会引发异常;因此,您可以交换的对象大小存在限制。
尤其是在 Windows 系统中,子进程必须能够将启动它们的主脚本作为模块导入。因此,请确保将主脚本中的所有顶级代码(指不应由子进程再次执行的代码)用通常的if name == 'main'惯用语包围起来,详见“主程序”。
如果进程在使用队列或持有同步原语时被突然终止(例如通过信号),它将无法对该队列或原语执行适当的清理。因此,队列或原语可能会损坏,导致所有尝试使用它的其他进程出现错误。
进程类
类 multiprocessing.Process 与 threading.Thread 非常相似;它提供了所有相同的属性和方法(参见表 15-2),以及一些额外的方法,在表 15-11 中列出。它的构造函数具有以下签名:
| 进程 | 类 Process(name=None, target=None, args=(), kwargs={}) 始终 使用命名参数 调用 Process:参数的数量和顺序不受规范保证,但参数名是固定的。要么实例化 Process 类本身,传递一个目标函数(p.run 在线程启动时调用 target(args, **kwargs));或者,而不是传递目标,扩展 Process 类并覆盖其 run 方法。在任一情况下,只有在调用 p.start 时执行将开始。name 成为 p 的名称。如果 name 为 None,Process 为 p 生成唯一名称。如果 Process 的子类 P 覆盖 init,P.init 必须 在任何其他 Process 方法之前在 self 上调用 Process.init(通常通过 super 内置函数)。 |
|---|
表 15-11. Process 类的附加属性和方法
| authkey | 进程的授权密钥,一个字节串。这是由 os.urandom 提供的随机字节初始化的,但如果需要,您可以稍后重新分配它。用于授权握手的高级用途我们在本书中没有涵盖。 |
|---|---|
| close | close() 关闭进程实例并释放所有与之关联的资源。如果底层进程仍在运行,则引发 ValueError。 |
| exitcode | None 当进程尚未退出时;否则,进程的退出码。这是一个整数:成功为 0,失败为 >0,进程被杀死为 <0。 |
| kill | kill() 与 terminate 相同,但在 Unix 上发送 SIGKILL 信号。 |
| pid | None 当进程尚未启动时;否则,进程的标识符由操作系统设置。 |
| terminate | terminate() 终止进程(不给予其执行终止代码的机会,如清理队列和同步原语;当进程正在使用队列或持有同步原语时,可能会引发错误!)。 |
队列的区别
类 multiprocessing.Queue 与 queue.Queue 非常相似,不同之处在于 multiprocessing.Queue 的实例 q 不提供方法 join 和 task_done(在“队列模块”中描述)。当 q 的方法由于超时而引发异常时,它们会引发 queue.Empty 或 queue.Full 的实例。multiprocessing 没有 queue 的 LifoQueue 和 PriorityQueue 类的等效物。
类 multiprocessing.JoinableQueue 确实提供了方法 join 和 task_done,但与 queue.Queue 相比有语义上的区别:对于 multiprocessing.JoinableQueue 的实例 q,调用 q.get 的进程在处理完工作单元后 必须 调用 q.task_done(这不是可选的,就像使用 queue.Queue 时那样)。
您放入 multiprocessing 队列的所有对象必须能够通过 pickle 进行序列化。在执行 q.put 和对象从 q.get 可用之间可能会有延迟。最后,请记住,进程使用 q 的突然退出(崩溃或信号)可能会使 q 对于任何其他进程不可用。
共享状态:类 Value、Array 和 Manager
为了在两个或多个进程之间共享单个原始值,multiprocessing 提供了类 Value,并且对于固定长度的原始值数组,它提供了类 Array。为了获得更大的灵活性(包括共享非原始值和在网络连接的不同系统之间“共享”但不共享内存的情况),multiprocessing 提供了 Manager 类,它是 Process 的子类,但开销较高。我们将在以下小节中详细看看这些类。
Value 类
类 Value 的构造函数具有以下签名:
| Value | class Value(typecode, *args, ***, lock=True) typecode 是一个字符串,定义值的基本类型,就像在 “数组模块” 中所讨论的 array 模块一样。(另外,typecode 可以是来自 ctypes 模块的类型,这在 第二十五章“ctypes” 中讨论过,但这很少需要。)args 被传递到类型的构造函数中:因此,args 要么不存在(在这种情况下,基本类型会按其默认值初始化,通常为 0),要么是一个单一的值,用于初始化该基本类型。
当 lock 为 True(默认情况下),Value 将创建并使用一个新的锁来保护实例。或者,您可以将现有的 Lock 或 RLock 实例作为锁传递。甚至可以传递 lock=False,但这很少是明智的选择:当您这样做时,实例不受保护(因此在进程之间不同步),并且缺少方法 get_lock。如果传递 lock,则必须将其作为命名参数,使用 lock=something。
一个类值的实例 v 提供了方法 get_lock,该方法返回(但不获取也不释放)保护 v 的锁,并且具有读/写属性 value,用于设置和获取 v 的基本原始值。
为了确保对 v 的基本原始值的操作是原子性的,请在 with v.get_lock(): 语句中保护该操作。一个典型的使用例子可能是增强赋值,如下所示:
`with` v.get_lock():
v.value += 1
然而,如果任何其他进程对相同的原始值执行了不受保护的操作——甚至是原子操作,比如简单的赋值操作,如 v.value = x——那么一切都不确定:受保护的操作和不受保护的操作可能导致系统出现 竞态条件。[³] 为了安全起见:如果 v.value 上的 任何 操作都不是原子的(因此需要通过位于 with v.get_lock(): 块中的保护),那么通过将它们放在这些块中来保护 v.value 上的 所有 操作。
Array 类
multiprocessing.Array 是一个固定长度的原始值数组,所有项都是相同的原始类型。Array 类的构造函数具有如下签名:
| Array | class Array(typecode, size_or_initializer, ***, lock=True) typecode 是定义值的原始类型的字符串,就像在 “数组模块” 中所介绍的那样,与模块 array 中的处理方式相同。(另外,typecode 可以是模块 ctypes 中的类型,讨论在 第二十五章“ctypes” 中,但这很少是必要的。)size_or_initializer 可以是可迭代对象,用于初始化数组,或者是用作数组长度的整数,在这种情况下,数组的每个项都初始化为 0。
当 lock 为 True(默认值)时,Array 会创建并使用一个新的锁来保护实例。或者,您可以将现有的 Lock 或 RLock 实例作为锁传递给 lock。您甚至可以传递 lock=False,但这很少是明智的:当您这样做时,实例不受保护(因此在进程之间不同步),并且缺少 get_lock 方法。如果您传递 lock,则必须将其作为命名参数传递,使用 lock=*something*。 |
Array 类的实例 a 提供了方法 get_lock,该方法返回(但不获取也不释放)保护 a 的锁。
a 通过索引和切片访问,并通过对索引或切片赋值来修改。a 是固定长度的:因此,当您对切片赋值时,您必须将一个与您要赋值的切片完全相同长度的可迭代对象赋值给它。a 也是可迭代的。
在特殊情况下,当 a 使用 'c' 类型码构建时,您还可以访问 a.value 以获取 a 的内容作为字节串,并且您可以将任何长度不超过 len(a) 的字节串分配给 a.value。当 s 是长度小于 len(a) 的字节串时,a.value = s 意味着 a[:len(s)+1] = s + b'\0';这反映了 C 语言中字符串的表示,以 0 字节终止。例如:
a = multiprocessing.Array('c', b'four score and seven')
a.value = b'five'
print(a.value) *`# prints`* *`b'five'`*
print(a[:]) *`# prints`* *`b'five\xOOscore and seven'`*
Manager 类
multiprocessing.Manager 是 multiprocessing.Process 的子类,具有相同的方法和属性。此外,它还提供方法来构建任何 multiprocessing 同步原语的实例,包括 Queue、dict、list 和 Namespace。Namespace 是一个类,允许您设置和获取任意命名属性。每个方法都以它所构建实例的类名命名,并返回一个代理到这样一个实例,任何进程都可以使用它来调用方法(包括 dict 或 list 实例的索引等特殊方法)。
代理对象大多数操作符、方法和属性访问都会传递给它们代理的实例;但是,它们不会传递比较操作符 —— 如果你需要比较,就需要获取代理对象的本地副本。例如:
manager = multiprocessing.Manager()
p = manager.list()
p[:] = [1, 2, 3]
print(p == [1, 2, 3]) *`# prints`* *`False`**`,`* * `it compares with p itself`*
print(list(p) == [1, 2, 3]) *`# prints`* *`True`**`,`* * `it compares with copy`*
Manager 的构造函数不接受任何参数。有高级方法来定制 Manager 子类,以允许来自无关进程的连接(包括通过网络连接的不同计算机上的进程),并提供不同的构建方法集,但这些内容不在本书的讨论范围之内。使用 Manager 的一个简单而通常足够的方法是显式地将它生成的代理传输给其他进程,通常通过队列或作为进程的目标函数的参数。
例如,假设有一个长时间运行的 CPU 绑定函数 f,给定一个字符串作为参数,最终返回对应的结果;给定一组字符串,我们希望生成一个字典,其键为字符串,值为相应的结果。为了能够跟踪 f 运行在哪些进程上,我们在调用 f 前还打印进程 ID。示例 15-1 展示了如何实现这一点。
示例 15-1. 将工作分配给多个工作进程
`import` multiprocessing `as` mp
`def` f(s):
*`"""Run a long time, and eventually return a result."""`*
`import` time`,` random
time.sleep(random.random()*2) *`# simulate slowness`*
`return` s+s *`# some computation or other`*
`def` runner(s, d):
print(os.getpid(), s)
d[s] = f(s)
`def` make_dict(strings):
mgr = mp.Manager()
d = mgr.dict()
workers = []
`for` s `in` strings:
p = mp.Process(target=runner, args=(s, d))
p.start()
workers.append(p)
`for` p `in` workers:
p.join()
`return` {**d}
进程池
在实际应用中,应始终避免创建无限数量的工作进程,就像我们在示例 15-1 中所做的那样。性能的提升仅限于您计算机上的核心数(可通过调用 multiprocessing.cpu_count 获取),或者略高于或略低于此数,具体取决于您的平台、代码是 CPU 绑定还是 I/O 绑定,计算机上运行的其他任务等微小差异。创建比这个最佳数量多得多的工作进程会带来大量额外的开销,却没有任何补偿性的好处。
因此,一个常见的设计模式是启动一个带有有限数量工作进程的池,并将工作分配给它们。multiprocessing.Pool 类允许您编排这种模式。
池类
类 Pool 的构造函数签名为:
| 池 | 类 Pool(processes=None, initializer=None, initargs=(), maxtasksperchild=None)
processes 是池中的进程数;默认值为 cpu_count 返回的值。当 initializer 不为 None 时,它是一个函数,在池中每个进程开始时调用,带有 initargs 作为参数,例如 initializer(*initargs)。
当 maxtasksperchild 不为 None 时,它是池中每个进程可以执行的最大任务数。当池中的进程执行了这么多任务后,它终止,然后一个新的进程启动并加入池。当 maxtasksperchild 为 None(默认值)时,每个进程的生命周期与池一样长。
类 Pool 的实例 p 提供了 表 15-12 中列出的方法(每个方法只能在构建实例 p 的进程中调用)。
表 15-12. 类 Pool 的实例 p 的方法
| apply | apply(func, args=(), kwds={}) 在工作进程中的任意一个,运行 func(*args, **kwds),等待其完成,并返回 func 的结果。 |
|---|---|
| apply_async | apply_async(func, args=(), kwds={}, callback=None) 在工作进程中的任意一个,开始运行 func(*args, **kwds),并且不等待其完成,立即返回一个 AsyncResult 实例,该实例最终提供 func 的结果,当结果准备就绪时。 (AsyncResult 类在下一节中讨论。)当 callback 不为 None 时,它是一个函数,用 func 的结果作为唯一参数调用(在调用 apply_async 的进程中的新线程中),当结果准备就绪时;callback 应该快速执行,因为否则会阻塞调用进程。如果参数是可变的,则 callback 可以改变其参数;callback 的返回值是无关紧要的(因此,最佳、最清晰的风格是让它返回 None)。 |
| close | close() 设置一个标志,禁止进一步向池提交任务。工作进程在完成所有未完成的任务后终止。 |
| imap | imap(func, iterable, chunksize=1) 返回一个迭代器,在每个 iterable 的项上调用 func,顺序执行。chunksize 确定连续发送给每个进程的项数;在非常长的 iterable 上,较大的 chunksize 可以提高性能。当 chunksize 为 1(默认值)时,返回的迭代器有一个方法 next(即使迭代器方法的规范名称是 next),接受一个可选的 timeout 参数(浮点数值,单位秒),在 timeout 秒后仍未准备就绪时引发 multiprocessing.TimeoutError。 |
| imap_unordered | imap_unordered(func, iterable, chunksize=1) 与 imap 相同,但结果的顺序是任意的(在迭代顺序不重要时,有时可以提高性能)。如果函数的返回值包含足够的信息,以允许将结果与用于生成它们的 iterable 的值关联,则通常很有帮助。 |
| join | join() 等待所有工作进程退出。您必须在调用 join 之前调用 close 或 terminate。 |
| map | map(func, iterable, chunksize=1) 在池中的工作进程上按顺序对 iterable 中的每个项目调用 func;等待它们全部完成,并返回结果列表。chunksize 确定每个进程发送多少连续项目;在非常长的 iterable 上,较大的 chunksize 可以提高性能。 |
| map_async | map_async(func, iterable, chunksize=1, callback=None) 安排在池中的工作进程上对可迭代对象 iterable 中的每个项目调用 func;在等待任何操作完成之前,立即返回一个 AsyncResult 实例(在下一节中描述),该实例最终提供 func 的结果列表,当该列表准备就绪时。
当 callback 不为 None 时,它是一个函数(在调用 map_async 的进程的单独线程中调用),其参数是按顺序排列的 func 的结果列表,当该列表准备就绪时。callback 应该快速执行,否则会阻塞进程。callback 可能会改变其列表参数;callback 的返回值是无关紧要的(因此,最好、最清晰的风格是让其返回 None)。|
| terminate | terminate() 一次性终止所有工作进程,而无需等待它们完成工作。 |
|---|
例如,这是一种基于池的方法来执行与 Example 15-1 中代码相同的任务:
`import` os, multiprocessing `as` mp
`def` f(s):
*`"""Run a long time, and eventually return a result."""`*
`import` time, random
time.sleep(random.random()*2) *`# simulate slowness`*
`return` s+s *`# some computation or other`*
`def` runner(s):
print(os.getpid(), s)
`return` s, f(s)
`def` make_dict(strings):
`with` mp.Pool() `as` pool:
d = dict(pool.imap_unordered(runner, strings))
`return` d
异步结果类
类 Pool 的方法 apply_async 和 map_async 返回类 AsyncResult 的一个实例。类 AsyncResult 的一个实例 r 提供了 Table 15-13 中列出的方法。
表 15-13. 类 AsyncResult 实例 r 的方法
| get | get(timeout=None) 阻塞并在结果准备就绪时返回结果,或在计算结果时重新引发引发的异常。当 timeout 不为 None 时,它是以秒为单位的浮点值;如果在超时秒后结果尚未准备就绪,则 get 会引发 multiprocessing.TimeoutError。 |
|---|---|
| ready | ready() 不阻塞;如果调用已完成并返回结果或已引发异常,则返回 True;否则返回 False。 |
| successful | successful() 不阻塞;如果结果已准备就绪且计算未引发异常,则返回 True;如果计算引发异常,则返回 False。如果结果尚未准备就绪,successful 将引发 AssertionError。 |
| wait | wait(timeout=None) 阻塞并等待结果准备就绪。当 timeout 不为 None 时,它是以秒为单位的浮点值:如果在超时秒后结果尚未准备就绪,则 wait 会引发 multiprocessing.TimeoutError。 |
线程池类
multiprocessing.pool 模块还提供了一个名为 ThreadPool 的类,其接口与 Pool 完全相同,但在单个进程中使用多个线程实现(而不是多个进程,尽管模块的名称如此)。使用 ThreadPool 的等效 make_dict 代码,与 示例 15-1 中使用 ThreadPoolExecutor 的代码类似:
`def` make_dict(strings):
num_workers=3
`with` mp.pool.ThreadPool(num_workers) `as` pool:
d = dict(pool.imap_unordered(runner, strings))
`return` d
由于 ThreadPool 使用多个线程但限于在单个进程中运行,因此最适合于其各个线程执行重叠 I/O 的应用程序。正如前面所述,当工作主要受 CPU 限制时,Python 线程提供的优势很小。
在现代 Python 中,您通常应优先使用模块 concurrent.futures 中的抽象类 Executor(下一节将介绍),以及其两个实现,ThreadPoolExecutor 和 ProcessPoolExecutor。特别是,由 concurrent.futures 实现的执行器类的 submit 方法返回的 Future 对象与 asyncio 模块兼容(正如前面提到的,我们不在本书中涵盖 asyncio,但它仍然是 Python 最近版本中许多并发处理的重要部分)。由 multiprocessing 实现的池类的 apply_async 和 map_async 方法返回的 AsyncResult 对象不兼容 asyncio。
并发.futures 模块
并发包提供了一个单一模块,即 futures。concurrent.futures 提供了两个类,ThreadPoolExecutor(使用线程作为工作者)和 ProcessPoolExecutor(使用进程作为工作者),它们实现了相同的抽象接口 Executor。通过调用该类并指定一个参数 max_workers 来实例化任何一种池,该参数指定池应包含的线程或进程数。您可以省略 max_workers,让系统选择工作者数。
Executor 类的实例 e 支持 表 15-14 中的方法。
表 15-14。类 Executor 的实例 e 的方法
| map | map(func, *iterables, timeout=None, chunksize=1) 返回一个迭代器 it,其项是按顺序使用多个工作线程或进程并行执行 func 的结果。当 timeout 不为 None 时,它是一个浮点秒数:如果在 timeout 秒内 next(it) 未产生任何结果,则引发 concurrent.futures.TimeoutError。
您还可以选择(仅按名称)指定参数 chunksize:对于 ThreadPoolExecutor 无效;对于 ProcessPoolExecutor,它设置每个可迭代项中的每个项目传递给每个工作进程的数量。 |
| shutdown | shutdown(wait=True) 禁止进一步调用 map 或 submit。当 wait 为 True 时,shutdown 阻塞直到所有待处理的 Future 完成;当 False 时,shutdown 立即返回。在任何情况下,进程直到所有待处理的 Future 完成后才终止。 |
|---|---|
| submit | submit(*func*, **a*, ***k*) 确保 func(*a, **k) 在线程池的任一进程或线程中执行。不会阻塞,立即返回一个 Future 实例。 |
任何 Executor 实例也是上下文管理器,因此适合在 with 语句中使用(exit 行为类似于 shutdown(wait=True))。
例如,这里是一个基于并发的方法来执行与 示例 15-1 中相同任务的方法:
`import` concurrent.futures `as` cf
`def` f(s):
*`"""run a long time and eventually return a result"""`*
*`# ...`* *`like before!`*
`def` runner(s):
`return` s, f(s)
`def` make_dict(strings):
`with` cf.ProcessPoolExecutor() `as` e:
d = dict(e.map(runner, strings))
`return` d
Executor 的 submit 方法返回一个 Future 实例。Future 实例 f 提供了表 15-15 中描述的方法。
表 15-15. Future 类实例 f 的方法
| add_done_callback | add_done_callback(*func*) 将可调用对象 func 添加到 f 上;当 f 完成(即取消或完成)时,会调用 func,并以 f 作为唯一参数。 |
|---|---|
| cancel | cancel() 尝试取消调用。当调用正在执行且无法被取消时,返回 False;否则返回 True。 |
| cancelled | cancelled() 返回 True 如果调用成功取消;否则返回 False。 |
| done | done() 返回 True 当调用完成(即已完成或成功取消)。 |
| exception | exception(timeout=**None**) 返回调用引发的异常,如果调用没有引发异常则返回 None。当 timeout 不为 None 时,它是一个浮点数秒数。如果调用在 timeout 秒内未完成,exception 抛出 concurrent.futures.TimeoutError;如果调用被取消,exception 抛出 concurrent.futures.CancelledError。 |
| result | result(timeout=**None**) 返回调用的结果。当 timeout 不为 None 时,它是一个浮点数秒数。如果调用在 timeout 秒内未完成,result 抛出 concurrent.futures.TimeoutError;如果调用被取消,result 抛出 concurrent.futures.CancelledError。 |
| running | running() 返回 True 当调用正在执行且无法被取消;否则返回 False。 |
concurrent.futures 模块还提供了两个函数,详见 表 15-16。
表 15-16. concurrent.futures 模块的函数
| as_completed | as_completed(*fs*, timeout=**None**) 返回一个迭代器 it,迭代 Future 实例,这些实例是可迭代对象 fs 的项。如果 fs 中有重复项,则每个项只返回一次。it 按顺序逐个返回已完成的 Future 实例。如果 timeout 不为 None,它是一个浮点数秒数;如果在 timeout 秒内从上一个已完成实例之后尚未返回新的实例,则 as_completed 抛出 concurrent.futures.Timeout。 |
|---|
| wait | wait(fs, timeout=None, return_when=ALL_COMPLETED) 等待 iterable fs中作为项目的未来实例。返回一个命名的 2 元组集合:第一个集合名为 done,包含在 wait 返回之前已完成(意味着它们已经完成或被取消)的未来实例;第二个集合名为 not_done,包含尚未完成的未来实例。
如果 timeout 不为None,则 timeout 是一个浮点数秒数,表示在返回之前允许等待的最长时间(当 timeout 为None时,wait 仅在满足 return_when 时返回,不管之前经过了多少时间)。
return_when 控制 wait 何时返回;它必须是 concurrent.futures 模块提供的三个常量之一:
ALL_COMPLETED
当所有未来实例完成或被取消时返回。
FIRST_COMPLETED
当任何一个未来完成或被取消时返回。
FIRST_EXCEPTION
当任何未来引发异常时返回;如果没有未来引发异常,则变成等价于 ALL_COMPLETED。
|
这个版本的 make_dict 演示了如何使用 concurrent.futures.as_completed 来在每个任务完成时处理它(与使用 Executor.map 的前一个示例形成对比,后者总是按照提交顺序返回任务):
`import` concurrent.futures `as` cf
`def` make_dict(strings):
`with` cf.ProcessPoolExecutor() `as` e:
futures = [e.submit(runner, s) `for` s `in` strings]
d = dict(f.result() `for` f `in` cf.as_completed(futures))
`return` d
线程程序架构
一个线程程序应该总是尽量安排一个单一线程“拥有”任何外部于程序的对象或子系统(如文件、数据库、GUI 或网络连接)。虽然有多个处理同一外部对象的线程是可能的,但通常会造成难以解决的问题。
当您的线程程序必须处理某些外部对象时,为这类交互专门分配一个线程,并使用一个队列对象,从中外部交互线程获取其他线程提交的工作请求。外部交互线程通过将结果放在一个或多个其他队列对象中来返回结果。以下示例展示了如何将此架构打包成一个通用的可重用类,假设外部子系统上的每个工作单元可以用可调用对象表示:
`import` threading, queue
`class` ExternalInterfacing(threading.Thread):
`def` __init__(self, external_callable, **kwds):
super().__init__(**kwds)
self.daemon = `True`
self.external_callable = external_callable
self.request_queue = queue.Queue()
self.result_queue = queue.Queue()
self.start()
`def` request(self, *args, **kwds):
*`"""called by other threads as external_callable would be"""`*
self.request_queue.put((args, kwds))
`return` self.result_queue.get()
`def` run(self):
`while` `True`:
a, k = self.request_queue.get()
self.result_queue.put(self.external_callable(*a, **k))
一旦实例化了某个 ExternalInterfacing 对象ei,任何其他线程都可以调用ei.request,就像在缺乏这种机制的情况下调用 external_callable 一样(根据需要带有或不带参数)。ExternalInterfacing 的优点在于对 external_callable 的调用是串行化的。这意味着仅有一个线程(绑定到ei的 Thread 对象)按照某个定义好的顺序顺序执行它们,没有重叠、竞争条件(依赖于哪个线程“恰好先到达”)或其他可能导致异常情况的问题。
如果您需要将多个可调用对象串行化在一起,可以将可调用对象作为工作请求的一部分传递,而不是在类 ExternalInterfacing 的初始化时传递它,以获得更大的通用性。以下示例展示了这种更通用的方法:
`import` threading, queue
`class` Serializer(threading.Thread):
def __init__(self, **kwds):
super().__init__(**kwds)
self.daemon = `True`
self.work_request_queue = queue.Queue()
self.result_queue = queue.Queue()
self.start()
`def` apply(self, callable, *args, **kwds):
*``"""called by other threads as `callable` would be"""``*
self.work_request_queue.put((callable, args, kwds))
`return` self.result_queue.get()
`def` run(self):
`while` `True`:
callable, args, kwds = self.work_request_queue.get()
self.result_queue.put(callable(*args, **kwds))
一旦实例化了一个 Serializer 对象ser,任何其他线程都可以调用ser.apply(external_callable),就像没有这种机制时会调用 external_callable 一样(根据需要可能带有或不带有进一步的参数)。Serializer 机制与 ExternalInterfacing 具有相同的优点,不同之处在于,所有调用由同一个或不同的可调用对象包装在单个ser实例中的调用现在都是串行化的。
整个程序的用户界面是一个外部子系统,因此应该由一个单独的线程来处理——具体来说,是程序的主线程(对于某些用户界面工具包,这是强制性的,即使使用其他不强制的工具包,这样做也是建议的)。因此,Serializer 线程是不合适的。相反,程序的主线程应仅处理用户界面问题,并将所有实际工作委派给接受工作请求的工作线程,这些工作线程在一个队列对象上接受工作请求,并在另一个队列上返回结果。一组工作线程通常被称为线程池。正如下面的示例所示,所有工作线程应共享一个请求队列和一个结果队列,因为只有主线程才会发布工作请求并获取结果:
`import` threading
`class` Worker(threading.Thread):
IDlock = threading.Lock()
request_ID = 0
`def` __init__(self, requests_queue, results_queue, **kwds):
super().__init__(**kwds)
self.daemon = `True`
self.request_queue = requests_queue
self.result_queue = results_queue
self.start()
`def` perform_work(self, callable, *args, **kwds):
*``"""called by main thread as `callable` would be, but w/o return"""``*
`with` self.IDlock:
Worker.request_ID += 1
self.request_queue.put(
(Worker.request_ID, callable, args, kwds))
`return` Worker.request_ID
`def` run(self):
`while` `True`:
request_ID, callable, a, k = self.request_queue.get()
self.result_queue.put((request_ID, callable(*a, **k)))
主线程创建两个队列,然后实例化工作线程,如下所示:
`import` queue
requests_queue = queue.Queue()
results_queue = queue.Queue()
number_of_workers = 5
`for` i `in` range(number_of_workers):
worker = Worker(requests_queue, results_queue)
每当主线程需要委派工作(执行可能需要较长时间来生成结果的可调用对象),主线程调用worker.perform_work(callable),就像没有这种机制时会调用callable一样(根据需要可能带有或不带有进一步的参数)。然而,perform_work 并不返回调用的结果。主线程得到的是一个标识工作请求的 ID。当主线程需要结果时,它可以跟踪该 ID,因为请求的结果在出现时都标有该 ID。这种机制的优点在于,主线程不会阻塞等待可调用的执行完成,而是立即变为可用状态,可以立即回到处理用户界面的主要业务上。
主线程必须安排检查 results_queue,因为每个工作请求的结果最终会出现在那里,标记有请求的 ID,当从队列中取出该请求的工作线程计算出结果时。主线程如何安排检查用户界面事件和从工作线程返回到结果队列的结果,取决于使用的用户界面工具包,或者——如果用户界面是基于文本的——取决于程序运行的平台。
一个广泛适用但并非始终最佳的一般策略是让主线程轮询(定期检查结果队列的状态)。在大多数类 Unix 平台上,模块 signal 的 alarm 函数允许轮询。tkinter GUI 工具包提供了一个 after 方法,可用于轮询。某些工具包和平台提供了更有效的策略(例如,让工作线程在将某些结果放入结果队列时警告主线程),但没有通用的、跨平台、跨工具包的方法可以安排这样的操作。因此,以下人工示例忽略了用户界面事件,只是通过在几个工作线程上评估随机表达式并引入随机延迟来模拟工作,从而完成了前面的示例:
`import` random, time, queue, operator
*`# copy here class Worker as defined earlier`*
requests_queue = queue.Queue()
results_queue = queue.Queue()
number_of_workers = 3
workers = [Worker(requests_queue, results_queue)
`for` i `in` range(number_of_workers)]
work_requests = {}
operations = {
'+': operator.add,
'-': operator.sub,
'*': operator.mul,
'/': operator.truediv,
'%': operator.mod,
}
`def` pick_a_worker():
return random.choice(workers)
`def` make_work():
o1 = random.randrange(2, 10)
o2 = random.randrange(2, 10)
op = random.choice(list(operations))
`return` f'{o1} {op} {o2}'
`def` slow_evaluate(expression_string):
time.sleep(random.randrange(1, 5))
op1, oper, op2 = expression_string.split()
arith_function = operations[oper]
`return` arith_function(int(op1), int(op2))
`def` show_results():
`while` `True`:
`try`:
completed_id, results = results_queue.get_nowait()
`except` queue.Empty:
`return`
work_expression = work_requests.pop(completed_id)
print(f'Result {completed_id}: {work_expression} -> {results}')
`for` i `in` range(10):
expression_string = make_work()
worker = pick_a_worker()
request_id = worker.perform_work(slow_evaluate, expression_string)
work_requests[request_id] = expression_string
print(f'Submitted request {request_id}: {expression_string}')
time.sleep(1.0)
show_results()
`while` work_requests:
time.sleep(1.0)
show_results()
进程环境
操作系统为每个进程P提供一个环境,即一组变量,变量名为字符串(通常按约定为大写标识符),其值也为字符串。在“环境变量”中,我们讨论了影响 Python 操作的环境变量。操作系统 shell 通过 shell 命令和其他在该部分提到的方法提供了检查和修改环境的方式。
进程环境是自包含的
任何进程P的环境在P启动时确定。启动后,只有P自己能够改变P的环境。对P环境的更改仅影响P本身:环境不是进程间通信的手段。P的任何操作都不会影响P的父进程(启动P的进程)的环境,也不会影响任何P之前启动的子进程现在正在运行的环境,或者与P无关的任何进程。P的子进程通常会在P创建该进程时获取一个P环境的副本作为启动环境。从这个狭义的意义上说,对P环境的更改确实会影响在此类更改之后由P启动的子进程。
模块 os 提供了属性 environ,这是一个映射,表示当前进程的环境。当 Python 启动时,它从进程环境初始化 os.environ。如果平台支持此类更新,对 os.environ 的更改将更新当前进程的环境。os.environ 中的键和值必须是字符串。在 Windows 上(但在类 Unix 平台上不是这样),os.environ 中的键隐式转换为大写。例如,以下是如何尝试确定您正在运行的 shell 或命令处理器:
`import` os
shell = os.environ.get('COMSPEC')
`if` shell `is` `None`:
shell = os.environ.get('SHELL')
`if` shell `is` `None`:
shell = 'an unknown command processor'
print('Running under ', shell)
当 Python 程序改变其环境(例如通过os.environ['X'] = 'Y'),这不会影响启动该程序的 shell 或命令处理器的环境。正如已经解释过的,对于所有编程语言,包括 Python,进程环境的更改仅影响进程本身,而不影响当前正在运行的其他进程。
运行其他程序
您可以通过 os 模块中的低级函数运行其他程序,或者(在更高且通常更可取的抽象级别上)使用 subprocess 模块。
使用子进程模块
subprocess 模块提供了一个非常广泛的类:Popen,支持许多不同的方式来运行另一个程序。Popen 的构造函数签名如下:
| Popen | class Popen(args, bufsize=0, executable=None, capture_output=False, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, text=None, universal_newlines=False, startupinfo=None, creationflags=0) Popen 启动一个子进程来运行一个独立的程序,并创建并返回一个对象p,代表该子进程。args是必需的参数,而许多可选的命名参数控制子进程运行的所有细节。
当在子进程创建过程中发生任何异常(在明确程序启动之前),Popen 会将该异常与名为 child_traceback 的属性一起重新引发到调用过程中,child_traceback 是子进程的 Python traceback 对象。这样的异常通常是 OSError 的一个实例(或者可能是 TypeError 或 ValueError,表示您传递给 Popen 的参数在类型或值上是无效的)。
subprocess.run()是 Popen 的一个便捷包装函数。
subprocess 模块包括 run 函数,该函数封装了一个 Popen 实例,并在其上执行最常见的处理流程。run 接受与 Popen 构造函数相同的参数,运行给定的命令,等待完成或超时,并返回一个 CompletedProcess 实例,其中包含返回码以及 stdout 和 stderr 的内容。
如果需要捕获命令的输出,则最常见的参数值将是将 capture_output 和 text 参数设置为True。
要运行什么,以及如何运行
args是一个字符串序列:第一项是要执行的程序的路径,后续的项(如果有)是要传递给程序的参数(当不需要传递参数时,args也可以只是一个字符串)。当 executable 不为None时,它将覆盖args以确定要执行的程序。当 shell 为True时,executable 指定要用来运行子进程的 shell;当 shell 为True且 executable 为None时,在类 Unix 系统上使用的 shell 是*/bin/sh*(在 Windows 上,它是 os.environ['COMSPEC'])。
子进程文件
stdin、stdout 和 stderr 分别指定了子进程的标准输入、输出和错误文件。每个可以是 PIPE,这会创建一个到/从子进程的新管道;None,意味着子进程要使用与此(“父”)进程相同的文件;或者是已经适当打开的文件对象(或文件描述符)(对于读取,用于标准输入;对于写入,用于标准输出和标准错误)。stderr 还可以是 subprocess.STDOUT,这意味着子进程的标准错误必须使用与其标准输出相同的文件。⁴ 当 capture_output 为 true 时,不能指定 stdout 或 stderr:行为就像每个都指定为 PIPE 一样。bufsize 控制这些文件的缓冲(除非它们已经打开),其语义与 open 函数中的相同参数的语义相同(默认为 0,表示“无缓冲”)。当 text(或其同义词 universal_newlines,提供向后兼容性)为 true 时,stdout 和 stderr(除非它们已经打开)将被打开为文本文件;否则,它们将被打开为二进制文件。当 close_fds 为 true 时,在子进程执行其程序或 shell 之前,所有其他文件(除了标准输入、输出和错误)将被关闭。
其他,高级参数
当 preexec_fn 不为 None 时,必须是一个函数或其他可调用对象,并且在子进程执行其程序或 shell 之前调用它(仅适用于类 Unix 系统,其中调用发生在 fork 之后和 exec 之前)。
当 cwd 不为 None 时,必须是一个给出现有目录的完整路径的字符串;在子进程执行其程序或 shell 之前,当前目录会切换到 cwd。
当 env 不为 None 时,必须是一个映射,其中键和值都是字符串,并完全定义了新进程的环境;否则,新进程的环境是当前父进程中活动环境的副本。
startupinfo 和 creationflags 是传递给 CreateProcess Win32 API 调用的 Windows-only 参数,用于创建子进程,用于特定于 Windows 的目的(本书不进一步涵盖它们,因为本书几乎完全专注于 Python 的跨平台使用)。
subprocess.Popen 实例的属性
类 Popen 的实例 p 提供了 Table 15-17 中列出的属性。
Table 15-17. 类 Popen 的实例 p 的属性
| args | Popen 的 args 参数(字符串或字符串序列)。 |
|---|---|
| pid | 子进程的进程 ID。 |
| returncode | None 表示子进程尚未退出;否则,是一个整数:0 表示成功终止,>0 表示以错误代码终止,或 <0 如果子进程被信号杀死。 |
| stderr, stdin, stdout | 当 Popen 的相应参数是 subprocess.PIPE 时,这些属性中的每一个都是包装相应管道的文件对象;否则,这些属性中的每一个都是 None。使用 p 的 communicate 方法,而不是从这些文件对象读取和写入,以避免可能的死锁。 |
subprocess.Popen 实例的方法
类 Popen 的实例 p 提供了表 15-18 中列出的方法。
表 15-18. 类 Popen 的实例 p 的方法
| communicate | p.communicate(input=None, timeout=None) 将字符串 input 作为子进程的标准输入(当 input 不为 None 时),然后将子进程的标准输出和错误文件读入内存中的字符串 so 和 se,直到两个文件都完成,最后等待子进程终止并返回对(两项元组)(so,se)。 |
|---|---|
| poll | p.poll() 检查子进程是否已终止;如果已终止,则返回 p.returncode;否则返回 None。 |
| wait | p.wait(timeout=None) 等待子进程终止,然后返回 p.returncode。如果子进程在 timeout 秒内未终止,则引发 TimeoutExpired 异常。 |
使用 os 模块运行其他程序
你的程序通常运行其他进程的最佳方式是使用前一节介绍的 subprocess 模块。然而,os 模块(在第十一章介绍)也提供了几种较低级别的方式来实现这一点,在某些情况下可能更简单。
运行另一个程序的最简单方法是通过 os.system 函数,尽管这种方法没有办法控制外部程序。os 模块还提供了几个以 exec 开头的函数,这些函数提供了精细的控制。由 exec 函数之一运行的程序会替换当前程序(即 Python 解释器)在同一进程中。因此,在实践中,您主要在支持使用 fork 的平台上使用 exec 函数(即类 Unix 平台)。以 spawn 和 popen 开头的 os 函数提供了中间简单性和强大性:它们是跨平台的,并且不像 system 那样简单,但对于许多目的来说足够简单。
exec 和 spawn 函数运行给定的可执行文件,给定可执行文件的路径、传递给它的参数,以及可选的环境映射。system 和 popen 函数执行一个命令,这是一个字符串传递给平台的默认 shell 的新实例(通常在 Unix 上是 /bin/sh,在 Windows 上是 cmd.exe)。命令是一个比可执行文件更一般的概念,因为它可以包含特定于当前平台的 shell 功能(管道、重定向和内置 shell 命令)使用的 shell 语法。
os 提供了表 15-19 列出的函数。
表 15-19. 与进程相关的 os 模块的函数
| execl, execle,
execlp,
execv,
execve,
execvp,
execvpe | execl(path, *args), execle(path, *args),
execlp(path,*args),
execv(path, args),
execve(path, args, env),
execvp(path, args),
execvpe(path, args, env)
运行由字符串 path 指示的可执行文件(程序),替换当前进程中的当前程序(即 Python 解释器)。函数名中编码的区别(在前缀 exec 之后)控制新程序的发现和运行的三个方面:
-
path 是否必须是程序可执行文件的完整路径,还是该函数可以接受一个名称作为 path 参数并在多个目录中搜索可执行文件,就像操作系统 shell 一样?execlp、execvp 和 execvpe 可以接受一个 path 参数,该参数只是一个文件名而不是完整路径。在这种情况下,函数将在 os.environ['PATH'] 中列出的目录中搜索具有该名称的可执行文件。其他函数要求 path 是可执行文件的完整路径。
-
函数是否接受新程序的参数作为单个序列参数 args,还是作为函数的单独参数?以 execv 开头的函数接受一个参数 args,该参数是要用于新程序的参数序列。以 execl 开头的函数将新程序的参数作为单独的参数(特别是 execle,它使用其最后一个参数作为新程序的环境)。
-
函数是否接受新程序的环境作为显式映射参数 env,或者隐式使用 os.environ?execle、execve 和 execvpe 接受一个参数 env,该参数是要用作新程序环境的映射(键和值必须是字符串),而其他函数则使用 os.environ 用于此目的。
每个 exec 函数使用 args 中的第一个项作为告知新程序其正在运行的名称(例如,在 C 程序的 main 中的 argv[0]);只有 args[1:] 是新程序的真正参数。
|
| popen | popen(cmd, mode='r', buffering=-1) 运行字符串命令 cmd 在一个新进程 P 中,并返回一个文件对象 f,该对象包装了与 P 的标准输入或来自 P 的标准输出的管道(取决于模式);f 使用文本流而不是原始字节。模式和缓冲区的含义与 Python 的 open 函数相同,见 “使用 open 创建文件对象”。当模式为 'r'(默认)时,f 是只读的,并包装 P 的标准输出。当模式为 'w' 时,f 是只写的,并包装 P 的标准输入。
f 与其他类似文件的主要区别在于方法 f.close 的行为。 f.close 等待 P 终止并返回 None,正如文件类对象的关闭方法通常所做的那样,当 P 成功终止时。然而,如果操作系统将整数错误码 c 与 P 的终止关联起来,表示 P 的终止失败,f.close 返回 c。在 Windows 系统上,c 是子进程的有符号整数返回码。 |
| spawnv, spawnve | spawnv(mode, path, args), spawnve(mode, path, args, env)
这些函数在新进程 P 中运行由 path 指示的程序,参数作为序列 args 传递。spawnve 使用映射 env 作为 P 的环境(键和值必须是字符串),而 spawnv 则使用 os.environ 来实现。仅在 Unix-like 平台上,还有其他 os.spawn 的变体,对应于 os.exec 的变体,但 spawnv 和 spawnve 是 Windows 上唯一存在的两个。
mode 必须是 os 模块提供的两个属性之一:os.P_WAIT 表示调用进程等待新进程终止,而 os.P_NOWAIT 表示调用进程继续与新进程同时执行。当 mode 是 os.P_WAIT 时,函数返回 P 的终止码 c:0 表示成功终止,c < 0 表示 P 被 信号 杀死,c > 0 表示正常但终止失败。当 mode 是 os.P_NOWAIT 时,函数返回 P 的进程 ID(或在 Windows 上是 P 的进程句柄)。没有跨平台的方法来使用 P 的 ID 或句柄;Unix-like 平台上的平台特定方法包括 os.waitpid,而在 Windows 上则包括第三方扩展包 pywin32。
例如,假设您希望交互式程序给用户一个机会编辑一个即将读取和使用的文本文件。您必须事先确定用户喜欢的文本编辑器的完整路径,例如在 Windows 上为 c:\windows\notepad.exe 或在类 Unix 平台上为 /usr/bin/vim。假设这个路径字符串绑定到变量 editor,并且您要让用户编辑的文本文件的路径绑定到 textfile:
`import` os
os.spawnv(os.P_WAIT, editor, (editor, textfile))
参数 args 的第一项作为“调用程序的名称”传递给被生成的程序。大多数程序不关心这一点,因此通常可以放置任何字符串。以防编辑程序确实查看这个特殊的第一个参数(例如某些版本的 Vim),最简单和最有效的方法是将与 os.spawnv 的第二个参数相同的字符串 editor 传递给它。
| 系统 | 系统(cmd) 在新进程中运行字符串命令 cmd,当新进程成功终止时返回 0。当新进程终止失败时,系统返回一个非零整数错误代码(具体的错误代码依赖于你运行的命令:这方面没有广泛接受的标准)。 |
|---|
mmap 模块
mmap 模块提供了内存映射文件对象。mmap 对象的行为类似于字节串,因此通常可以在需要字节串的地方传递 mmap 对象。然而,它们也有一些区别:
-
mmap 对象不提供字符串对象的方法。
-
mmap 对象是可变的,类似于 bytearray,而字节对象是不可变的。
-
mmap 对象也对应于一个打开的文件,并且在多态性方面表现为 Python 文件对象(如“类似文件对象和多态性”中所述)。
mmap 对象 m 可以进行索引或切片操作,产生字节串。由于 m 是可变的,你也可以对 m 的索引或切片进行赋值。然而,当你对 m 的切片赋值时,赋值语句的右侧必须是与你要赋值的切片具有完全相同长度的字节串。因此,许多在列表切片赋值中可用的有用技巧(如“修改列表”中所述)不适用于 mmap 切片赋值。
mmap 模块在 Unix-like 系统和 Windows 上提供了稍有不同的工厂函数:
| mmap | Windows: mmap(filedesc, length, tagname='', access=None, offset=None) Unix: mmap(filedesc, length, flags=MAP_SHARED, prot=PROT_READ|PROT_WRITE, access=None, offset=0)
创建并返回一个 mmap 对象 m,它映射到内存中文件描述符 filedesc 指示的文件的前 length 字节。filedesc 必须是一个同时打开读写的文件描述符,除非在类 Unix 平台上,参数 prot 仅请求读或仅请求写。(文件描述符在“文件描述符操作”中有介绍。)要为 Python 文件对象 f 获取 mmap 对象 m,可以使用 m=mmap.mmap(f.fileno(), length)。filedesc 可以为 -1,以映射匿名内存。
在 Windows 上,所有内存映射都是可读写的,并在进程之间共享,因此所有在文件上有内存映射的进程都可以看到其他进程所做的更改。仅在 Windows 上,你可以传递一个字符串 tagname 来为内存映射指定显式的 标签名。这个标签名允许你在同一个文件上拥有几个独立的内存映射,但这很少是必需的。仅使用两个参数调用 mmap 的优点是在 Windows 和 Unix-like 平台之间保持代码的可移植性。 |
| mmap (续) | 仅在类 Unix 平台上,您可以传递 mmap.MAP_PRIVATE 作为标志以获得一个对您的进程私有并且写时复制的映射。mmap.MAP_SHARED 是默认值,它获取一个与其他进程共享的映射,以便所有映射文件的进程都可以看到一个进程(与 Windows 上相同)所做的更改。您可以将 mmap.PROT_READ 作为 prot 参数传递,以获取仅可读而不可写的映射。传递 mmap.PROT_WRITE 获取仅可写而不可读的映射。默认值,位或 mmap.PROT_READ|mmap.PROT_WRITE,获取一个既可读又可写的映射。您可以传递命名参数 access 而不是标志和 prot(传递 access 和其他两个参数中的一个或两个是错误的)。access 的值可以是 ACCESS_READ(只读),ACCESS_WRITE(写透传,Windows 上的默认值)或 ACCESS_COPY(写时复制)。
您可以传递命名参数 offset 以在文件开始后开始映射;offset 必须是大于等于 0 的整数,是 ALLOCATIONGRANULARITY 的倍数(或者在 Unix 上是 PAGESIZE 的倍数)。
mmap 对象的方法
mmap 对象 m 提供了详细方法,参见 Table 15-20。
Table 15-20. mmap 实例 m 的方法
| close | m.close() 关闭 m 的文件。 |
|---|---|
| find | m.find(sub, start=0, end=None) 返回大于等于 start 的最低 i,使得 sub == m[i:i+len(sub)](并且当您传递 end 时,i+len(sub)-1 <= end)。如果没有这样的 i,m.find 返回 -1。这与 str 的 find 方法的行为相同,该方法在 Table 9-1 中介绍。 |
| flush | m.flush([offset, n]) 确保所有对 m 所做的更改都存在于 m 的文件中。在调用 m.flush 之前,文件是否反映了 m 的当前状态是不确定的。您可以传递起始字节偏移量 offset 和字节计数 n 来将刷新效果的保证限制为 m 的一个切片。传递这两个参数,或者两者都不传递:仅传递一个参数调用 m.flush 是错误的。 |
| move | m.move(dstoff, srcoff, n) 类似于切片赋值 m[dstoff:dstoff+n] = m[srcoff:srcoff+n],但可能更快。源切片和目标切片可以重叠。除了可能的重叠外,move 方法不会影响源切片(即,move 方法复制字节但不移动它们,尽管该方法的名称为“移动”)。 |
| read | m.read(n) 读取并返回一个字节字符串 s,包含从 m 的文件指针开始的最多 n 个字节,然后将 m 的文件指针前进 s 的长度。如果 m 的文件指针和 m 的长度之间的字节数少于 n,则返回可用的字节。特别是,如果 m 的文件指针在 m 的末尾,则返回空字节串 b''。 |
| read_byte | m.read_byte() 返回包含m的文件指针处的字节的长度为 1 的字节字符串,然后将m的文件指针推进 1。m.read_byte()类似于m.read(1)。但是,如果m的文件指针在m的末尾,则m.read(1)返回空字符串 b''且不推进,而m.read_byte()会引发 ValueError 异常。 |
| readline | m.readline() 从m文件的当前文件指针读取并返回一个字节字符串,直到下一个'\n'(包括'\n'),然后将m的文件指针推进到刚刚读取的字节之后。如果m的文件指针在m的末尾,则 readline 返回空字符串 b''。 |
| resize | m.resize(n) 改变m的长度,使得 len(m)变为n。不影响m的文件大小。m的长度和文件大小是独立的。要将m的长度设置为文件的大小,请调用m.resize(m.size())。如果m的长度大于文件的大小,则m将填充空字节(\x00)。 |
| rfind | rfind(sub, start=0, end=None) 返回大于等于 start 的最高i,使得sub == m[i:i+len(sub)](当你传递 end 时,i+len(sub)-1 <= end)。如果不存在这样的i,m.rfind 返回-1。这与字符串对象的 rfind 方法相同,详见 Table 9-1。 |
| seek | m.seek(pos, how=0) 将m的文件指针设置为整数字节偏移量pos,相对于由 how 指示的位置:
0 or os.SEEK_SET
偏移量相对于m的起始位置
1 or os.SEEK_CUR
偏移量相对于m的当前文件指针
2 or os.SEEK_END
偏移量相对于m的末尾
试图将m的文件指针设置为负偏移量或超出m长度的偏移量会引发 ValueError 异常。
| size | m.size() 返回m文件的长度(以字节为单位),而不是m本身的长度。要获取m的长度,使用 len(m)。 |
|---|---|
| tell | m.tell() 返回m的当前文件指针位置,即m文件中的字节偏移量。 |
| write | m.write(b) 将字节串b写入m的当前文件指针位置,覆盖已有的字节,然后将m的文件指针推进 len(b)。如果m的文件指针与m的长度之间的字节数少于 len(b),write 会引发 ValueError 异常。 |
| write_byte | m.write_byte(byte) 向映射 m 的当前位置写入必须是整数的 byte,覆盖原有的字节,然后将 m 的文件指针前移 1。 m.write_byte(x) 与 m.write(x.to_bytes(1, 'little')) 类似。然而,如果 m 的文件指针位于 m 的末尾,则 m.write_byte(x) 静默不做任何操作,而 m.write(x.to_bytes(1, 'little')) 会引发 ValueError 异常。请注意,这与文件末尾的 read 和 read_byte 之间的关系相反:write 和 read_byte 可能会引发 ValueError,而 read 和 write_byte 则从不会。 |
使用 mmap 对象进行 IPC
进程使用 mmap 通信的方式与它们使用文件基本相同:一个进程写入数据,另一个进程稍后读取相同的数据。由于 mmap 对象有一个底层文件,因此可以有一些进程在文件上进行 I/O(如在 “The io Module” 中介绍的),而其他进程在同一个文件上使用 mmap。在便利性和功能性之间选择 mmap 和文件对象的 I/O,性能大致相当。例如,这里是一个简单的程序,反复使用文件 I/O,使文件的内容等于用户交互式输入的最后一行:
fileob = open('xxx','wb')
`while` `True`:
data = input('Enter some text:')
fileob.seek(0)
fileob.write(data.encode())
fileob.truncate()
fileob.flush()
并且这里有另一个简单的程序,当在与前者相同的目录中运行时,使用 mmap(以及在 Table 13-2 中涵盖的 time.sleep 函数)每秒钟检查文件的变化,并打印出文件的新内容(如果有任何变化的话):
`import` mmap, os, time
mx = mmap.mmap(os.open('xxx', os.O_RDWR), 1)
last = `None`
`while` `True`:
mx.resize(mx.size())
data = mx[:]
`if` data != last:
print(data)
last = data
time.sleep(1)
¹ 我们遇到的最好的关于异步编程的入门工作,尽管现在已经过时(因为 Python 中的异步方法不断改进),是 Using Asyncio in Python,作者是 Caleb Hattingh(O’Reilly)。我们建议你也学习 Brad Solomon 的 Asyncio 演示 在 Real Python 上。
² 在线文档包括一个特别有用的 “编程指南”部分,列出了使用 multiprocessing 模块时的许多额外实用建议。
³ 竞争条件是一种情况,其中不同事件的相对时间通常是不可预测的,可能会影响计算的结果... 这从来都不是一件好事!
⁴ 就像在 Unix 风格的 shell 命令行中指定 2>&1 一样。