查看原文
其他

PEP 318:函数和方法的装饰器

(给Python开发者加星标,提升Python技能

作者:豌豆花下猫 

警告警告警告


本文档旨在描述装饰器语法和做出决定的过程。它既不试图涵盖大量潜在的替代语法,也不试图详尽列出每种形式的所有优点和缺点。

摘要


当前用于转换函数和方法的方式(例如,将它们声明为类或静态方法)很笨拙,并且可能导致难以理解的代码。在理想的情况下,这些转换应该在代码中作声明的位置进行。本 PEP 引入了对函数或方法声明作转换的新语法。

动机


当前对函数或方法作变换的方式会把实际的变换置于函数体之后。对于大型函数,这会将函数行为的关键组成部分与其余的函数外部接口的定义分开。例如:

def foo(self):
    perform method operation
foo = classmethod(foo)

对于较长的方法,这变得不太可读。在概念上只是声明一个函数,使用其名称三遍就很不 pythonic。此问题的解决方案是将方法的转换移到方法本身的声明附近。新语法的意图是替换

def foo(cls):
    pass
foo = synchronized(lock)(foo)
foo = classmethod(foo)

成为一种将装饰符放置在函数的声明中的写法:

@classmethod
@synchronized(lock)
def foo(cls):
    pass

以这种方式来修改类也是可能的,尽管好处不能立即体现。几乎可以肯定,使用类装饰器可以完成的任何事情都可以使用元类来完成,但是使用元类非常晦涩,所以就有吸引力找到一种对类进行简单修改的更简便的方法。对于 Python 2.4 来说,仅添加了函数/方法装饰器。

PEP 3129   提议从 Python 2.6 开始添加类装饰器

为什么这很难?

自 2.2 版本以来,Python 中提供了两个装饰器(classmethod() 和 staticmethod() )。大约从那时起,就已经假设最终会在语言中添加对它们的一些语法支持。既然有了此假设,人们可能想知道为什么还会很难达成共识。

在 comp.lang.python 和 python-dev 邮件列表中,关于如何最好地实现函数装饰器的讨论,时不时就会展开。没有一个明确的争辩理由,但是如下问题看起来分歧最大。

  • 关于“意图的声明”放置何处的分歧。几乎所有人都同意,在函数定义的末尾装饰/转换函数不是最佳的。除此之外,似乎没有明确的共识将这些信息放在何处。
  • 语法约束。Python 是一种语法简单的语言,除开“捣乱”(无论从外表上还是考虑到语言解析器),对可以完成和不能完成的事情都有相当严格的约束。没有明显的方法来组织这些信息,以便刚接触该概念的人们会想:“哦,是的,我知道你在做什么。” 看起来最好的办法就是防止新用户对语法的含义形成错误的心智模型。
  • 总体上不熟悉该概念。对于那些熟悉代数(或者只是基本算术)或至少使用过其它编程语言的人来说,Python 的大部分内容都是符合直觉的。但在 Python 中遇到装饰器概念之前,很少有人会接触到这个概念。没有一个很强的先验模因(preexisting meme)能包含这个概念。
  • 语法上的讨论所获得的关注,大体上超过了所有其它东西所获得的关注。读者可以看到的三元运算符讨论,与PEP 308相关,也是这样的例子。


背景


人们普遍同意,装饰器语法对于当前而言是可取的。Guido 在第十届Python大会 [3] 的 DevDay 主题演讲中提到了对装饰器的语法支持[2],尽管他后来说[5],这只是他“半开玩笑”提议的几种扩展之一。会议结束后不久,Michael Hudson 在 python-dev 上提出了主题[4],将最初的括号语法归因于Gareth McCaughan [6] 先前在 comp.lang.python 上的提议。

类装饰器似乎是显而易见的下一步,因为类定义和函数定义在语法上相似,但是 Guido 仍然有疑虑,类装饰器几乎肯定不会在 Python 2.4 中出现。

从 2002 年 2 月到 2004 年 7 月,python-dev 里的讨论一直此起彼伏。数百篇回帖,人们提出了许多可能的语法变体。Guido 列了一份提案清单,带到 EuroPython 2004 [7] 上讨论。之后,他决定使用Java风格的[10] @decorator 语法,该语法在 2.4a2 中首次出现。

Barry Warsaw 将其命名为“pie-decorator”语法,以纪念 Pie-thon Parrot 比赛(译注:这是当年的一件逸事,Parrot 虚拟机与 CPython 虚拟机比赛性能优劣),该事件与装饰器语法几乎同时发生,而且 @ 看起来有点像馅饼。Guido 在 Python-dev 上概述了他的要点[8],其中包括 这篇文章[9],谈论了一些(许多)被否决的内容。

关于“Decorator”名称


对于将此特性命名为“decorator”,有很多人抱怨。主要问题是该名称与GoF书 中的用法不一致[11]。名称“ decorator”可能更多是用在编译器领域中——一个语法树被遍历和注解。很有可能会出现一个更好的名称。

设计目标


新的语法应该:
  • 适用于任意包装器(wrapper),包括用户定义的可调用对象以及现有的内置类型classmethod() 和 staticmethod() 。此要求还意味着装饰器语法必须支持将参数传递给 wrapper 的构造函数  
  • 每个定义需支持多重包装器
  • 过程应清晰可见;至少应该明显到令新用户在编写代码时可以安全地忽略它
  • 成为一种“……一旦解释就容易记住的”语法
  • 不会使将来的扩展变困难
  • 易于输入;使用了它的程序应该期望经常使用它
  • 不会对快速浏览代码造成困难。搜索所有定义、特定定义或函数的入参应该要容易
  • 不应使辅助支持工具,如语言敏感的编辑器和其它“ 玩具解析器工具 ”[12] ,变得复杂化
  • 允许将来的编译器针对装饰器进行优化。Python 的 JIT 编译器有希望在将来成为现实,这就要求装饰器的语法要先于函数的定义
  • 从当前隐藏的函数末尾,移到最前面[13]

安德鲁·库奇林(Andrew Kuchling)在他的博客[14]中链接了许多有关动机和用例的讨论。特别值得注意的是Jim Huginin 的用例列表[15]。

当前语法


当前在 Python 2.4a2 中实现的函数装饰器的语法为:

@dec2
@dec1
def func(arg1, arg2, ...):
    pass

这等效于:

def func(arg1, arg2, ...):
    pass
func = dec2(dec1(func))

但没有对变量 func 的过渡性赋值。装饰器靠近函数的声明。@ 符号清楚地表明这里正在发生新的事情。

应用顺序[16](从下到上)的基本原理是,它与函数用途的一般顺序相匹配。在数学中,组合函数 (g o f)(x) 会转换为 g(f(x))。在 Python 中,"@g @f def foo()" 转换为 foo = g(f(foo))。

装饰器语句是被约束的——任意的表达式都不能用。Guido 出于直觉[17],更喜欢这种方式。

当前语法还允许装饰器在声明时,可以调用一个返回装饰器的函数:

@decomaker(argA, argB, ...)
def func(arg1, arg2, ...):
    pass

这等效于:

func = decomaker(argA, argB, ...)(func)

使用返回装饰器的函数的基本原理是,@ 符号后的部分可以被视为表达式(尽管句法上被限为一个函数),然后该表达式返回的任何内容将被调用。参见声明参数[16]。

语法的选择


大量的[18]不同语法被提了出来——与其尝试令这些语法单独起作用,更值得将它们分为多个领域讨论。试图单独讨论每种可能的语法[19]将是一种疯狂的举动,并且会产生一个完全不明智的 PEP。

装饰器位置

第一个语法点是装饰器的位置。对于以下示例,我们使用了 2.4a2 中的 @ 语法。

def 语句之前的装饰器是第一种选择,并且在 2.4a2 中就使用了它:

@classmethod
def foo(arg1,arg2):
    pass

@accepts(int,int)
@returns(float)
def bar(low,high):
    pass

有许多人对该位置提出了反对意见——最主要的反对意见是,这是 Python 中第一个真正的前一行代码会对下一行产生影响的情况。2.4a3 中可用的语法要求每行一个装饰器(在 a2 中,可以在同一行上指定多个装饰器),最后在 2.4 的最终版本中,每行只保留一个装饰器。

人们还抱怨说,当使用多个装饰器时,语法很快会变得笨重。但是,有人指出,在单个函数上使用大量装饰器的可能性很小,因此这并不是一个大问题。

这种形式的一些优点是装饰器位于方法的主体之外——显然,它们是在定义函数时执行的。

另一个好处是,写在函数定义的前面,适合在不知道代码内容时,就改变代码的语义,也就是说,你知道如何正确地解释代码的语义,如果该语法没有出现在函数定义之前,你需要回看并改变初始的理解。

Guido 决定他更喜欢[20]在“def”的前面行里放置装饰器,因为长长的参数列表就意味着装饰器最好被“隐藏”起来 。

第二种形式是把装饰器放在 def 与函数名称之间,或者在函数名称与参数列表之间:

def @classmethod foo(arg1,arg2):
    pass

def @accepts(int,int),@returns(float) bar(low,high):
    pass

def foo @classmethod (arg1,arg2):
    pass

def bar @accepts(int,int),@returns(float) (low,high):
    pass

对该形式有两个异议。第一,它很容易破坏源代码的“可扩展性”——你无法再通过搜索“def foo(”来找到函数的定义;第二,更严重的是,在使用多个装饰器的情况下,语法将会非常笨拙。

接下来的一种形式,它有一定数量的坚定支持者,就是把装饰器放在"def"行的参数列表与末尾的“:”号之间:

def foo(arg1,arg2) @classmethod:
    pass

def bar(low,high) @accepts(int,int),@returns(float):
    pass
Guido 将反对这种形式的论点(其中许多也适用于以前的形式)总结 [13]为:
  • 它把重要的信息(例如,这是一种静态方法)藏在了签名之后,很容易就看漏

  • 很容易错过长参数列表和长装饰器列表之间的过渡信息
  • 剪切并粘贴装饰器列表以进行重用很麻烦,因为它在代码行的中间开始和结束

下一种形式是将装饰器语法放在方法体的开头,与当前文档字符串(doctring)的所在位置相同:

def foo(arg1,arg2):
    @classmethod
    pass

def bar(low,high):
    @accepts(int,int)
    @returns(float)
    pass

对此形式的主要反对意见是,它需要“窥视”方法体才能确定装饰器。另外,即使装饰器代码在方法体内,但它并不是在运行方法时执行。Guido 认为 docstring 并不构成一个很好的反例,甚至“docstring”装饰器很有可能有助于将 docstring 移到函数体之外。

最后一种形式是用一个代码块将方法的代码嵌套起来。在此示例中,我们将使用“decorate”关键字,因为 @ 语法毫无意义。

decorate:
    classmethod
    def foo(arg1,arg2):
        pass

decorate:
    accepts(int,int)
    returns(float)
    def bar(low,high):
        pass

这种形式将导致被装饰方法和非装饰方法的缩进不一致。此外,被装饰的方法体将从第三层缩进开始。

语法形式

  • @decorator:
@classmethod
def foo(arg1,arg2):
    pass

@accepts(int,int)
@returns(float)
def bar(low,high):
    pass

反对这种语法的主要意见是 Python 中当前未使用过 @ 符号(IPython 和 Leo 均使用了@符号),并且 @ 符号没有意义。另一个反对意见是,这会将当前未使用的字符(从有限的集合中)“浪费”在不被认为是主要用途的事物上。

  • | decorator:
|classmethod
def foo(arg1,arg2):
    pass

|accepts(int,int)
|returns(float)
def bar(low,high):
    pass

这是 @decorator 语法的一个变体——它的优点是不会破坏 IPython 和 Leo。与 @ 语法相比,它的主要缺点是 | 符号看起来像大写字母 I 和小写字母 l。

  • 列表语法:
[classmethod]
def foo(arg1,arg2):
    pass

[accepts(int,int), returns(float)]
def bar(low,high):
    pass

对列表语法的主要反对意见是它当前是有意义的(当在方法之前使用时)。而且也没有任何迹象表明该表达式是个装饰器。

  • 使用其它括号(<…>,[[…]],…)的列表语法:
<classmethod>
def foo(arg1,arg2):
    pass

<accepts(int,int), returns(float)>
def bar(low,high):
    pass

这些替代写法都没有太大的吸引力。涉及其它括号的写法仅用于使装饰器构造得不像是个列表。它们没有做到任何使解析变得更容易的事情。'<…>'写法存在解析问题,因为'<'和'>'已经解析为未配对。它们还引起了进一步的解析歧义,因为右尖括号(>)可能是一个大于号,而不是装饰器的闭合符。

  • decorate()

该写法提议不用新的语法来实现——它提议用一个可自省的魔术函数来控制其后的函数。Jp Calderone 和 Philip Eby 都提供了此功能的实现。Guido 坚决反对这一点——不用新的语法,这样的函数的魔力会极其高:

通过 sys.settraceback 使用具有“远距动作”(action-at-a-distance)功能的函数,可能会适合一种潜在的功能,该功能无法通过其它任何不更改语言的方式实现,但是对于装饰器而言,情况并非如此。此处普遍持有的观点是,需要添加装饰器作为一种语法功能,以避免 2.2 和 2.3 中使用的后缀表示法带来的问题。装饰器被认定为一项重要的新语言功能,其设计需要具有前瞻性,而不是受到 2.3 版中可以实现的东西所约束。

  • 新关键字(和代码块)
这个想法是来自 comp.lang.python 的共识(有关更多信息,请参见下面的社区共识。)Robert Brewer 撰写了详细的J2 提案[21]文档,概述了支持这种形式的论点。此形式的最初问题有:
    - 它需要一个新关键字,因此还需要一个"from __future__ import decorators"的语句。 
    - 关键字的选择仍有争议。但是,"using"已成为该共识的选择,并被用于提案和实现中。 
    - 关键字/代码块形式会产生类似于普通代码块的内容,但并不是。尝试在此块中使用语句将导致语法错误,这可能会使用户感到困惑。

几天后,Guido 出于两个主要理由拒绝了该提案[22]。首先:

… 缩进块的句法形式强烈暗示了其内容应为语句序列,但实际上它却不是——只有表达式是允许的,并且这些表达式存在隐式的“收集中”状态,直到它们可以被应用在随后的函数定义为止。

其次:

… 关键字开始于块的开头,会引起很多关注。对于“ if”、“ while”、“ for”、“ try”、“ def”和“ class”,这是正确的。但是,“ using”关键字(或其它位置的关键字)不值得引起这种关注。重点应该放在装饰器或装饰器套件上,因为它们是随后的函数定义的重要装饰符。

请读者阅读完整的回复[22]。

  • 其它形式

Wiki 页面[23]上还有许多其它变体和提议。

为什么是@?

Java 中有一些时间最初使用 @ 作为Javadoc 注释[24]中的标记,后来在 Java 1.5 中用作注解[10],这与 Python 的装饰器相似。@ 以前没有在 Python 中用作标记的事实也意味着,很显然早期版本的 Python 不可能解析此类代码,从而可能导致细微的语义错误。这也意味着,什么是装饰器和什么不是装饰器,这种不确定性被移除了。也就是说,@ 仍然是一个相当随意的选择。有些人建议使用 | 代替。

对于使用类似列表的语法(无论出现在何处)来指定装饰器,一些替代方法被提了出来:[| … |],* […] * 和 <…>。

当前实现与历史


Guido 征集一名志愿者来实现他所偏好的语法,Mark Russell 响应并向 SF 提交了补丁[25]。这个新语法在 2.4a2 中可用。

@dec2
@dec1
def func(arg1, arg2, ...):
    pass

这等效于:

def func(arg1, arg2, ...):
    pass
func = dec2(dec1(func))

尽管没有在中间创建名为 func 的变量。

在 2.4a2 中实现的版本允许在一行上包含多个 @decorator 子句。在 2.4a3 版中,此规定已严格限制为每行只允许一个装饰器。

Michael Hudson 的一个实现了“list-after-def”语法的 早期补丁[26] 还继续活跃着。

在发布 2.4a2 之后,Guido 表示,如果社区可以达成社区共识、提供一份体面的提案和实现方案,他将对社区提案进行重新审核,以回应社区的反应。在出现了惊人数量的帖子之后,Python Wiki [18]收集了大量的替代方案,社区共识出现了(见下)。Guido 随后拒绝了此方案[22],但补充说:

在 Python 2.4a3(将于本周四发布)中,一切还保存在 CVS 中。对于 2.4b1,我将考虑将 @ 更改为其它单个字符,尽管我认为 @ 具有与 Java 类似功能所使用的相同字符的优点。有人认为这并不完全相同,因为 Java 中的 @ 用于不更改语义的属性。但是 Python 的动态特性使它的语法元素永远不会与其它语言中的类似构造具有完全相同的含义,并且肯定存在明显的重叠。关于对第三方工具的影响:IPython 的作者认为不会有太大影响;Leo 的作者说 Leo 将幸免于难(尽管这将使他和他的使用者有一些过渡性的痛苦)。我实际上觉得选择一个在 Python 语法中其它地方已经使用过的字符,可能会使外部工具更难以适应,因为在这种情况下解析将变得更加微妙。但坦率地说,我还没有决定,所以这里有些摆动的空间。我现在不想再考虑其它的语法选择:必须在某个时候停止,每个人都有话说,但演出必须继续。

社区共识

本节记录了被否决的 J2 语法,为了历史的完整性而将其包括在内。

在 comp.lang.python 上出现的共识是要提议 J2 语法(“J2”是在 PythonDecorators Wiki 页面上的叫法):在 def 语句之前,作为前缀的新关键字using 及装饰器代码块。例如:

using:
    classmethod
    synchronized(lock)
def func(cls):
    pass
该语法的主要论点来自“可读性计数”(readability counts)学说。简而言之,它们是:
  • 一个套件比多个 @ 行更好。using 关键字和其代码块将单块的 def 语句转换成多块的复合结构,类似于 try/finally 和其它。
  • 关于标识符(token),关键字比标点符号更好。关键字与标识符的现有用法相符。不需要新的标识符类别。关键字将 Python 装饰器与 Java 注解和 .Net 属性区分开,它们显而易见并非同类。

罗伯特·布鲁尔(Robert Brewer)为此形式撰写了详细的提案[21],迈克尔·斯帕克斯(Michael Sparks)制作了补丁[27]。

如前所述,Guido 否决了此形式,并在给 python-dev 和 comp.lang.python 的消息[22]中概述了它的问题。

例子


在 comp.lang.python 和 python-dev 邮件列表里的许多讨论,都集中在装饰器的使用上,认为它是一种比 staticmethod() 和 classmethod() 内置函数更简洁的方法。当然其能力要比那个强大得多。本节介绍了一些使用示例。

  1. 定义在退出时执行的函数。请注意,该函数实际上并不是通常意义上的“包装”。
def onexit(f):
    import atexit
    atexit.register(f)
    return f

@onexit
def func():
    ...

请注意,此示例可能不适合实际使用,仅用于演示目的。

  1. 用单例实例定义一个类。请注意,一旦类消失,进取的程序员需要更有创造力才能创建更多的实例。(出自 python-dev 上的 Shane Hathaway )
def singleton(cls):
    instances = {}
    def getinstance():
        if cls not in instances:
            instances[cls] = cls()
        return instances[cls]
    return getinstance

@singleton
class MyClass:
    ...
  1. 向一个函数添加属性。(基于 Anders Munch 在 python-dev 上发布的示例)
def attrs(**kwds):
    def decorate(f):
        for k in kwds:
            setattr(f, k, kwds[k])
        return f
    return decorate

@attrs(versionadded="2.2",
       author="Guido van Rossum")
def mymethod(f):
    ...
  1. 限定函数参数和返回类型。请注意,这会将 func_name 属性从旧函数复制到新函数。func_name 在 Python 2.4a3 中是可写的:
def accepts(*types):
    def check_accepts(f):
        assert len(types) == f.func_code.co_argcount
        def new_f(*args, **kwds):
            for (a, t) in zip(args, types):
                assert isinstance(a, t), \
                       "arg %r does not match %s" % (a,t)
            return f(*args, **kwds)
        new_f.func_name = f.func_name
        return new_f
    return check_accepts

def returns(rtype):
    def check_returns(f):
        def new_f(*args, **kwds):
            result = f(*args, **kwds)
            assert isinstance(result, rtype), \
                   "return value %r does not match %s" % (result,rtype)
            return result
        new_f.func_name = f.func_name
        return new_f
    return check_returns

@accepts(int, (int,float))
@returns((int,float))
def func(arg1, arg2):
    return arg1 * arg2
  1. 声明一个类实现特定的一个(一组)接口。摘自 Bob Ippolito 在 python-dev 上发表的文章,基于其在PyProtocols [28]的经验基础上。
def provides(*interfaces):
     """
     An actual, working, implementation of provides for
     the current implementation of PyProtocols.  Not
     particularly important for the PEP text.
     """

     def provides(typ):
         declareImplementation(typ, instancesProvide=interfaces)
         return typ
     return provides

class IBar(Interface):
     """Declare something about IBar here"""

@provides(IBar)
class Foo(object):
        """Implement something here..."""

当然,尽管没有语法上的支持,但所有这些示例如今都是可能的。

(不再是)未决问题


  1. 尚不确定类装饰器是否会在将来集成到 Python 中。Guido 表达了对这一概念持怀疑态度,但不同的人在 python-dev 里提出了一些有力的论据[29](搜索 PEP 318 -- 发帖草案)。类装饰器在 Python 2.4 中是极不可能的。

    PEP 3129 [#PEP-3129]提议从 Python 2.6 开始添加类装饰器。

  2. @ 字符的选择将在 Python 2.4b1 之前重新检查。(最后,@ 字符被保留。

参考资料


[1] PEP 3129, "Class Decorators", Winter http://www.python.org/dev/peps/pep-3129

[2] http://www.python.org/doc/essays/ppt/python10/py10keynote.pdf

[3] http://www.python.org/workshops/2002-02/

[4] https://mail.python.org/pipermail/python-dev/2002-February/020005.html

[5] https://mail.python.org/pipermail/python-dev/2002-February/020017.html

[6] http://groups.google.com/groups?hl=en&lr=&ie=UTF-8&oe=UTF-8&selm=slrna40k88.2h9o.Gareth.McCaughan%40g.local

[7] http://www.python.org/doc/essays/ppt/euro2004/euro2004.pdf

[8] https://mail.python.org/pipermail/python-dev/2004-August/author.html

[9] https://mail.python.org/pipermail/python-dev/2004-August/046672.html

[10] (1, 2) http://java.sun.com/j2se/1.5.0/docs/guide/language/annotations.html

[11] http://patterndigest.com/patterns/Decorator.html

[12] http://groups.google.com/groups?hl=en&lr=&ie=UTF-8&oe=UTF-8&selm=mailman.1010809396.32158.python-list%40python.org

[13] (1, 2) https://mail.python.org/pipermail/python-dev/2004-August/047112.html

[14] http://www.amk.ca/diary/archives/cat_python.html#003255

[15] https://mail.python.org/pipermail/python-dev/2004-April/044132.html

[16] (1, 2) https://mail.python.org/pipermail/python-dev/2004-September/048874.html

[17] https://mail.python.org/pipermail/python-dev/2004-August/046711.html

[18] (1, 2) http://www.python.org/moin/PythonDecorators

[19] http://ucsu.colorado.edu/~bethard/py/decorators-output.py

[20] https://mail.python.org/pipermail/python-dev/2004-March/043756.html

[21] (1, 2) http://www.aminus.org/rbre/python/pydec.html

[22] (1, 2, 3, 4) https://mail.python.org/pipermail/python-dev/2004-September/048518.html

[23] https://wiki.python.org/moin/PythonDecoratorProposals

[24] http://java.sun.com/j2se/javadoc/writingdoccomments/

[25] https://bugs.python.org/issue979728

[26] http://starship.python.net/crew/mwh/hacks/meth-syntax-sugar-3.diff

[27] https://bugs.python.org/issue1013835

[28] http://peak.telecommunity.com/PyProtocols.html

[29] https://mail.python.org/pipermail/python-dev/2004-March/thread.html


版权


本文档已经放置在公共领域。源文档:https://github.com/python/peps/blob/master/pep-0318.txt


【本文作者】


作者:豌豆花下猫,生于广东毕业于武大,现为苏漂程序员,有一些极客思维,也有一些人文情怀,有一些温度,还有一些态度。



推荐阅读

(点击标题可跳转阅读)

PEP 255 :简单的生成器

学习Python,怎能不懂点PEP呢?



觉得本文对你有帮助?请分享给更多人

关注「Python开发者」加星标,提升Python技能

好文章,我在看❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存