PEP 343 – “with” 语句
- 作者:
- Guido van Rossum, Alyssa Coghlan
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2005 年 5 月 13 日
- Python 版本:
- 2.5
- 发布历史:
- 2005 年 6 月 2 日,2005 年 10 月 16 日,2005 年 10 月 29 日,2006 年 4 月 23 日,2006 年 5 月 1 日,2006 年 7 月 30 日
摘要
本 PEP 向 Python 语言添加了一个新语句 “with”,以实现 try/finally 语句的标准用法。
在本 PEP 中,上下文管理器提供 __enter__() 和 __exit__() 方法,这些方法在进入和退出 with 语句的主体时被调用。
引言
在对 PEP 340 和替代方案进行了大量讨论之后,我决定撤回 PEP 340 并提出了 PEP 310 的一个微小变体。经过进一步讨论,我重新添加了一种机制,通过 throw() 方法在挂起的生成器中引发异常,以及一个 close() 方法,该方法引发一个新的 GeneratorExit 异常;这些添加最初是在 [2] 中的 python-dev 上提出的,并获得普遍批准。我还将关键字更改为 “with”。
在本 PEP 获得接受后,由于重叠,以下 PEP 被拒绝:
- PEP 310,可靠的获取/释放对。这是最初的 with-statement 提案。
- PEP 319,Python 同步/异步块。它的用例可以通过当前的 PEP 提供合适的 with-statement 控制器来涵盖:对于 “synchronize”,我们可以使用示例 1 中的“锁定”模板;对于 “asynchronize”,我们可以使用类似的“解锁”模板。我不认为将一个“匿名”锁与代码块关联起来是那么重要;事实上,始终明确使用哪个互斥锁可能更好。
PEP 340 和 PEP 346 也与本 PEP 重叠,但在本 PEP 提交时自愿撤回。
本 PEP 早期版本的某些讨论在 Python Wiki 上进行过 [3]。
动机与摘要
PEP 340,匿名块语句,结合了许多强大的思想:使用生成器作为块模板,向生成器添加异常处理和终结,等等。除了受到赞扬之外,它还遭到了很多反对,因为人们不喜欢它在幕后是一个(潜在的)循环构造。这意味着块语句中的 break 和 continue 会中断或继续块语句,即使它被用作非循环资源管理工具。
但最后一击是当我读到 Raymond Chen 关于流控制宏的咆哮时 [1]。Raymond 令人信服地指出,在宏中隐藏流控制会使你的代码难以理解,我发现他的论点适用于 Python 和 C。我意识到 PEP 340 模板可以隐藏各种控制流;例如,它的示例 4 (auto_retry()) 捕获异常并重复该块最多三次。
然而,PEP 310 的 with 语句在我看来**没有**隐藏控制流:虽然 finally-suite 暂时暂停了控制流,但最终,控制流会像 finally-suite 完全不存在一样恢复。
请记住,PEP 310 大致提出了这种语法(“VAR =” 部分是可选的)
with VAR = EXPR:
BLOCK
这大致翻译为
VAR = EXPR
VAR.__enter__()
try:
BLOCK
finally:
VAR.__exit__()
现在考虑这个例子
with f = open("/etc/passwd"):
BLOCK1
BLOCK2
在这里,就像第一行是“if True”一样,我们知道如果 BLOCK1 没有异常完成,BLOCK2 将会执行;如果 BLOCK1 抛出异常或执行非局部跳转(break、continue 或 return),BLOCK2 将**不会**执行。with 语句在末尾添加的魔力不会影响这一点。
(你可能会问,如果 __exit__() 方法中出现错误导致异常怎么办?那么一切都将丢失——但这并不比其他异常更糟糕;异常的本质是它们可能发生在**任何地方**,你必须接受这一点。即使你编写了无错误的代码,KeyboardInterrupt 异常仍然可能导致它在任何两个虚拟机操作码之间退出。)
这个论点差点让我支持 PEP 310,但我从 PEP 340 的狂热中还剩下一个想法,我还没有准备好放弃:使用生成器作为抽象的“模板”,例如获取和释放锁或打开和关闭文件,这是一个强大的想法,这可以从该 PEP 中的示例中看出。
受 Phillip Eby 对 PEP 340 的反建议的启发,我尝试创建一个装饰器,将合适的生成器转换为具有必要的 __enter__() 和 __exit__() 方法的对象。在这里我遇到了一个问题:虽然对于锁定示例来说这并不太难,但对于打开示例来说却不可能。最初的想法是这样定义模板
@contextmanager
def opening(filename):
f = open(filename)
try:
yield f
finally:
f.close()
并像这样使用它
with f = opening(filename):
...read data from f...
问题在于,在 PEP 310 中,调用 EXPR 的结果直接赋值给 VAR,然后 VAR 的 __exit__() 方法在退出 BLOCK1 时被调用。但在这里,VAR 显然需要接收打开的文件,这意味着 __exit__() 必须是文件上的一个方法。
虽然这可以通过使用代理类解决,但这很笨拙,让我意识到稍微不同的翻译会使编写所需的装饰器变得轻而易举:让 VAR 接收调用 __enter__() 方法的结果,并保存 EXPR 的值以便稍后调用其 __exit__() 方法。然后,装饰器可以返回一个包装器类的实例,其 __enter__() 方法调用生成器的 next() 方法并返回 next() 返回的任何值;包装器实例的 __exit__() 方法再次调用 next() 但期望它引发 StopIteration。(详细信息请参见下面的“可选生成器装饰器”部分。)
所以现在最后的障碍是 PEP 310 语法
with VAR = EXPR:
BLOCK1
会具有欺骗性,因为 VAR **不会**接收 EXPR 的值。借鉴 PEP 340,它很容易变成
with EXPR as VAR:
BLOCK1
进一步的讨论表明,人们确实喜欢在生成器中“看到”异常,即使只是为了记录它;生成器不允许生成另一个值,因为 with 语句不应该用作循环(引发不同的异常勉强可以接受)。为了实现这一点,提议了一个新的生成器 throw() 方法,它以通常的方式(类型、值、回溯)接受一到三个参数,并在生成器暂停的位置引发它。
一旦我们有了这个,提出另一个生成器方法 close() 就很自然了,它会用一个特殊的异常 GeneratorExit 调用 throw()。这会告诉生成器退出,从那里再向前一小步,就是提议在生成器被垃圾回收时自动调用 close()。
然后,最终,我们可以在 try-finally 语句中允许 yield 语句,因为我们现在可以保证 finally 子句将(最终)被执行。关于终结的常见注意事项仍然适用——进程可能突然终止而未终结任何对象,并且对象可能由于应用程序中的循环或内存泄漏(与 Python 实现中的循环或泄漏不同,后者由 GC 处理)而永远存活。
请注意,我们不保证 finally 子句在生成器对象变得未使用后立即执行,即使 CPython 中是这样工作的。这类似于自动关闭文件:虽然像 CPython 这样的引用计数实现会在对象的最后一个引用消失后立即释放对象,但使用其他 GC 算法的实现不提供相同的保证。这适用于 Jython、IronPython,可能也适用于在 Parrot 上运行的 Python。
(对生成器所做更改的详细信息现在可以在 PEP 342 中找到,而不是在当前的 PEP 中)
用例
请参见末尾附近的“示例”部分。
规范:“with” 语句
提出了一种具有以下语法的新语句:
with EXPR as VAR:
BLOCK
在这里,'with' 和 'as' 是新关键字;EXPR 是一个任意表达式(但不是表达式列表),VAR 是一个单一的赋值目标。它**不能**是逗号分隔的变量序列,但它**可以**是**带括号的**逗号分隔的变量序列。(此限制使得未来语法扩展成为可能,以拥有多个逗号分隔的资源,每个资源都有自己的可选 as 子句。)
“as VAR”部分是可选的。
上述语句的翻译是
mgr = (EXPR)
exit = type(mgr).__exit__ # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
try:
VAR = value # Only if "as VAR" is present
BLOCK
except:
# The exceptional case is handled here
exc = False
if not exit(mgr, *sys.exc_info()):
raise
# The exception is swallowed if exit() returns true
finally:
# The normal and non-local-goto cases are handled here
if exc:
exit(mgr, None, None, None)
在这里,小写变量(mgr、exit、value、exc)是内部变量,用户无法访问;它们最有可能被实现为特殊寄存器或栈位置。
上述翻译的细节旨在规定精确的语义。如果未按预期找到任何相关方法,解释器将按尝试的顺序(__exit__、__enter__)引发 AttributeError。同样,如果任何调用引发异常,效果将与上述代码中完全相同。最后,如果 BLOCK 包含 break、continue 或 return 语句,则 __exit__() 方法将以三个 None 参数调用,就像 BLOCK 正常完成一样。(即,这些“伪异常”不会被 __exit__() 视为异常。)
如果语法中的 “as VAR” 部分被省略,则翻译中的 “VAR =” 部分也被省略(但 mgr.__enter__() 仍然会被调用)。
mgr.__exit__() 的调用约定如下。如果 finally-suite 是通过 BLOCK 正常完成或通过非局部跳转(BLOCK 中的 break、continue 或 return 语句)到达的,则 mgr.__exit__() 将以三个 None 参数调用。如果 finally-suite 是通过 BLOCK 中引发的异常到达的,则 mgr.__exit__() 将以代表异常类型、值和回溯的三个参数调用。
重要提示:如果 mgr.__exit__() 返回一个“真”值,则异常将被“吞噬”。也就是说,如果它返回“真”,即使在 with 语句中发生了异常,执行也会在 with 语句之后的下一个语句继续。但是,如果 with 语句通过非局部跳转(break、continue 或 return)离开,则当 mgr.__exit__() 返回时,无论返回值如何,都会恢复此非局部返回。此细节的动机是为了使 mgr.__exit__() 能够吞噬异常,而又不至于过于容易(因为默认返回值 None 为假,这会导致异常重新引发)。吞噬异常的主要用例是为了能够编写 @contextmanager 装饰器,以便装饰生成器中的 try/except 块的行为与生成器主体在 with 语句位置内联展开的行为完全相同。
将异常详情传递给 __exit__() 的动机(与 PEP 310 中无参数的 __exit__() 相反)是由 transactional() 用例(下面的示例 3)给出的。该示例中的模板必须根据是否发生异常来提交或回滚事务。我们不只是提供一个布尔标志来指示是否发生异常,而是传递完整的异常信息,例如为了异常日志记录工具。依赖 sys.exc_info() 获取异常信息被拒绝;sys.exc_info() 具有非常复杂的语义,并且完全有可能它返回很久以前捕获的异常信息。还提议添加一个额外的布尔值来区分到达 BLOCK 的末尾和非局部跳转。这被拒绝为过于复杂和不必要;非局部跳转应被视为异常,以便进行数据库事务回滚决策。
为了方便在直接操作上下文管理器的 Python 代码中进行上下文链式调用,__exit__() 方法**不应该**重新抛出传递给它们的错误。在这种情况下,任何重新抛出错误都始终是 __exit__() 方法的**调用者**的责任。
这样,如果调用者需要判断 __exit__() 调用是否**失败**(而不是在传播原始错误之前成功清理),它就可以做到。
如果 __exit__() 无错误返回,则可以将其解释为 __exit__() 方法本身成功(无论原始错误是否要传播或抑制)。
但是,如果 __exit__() 向其调用者传播异常,这意味着 __exit__() **本身**失败了。因此,__exit__() 方法应该避免引发错误,除非它们确实失败了。(并且允许原始错误继续发生不算失败。)
过渡计划
在 Python 2.5 中,仅当存在未来语句时才识别新语法
from __future__ import with_statement
这将使 'with' 和 'as' 都成为关键字。如果没有 future 语句,使用 'with' 或 'as' 作为标识符将向 stderr 发出警告。
在 Python 2.6 中,新语法将始终被识别;'with' 和 'as' 始终是关键字。
生成器装饰器
随着 PEP 342 被接受,可以编写一个装饰器,使得可以使用一个只 yield 一次的生成器来控制一个 with 语句。以下是这样一个装饰器的草图:
class GeneratorContextManager(object):
def __init__(self, gen):
self.gen = gen
def __enter__(self):
try:
return self.gen.next()
except StopIteration:
raise RuntimeError("generator didn't yield")
def __exit__(self, type, value, traceback):
if type is None:
try:
self.gen.next()
except StopIteration:
return
else:
raise RuntimeError("generator didn't stop")
else:
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopIteration:
return True
except:
# only re-raise if it's *not* the exception that was
# passed to throw(), because __exit__() must not raise
# an exception unless __exit__() itself failed. But
# throw() has to raise the exception to signal
# propagation, so this fixes the impedance mismatch
# between the throw() protocol and the __exit__()
# protocol.
#
if sys.exc_info()[1] is not value:
raise
def contextmanager(func):
def helper(*args, **kwds):
return GeneratorContextManager(func(*args, **kwds))
return helper
这个装饰器可以如下使用
@contextmanager
def opening(filename):
f = open(filename) # IOError is untouched by GeneratorContext
try:
yield f
finally:
f.close() # Ditto for errors here (however unlikely)
这个装饰器的健壮实现将成为标准库的一部分。
标准库中的上下文管理器
某些对象,如文件、套接字和锁,可以被赋予 __enter__() 和 __exit__() 方法,这样就不必写
with locking(myLock):
BLOCK
可以直接写成
with myLock:
BLOCK
我认为我们应该小心这一点;它可能会导致错误,例如
f = open(filename)
with f:
BLOCK1
with f:
BLOCK2
这并没有实现人们想象的功能(f 在进入 BLOCK2 之前就已经关闭了)。
另一方面,此类错误很容易诊断;例如,上面的生成器上下文装饰器在第二个 with 语句再次调用 f.__enter__() 时会引发 RuntimeError。如果对已关闭的文件对象调用 __enter__,也会引发类似的错误。
对于 Python 2.5,以下类型已被识别为上下文管理器
- file
- thread.LockType
- threading.Lock
- threading.RLock
- threading.Condition
- threading.Semaphore
- threading.BoundedSemaphore
十进制模块还将添加一个上下文管理器,以支持在 with 语句主体内使用本地十进制算术上下文,并在 with 语句退出时自动恢复原始上下文。
标准术语
本 PEP 建议将由 __enter__() 和 __exit__() 方法组成的协议称为“上下文管理协议”,并将实现该协议的对象称为“上下文管理器”。[4]
with 关键字之后紧随的表达式是“上下文表达式”,因为该表达式提供了关于上下文管理器在语句主体持续时间内建立的运行时环境的主要线索。
with 语句主体中的代码以及 as 关键字后的变量名(或多个名称)目前并没有特殊术语。可以使用通用术语“语句主体”和“目标列表”,如果术语不明确,则可冠以“with”或“with 语句”作为前缀。
考虑到十进制模块的算术上下文等对象的存在,“上下文”一词不幸地存在歧义。如有必要,可以通过使用“上下文管理器”表示上下文表达式创建的具体对象,以及“运行时上下文”或(最好是)“运行时环境”表示上下文管理器实际进行的状态修改来使其更具体。当仅仅讨论 with 语句的使用时,歧义应该不会有太大影响,因为上下文表达式完全定义了对运行时环境所做的更改。当讨论 with 语句本身的机制以及如何实际实现上下文管理器时,这种区别更为重要。
缓存上下文管理器
许多上下文管理器(例如文件和基于生成器的上下文)将是单次使用的对象。一旦调用了 __exit__() 方法,上下文管理器将不再处于可用状态(例如,文件已关闭,或底层生成器已完成执行)。
要求每个 with 语句使用一个新的管理器对象是避免多线程代码和嵌套 with 语句试图使用相同上下文管理器问题的最简单方法。所有支持重用的标准库上下文管理器都来自 threading 模块并非巧合——它们都已经被设计用于处理由线程和嵌套使用引起的问题。
这意味着为了保存一个带有特定初始化参数的上下文管理器,以便在多个 with 语句中使用,通常需要将其存储在一个零参数可调用对象中,然后在每个语句的上下文表达式中调用该对象,而不是直接缓存上下文管理器。
当此限制不适用时,受影响的上下文管理器的文档应明确说明。
已解决的问题
以下问题已通过 BDFL 批准(以及 python-dev 上没有重大异议)解决。
GeneratorContextManager在底层生成器迭代器行为不当时应该引发什么异常?以下引用是 Guido 选择RuntimeError的原因,无论是对于这种情况还是对于 PEP 342 中生成器close()方法(来自 [8]):“我宁愿不为此目的引入一个新的异常类,因为它不是我希望人们捕获的异常:我希望它变成一个回溯,由程序员看到然后修复代码。所以现在我相信它们都应该引发
RuntimeError。对此有一些先例:Python 核心代码在检测到无限递归、未初始化对象(以及各种杂项情况)时会引发它。”- 如果 with 语句中涉及的类上不存在相关方法,则引发
AttributeError而不是TypeError是可以的。抽象对象 C API 引发TypeError而不是AttributeError是历史的偶然,而不是深思熟虑的设计决策 [11]。 - 具有
__enter__/__exit__方法的对象被称为“上下文管理器”,将生成器函数转换为上下文管理器工厂的装饰器是contextlib.contextmanager。在 2.5 发布周期中还有一些其他建议 [15],但没有令人信服的理由放弃 PEP 实现中使用的术语。
被拒绝的选项
几个月来,PEP 禁止抑制异常以避免隐藏的流控制。实施后发现这非常痛苦,因此 Guido 恢复了此功能 [12]。
PEP 的另一个引发无尽问题和术语争议的方面是提供一个类似于可迭代对象的 __iter__() 方法的 __context__() 方法 [5] [7] [9]。持续存在的问题 [10] [12] 关于解释它是什么、为什么存在以及如何工作最终导致 Guido 彻底取消了这个概念 [14](大家欢呼雀跃!)。
直接使用 PEP 342 生成器 API 来定义 with 语句的想法也曾被短暂考虑过 [6],但很快就被驳回了,因为它使得编写非基于生成器的上下文管理器变得过于困难。
示例
基于生成器的示例依赖于 PEP 342。此外,有些示例在实践中是不必要的,因为诸如 threading.RLock 之类的适当对象可以直接用于 with 语句中。
示例上下文名称中使用的时态并非任意。过去时(“-ed”)用于名称指的是在 __enter__ 方法中完成并在 __exit__ 方法中撤销的操作。进行时(“-ing”)用于名称指的是要在 __exit__ 方法中完成的操作。
- 一个模板,用于确保在块开始时获取的锁在块离开时被释放
@contextmanager def locked(lock): lock.acquire() try: yield finally: lock.release()
用法如下
with locked(myLock): # Code here executes with myLock held. The lock is # guaranteed to be released when the block is left (even # if via return or by an uncaught exception).
- 一个用于打开文件的模板,确保在块离开时文件被关闭
@contextmanager def opened(filename, mode="r"): f = open(filename, mode) try: yield f finally: f.close()
用法如下
with opened("/etc/passwd") as f: for line in f: print line.rstrip()
- 提交或回滚数据库事务的模板
@contextmanager def transaction(db): db.begin() try: yield None except: db.rollback() raise else: db.commit()
- 示例 1 在没有生成器的情况下重写
class locked: def __init__(self, lock): self.lock = lock def __enter__(self): self.lock.acquire() def __exit__(self, type, value, tb): self.lock.release()
(这个例子很容易修改以实现其他相对无状态的例子;它表明如果不需要保留特殊状态,很容易避免使用生成器。)
- 暂时重定向标准输出
@contextmanager def stdout_redirected(new_stdout): save_stdout = sys.stdout sys.stdout = new_stdout try: yield None finally: sys.stdout = save_stdout
用法如下
with opened(filename, "w") as f: with stdout_redirected(f): print "Hello world"
当然,这不是线程安全的,但手动执行相同的操作也不是。在单线程程序(例如脚本)中,这是一种流行的做法。
opened()的一个变体,它也返回一个错误条件@contextmanager def opened_w_error(filename, mode="r"): try: f = open(filename, mode) except IOError, err: yield None, err else: try: yield f, None finally: f.close()
用法如下
with opened_w_error("/etc/passwd", "a") as (f, err): if err: print "IOError:", err else: f.write("guido::0:0::/:/bin/sh\n")
- 另一个有用的例子是阻塞信号的操作。用法可能如下:
import signal with signal.blocked(): # code executed without worrying about signals
一个可选参数可能是一个要阻塞的信号列表;默认情况下所有信号都被阻塞。实现留给读者自行完成。
- 此功能的另一个用途是十进制上下文。这是一个简单的例子,改编自 Michael Chermside 发布的一个例子:
import decimal @contextmanager def extra_precision(places=2): c = decimal.getcontext() saved_prec = c.prec c.prec += places try: yield None finally: c.prec = saved_prec
示例用法(改编自 Python 库参考)
def sin(x): "Return the sine of x as measured in radians." with extra_precision(): i, lasts, s, fact, num, sign = 1, 0, x, 1, x, 1 while s != lasts: lasts = s i += 2 fact *= i * (i-1) num *= x * x sign *= -1 s += num / fact * sign # The "+s" rounds back to the original precision, # so this must be outside the with-statement: return +s
- 以下是十进制模块的一个简单上下文管理器
@contextmanager def localcontext(ctx=None): """Set a new local decimal context for the block""" # Default to using the current context if ctx is None: ctx = getcontext() # We set the thread context to a copy of this context # to ensure that changes within the block are kept # local to the block. newctx = ctx.copy() oldctx = decimal.getcontext() decimal.setcontext(newctx) try: yield newctx finally: # Always restore the original context decimal.setcontext(oldctx)
示例用法
from decimal import localcontext, ExtendedContext def sin(x): with localcontext() as ctx: ctx.prec += 2 # Rest of sin calculation algorithm # uses a precision 2 greater than normal return +s # Convert result to normal precision def sin(x): with localcontext(ExtendedContext): # Rest of sin calculation algorithm # uses the Extended Context from the # General Decimal Arithmetic Specification return +s # Convert result to normal context
- 一个通用的“对象关闭”上下文管理器
class closing(object): def __init__(self, obj): self.obj = obj def __enter__(self): return self.obj def __exit__(self, *exc_info): try: close_it = self.obj.close except AttributeError: pass else: close_it()
这可以用于确定性地关闭任何具有关闭方法的对象,无论是文件、生成器还是其他。即使不保证对象需要关闭(例如,接受任意可迭代对象的函数),也可以使用它。
# emulate opening(): with closing(open("argument.txt")) as contradiction: for line in contradiction: print line # deterministically finalize an iterator: with closing(iter(data_source)) as data: for datum in data: process(datum)
(Python 2.5 的 contextlib 模块包含此上下文管理器的一个版本)
- PEP 319 提供了一个用例,用于也提供
released()上下文来暂时释放先前获取的锁;这可以与上面的锁定上下文管理器非常相似地编写,通过交换acquire()和release()调用来实现class released: def __init__(self, lock): self.lock = lock def __enter__(self): self.lock.release() def __exit__(self, type, value, tb): self.lock.acquire()
示例用法
with my_lock: # Operations with the lock held with released(my_lock): # Operations without the lock # e.g. blocking I/O # Lock is held again here
- 一个“嵌套”上下文管理器,自动从左到右嵌套提供的上下文以避免过度缩进
@contextmanager def nested(*contexts): exits = [] vars = [] try: try: for context in contexts: exit = context.__exit__ enter = context.__enter__ vars.append(enter()) exits.append(exit) yield vars except: exc = sys.exc_info() else: exc = (None, None, None) finally: while exits: exit = exits.pop() try: exit(*exc) except: exc = sys.exc_info() else: exc = (None, None, None) if exc != (None, None, None): # sys.exc_info() may have been # changed by one of the exit methods # so provide explicit exception info raise exc[0], exc[1], exc[2]
示例用法
with nested(a, b, c) as (x, y, z): # Perform operation
等同于
with a as x: with b as y: with c as z: # Perform operation
(Python 2.5 的 contextlib 模块包含此上下文管理器的一个版本)
参考实现
该 PEP 于 2005 年 6 月 27 日在 Guido 的 EuroPython 主题演讲中首次被接受。后来再次被接受,并添加了 the __context__ 方法。该 PEP 在 Subversion 中为 Python 2.5a1 实现。__context__() 方法在 Python 2.5b1 中被移除。
致谢
许多人对本 PEP 中的思想和概念做出了贡献,包括 PEP 340 和 PEP 346 致谢中提到的所有人。
额外感谢(无特定顺序):Paul Moore、Phillip J. Eby、Greg Ewing、Jason Orendorff、Michael Hudson、Raymond Hettinger、Walter Dörwald、Aahz、Georg Brandl、Terry Reedy、A.M. Kuchling、Brett Cannon 以及所有参与 python-dev 讨论的人。
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0343.rst
最后修改时间: 2025-02-01 08:59:27 GMT