Yi's Blog

PEP 318 函数与方法装饰器

2017-09-16

这两天回顾 Python 知识点,想到 Python 中的装饰器和 Java 的注解在语法上如此相像,它们是否有什么渊源?又为什么选用了@符号呢?在 PEP 318 中我找到了答案,在此翻译记录。

PEP 318 – Decorators for Functions and Methods

PEP: 318
Title: Decorators for Functions and Methods
Author: Kevin D. Smith <Kevin.Smith at theMorgue.org>, Jim J. Jewett, Skip Montanaro, Anthony Baxter
Status: Final
Type: Standards Track
Created: 05-Jun-2003
Python-Version: 2.4
Post-History: 09-Jun-2003, 10-Jun-2003, 27-Feb-2004, 23-Mar-2004, 30-Aug-2004, 2-Sep-2004

警告

此文档旨在描述装饰器的语法以及确定此语法的过程。本文不会列出那些数量之多的可选语法,也不会一一列举它们的优缺点。

摘要

当前用来转换functionmethod的方法(例如把它们声明为类方法或静态方法)是笨拙且难于理解的。理想的情况是,functionmethod的转换和声明应该在代码的同一处位置。本 PEP 介绍了一种新的方法来转换functionmethod

起因

当前对functionmethod转换的方法是把实际的转换置于函数体之后。对于大函数来说,这会将改变函数行为的一个关键组件和函数的对外定义相分离。

1
2
3
def foo(self):
perform method operation
foo = classmethod(foo)

method变得很长时,代码的可读性就会下降。对一个函数多次命名的这种写法看起来也不够 pythonic。一个解决方案是把方法的转换移动到靠近它声明的位置。为了取代下面这种写法:

1
2
3
4
def foo(cls):
pass
foo = synchronized(lock)(foo)
foo = classmethod(foo)

新的语法在函数的声明处放置装饰器:

1
2
3
4
@classmethod
@synchronized(lock)
def foo(cls):
pass

这种用装饰器的方法去修改类也是可能的,虽然好处没那么显而易见。但可以肯定的是,任何可以用元类(metaclasses)来做的事,同样也可以用装饰器来实现。但使用元类让人感到晦涩难懂,修改类还有更加简单且吸引人的方法。在 Python 2.4 中仅仅加入了 function/method装饰器。

PEP 3129 在 Python 2.6 中加入类装饰器的提案。

为什么这么难?

classmethod()staticmethod()这两个装饰器在 Python 2.2 中就可用了。从那时起,人们就猜测它会被作为一种语法特性加入到 Python 中。有了这个猜测,有人会想知道,为什么最终达成共识是如此的困难。关于如何最好的实现函数装饰器的讨论一直在 comp.lang.python 和 python-dev 邮件列表中不断进行着。讨论没有得到一个明确的结果,但是发现了几个分歧最大的问题:

  • 在哪里做装饰声明的分歧。几乎所有人都认为把装饰声明放在函数之后是不够好的,但是放在哪里却没有达成明确的共识。
  • 语法约束。Python 是一门简单的语言,同时也有足够的限制规定什么能做而什么不能。现在没有一种好的方法来构造这种信息,可以使得用户在第一次看到时就会想,“哦,是的,我明白这是在做什么”。最好避免使这个新概念让用户产生误解。
  • 陌生的概念。对于学过代数(甚至仅仅是算术)或至少使用过一门其他编程语言的人来说,Python 是很符合直觉的。很少有人在使用 Python 之前遇到过装饰器的概念。没有足够的经验让人们理解装饰器这个概念。
  • 对语法的讨论比其他任何东西都多。另一个例子是,PEP 308中有关的关于三元运算符的讨论。

背景

当前的语法支持基本得到了认可。在第十届 Python 大会上,Guido 在他的 DevDay 演讲中提到了有关装饰器的语法支持,虽然后来他说,那只是他“半开玩笑的”提出的几项扩展之一。在那次会议后不久,Michael Hudson 在 python-dev 上提出,括号语法最早是 Gareth McCaughan 在 comp.lang.python 上的提出的。

看起来下一步就是类装饰器了,因为函数的定义和类定义在语法上很相似。然而 Guido 对此持怀疑态度,几乎可以确定在 Python 2.4 中不会出现类装饰器。

讨论从2002年的2月一直持续到2004年的7月,人们发出成百上千封邮件给出可能的语法提议。Guido 把一份提议列表拿到 EuroPython 2004 大会上讨论。在这之后,他决定了我们要使用 Java 风格的 @ 装饰器语法。这最早出现在 2.4a2 版本中。Barry Warsaw 将这命名为“Pie-装饰器”,用来纪念发生在当时的 Pie-thon Parrot shootout(译者注:可能是指当时的Python vs Parrot challenge),同时 @ 看起来也有点像一个 Pie。在 Python-dev 会上 Guido 提到了他的想法,包括一些被拒绝的形式。

关于“装饰器(Decorator)”这个名字

关于把这个特性命名为“装饰器(decorator)”有很多抱怨。最主要的原因是装饰器这个名字与 GoF Book (译者注:GoF 即 Gang of Four:Erich Gamma, Richard Helm, Ralph Johnson,John Vlissides是软件设计领域的四位世界顶级大师.合著有《设计模式:可复用面向对象软件的基础》。来源:百度百科)中的意义不一致。装饰器这个名词可能更多的用在编译器领域——语法树的遍历和注解。应该会有比这更好的名字。

设计目标

新的语法应该做到:

  • 支持任意的包装,包括用户定义的可调用的对象及已经存在的内置方法classmethodstaticmethod。这意味着装饰器语法必须支持传递参数到包装结构。
  • 支持对一个定义多次包装。
  • 用途显而易见。至少对新用户而言,可放心的忽略它去编写自己的代码。
  • 作为一种学一次就可以记住的语法。
  • 不使以后的扩展更困难。
  • 容易输入。它将会在编程时经常被使用到。
  • 不使快速的代码扫描更困难。对搜索全部的定义,特定的定义及函数接收的参数应该仍然简单。
  • 不需要复杂的辅助工具支持,例如语言敏感的编辑器或其他的解析工具。
  • 允许以后的编译器对装饰器优化。随着 Python JIT 编译器即将到来,装饰器的语法应该在函数定义之前。
  • 从现在藏在函数尾部的位置,移到更靠前让你一眼就看到的位置。

在 Andrew Kuchling 的博客中有很多关于动机和用例的链接。尤其值得注意的是 Jim Huginin 的用例列表

现在的语法

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

1
2
3
4
@dec2
@dec1
def func(arg1, arg2, ...):
pass

它等同于:

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

没有给变量 func 赋值。装饰器靠近函数的声明。@标记清楚的表示出这里有一些新东西。

自下而上应用的顺序原则与通常函数应用的规则一致。在数学中,复合函数 (g o f)(x) 写为 g(f(x)),在 Python 中@g @f def foo() 也等同于 foo=g(f(foo)

装饰器语句并不能接收任意的语句。
下面这些是不被允许的:

1
2
3
4
> @foo().bar()
> @foo or bar
> @mydecorators['foo']
> @lambda f: foo(f) or bar(f)

这些看起来太复杂了,Guido 在直觉上不喜欢这样。
当前装饰器的语法允许装饰器的声明是一个可以返回装饰器的函数:

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

这等同于:

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

装饰器的声明可以是一个返回装饰器的函数,它的原理是@符号后面的部分可以看作是一个表达式(虽然被限制为只能是一个函数),并且表达式的返回是可调用的。可以参考declaration arguments

可选的语法

曾经有大量不同的语法提议,数量远远超出了可以逐个讨论它们的可能性。逐个分析讨论它们是不明智的,可取的方式是分部分讨论。

装饰器的位置

第一个语法点是装饰器的位置。下面的例子,我们用 2.4a2 中@语法。
装饰器在 def 语句之前是第一个可选项,并被用在 2.4a2 中:

1
2
3
4
5
6
7
8
@classmethod
def foo(arg1,arg2):
pass

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

对这个位置有很多反对意见。最主要的一个是,在 Python 中,前一行代码会影响后一行代码的情况之前从没有过。这个语法在 Python 2.4a3 要求每个装饰器独占一行(在 2.4a2 版中多个装饰器可以写在一行),在 2.4 的最终版本中,要求是每个装饰器独占一行。

人们还抱怨当同时使用多个装饰器的时候,语法看起来就不够优雅了。虽然如此,但在一个函数上使用大量装饰器的情况很少,因此也不必担心。

装饰器在方法体外这种形式的一些好处是:它们在函数定义时就执行了。

另一个好处是,在函数定义前的前缀有助于在理解代码本身之前了解代码语义上的改变。这样你在阅读代码的时候就可以顺畅的理解而不至于看到后面又改变了前面的想法。

Guido 青睐于把装饰器放在def之前,这样就不会因为当参数列表很长时,装饰器看起来不够醒目。

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

1
2
3
4
5
6
7
8
9
10
11
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

对这种形式也有几个反对观点。第一,这破坏了代码的易查找性(”greppability”)——你无法再搜索“def foo(”来查找函数定义了。第二点,更严重的是,使用多个装饰器时会让语法看起来很混乱。

另一种有很多支持者的形式是,把装饰器放在参数列表和def定义行尾的:之间:

1
2
3
4
5
def foo(arg1,arg2) @classmethod:
pass

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

Guido 总结了对这种形式的反对意见(有很多也同样适用于前一种):

  • 它隐藏了函数签名后的关键信息(例如:这是一个静态方法)
  • 当参数列表和装饰器都很多很长时容易被忽视
  • 当想要重用装饰器是很不方便复制和粘贴,因为它在一行中间的位置

接下来的一种形式是把装饰器放在函数体内部的起始位置,和现在文档注释的位置一样:

1
2
3
4
5
6
7
8
def foo(arg1,arg2):
@classmethod
pass

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

对此的主要反对意见是,检查装饰器还要去看函数的内部,另外,虽然装饰器在方法体内,当它并不会在方法运行时执行。Guido 认为 docstring 就是个不好的例子,而且文档注释装饰器甚至有可能把 docstring 移到方法体外面。

最后一种形式是用一个新的代码块包含原有代码。这个例子中使用“decorator”关键词,这个@语法比起来没什么意义。

1
2
3
4
5
6
7
8
9
10
decorate:
classmethod
def foo(arg1,arg2):
pass

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

这种形式会导致有装饰器的方法和没有装饰器的方法缩进不一致。另外,一个有装饰器的方法的方法体在第三级缩进才开始。

语法形式

  • @decorator
    1
    2
    3
    4
    5
    6
    7
    8
    @classmethod
    def foo(arg1,arg2):
    pass

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

对此语法的主要反对是@符号之前从没在 Python 中使用过(但在 IPython 和 Leo 中都用过),而且@符号本身没什么意义。另一个反对点是这浪费了一个未使用过的字符在一个并不认为是重要的功能上。

  • |decorator
    1
    2
    3
    4
    5
    6
    7
    8
    |classmethod
    def foo(arg1,arg2):
    pass

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

这是 @decorator 语法的一种变体。它的好处是不会影响到 IPython 和 Leo。和 @decorator 相比主要缺点是|符号看起来像大写字母 I 和小写字母 l。

  • list 语法
    1
    2
    3
    4
    5
    6
    7
    [classmethod]
    def foo(arg1,arg2):
    pass

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

对此的反对意见主要是 list 语法在此之前已经有相应的用途了。而且也缺乏指示这个语句是一个装饰器。

  • 使用其他括号的 list 语法 (<…>, [[…]], …)
    1
    2
    3
    4
    5
    6
    7
    <classmethod>
    def foo(arg1,arg2):
    pass

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

这些选项并没有获得太多的关注。和方括号相比,这些只是明显的表示这不是一个list。它们没有使代码解析更加简单。其中“<…>”这一选项还会因为已经有非成对出现的用途而带来代码解析上的问题。向右的尖括号更有可能是大于号而不是一个装饰器的结束符号。

  • decorate()
    (译者注:我对本小节还没有清楚的理解,不敢轻易翻译,以免误导读者)
    The decorate() proposal was that no new syntax be implemented – instead a magic function that used introspection to manipulate the following function. Both Jp Calderone and Philip Eby produced implementations of functions that did this. Guido was pretty firmly against this – with no new syntax, the magicness of a function like this is extremely high:

Using functions with “action-at-a-distance” through sys.settraceback may be okay for an obscure feature that can’t be had any other way yet doesn’t merit changes to the language, but that’s not the situation for decorators. The widely held view here is that decorators need to be added as a syntactic feature to avoid the problems with the postfix notation used in 2.2 and 2.3. Decorators are slated to be an important new language feature and their design needs to be forward-looking, not constrained by what can be implemented in 2.3.

  • 新的关键词(和代码块)
    这个想法是 comp.lang.python 中的一个公认选项(详情可以看下面的 社区意见 章节)。 Robert Brewer 写了一份详细的 J2 提案 阐述他青睐于此的理由。对于这种语法形式有一些问题是:
    • 需要一个新的关键词。那么就会需要from __future__ import decorators语句。
    • 关键词选择的争议。最终using被选了出来并用在提案和实现方案中。
    • 关键词/代码块的形式看起来就像一个普通的代码块,但并不是这样的。尝试在这个代码块中写语句会导致语法错误,这会让用户费解。

几天后 Guido 否决了这项提案,主要有两个理由。其一:

块语法强烈建议它的内容应该是一系列 statements,但实际上不是——只有 expressions 被允许。并且会默默的“收集”这些 expressions 直到它们可以被应用到后面的函数定义上。

其二:

以关键词起头的代码块会吸引太多的注意。这对 if, while,for,try,def 和 class 这些是应该的。但是关键词”using“或是任何其他作此用的关键词都不值得吸引那么多注意。重点应该放在装饰器本身,它们才是重要的。

读者请看详细回答

  • 其他形式
    还有各种各样的形式和提议在 wiki页

为什么用@

最初 Java 在 Javadoc comments 中使用@作为标识符,后来在 Java 1.5 中又用作为注解(annotations),和 Python 的装饰器很相似。实际上在 Python 之前的版本中从未使用过@作为标识符,这意味着使用@不会导致早期的 Python 代码产生奇怪的语义错误。也就是说,什么是装饰器,什么不是装饰器这种模糊的概念被消除了。这样说,@其实还是一个相当随意的选择,有建议使用|来代替。

一些提议用像列表一样的语法表示装饰器:
[|...|], *[...]*<...>

当前的实现,历史

Guido 要找一个志愿者实现他所倾向的语法,Mark Russell 向 SF 提交了一个补丁。
新的语法在 2.4a2 中可用了。

1
2
3
4
@dec2
@dec1
def func(arg1, arg2, ...):
pass

等同于:

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

而没有创建变量func的过程。

在 2.4a2 版本中的实现允许多个装饰器写在一行。到了 2.4a3 中做出了限制,装饰器必须每行一个。

Michael Hudson 实现的 list-after-def 的语法补丁也被剔除了。

在 2.4a2 发布之后,面对社区的反应,Guido表示,如果社区中可以提出一个公认的靠谱的提议和代码实现,他会重新审视这个提议的。后来在 Python wiki 中收到了大量的选项,一个社区公认的方案出现了。Guido 后来拒绝了这个方案,说道:

本周四要发布的 Python 2.4a3 将保持现在 CVS 中的一切。对于 Python 2.4b1,我在考虑用其他的字符替换@,虽然我认为用@是有好处的,它和 Java 中的@特性有些相似。对此还有些争议,它们也不完全相同,@在 Java 中用于不改变语义的属性,但是由于 Python 的动态特性使它的语法元素从来不与其他语言中相似的结构完全一致,而且有明显的重叠。对于第三方工具的影响:IPython 的作者表示不会有太大的影响。Leo 的作者也说 Leo 可以接受(虽然他和他的用户会有一些过渡的痛苦)。实际上我还是认为选择一个 Python 中已经使用过的字符会让外部工具更难适配,因为对此要做更加细致微妙的解析。坦白说我还没有决定,仍然有回旋的余地。我不想在语法上继续考虑下去了: the buck has to stop at some point, everyone has had their say, and the show must go on.

社区意见

这部分介绍被拒绝的 J2 语法,为了历史的完整性写在这里。

J2 语法在 comp.lang.python 上提出(”J2” 是它在 PythonDecorator wiki 页上的引用):在关键词def之前使用一个由using开头的代码块,例如:

1
2
3
4
5
using:
classmethod
synchronized(lock)
def func(cls):
pass

对此语法的争论主要归结于“可读性”原则:

  • 代码块好于多行@结构。使用using关键词将def声明转为多代码块的结构,类似于try/finally等。
  • 关键词好于新加一个标识符。关键词可以使用现有的标识符,没有必要新加一个标识符。关键词也可以将 Python 的装饰器与 Java 的注解和 .Net 中的属性区分开,它们明显是不同的东西。

Robert Brewer 为此写了一份详细的提议,Michael Sparks 开发了补丁

如之前提到的,Guido 拒绝了这种形式,在 python-dev 和 comp.lang.python 上发消息写明了他的问题。

实例

在 comp.lang.python 和 python-dev 上有很多讨论集中在优雅的使用装饰器就像使用内置的staticmethod()classmethod()一样简洁。装饰器远比这更强大,下面介绍一些:

  1. 定义一个函数在退出时执行。注意这个函数没有被“包含”在通常的场景中。
    1
    2
    3
    4
    5
    6
    7
    8
    def onexit(f):
    import atexit
    atexit.register(f)
    return f

    @onexit
    def func():
    ...

请注意这可能不适合真实的场景,只作为示例。

  1. 定义一个单例模式的类。注意,一旦类消失了,程序员需要去创建更多的实例。(来自 python-dev 上的 Shane Hathaway )

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def singleton(cls):
    instances = {}
    def getinstance():
    if cls not in instances:
    instances[cls] = cls()
    return instances[cls]
    return getinstance

    @singleton
    class MyClass:
    ...
  2. 给函数添加属性。(基于 python-dev 上 Anders Munch 提交的例子)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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):
    ...
  3. 强制函数的参数及返回类型。请注意这从旧函数向新函数拷贝了函数名属性(func_name)。
    func_name 在 Python 2.4a3 中被允许修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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
  4. 声明一个类实现指定接口。来自 python-dev 上 Bob Ippolito 基于 PyProtocols的经验的投稿。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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. 还不确定类装饰器是否会被作为一种特性加入到语言中。Guido 对此概念表示了怀疑。但是在 python-dev 上,人们都有支持他们自己观点的有力论据。在 Python 2.4 中很可能不会有类装饰器了。
  2. 在 Python 2.4b1 中会重新审视对字符@的选择。
    在最后,@还是被保留下来了。

我的总结

我最早接触 Python 是在2015年年初,那时的 Python 版本是 2.7.x,在整个 Python 的历史中已经是非常完备非常现代的一个版本了。在这次对 Python 装饰器起源的探索中,我浏览了一些年代久远的邮件列表,看到了前辈大神们讨论问题中的种种趣事,不禁产生崇拜与感激之情。感谢前辈们无私的付出。

在了解了 Python 装饰器的起源之后,我对这一特性也有了更深的理解。这也是我第一次了解到一项技术,一项特性从无到有的诞生过程。这其中有各位前辈大神的指引,也有遍布全球的热心开发者积极贡献建议,无数的想法与智慧在一起碰撞,融合。现在的成果都是在这一次次的取舍中演进,逐步完善。这真是令人感动。

最后,希望自己能保持好奇心,多思考,去探究事物的本质,工作原理,设计思路。本文中的翻译我尽力保证准确,但因个人的理解偏差与能力范围难免产生错误,如有发现,请务必向我指出以免对更多人产生误导。