WeeklyPEP-3-PEP 318-函数装饰器-overview

2024-02-26 09:09:15 浏览数 (1)

前言

本文的主体内容大部分来自对 PEP 318 原文的翻译,剩余部分是本人对原文的理解,在整理过程中我没有刻意地区分二者,这两部分被糅杂在一起形成了本文。因此请不要带着「本文的内容是百分百正确」的想法阅读。如果文中的某些内容让你产生疑惑,你可以给我留言与我讨论或者对比 PEP 318 的原文加以确认。

注:PEP 318 创建于 2003-06-05,Python 2.4

警告和摘要

本文档的主要目的是描述装饰器语法和做出相关决策过程。它既不试图涵盖全部潜在的替代语法,也不试图详尽地罗列出每种语法的优缺点。

当前(Python 2.4 之前)转换一个函数或方法(例如将它们定义为一个类方法或静态方法)的方案很笨拙,并且可能会导致降低代码的可读性。理想情况下,这类转换应该与函数或方法的定义同步进行。本 PEP 为函数或方法实现这类转换引入了全新的语法。

动机

当前(Python 2.4 之前)实现一个函数或方法转换的方案是将转换定义在函数声明的后面。 对于一些大型函数来说,这样做会让函数行为的关键部分与函数外部内容形成割裂感,例如:

代码语言:javascript复制
def foo(self):
	# perform method operation
	pass
foo = classmethod(foo)

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

这种方案不仅使得那些长函数的可读性变差,还会使得一个单一的概念存在多次声明,很不 pythonic。一个解决此问题的方案是让函数转换贴近函数自身的声明。新语法的意图就是将装饰器放在函数声明中以替代现有方案:

代码语言:javascript复制
@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 邮件列表中一直不断,主要的分歧集中在以下几个问题上:

  1. 声明位置:几乎所有人都同意,在函数主体声明之后进行转换是不理想的,但具体应该放在哪里并没有形成共识。
  2. 语法:Python 是一个语法十分简明的编程语言,为了保持这种简明(无论是体现在直觉上还是具体实现上)它对什么可以做和什么不能做都有相当严格的限制。没有一种特别合适的方法能够让第一次接触这个概念的使用者在看到这种语法时就能快速理解这个语法代表了什么。似乎最好的方案就是能防止初学者对这种语法产生错误的第一印象。
  3. 对概念不熟悉:对于那些对代数(甚至基础算数)有一定了解或至少使用过一种其他编程语言的人来说,Python 中大部分语法和概念都是符合直觉的。但是在使用 Python 装饰器之前,很少有人对「装饰器」这个概念有了解,也没有一个较好的类比对象来帮助人们快速的理解。

语法往往比其他任何事情都容易引起更多的争论,[PEP 308] 中与三元运算符语法相关的讨论是另一个例子。

背景

人们普遍认为,以当前的状态,为装饰器提供语法支持是可取的。Guido 也在第十届 Python 大会的 DevDay 主题演讲中提到了对装饰器的语法支持,尽管 他后来说 这只是他在那里半开玩笑地提出的几个拓展之一。在会议结束不久之后,Michael Hudson 在 python-dev 上发布了这个 主题,并将最初的方括号语法归因于 Gareth McCaughan 在 comp.lang.python 上的 早期提案。

类装饰器似乎会顺理成章的成为下一个目标,因为类的定义和函数的定义在语法上是相似的,但 Guido 任然保持怀疑,因此类装饰器几乎可以确认不会在 Python 2.4 中出现。

关于装饰器这个名字

有很多人抱怨为这个特性选择「装饰器」这个名字。其中最主要的原因是这个名字与 GoF 书(设计模式:可复用面向对象软件的基础)中所阐述的概念并不一致。选择「装饰器」这个名字更多的是由于它在编译器领域的使用——语法树被遍历和注释。很可能会出现一个更好的名字(目前看来并没有)。

设计目标

注:译者猜测在设计时还没有明确装饰器这个概念所以原文使用 wrapper 来表示被设计的主体(也就是装饰器)。

新语法应该:

  1. 能够适应任何使用场景,包括使用者定义的可调用对象和内置的 classmethod() 以及 staticmethod()。这项需求同时意味着必须能够向 wrapper constructor 传递参数;
  2. 允许在一个定义中使用多个 wrappers;
  3. 能够明显的表现出它的作用,至少要做到明显,并且初学者在编写自己的代码时可以放心的忽略它;
  4. 是一种经过讲解后很容易就记住的语法;
  5. 拥有较好的拓展性;
  6. 容易使用,在需要使用的地方可以频繁的使用;
  7. 不能削弱代码的可读性,让函数的定义保持简明;
  8. 不会不必要地增加辅助工具(例如语言敏感的编辑器或其他解析器工具)的复杂性;
  9. 允许将来的编译器为装饰器进行优化,由于 Python 的 JIT 编译器有希望在某个时间实现,这就需要装饰器的语法出现在函数声明之前;
  10. 从函数声明的尾部移动到头部。

Andrew Kuchling 在他的博客(已经无法访问)中有一些关于动机和用例的讨论的链接,特别值得注意的是 Jim Huginin 的用例列表。

当前语法

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

代码语言:javascript复制
@dec2
@dec1
def func(arg1, arg2, ...):
	pass

这相当于:

代码语言:javascript复制
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 喜欢这样,因为更符合直觉。

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

代码语言:javascript复制
@decomaker(argA, argB, ...)
def func(arg1, arg2, ...):
	pass

这相当于:

代码语言:javascript复制
func = decomaker(argA, argB, ...)(func)

这个语法生效的逻辑是将 @ 符号后面的内容视作一个表达式(语法上被限制为:只能是一个函数),并且无论该表达式返回什么都会被调用。

语法方案讨论

目前已经提出了大量不同的语法,与其试图逐一讨论这些语法,不如将「语法讨论」分成几个方面。试图对每种可能的语法进行讨论是一种疯狂的行为,并且会产生一个非常臃肿的 PEP。

装饰器位置

第一个值得讨论的语法问题是:装饰器的位置。下面的代码示例中会使用 Python 2.4a2 中的最终确定的 @ 符号作为装饰器符号。

def 语句上面
代码语言:javascript复制
@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 和 函数名之间或函数名和参数列表之间
代码语言:javascript复制
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使用多个装饰器的情况下语法会显得及其笨重。

函数声明尾部的 : 之前
代码语言:javascript复制
def foo(arg1, arg2) @classmethod:
	pass

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

Gudio 总结了反对这个方案的几种论点(其中很多也适用于前一种形式):

  1. 它在签名之后隐藏了关键信息(例如,它是一个静态方法),这很容易被遗漏;
  2. 很容易遗忘长参数和长装饰器列表之间的过渡;
  3. 使用剪切和粘贴来重用装饰器列表变得很麻烦,因为它在一行的中间开始和结束;
与 docstring 当前所在位置相同
代码语言:javascript复制
def foo(arg1, arg2):
	@classmethod
	pass

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

这种形式的主要缺点是,它需要“窥视”函数内部才能确定装饰器。此外这些位于函数内部的内容,在运行时也不会执行。Gudio 认为 docstring 不是一个很好的反例,并且使用 docstring 来放置装饰器很有可能会使得最终不得不把文档字符串移动到函数声明外部。

创建一个新的代码块
代码语言:javascript复制
decorate:
	classmethod
	def foo(arg1, arg2):
		pass

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

这种形式会导致使用装饰器函数和没使用装饰器的函数的缩进不一致,另外被装饰的函数的声明需要写在第三层缩进。

语法形式

@decorator
代码语言:javascript复制
@classmethod
def foo(arg1, arg2):
	pass

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

反对这种语法的主要理由是 @ 符号从未在 Python 中使用过(但是在 IPython 和 Leo 中都有使用),并且 @ 符号没有意义。另外一种反对意见是,这种方案浪费了一种从未使用的字符(一个有限的集合),这些字符应该被用在更重要的场合。

|decorator
代码语言:javascript复制
|classmethod
def foo(arg1,arg2):
    pass

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

这是 @decorator 的变体,它的优点是不会破坏 IPython 和 Leo,主要缺点是符号 | 看起来既像大写的 I 又像小写的 l

列表语法
代码语言:javascript复制
[classmethod]
def foo(arg1,arg2):
    pass

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

列表语法最重要的缺点是它在 Python 中是有具体含义的,其次它也不能很好地表明该表达式是一个装饰器。

使用其他符号的列表语法
代码语言:javascript复制
<classmethod>
def foo(arg1,arg2):
    pass

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

这些替代方案都没有获得太多支持。使用双方括号的替代方案只是为了表明这是一个装饰器不是一个列表,并没有使解析变得更容易。尖括号的替代方案也存在解析问题,因为 <> 都有独立的含义,对于装饰器来说 > 可能是一个大于号而不是装饰器定义的关闭符号。

decorate()

decorate() 的方案是不实现新的语法,而是实现一个能够使用内省来控制其后面紧跟的函数的内置函数。Jp Calderone 和 Philip Eby 都实现了这样的函数。Gudio 非常坚决地反对这样(不使用新的语法)做,这种方案带来了极大的不确定性。

新的关键字(和块)

这个想法是 comp.lang.python 的共识替代方案,在下面的 [社区共识](# 社区共识) 中有更多关于这一点的内容。Robert Brewer 写了一份详细的 J2 提案文件(无法访问),概述了支持这种形式的论点。初始问题是:

  1. 需要一个新的关键字,因此需要一个 from __future__ import decorators 语句。
  2. 关键词的选择是有争议的。然而, using 作为共识选择出现,并在提案和实现中使用。
  3. 关键字/块形式产生的东西看起来像一个正常的代码块,但不是。尝试在此块中使用语句将导致语法错误,这可能会使使用者感到困惑。

几天后,Guido 基于 两个主要理由 拒绝了这项提议。

其他形式

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

为什么使用 @

在 Java 的历史中,@ 最初在 Javadoc comments 中使用被作为标记,后来在 Java 1.5 中用于 annotations,类似于 Python 装饰器。在此之前,@ 从未在 Python 中用作标记,这样的代码不能被早期的 Python 版本解析,可能会导致微妙的语义错误。这也意味着什么是装饰器,什么不是的模糊性被消除了。也就是说,@ 仍然是一个相当随意的选择。有些人建议使用 |

后记

在原文中还有两部分分别描述了最终实施的过程和一些示例,这里我就不展示了,感兴趣的可以自行翻阅原文。

参考

  1. PEP 318 – Decorators for Functions and Methods
  2. PEP 20 – The Zen of Python

0 人点赞