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

Python 增强提案

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目前都处于推迟状态,主要是因为这两个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 = <定义的函数或类>”名称绑定操作成为可能,特别是在不需要局部名称绑定的情况下。

根据本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()的key参数
  • 部分函数应用
  • 提供回调(例如,用于弱引用或异步IO)
  • NumPy中的数组广播操作

然而,直接采用Ruby的块语法对Python行不通,因为Ruby块的有效性在很大程度上依赖于函数定义方式的各种约定(特别是使用Ruby的yield语法直接调用块,以及&arg机制接受块作为函数的最后一个参数)。

由于Python长期以来一直依赖命名函数,接受回调的API签名种类繁多,因此需要一种解决方案,允许将一次性函数插入到适当的位置。

本PEP采用的方法是保留显式命名函数的要求,但允许更改定义和引用它的语句的相对顺序,以匹配开发人员的思维流程。其基本原理与引入装饰器时使用的原理相同,但涵盖了更广泛的应用。

与PEP 3150的关系

PEP 3150(语句局部命名空间)将其主要动机描述为将普通赋值语句提升到与classdef语句同等重要的地位,即在计算项的值的细节之前,向读者提前呈现要定义的项的名称。本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:实现应该相对简单。

@in子句将包含在关联函数或类定义及其引用语句的AST中。当@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

最后修改:2025-02-01 08:59:27 GMT