PEP 3104 – 外部作用域中的名称访问
- 作者:
- Ka-Ping Yee <ping at zesty.ca>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2006年10月12日
- Python版本:
- 3.0
- 历史记录:
摘要
在大多数支持嵌套作用域的语言中,代码可以引用或重新绑定(赋值给)最近封闭作用域中的任何名称。目前,Python代码可以引用任何封闭作用域中的名称,但它只能重新绑定两个作用域中的名称:局部作用域(通过简单赋值)或模块全局作用域(使用global
声明)。
此限制已在Python-Dev邮件列表和其他地方多次提出,并引发了广泛的讨论和许多关于如何消除此限制的提案。本PEP总结了各种建议的替代方案,以及针对每个方案提到的优缺点。
基本原理
在2.1版本之前,Python对作用域的处理类似于标准C:在一个文件中只有两个作用域级别,全局和局部。在C中,这是函数定义不能嵌套这一事实的自然结果。但在Python中,尽管函数通常定义在顶层,但函数定义可以在任何地方执行。这使得Python具有嵌套作用域的语法外观,但没有语义,并产生了令一些程序员感到意外的不一致性——例如,在顶层工作的递归函数在移动到另一个函数内部时将停止工作,因为递归函数自己的名称将不再在其主体作用域中可见。这违反了函数在放置在不同上下文中时应该保持一致的直觉。这是一个例子
def enclosing_function():
def factorial(n):
if n < 2:
return 1
return n * factorial(n - 1) # fails with NameError
print factorial(5)
Python 2.1通过使所有封闭作用域中绑定的名称可见,更接近静态嵌套作用域(参见PEP 227)。此更改使上述代码示例按预期工作。但是,由于对名称的任何赋值都隐式地声明该名称是局部的,因此不可能重新绑定外部作用域中的名称(除非global
声明强制该名称为全局)。因此,以下代码旨在显示可以通过单击按钮递增和递减的数字,其工作方式与熟悉词法作用域的人员期望的不同
def make_scoreboard(frame, score=0):
label = Label(frame)
label.pack()
for i in [-10, -1, 1, 10]:
def increment(step=i):
score = score + step # fails with UnboundLocalError
label['text'] = score
button = Button(frame, text='%+d' % i, command=increment)
button.pack()
return label
Python语法没有提供方法来指示increment
中提到的名称score
是指在make_scoreboard
中绑定的变量score
,而不是increment
中的局部变量。Python的用户和开发者表达了对消除此限制的兴趣,以便Python能够拥有现在许多编程语言(包括JavaScript、Perl、Ruby、Scheme、Smalltalk、带有GNU扩展的C和C# 2.0)中标准的Algol风格作用域模型的全部灵活性。
有人认为这样的功能不是必需的,因为可重新绑定的外部变量可以通过将其包装在可变对象中来模拟
class Namespace:
pass
def make_scoreboard(frame, score=0):
ns = Namespace()
ns.score = 0
label = Label(frame)
label.pack()
for i in [-10, -1, 1, 10]:
def increment(step=i):
ns.score = ns.score + step
label['text'] = ns.score
button = Button(frame, text='%+d' % i, command=increment)
button.pack()
return label
但是,此解决方法仅突出了现有作用域的缺点:函数的目的是将其代码封装在自己的命名空间中,因此程序员必须创建额外的命名空间来弥补现有局部作用域中缺少的功能,然后必须决定每个名称应该驻留在真实作用域还是模拟作用域中,这似乎是不幸的。
另一个常见的反对意见是,所需的功能可以编写为一个类,尽管有点冗长。对此反对意见的反驳是,存在不同的实现风格不是留下受支持的编程结构(嵌套作用域)功能不完整的理由。Python有时被称为“多范式语言”,因为它从其对多种编程范式的支持和优雅集成中获得了如此强大的力量、实践灵活性和教学能力。
早在1994年[1],在PEP 227对嵌套作用域的支持被采纳之前,Python-Dev上就出现了关于作用域语法的提案。当时,Guido的回应是
这非常接近于引入CSNS[经典静态嵌套作用域]。如果你要这样做,你提出的作用域语义似乎没问题。我仍然认为CSNS的需求不足以保证这种构造……
在PEP 227之后,“外部名称重新绑定讨论”在Python-Dev上重新出现过很多次,以至于它已成为一个熟悉的事件,至少从2003年开始就以其目前的形式反复出现[2]。尽管这些讨论中提出的语言更改尚未被采用,但Guido已承认值得考虑语言更改[12]。
其他语言
为了提供一些背景信息,本节描述了一些其他语言如何处理嵌套作用域和重新绑定。
JavaScript、Perl、Scheme、Smalltalk、GNU C、C# 2.0
这些语言使用变量声明来指示作用域。在JavaScript中,词法作用域变量用var
关键字声明;未声明的变量名称被假定为全局。在Perl中,词法作用域变量用my
关键字声明;未声明的变量名称被假定为全局。在Scheme中,所有变量都必须声明(使用define
或let
,或作为形式参数)。在Smalltalk中,任何块都可以通过在竖线之间声明局部变量名称列表来开始。C和C#要求所有变量都进行类型声明。对于所有这些情况,变量都属于包含声明的作用域。
Ruby(截至1.8)
Ruby是一个有启发性的例子,因为它似乎是唯一一个像Python一样尝试支持静态嵌套作用域而不需要变量声明的流行语言,因此必须想出一个不寻常的解决方案。Ruby中的函数可以包含其他函数定义,它们还可以包含用花括号括起来的代码块。块可以访问外部变量,但嵌套函数不能。在一个块中,对名称的赋值仅当它不会隐藏已经在外部作用域中绑定的名称时才意味着局部变量的声明;否则赋值被解释为外部名称的重新绑定。Ruby的作用域语法和规则也已被广泛讨论,并且在Ruby 2.0中似乎可能发生变化[28]。
提案概述
在Python-Dev上,已经提出了许多关于如何在外部作用域中重新绑定名称的不同提案。它们都分为两类:在绑定名称的作用域中使用新语法,或在使用名称的作用域中使用新语法。
绑定(外部)作用域中的新语法
作用域覆盖声明
此类别中的提案都建议使用一种类似于JavaScript的var
的新型声明语句。已经为这个目的提出了一些可能的关键字
在所有这些提案中,特定作用域S中的声明(例如var x
)将导致嵌套在S内的作用域中对x
的所有引用都引用在S中绑定的x
。
此类提案的主要反对意见是,函数定义的含义将变得对上下文敏感。将函数定义移动到其他块内可能会导致函数中的任何局部名称引用变得非局部,这是由于封闭块中的声明造成的。对于Ruby 1.8中的块,实际上就是这样;在以下示例中,两个setter具有不同的效果,即使它们看起来相同
setter1 = proc { | x | y = x } # y is local here
y = 13
setter2 = proc { | x | y = x } # y is nonlocal here
setter1.call(99)
puts y # prints 13
setter2.call(77)
puts y # prints 77
请注意,尽管此提案类似于JavaScript和Perl中的声明,但对语言的影响是不同的,因为在这些语言中,未声明的变量默认为全局,而在Python中,未声明的变量默认为局部。因此,在JavaScript或Perl中将函数移动到其他块内只能减少以前全局名称引用的作用域,而在使用此提案的Python中,它可以扩展以前局部名称引用的作用域。
必需的变量声明
一个更激进的提案[21]建议完全删除Python的作用域猜测约定,并要求所有名称都在要绑定它们的作用域中声明,就像Scheme一样。使用此提案,var x = 3
将同时声明x
属于局部作用域并将其绑定,而x = 3
将重新绑定现有的可见x
。在没有包含var x
声明的封闭作用域的上下文中,语句x = 3
将在静态上确定为非法。
此提案产生了简单一致的模型,但它与所有现有的Python代码不兼容。
引用(内部)作用域中的新语法
此类别中有三种提案。
外部引用表达式
此类提案建议了一种在表达式中使用变量时引用外部作用域中变量的新方法。为此建议的一种语法是.x
[7],它将引用x
而不会为其创建局部绑定。对该提案的一个担忧是,在许多上下文中,x
和.x
可以互换使用,这会让读者感到困惑[31]。一个密切相关的想法是使用多个点来指定要上升的作用域级别数[8],但大多数人认为这容易出错[17]。
重新绑定运算符
本提案建议一个新的类似赋值的操作符,它可以重新绑定一个名称,而无需将该名称声明为局部变量[2]。语句 x = 3
既声明 x
为局部变量,又将其绑定到 3,而语句 x := 3
将更改 x
的现有绑定,而不会将其声明为局部变量。
这是一个简单的解决方案,但根据PEP 3099,它已被拒绝(可能是因为它太容易被忽略或与 =
混淆)。
作用域覆盖声明
此类别中的提案建议在内部作用域中使用一种新的声明语句,以防止名称成为局部变量。此语句在本质上类似于 global
语句,但它不是使名称引用顶级模块级作用域中的绑定,而是使名称引用最近的封闭作用域中的绑定。
这种方法很有吸引力,因为它与熟悉的 Python 结构类似,并且因为它保留了函数定义的上下文无关性。
这种方法在安全性和调试方面也具有优势。生成的 Python 不仅会匹配其他嵌套作用域语言的功能,而且还会使用语法,这对于防御性编程来说可以说是更好的。在大多数其他语言中,声明会缩小现有名称的作用域,因此意外省略声明可能会产生比预期更广泛(即更危险)的影响。在使用此提案的 Python 中,添加声明的额外工作与非局部效应的增加风险相一致(即,阻力最小的路径是最安全的路径)。
许多拼写都已建议用于此类声明
scoped x
[1]global x in f
[3](显式指定作用域)free x
[5]outer x
[6]use x
[9]global x
[10](更改global
的含义)nonlocal x
[11]global x outer
[18]global in x
[18]not global x
[18]extern x
[20]ref x
[22]refer x
[22]share x
[22]sharing x
[22]common x
[22]using x
[22]borrow x
[22]reuse x
[23]scope f x
[25](显式指定作用域)
最常讨论的选择似乎是 outer
、global
和 nonlocal
。outer
已经在标准库中用作变量名和属性名。单词 global
具有冲突的含义,因为“全局变量”通常被理解为具有顶级作用域的变量[27]。在 C 中,关键字 extern
表示名称引用不同编译单元中的变量。虽然 nonlocal
有点长,而且听起来不如其他一些选项悦耳,但它确实具有完全正确的含义:它声明了一个非局部名称。
提议的解决方案
本 PEP 提出的解决方案是在引用(内部)作用域中添加作用域覆盖声明。Guido 在 Python-Dev 上表达了对这类解决方案的偏好[14],并且对 nonlocal
作为关键字表示认可[19]。
建议的声明
nonlocal x
防止 x
在当前作用域中成为局部名称。当前作用域中 x
的所有出现都将引用在外部封闭作用域中绑定的 x
。与 global
一样,允许多个名称
nonlocal x, y, z
如果在封闭作用域中不存在预先存在的绑定,则编译器将引发 SyntaxError。(将此称为语法错误可能有点牵强,但到目前为止,SyntaxError 用于所有编译时错误,包括例如具有未知功能名称的 __future__ 导入。)Guido 曾经说过,在没有外部绑定的情况下,此类声明应被视为错误[16]。
如果 nonlocal
声明与局部作用域中形式参数的名称冲突,则编译器将引发 SyntaxError。
还允许使用简写形式,其中 nonlocal
放在赋值或增强赋值之前
nonlocal x = 3
以上与 nonlocal x; x = 3
的含义完全相同。(Guido 支持 global
语句的类似形式[24]。)
在简写形式的左侧,仅允许标识符,不允许 x[0]
之类的目标表达式。否则,允许所有形式的赋值。建议的 nonlocal
语句语法为
nonlocal_stmt ::=
"nonlocal" identifier ("," identifier)*
["=" (target_list "=")+ expression_list]
| "nonlocal" identifier augop expression_list
允许所有这些赋值形式的原因是为了简化对 nonlocal
语句的理解。将简写形式分解为声明和赋值足以理解它的含义以及它是否有效。
向后兼容性
此 PEP 以 Python 3000 为目标,正如 Guido 所建议的那样[19]。但是,其他人也注意到,本 PEP 中考虑的一些选项可能足够小,可以在 Python 2.x 中实现[26],在这种情况下,本 PEP 可能被移动到成为 2.x 系列 PEP。
作为引入新关键字影响程度的(非常粗略的)衡量标准,以下是根据 2006 年 11 月 5 日对 Python SVN 存储库的扫描,一些建议的关键字作为标识符在标准库中出现的次数
nonlocal 0
use 2
using 3
reuse 4
free 8
outer 147
global
作为现有关键字出现 214 次。作为使用 global
作为外部作用域关键字影响程度的衡量标准,标准库中有 18 个文件将因此类更改而中断(因为函数在全局作用域中引入变量 global
之前声明了一个变量 global
)
cgi.py
dummy_thread.py
mhlib.py
mimetypes.py
idlelib/PyShell.py
idlelib/run.py
msilib/__init__.py
test/inspect_fodder.py
test/test_compiler.py
test/test_decimal.py
test/test_descr.py
test/test_dummy_threading.py
test/test_fileinput.py
test/test_global.py (not counted: this tests the keyword itself)
test/test_grammar.py (not counted: this tests the keyword itself)
test/test_itertools.py
test/test_multifile.py
test/test_scope.py (not counted: this tests the keyword itself)
test/test_threaded_import.py
test/test_threadsignals.py
test/test_warnings.py
参考文献
[15] 显式词法作用域 (PEP 之前?) (Guido van Rossum) https://mail.python.org/pipermail/python-dev/2006-July/066995.html
致谢
本 PEP 中提到的想法和建议来自无数的 Python-Dev 帖子。感谢 Jim Jewett、Mike Orr、Jason Orendorff 和 Christian Tanzer 建议对本 PEP 进行具体编辑。
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-3104.rst
上次修改时间:2023-10-11 12:05:51 GMT