PEP 403 – 通用装饰器子句(又名“@in”子句)
- 作者:
- Alyssa Coghlan <ncoghlan at gmail.com>
- 状态:
- 延期
- 类型:
- 标准跟踪
- 创建:
- 2011年10月13日
- Python 版本:
- 3.4
- 历史记录:
- 2011年10月13日
摘要
本 PEP 提出添加一个新的 @in
装饰器子句,它使得能够覆盖函数或类定义的名称绑定步骤成为可能。
新子句接受一个简单的语句,该语句可以向前引用被装饰的函数或类定义。
此新子句旨在用于需要“一次性”函数或类的情况,并且将函数或类定义放在使用它的语句之前实际上会使代码更难阅读。它还通过确保新名称仅对 @in
子句中的语句可见来避免任何名称遮蔽问题。
本 PEP 在很大程度上基于 PEP 3150(语句局部命名空间)中的许多想法,因此一些基本原理对该 PEP 的读者来说会很熟悉。这两个 PEP 目前都处于延期状态,主要是因为这两个 PEP 都缺乏引人注目的现实世界用例。
基本示例
在深入探讨此问题的悠久历史以及此特定提议解决方案的详细基本原理之前,这里有一些简单的示例,说明它旨在简化哪种代码。
作为一个简单的例子,弱引用回调可以定义如下
@in x = weakref.ref(target, report_destruction)
def report_destruction(obj):
print("{} is being destroyed".format(obj))
这与当前(概念上)“乱序”的此操作语法形成对比
def report_destruction(obj):
print("{} is being destroyed".format(obj))
x = weakref.ref(target, report_destruction)
当您多次使用可调用对象时,该结构还可以,但在一次性操作中被迫使用它却很烦人。
如果名称的重复显得特别烦人,则可以使用像 f
这样的临时名称
@in x = weakref.ref(target, f)
def f(obj):
print("{} is being destroyed".format(obj))
类似地,对特定定义不明确的类型的排序操作现在可以定义为
@in sorted_list = sorted(original, key=f)
def f(item):
try:
return item.calc_sort_order()
except NotSortableError:
return float('inf')
而不是
def force_sort(item):
try:
return item.calc_sort_order()
except NotSortableError:
return float('inf')
sorted_list = sorted(original, key=force_sort)
并且列表推导式中的早期绑定语义可以通过以下方式获得
@in funcs = [adder(i) for i in range(10)]
def adder(i):
return lambda x: x + i
提案
本 PEP 提出添加一个新的 @in
子句,它是现有类和函数装饰器语法的变体。
新的 @in
子句位于装饰器行之前,并允许向前引用后面的函数或类定义。
后面的函数或类定义始终被命名 - 然后使用尾随定义的名称从 @in
子句进行前向引用。
允许 @in
子句包含任何简单语句(包括在该上下文中没有任何意义的语句,例如 pass
- 虽然此类代码是合法的,但编写它没有任何意义)。这种宽松的结构更容易定义和解释,但更严格的方法仅允许“有意义”的操作也是可能的(请参阅 PEP 3150 以获取可能的候选列表)。
@in
子句不会创建新的作用域 - 除了尾随函数或类定义之外的所有名称绑定操作都将影响包含的作用域。
在尾随函数或类定义中使用的名称仅对关联的 @in
子句可见,并且其行为就像在该作用域中定义的普通变量一样。如果在 @in
子句或尾随函数或类定义中创建任何嵌套作用域,则这些作用域将看到尾随函数或类定义,而不是包含作用域中该名称的任何其他绑定。
从某种意义上说,本提案是关于使能够覆盖作为每个函数或类定义的一部分的隐式“name = <defined function or class>”名称绑定操作成为可能,特别是在实际上不需要局部名称绑定时。
在本 PEP 下,普通的类或函数定义
@deco2
@deco1
def name():
...
可以解释为大致等价于
@in name = deco2(deco1(name))
def name():
...
语法更改
在语法上,只需要一个新的语法规则
in_stmt: '@in' simple_stmt decorated
语法:http://hg.python.org/cpython/file/default/Grammar/Grammar
设计讨论
背景
“多行 lambda”的问题长期以来一直困扰着许多 Python 用户,并且我探索了 Ruby 的块功能后才最终理解了为什么这会让大家如此烦恼:Python 要求函数在需要它的操作之前被命名和引入,这打破了开发人员的思维流程。他们到达一个他们认为“我需要一个执行 <X> 的一次性操作”的点,并且他们不能直接说出来,而是必须回退,命名一个执行 <X> 的函数,然后从他们实际上想要在第一时间执行的操作中调用该函数。Lambda 表达式有时可以提供帮助,但它们不能替代能够使用完整块。
Ruby 的块语法也极大地启发了本 PEP 中解决方案的风格,因为它明确表明,即使限制为每个语句一个匿名函数,匿名函数仍然非常有用。考虑 Python 有多少结构,其中一个表达式负责大部分繁重的工作
- 推导式、生成器表达式、map()、filter()
- sorted()、min()、max() 的键参数
- 部分函数应用
- 提供回调(例如,用于弱引用或异步 IO)
- NumPy 中的数组广播操作
但是,直接采用 Ruby 的块语法对 Python 不起作用,因为 Ruby 块的有效性很大程度上依赖于函数定义方式中的各种约定(具体而言,使用 Ruby 的 yield
语法直接调用块以及 &arg
机制将块作为函数的最后一个参数)。
由于 Python 长期以来一直依赖于命名函数,因此接受回调的 API 的签名多种多样,因此需要一个解决方案,允许将一次性函数插入到适当的位置。
本 PEP 中采取的方法是保留显式命名函数的要求,但允许定义和引用它的语句的相对顺序更改以匹配开发人员的思维流程。其基本原理与引入装饰器时使用的基本原理相同,但涵盖了更广泛的应用。
与 PEP 3150 的关系
PEP 3150(语句局部命名空间)将其主要动机描述为将普通的赋值语句提升到与 class
和 def
语句相同的地位,其中要定义的项目的名称在计算该项目的值的详细信息之前呈现给读者。本 PEP 通过允许用其他内容(如将函数的结果赋值给值)替换标准函数定义的简单名称绑定来以不同的方式实现相同的目标。
尽管作者相同,但这两个 PEP 却直接相互竞争。PEP 403 代表了一种极简主义方法,试图通过对现状进行最少的更改来实现有用的功能。相反,本 PEP 旨在实现更灵活的独立语句设计,这需要对语言进行更大程度的更改。
请注意,在 PEP 403 更适合解释生成器表达式的行为的地方,本 PEP 能够更好地解释装饰器子句的一般行为。这两个 PEP 都支持对容器推导式的语义进行充分的解释。
关键字选择
该提案肯定需要某种前缀以避免解析歧义以及与现有结构的后向兼容性问题。它还需要向读者明确突出显示,因为它声明以下代码段将仅在执行尾随函数或类定义后才执行。
选择 in
关键字作为现有的关键字,可以用来表示前向引用的概念。
包含 @
前缀是为了利用 Python 程序员已经习惯将装饰器语法作为乱序执行的指示这一事实,其中函数或类实际上是首先定义的,然后以相反的顺序应用装饰器。
对于函数,该结构旨在解读为“在 <引用 NAME 的此语句> 中定义 NAME 为执行 <操作> 的函数”。
类定义情况下的英语散文映射并不那么明显,但概念仍然相同。
对具有短名称的函数和类的更好的调试支持
反对广泛使用 lambda 表达式的原因之一是它们对回溯可读性和其他方面的影响。类似的反对意见针对那些促进使用短而神秘的函数名称的结构(包括此结构,它要求至少两次提供尾随定义的名称,从而鼓励使用像 f
这样的简写占位符名称)。
但是,在 PEP 3155 中引入限定名称意味着即使是匿名类和函数现在在不同的作用域中也会有不同的表示形式。例如
>>> def f():
... return lambda: y
...
>>> f()
<function f.<locals>.<lambda> at 0x7f6f46faeae0>
同一作用域内的匿名函数(或共享名称的函数)仍然会共享表示形式(除了对象 ID 之外),但这仍然是对历史情况的重大改进,在历史情况中,除了对象 ID 之外,所有内容都相同。
可能的实现策略
与 PEP 3150 相比,本提案至少具有一项巨大的优势:实现应该相对简单。
关联的函数或类定义以及引用它的语句的 AST 中将包含 @in
子句。当存在 @in
子句时,它将代替函数或类定义通常隐含的局部名称绑定操作。
唯一可能棘手的地方是更改在 in
语句的作用域内对语句局部函数或命名空间的引用的含义,但这不应该太难解决,只需在编译器中维护一些额外的状态即可(对于单个名称处理起来要比处理完整嵌套套件中未知数量的名称容易得多)。
解释容器推导式和生成器表达式
该提议的构造的一个有趣特性是,它可以用作一个基本元素来解释生成器表达式和容器推导式的作用域和执行顺序语义。
seq2 = [x for x in y if q(x) for y in seq if p(y)]
# would be equivalent to
@in seq2 = f(seq):
def f(seq)
result = []
for y in seq:
if p(y):
for x in y:
if q(x):
result.append(x)
return result
在此扩展中,重点是解释了为什么推导式在类作用域中表现出错误行为:只有最外层的迭代器在类作用域中求值,而所有谓词、嵌套迭代器和值表达式都在嵌套作用域内求值。
生成器表达式也可以进行等效的扩展。
gen = (x for x in y if q(x) for y in seq if p(y))
# would be equivalent to
@in gen = g(seq):
def g(seq)
for y in seq:
if p(y):
for x in y:
if q(x):
yield x
更多示例
计算属性而不污染局部命名空间(来自 os.py)
# Current Python (manual namespace cleanup)
def _createenviron():
... # 27 line function
environ = _createenviron()
del _createenviron
# Becomes:
@in environ = _createenviron()
def _createenviron():
... # 27 line function
循环早期绑定
# Current Python (default argument hack)
funcs = [(lambda x, i=i: x + i) for i in range(10)]
# Becomes:
@in funcs = [adder(i) for i in range(10)]
def adder(i):
return lambda x: x + i
# Or even:
@in funcs = [adder(i) for i in range(10)]
def adder(i):
@in return incr
def incr(x):
return x + i
尾随类可以用作语句局部命名空间。
# Evaluate subexpressions only once
@in c = math.sqrt(x.a*x.a + x.b*x.b)
class x:
a = calculate_a()
b = calculate_b()
函数可以直接绑定到不是有效标识符的位置。
@in dispatch[MyClass] = f
def f():
...
可以消除那些接近装饰器滥用的结构。
# Current Python
@call
def f():
...
# Becomes:
@in f()
def f():
...
参考实现
目前还没有。
致谢
非常感谢 Gary Bernhardt 直言不讳地指出我在批评 Ruby 的块时毫无头绪,从而开启了一个相当有启发意义的调查过程。
被拒绝的概念
为了避免重复之前讨论过的内容,本节记录了一些被拒绝的备选方案。
省略装饰器前缀字符
此提案的早期版本省略了 @
前缀。但是,如果没有该前缀,裸 in
关键字与后续的函数或类定义的关联性不够紧密。重用装饰器前缀并明确将新构造描述为一种装饰器子句,旨在帮助用户将这两个概念联系起来,并将它们视为同一想法的两个变体。
匿名前向引用
此 PEP 的早期版本(参见 [1])提出了一个语法,其中使用 :
引入新的子句,并使用 @
编写前向引用。对此变体的反馈几乎都是负面的,因为它被认为既难看又过于神奇。
:x = weakref.ref(target, @)
def report_destruction(obj):
print("{} is being destroyed".format(obj))
最近的一个变体始终使用 ...
进行前向引用,以及真正匿名的函数和类定义。但是,在更复杂的情况下,这很快退化为一堆难以理解的点。
in funcs = [...(i) for i in range(10)]
def ...(i):
in return ...
def ...(x):
return x + i
in c = math.sqrt(....a*....a + ....b*....b)
class ...:
a = calculate_a()
b = calculate_b()
使用嵌套块
使用完整嵌套套件的问题在 PEP 3150 中有最好的描述。它在实现上相对困难,作用域语义更难解释,并且会创建很多可以使用两种方法完成的事情的情况,而没有明确的指导方针来选择其中一种(因为几乎任何可以用普通命令式代码表达的构造都可以用给定语句来表达)。虽然 PEP 确实提出了一些新的 PEP 8 指导原则来帮助解决最后一个问题,但实现上的困难却不容易解决。
相比之下,此 PEP 中受装饰器启发的语法明确地将新功能限制在应该实际提高可读性而不是损害可读性的情况下。与最初引入装饰器的情况一样,这种新语法的理念是,如果它*可以*使用(即函数的局部名称绑定完全没有必要),那么它可能*应该*使用。
此想法的另一个可能的变体是保留此 PEP 的基于装饰器的*语义*,同时采用 PEP 3150 中更漂亮的语法。
x = weakref.ref(target, report_destruction) given:
def report_destruction(obj):
print("{} is being destroyed".format(obj))
这种方法存在几个问题。主要问题是这种语法变体使用了看起来像套件但实际上不是套件的东西。第二个问题是不清楚编译器如何知道前导表达式中的哪个名称是前向引用(尽管这可以通过在语言语法中对不是套件的套件进行适当的定义来解决)。
但是,嵌套套件尚未完全排除。 PEP 3150 的最新版本使用了显式前向引用和名称绑定方案,极大地简化了语句的语义,并且确实提供了允许定义任意子表达式而不是仅限于单个函数或类定义的优势。
参考文献
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0403.rst