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

Python增强提案

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版本以来,两个装饰器(classmethod()staticmethod())一直可用。从大约那个时候起,人们就假设最终会向语言添加一些对它们的语法支持。鉴于此假设,人们可能会想知道为什么达成共识如此困难。关于如何在comp.lang.python和python-dev邮件列表中以最佳方式实现函数装饰器的讨论时断时续地进行。没有一个明确的原因可以解释为什么会这样,但一些问题似乎最具争议性。

  • 关于“意图声明”属于哪个位置存在分歧。几乎每个人都同意,在函数定义结束时装饰/转换函数不是最佳做法。除此之外,似乎没有明确的共识在哪里放置此信息。
  • 语法约束。Python是一种语法简单的语言,对可以在不“搞砸”的情况下(从视觉上和语言解析器的角度来看)可以做什么和不能做什么有相当强的约束。没有明显的方法来构建此信息,以便对该概念不熟悉的人会认为,“哦,是的,我知道你在做什么。” 最好的方法似乎是防止新用户创建对语法含义的完全错误的心理模型。
  • 对该概念的整体不熟悉。对于那些对代数(甚至基本算术)有粗略了解或至少使用过其他编程语言的人来说,Python的大部分内容都是直观的。很少有人在 Python 中遇到装饰器概念之前有任何相关经验。根本没有强大的预先存在的模因来捕捉这个概念。
  • 总的来说,语法讨论似乎比几乎任何其他事情都引起更多争议。读者可以参考与PEP 308相关的三元运算符讨论,以了解另一个例子。

背景

人们普遍认为,语法支持是当前状态下可取的。Guido在他的DevDay主题演讲中提到了第10届Python大会上的装饰器的语法支持,尽管他后来表示,这只是他在那里“半开玩笑地”提出的几个扩展之一。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-thon Parrot shootout,以及因为@看起来有点像派。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

这种形式会导致已装饰方法和未装饰方法的缩进不一致。此外,已装饰方法的主体将从缩进级别 3 开始。

语法形式

  • @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()

    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”关键字(或任何其他替代它的关键字)不值得关注;重点应该放在套件内的装饰器或装饰器上,因为这些装饰器是对后续函数定义的重要修饰符。…

    欢迎读者阅读完整的回复

  • 其他形式

    维基页面上还有很多其他变体和提案。

为什么用@?

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 的动态特性使得其语法元素与其他语言中的类似构造永远不会完全相同,并且肯定存在明显的重叠。关于对第三方工具的影响: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.pythonpython-dev邮件列表上的许多讨论都集中在使用装饰器作为更简洁的方式来使用staticmethod()classmethod()内置函数。这种能力远不止于此。本节提供了一些使用示例。

  1. 定义一个在退出时执行的函数。请注意,该函数实际上并没有以通常的意义被“包装”。
    def onexit(f):
        import atexit
        atexit.register(f)
        return f
    
    @onexit
    def func():
        ...
    

    请注意,此示例可能不适用于实际使用,仅供示例说明。

  2. 定义一个具有单例实例的类。请注意,一旦类消失,有进取心的程序员将不得不更具创造力才能创建更多实例。(来自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:
        ...
    
  3. 向函数添加属性。(基于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):
        ...
    
  4. 强制执行函数参数和返回值类型。请注意,这会将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
    
  5. 声明一个类实现了特定(组)接口。这来自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..."""
    

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

(不再是)开放问题

  1. 目前尚不确定类装饰器是否会在将来的某个时间点被纳入语言中。Guido对这个概念表示怀疑,但一些人在python-dev上提出了一些强有力的论点(搜索PEP 318 -- posting draft)来支持它们。类装饰器极不可能出现在Python 2.4中。

    PEP 3129 提案自Python 2.6开始添加类装饰器。

  2. 在Python 2.4b1之前,将重新审查@字符的选择。

    最终,@字符被保留了下来。


来源:https://github.com/python/peps/blob/main/peps/pep-0318.rst

上次修改时间:2023-09-09 17:39:29 GMT