Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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年,也就是 PEP 227 对嵌套作用域的支持被采纳之前,Python-Dev 上就出现了一个关于作用域语法的提案 [1]。当时,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 中,所有变量都必须声明(使用 definelet,或作为形式参数)。在 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 中,增加声明的额外工作与非局部效应的增加风险(即阻力最小的路径是更安全的路径)相一致。

对于这种声明,已经提出了许多写法:

最常讨论的选择似乎是 outerglobalnonlocalouter 已经被用作标准库中的变量名和属性名。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__ import 与未知特性名称一起使用的情况。)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 的原始实现中添加。后来的讨论 [29] [30] 得出结论,不应实现此语法。

向后兼容性

本 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 变量)

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] Explicit Lexical Scoping (pre-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

最后修改: 2025-02-01 08:55:40 GMT