PEP 318 – 函数和方法的装饰器
- 作者:
- Kevin D. Smith <Kevin.Smith at theMorgue.org>,Jim J. Jewett,Skip Montanaro,Anthony Baxter
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2003年6月5日
- Python 版本:
- 2.4
- 发布历史:
- 2003年6月9日,2003年6月10日,2004年2月27日,2004年3月23日,2004年8月30日,2004年9月2日
警告警告警告
本文档旨在描述装饰器语法以及导致这些决策的流程。它不试图涵盖大量潜在的替代语法,也不试图详尽列出每种形式的所有优缺点。
摘要
当前转换函数和方法(例如,将它们声明为类方法或静态方法)的方法很笨拙,可能导致难以理解的代码。理想情况下,这些转换应该在声明本身的代码的同一位置进行。此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中添加类装饰器。
为什么这如此困难?
自Python 2.2版本以来,Python中提供了两个装饰器(classmethod()和staticmethod())。从那时起,人们就一直认为最终会向语言中添加对它们的语法支持。鉴于此假设,人们可能会想知道为什么如此难以达成共识。在comp.lang.python和python-dev邮件列表中,关于如何最好地实现函数装饰器的讨论时断时续。没有一个明确的原因说明为什么会这样,但一些问题似乎最具争议。
- 关于“意图声明”属于何处的争论。几乎所有人都同意在函数定义结束时装饰/转换函数是次优的。除此之外,似乎没有明确的共识将此信息放置在哪里。
- 语法限制。Python是一种语法简单的语言,对什么可以做和不能做有相当严格的限制,而不会“搞砸”(无论是视觉上还是关于语言解析器)。没有明显的方法可以构造这些信息,以便新手会认为“哦,是的,我知道你在做什么。”似乎最好的办法是防止新用户对语法含义形成 wildly incorrect 的心理模型。
- 对概念的整体陌生。对于对代数(甚至基本算术)有一点了解或至少使用过一种其他编程语言的人来说,Python的很多内容都是直观的。很少有人在Python中遇到装饰器概念之前有过任何经验。没有强大的预先存在的模因来捕捉这个概念。
- 一般来说,语法讨论似乎比其他任何事情都更容易引起争议。读者可以参考与PEP 308相关的三元运算符讨论,作为另一个例子。
背景
普遍认为语法支持比现状更可取。Guido在他的第10届Python大会DevDay主题演讲中提到了装饰器的语法支持,尽管他后来表示这只是他“半开玩笑”提出的几个扩展之一。Michael Hudson在会议后不久在python-dev上提出了这个话题,将最初的括号语法归因于Gareth McCaughan之前在comp.lang.python上的一个提议。
类装饰看起来是显而易见的下一步,因为类定义和函数定义在语法上相似,但是Guido仍然不相信,并且类装饰几乎肯定不会出现在Python 2.4中。
讨论从2002年2月到2004年7月在python-dev上断断续续地进行。发布了数百数百篇文章,人们提出了许多可能的语法变体。Guido带着一份提案列表参加了EuroPython 2004,在那里进行了讨论。此后,他决定我们将采用Java风格的@装饰器语法,这首次出现在2.4a2中。Barry Warsaw将此命名为“pie-decorator”语法,以纪念在装饰器语法同时发生的Pie-thon Parrot枪战,并且因为@看起来有点像一个馅饼。Guido在Python-dev上概述了他的理由,包括关于一些(许多)被拒绝的形式的这篇文章。
关于名称“装饰器”
关于此功能选择名称“装饰器”的抱怨很多。主要原因是该名称与其在GoF书中的用法不一致。“装饰器”这个名称可能更多地源于它在编译器领域的使用——语法树被遍历和注释。很有可能出现更好的名称。
设计目标
新语法应该
- 适用于任意包装器,包括用户定义的可调用对象和现有的内置函数
classmethod()和staticmethod()。此要求还意味着装饰器语法必须支持向包装器构造函数传递参数 - 每个定义使用多个包装器
- 使其发生的事情显而易见;至少,新用户在编写自己的代码时可以安全地忽略它应该很明显
- 是一个“……一旦解释就容易记住”的语法
- 不使未来的扩展更加困难
- 易于输入;使用它的程序预计会非常频繁地使用它
- 不使其更难以快速浏览代码。仍然应该易于搜索所有定义、特定定义或函数接受的参数
- 不会不必要地使次要支持工具(例如语言敏感编辑器和其他“玩具解析器工具”)复杂化
- 允许未来的编译器优化装饰器。随着Python JIT编译器在某个时候出现的希望,这往往要求装饰器语法出现在函数定义之前
- 从函数的末尾(目前它被隐藏在那里)移到前面,在那里它更引人注目
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。
装饰器位置
第一个语法点是装饰器的位置。对于以下示例,我们使用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决定他更喜欢将装饰器放在“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
对此形式有几个反对意见。首先是它会破坏源代码的易于“grep”性——您不能再搜索“def foo(”并找到函数的定义。第二个更严重的反对意见是,在有多个装饰器的情况下,语法会非常笨拙。
下一种形式,有许多强烈支持者,是将装饰器放在参数列表和“def”行中的尾随:之间
def foo(arg1,arg2) @classmethod:
pass
def bar(low,high) @accepts(int,int),@returns(float):
pass
Guido 总结了反对这种形式的论点(其中许多也适用于前一种形式),如下所示
- 它将关键信息(例如,它是静态方法)隐藏在签名之后,容易被忽略
- 很容易错过长参数列表和长装饰器列表之间的过渡
- 剪切和粘贴装饰器列表以供重用很麻烦,因为它开始和结束在行中间
下一种形式是装饰器语法在方法体内部开头,与文档字符串现在所在的位置相同
def foo(arg1,arg2):
@classmethod
pass
def bar(low,high):
@accepts(int,int)
@returns(float)
pass
反对这种形式的主要理由是它需要“窥视”方法体才能确定装饰器。此外,即使代码在方法体内部,它也不会在方法运行时执行。Guido认为文档字符串不是一个好的反例,而且很有可能“文档字符串”装饰器可以帮助将文档字符串移到函数体之外。
最后一种形式是一个包围方法代码的新块。对于这个例子,我们将使用“decorate”关键字,因为它与@语法无关。
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
这是@装饰器语法的一个变体——它的优点是它不会破坏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()decorate()提案是*不*实现新语法——而是使用一个魔术函数,通过自省来操作后续函数。Jp Calderone和Philip Eby都实现了这样的函数。Guido对此非常坚决地反对——如果没有新语法,这样的函数的魔术性将非常高通过sys.settraceback使用具有“远距离作用”的函数可能对于一个无法通过其他方式获得但又不需要修改语言的晦涩功能来说是可以的,但这不适用于装饰器。这里普遍的观点是,装饰器需要作为一种语法特性添加,以避免2.2和2.3中使用的后缀表示法的问题。装饰器注定是一个重要的新的语言特性,其设计需要具有前瞻性,而不是受限于2.3中可以实现的功能。- 新关键字(和块)
这个想法是comp.lang.python的共识替代方案(更多内容见下面的社区共识)。Robert Brewer撰写了一份详细的J2提案文档,概述了支持这种形式的论点。这种形式的最初问题是
- 它需要一个新关键字,因此需要一个
from __future__ import decorators语句。 - 关键字的选择有争议。然而,
using成为了共识选择,并用于提案和实现中。 - 关键字/块形式产生了一个看起来像普通代码块的东西,但实际上不是。尝试在此块中使用语句会导致语法错误,这可能会使用户感到困惑。
几天后,Guido 拒绝了该提案,主要基于两点,首先是
...缩进块的语法形式强烈暗示其内容应该是一系列语句,但实际上并非如此——只允许表达式,并且正在对这些表达式进行隐式“收集”,直到它们可以应用于随后的函数定义为止。...其次
...块开头的关键字会引起很多关注。对于“if”、“while”、“for”、“try”、“def”和“class”都是如此。但是“using”关键字(或任何其他代替它的关键字)不值得这种关注;重点应该放在套件内部的装饰器或装饰器上,因为它们是对后续函数定义的重要修饰符。...邀请读者阅读完整的回复。
- 它需要一个新关键字,因此需要一个
- 其他形式
wiki页面上还有很多其他变体和提案。
为什么是@?
Java在Javadoc注释中最初使用@作为标记,后来在Java 1.5中用于注解,这与Python装饰器类似,有一些历史渊源。@以前未在Python中用作标记的事实也意味着此类代码不可能被早期版本的Python解析,从而可能导致微妙的语义错误。这也意味着消除了什么是装饰器和什么不是装饰器的歧义。尽管如此,@仍然是一个相当任意的选择。有人建议使用|代替。
对于使用列表式语法(无论出现在何处)指定装饰器的语法选项,提出了几种替代方案:[|...|]、*[...]*和<...>。
当前实现,历史
Guido要求志愿者实现他偏爱的语法,Mark Russell挺身而出,向SF提交了一个补丁。这种新语法在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之前的一个实现了def后列表语法的补丁也仍然存在。
2.4a2发布后,为了回应社区的反应,Guido表示,如果社区能够提出社区共识、一份不错的提案和一份实现,他将重新审视社区提案。在令人惊讶的大量帖子之后,在Python wiki中收集了大量的替代方案后,社区共识出现了(如下)。Guido随后拒绝了这种替代形式,但补充说
在Python 2.4a3(本周四发布)中,一切保持与CVS中相同。对于2.4b1,我将考虑将@更改为其他单个字符,尽管我认为@的优点是它与Java中类似功能使用的字符相同。有人争论说它不完全相同,因为Java中的@用于不改变语义的属性。但Python的动态特性使得它的语法元素与T其他语言中的类似结构永远不会完全相同,并且确实存在显著的重叠。关于对第三方工具的影响:IPython的作者认为不会有太大影响;Leo的作者表示Leo会继续存在(尽管会给他和他的用户带来一些过渡性痛苦)。我实际上预计,选择一个Python语法中已在其他地方使用的字符可能更难让外部工具适应,因为在这种情况下解析将不得不更微妙。但我坦率地说还没有决定,所以这里还有一些回旋余地。我目前不想考虑进一步的语法替代方案:事情总要有个了断,每个人都发表了自己的看法,并且演出必须继续。
社区共识
本节记录了被拒绝的J2语法,并为了历史完整性而包含在内。
在comp.lang.python上出现的共识是提议的J2语法(“J2”是它在PythonDecorators wiki页面上的引用方式):在新关键字using前缀的def语句之前的装饰器块。例如
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()内置函数更简洁的方式。此功能比这强大得多。本节提供了一些使用示例。
- 定义一个在退出时执行的函数。请注意,该函数实际上并未按通常意义进行“包装”。
def onexit(f): import atexit atexit.register(f) return f @onexit def func(): ...
请注意,此示例可能不适用于实际使用,仅用于示例目的。
- 使用单例实例定义一个类。请注意,一旦类消失,有进取心的程序员将不得不更有创意地创建更多实例。(来自Shane Hathaway在
python-dev上的帖子。)def singleton(cls): instances = {} def getinstance(): if cls not in instances: instances[cls] = cls() return instances[cls] return getinstance @singleton class MyClass: ...
- 向函数添加属性。(基于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): ...
- 强制执行函数参数和返回类型。请注意,这将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
- 声明一个类实现一个特定的接口(或一组接口)。这是Bob Ippolito在
python-dev上发布的一篇帖子,基于与PyProtocols的经验。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..."""
当然,所有这些示例在今天都是可能实现的,尽管没有语法支持。
(不再)未解决的问题
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0318.rst