本文的主体内容大部分来自对 PEP 318 原文的翻译,剩余部分是本人对原文的理解,在整理过程中我没有刻意地区分二者,这两部分被糅杂在一起形成了本文。因此请不要带着「本文的内容是百分百正确」的想法阅读。如果文中的某些内容让你产生疑惑,你可以给我留言与我讨论或者对比 PEP 318 的原文加以确认。
注:PEP 318 创建于 2003-06-05,Python 2.4
本文档的主要目的是描述装饰器语法和做出相关决策过程。它既不试图涵盖全部潜在的替代语法,也不试图详尽地罗列出每种语法的优缺点。
当前(Python 2.4 之前)转换一个函数或方法(例如将它们定义为一个类方法或静态方法)的方案很笨拙,并且可能会导致降低代码的可读性。理想情况下,这类转换应该与函数或方法的定义同步进行。本 PEP 为函数或方法实现这类转换引入了全新的语法。
当前(Python 2.4 之前)实现一个函数或方法转换的方案是将转换定义在函数声明的后面。 对于一些大型函数来说,这样做会让函数行为的关键部分与函数外部内容形成割裂感,例如:
def foo(self):
# perform method operation
pass
foo = classmethod(foo)
def bar(cls):
pass
bar = synchronized(lock)(foo)
bar = classmethod(bar)
这种方案不仅使得那些长函数的可读性变差,还会使得一个单一的概念存在多次声明,很不 pythonic。一个解决此问题的方案是让函数转换贴近函数自身的声明。新语法的意图就是将装饰器放在函数声明中以替代现有方案:
@classmethod
@synchronized(lock)
def bar(cls):
pass
以这种形式修改类是完全可行的,尽管这样做的受益并没有那么明显。当然,任何可以使用类装饰器完成的事情都可以使用元类完成。但是使用元类是一种高阶的方案,所以「能以一种更简洁明了的方式对类进行简单修改」是有吸引力的。Python 2.4 中仅添加了函数/方法装饰器。
PEP 3129 建议从 Python 2.6 开始添加类装饰器。
在 Python 2.2 之后就有两个装饰器(classmethod()
和 staticmethod()
)可以被使用。差不多从这时起,大家便认为 Python 最终会在语言层面为它们添加语法上的支持。也许你会好奇,为什么达成最终的共识如此困难(从 Python 2.2 到 Python 2.4)。函数装饰器最佳实现方案相关的讨论在 comp.lang.python 和 python-dev 邮件列表中一直不断,主要的分歧集中在以下几个问题上:
语法往往比其他任何事情都容易引起更多的争论,[PEP 308] 中与三元运算符语法相关的讨论是另一个例子。
人们普遍认为,以当前的状态,为装饰器提供语法支持是可取的。Guido 也在第十届 Python 大会的 DevDay 主题演讲中提到了对装饰器的语法支持,尽管 他后来说 这只是他在那里半开玩笑地提出的几个拓展之一。在会议结束不久之后,Michael Hudson 在 python-dev 上发布了这个 主题,并将最初的方括号语法归因于 Gareth McCaughan 在 comp.lang.python 上的 早期提案。
类装饰器似乎会顺理成章的成为下一个目标,因为类的定义和函数的定义在语法上是相似的,但 Guido 任然保持怀疑,因此类装饰器几乎可以确认不会在 Python 2.4 中出现。
有很多人抱怨为这个特性选择「装饰器」这个名字。其中最主要的原因是这个名字与 GoF 书(设计模式:可复用面向对象软件的基础)中所阐述的概念并不一致。选择「装饰器」这个名字更多的是由于它在编译器领域的使用——语法树被遍历和注释。很可能会出现一个更好的名字(目前看来并没有)。
注:译者猜测在设计时还没有明确装饰器这个概念所以原文使用 wrapper 来表示被设计的主体(也就是装饰器)。
新语法应该:
classmethod()
以及 staticmethod()
。这项需求同时意味着必须能够向 wrapper constructor 传递参数;Andrew Kuchling 在他的博客(已经无法访问)中有一些关于动机和用例的讨论的链接,特别值得注意的是 Jim Huginin 的用例列表。
在 Python 2.4a2 中实现的函数装饰器的语法是:
@dec2
@dec1
def func(arg1, arg2, ...):
pass
这相当于:
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))
。
装饰器语句所能接受的内容是有限的(任何表达式都不起作用)。Guido 喜欢这样,因为更符合直觉。
当前语法还允许装饰器声明调用一个返回装饰器的函数:
@decomaker(argA, argB, ...)
def func(arg1, arg2, ...):
pass
这相当于:
func = decomaker(argA, argB, ...)(func)
这个语法生效的逻辑是将 @
符号后面的内容视作一个表达式(语法上被限制为:只能是一个函数),并且无论该表达式返回什么都会被调用。
目前已经提出了大量不同的语法,与其试图逐一讨论这些语法,不如将「语法讨论」分成几个方面。试图对每种可能的语法进行讨论是一种疯狂的行为,并且会产生一个非常臃肿的 PEP。
第一个值得讨论的语法问题是:装饰器的位置。下面的代码示例中会使用 Python 2.4a2 中的最终确定的 @
符号作为装饰器符号。
@clasmethod
def foo(arg1, arg2):
pass
@accepts(int, int)
@returns(float)
def bar(low, high):
pass
人们对这种方案有一些反对意见,其中最主要的是:这是(当时) Python 中第一例某行代码会对下一行代码产生影响的案例。在 2.4a3 版本中要求每行一个装饰器(在 2.4a2 版本中,可以在同一行指定多个装饰器),而 2.4final 的最终决定是每行一个装饰器。也有人抱怨说这种语法会是的在使用多个装饰器时变得笨重。不过有人指出,在一个函数上使用大量装饰器的可能性很小,因此这不是一个大问题。
这种方案的优点是装饰器位于函数声明外部,这使得人们能够直观地理解装饰器会在定义函数时执行。另一个优点是,在函数定义上添加前缀符合在代码本身之前了解代码语义变化的要求。使用者可以正确并快速地理解代码的语义,而不必在阅读代码时反复查看上下文。
Gudio 也更偏向于将装饰器定义在 def 的上一行,因为长的参数列表意味着装饰器可能被忽略。
def @classmethod foo(arg1, arg2):
pass
def @accept(int, int),@returns(float) bar(low, high):
pass
def foo @classmethod (arg1, arg2):
pass
def bar @accept(int, int),@return(float) (low, high):
pass
这个方案也一些反对意见。首先,它很容易破坏源代码的「可重命名性」,你不再能通过搜索 def foo(
并找到函数定义。第二个更严重的反对意见是,在s使用多个装饰器的情况下语法会显得及其笨重。
:
之前def foo(arg1, arg2) @classmethod:
pass
def bar(low, high) @accepts(int, int),@returns(float):
pass
Gudio 总结了反对这个方案的几种论点(其中很多也适用于前一种形式):
def foo(arg1, arg2):
@classmethod
pass
def bar(low, high):
@accepts(int, int)
@returns(float)
pass
这种形式的主要缺点是,它需要“窥视”函数内部才能确定装饰器。此外这些位于函数内部的内容,在运行时也不会执行。Gudio 认为 docstring 不是一个很好的反例,并且使用 docstring 来放置装饰器很有可能会使得最终不得不把文档字符串移动到函数声明外部。
decorate:
classmethod
def foo(arg1, arg2):
pass
decorate:
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
反对这种语法的主要理由是 @
符号从未在 Python 中使用过(但是在 IPython 和 Leo 中都有使用),并且 @
符号没有意义。另外一种反对意见是,这种方案浪费了一种从未使用的字符(一个有限的集合),这些字符应该被用在更重要的场合。
|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
列表语法最重要的缺点是它在 Python 中是有具体含义的,其次它也不能很好地表明该表达式是一个装饰器。
<classmethod>
def foo(arg1,arg2):
pass
<accepts(int,int), returns(float)>
def bar(low,high):
pass
这些替代方案都没有获得太多支持。使用双方括号的替代方案只是为了表明这是一个装饰器不是一个列表,并没有使解析变得更容易。尖括号的替代方案也存在解析问题,因为 <
和 >
都有独立的含义,对于装饰器来说 >
可能是一个大于号而不是装饰器定义的关闭符号。
decorate()
的方案是不实现新的语法,而是实现一个能够使用内省来控制其后面紧跟的函数的内置函数。Jp Calderone 和 Philip Eby 都实现了这样的函数。Gudio 非常坚决地反对这样(不使用新的语法)做,这种方案带来了极大的不确定性。
这个想法是 comp.lang.python 的共识替代方案,在下面的 [社区共识](# 社区共识) 中有更多关于这一点的内容。Robert Brewer 写了一份详细的 J2 提案文件(无法访问),概述了支持这种形式的论点。初始问题是:
from __future__ import decorators
语句。using
作为共识选择出现,并在提案和实现中使用。几天后,Guido 基于 两个主要理由 拒绝了这项提议。
在 维基页面 上还有很多其他的变体和提案。
在 Java 的历史中,@
最初在 Javadoc comments 中使用被作为标记,后来在 Java 1.5 中用于 annotations,类似于 Python 装饰器。在此之前,@
从未在 Python 中用作标记,这样的代码不能被早期的 Python 版本解析,可能会导致微妙的语义错误。这也意味着什么是装饰器,什么不是的模糊性被消除了。也就是说,@
仍然是一个相当随意的选择。有些人建议使用 |
。
在原文中还有两部分分别描述了最终实施的过程和一些示例,这里我就不展示了,感兴趣的可以自行翻阅原文。