《Python Cookbook(第 3 版)中文版》(David Beazley 和 Brian K. Jones 著)的第 9 章“元编程”有几个诀窍构建了基本的装饰器和特别复杂的装饰器。其中,“9.6 定义一个能接收可选参数的装饰器”一节中的装饰器可以作为常规的装饰器调用,也可以作为装饰器工厂函数调用,例如 @clock 或 @clock()。
Graham Dumpleton 写了一系列博客文章(https://github.com/GrahamDumpleton/wrapt/blob/develop/blog/README.md),深入剖析了如何实现行为良好的装饰器,第一篇是“How You Implemented Your Python Decorator is Wrong”(https://github.com/GrahamDumpleton/wrapt/blob/develop/blog/01-how-you-implemented-your-python-decorator-is-wrong.md)。他在这方面的深厚知识充分体现在在他编写的 wrapt 模块(http://wrapt.readthedocs.org/en/latest/)中。这个模块的作用是简化装饰器和动态函数包装器的实现,即使多层装饰也支持内省,而且行为正确,既可以应用到方法上,也可以作为描述符使用。(描述符在本书第 20 章讨论。)
Michele Simionato 开发了一个包,根据文档,它旨在“简化普通程序员使用装饰器的方式,并且通过各种复杂的示例推广装饰器”。这个包是 decorator(https://pypi.python.org/pypi/decorator),可通过 PyPI 安装。
Python Decorator Library 维基页面(https://wiki.python.org/moin/PythonDecoratorLibrary)在 Python 刚添加装饰器这个特性时就创建了,里面有很多示例。由于那个页面是几年前开始编写的,有些技术已经过时了,不过仍是很棒的灵感来源。
PEP 443(http://www.python.org/dev/peps/pep-0443/)对单分派泛函数的基本原理和细节做了说明。Guido van Rossum 很久以前(2005 年 3 月)写的一篇博客文章“Five-Minute Multimethods in Python”(http://www.artima.com/weblogs/viewpost.jsp?thread=101605)详细说明了如何使用装饰器实现泛函数(也叫多方法)。他给出的代码支持多分派(即根据多个定位参数进行分派)。Guido 写的多方法代码很棒,但那只是教学示例。如果想使用现代的技术实现多分派泛函数,并支持在生产环境中使用,可以用 Martijn Faassen 开发的 Reg(http://reg.readthedocs.io/en/latest/)。Martijn 还是模型驱动型 REST 式 Web 框架 Morepath(http://morepath.readthedocs.org/en/latest/)的开发者。
Fredrik Lundh 写的一篇短文“Closures in Python”(http://effbot.org/zone/closure.htm)解说了闭包这个术语。
“PEP 3104—Access to Names in Outer Scopes”(http://www.python.org/dev/peps/pep-3104/)说明了引入 nonlocal 声明的原因:重新绑定既不在本地作用域中也不在全局作用域中的名称。这份 PEP 还概述了其他动态语言(Perl、Ruby、JavaScript,等等)解决这个问题的方式,以及 Python 中可用设计方案的优缺点。
“PEP 227—Statically Nested Scopes”(http://www.python.org/dev/peps/pep-0227/)更偏重于理论,说明了 Python 2.1 引入的词法作用域。词法作用域在这一版里是一种方案,到 Python 2.2 就变成了标准。此外,这份 PEP 还说明了 Python 中闭包的基本原理和实现方式的选择。
杂谈
任何把函数当作一等对象的语言,它的设计者都要面对一个问题:作为一等对象的函数在某个作用域中定义,但是可能会在其他作用域中调用。问题是,如何计算自由变量?首先出现的最简单的处理方式是使用“动态作用域”。也就是说,根据函数调用所在的环境计算自由变量。
如果 Python 使用动态作用域,不支持闭包,那么
avg(与示例 7-9 类似)可以写成这样:>>> ### 这不是真实的Python控制台会话! ### >>> avg = make_averager() >>> series = [] # ➊ >>> avg(10) 10.0 >>> avg(11) # ➋ 10.5 >>> avg(12) 11.0 >>> series = [1] # ➌ >>> avg(5) 3.0❶ 使用
avg之前要自己定义series = [],因此我们必须知道averager(在make_averager内部)引用的是一个列表。❷ 在背后使用
series累计要计入平均值的值。❸ 执行
series = [1]后,之前的列表消失了。同时计算两个独立的移动平均值时可能会发生这种意外。函数应该是黑盒,把实现隐藏起来,不让用户知道。但是对动态作用域来说,如果函数使用自由变量,程序员必须知道函数的内部细节,这样才能搭建正确运行所需的环境。
另一方面,动态作用域易于实现,这可能就是 John McCarthy 创建 Lisp(第一门把函数视作一等对象的语言)时采用这种方式的原因。Paul Graham 写的“The Roots of Lisp”一文(http://www.paulgraham.com/rootsoflisp.html)对 John McCarthy 关于 Lisp 语言那篇论文(“Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I”,http://www-formal.stanford.edu/jmc/recursive/recursive.html)做了通俗易懂的解说。McCarthy 那篇论文是和贝多芬第九交响曲一样伟大的杰作。Paul Graham 使用通俗易懂的语言翻译了那篇论文,把数学原理转换成了英语和可运行的代码。
Paul Graham 的注解还指出动态作用域难以实现。下面这段文字引自“The Roots of Lisp”一文:
就连第一个 Lisp 高阶函数示例都因为动态作用域而无法运行,这充分证明了动态作用域的危险性。McCarthy 在 1960 年可能没有全面认识到动态作用域的影响。动态作用域在各种 Lisp 实现中存在的时间特别长,直到 Sussman 和 Steele 在 1975 年开发出 Scheme 为止。词法作用域不会把
eval的定义变得多么复杂,只是编译器可能更难编写。如今,词法作用域已成常态:根据定义函数的环境计算自由变量。词法作用域让人更难实现支持一等函数的语言,因为需要支持闭包。不过,词法作用域让代码更易于阅读。Algol 之后出现的语言大都使用词法作用域。
多年来,Python 的
lambda表达式不支持闭包,因此在博客圈的函数式编程极客群体中,这个特性的名声并不好。Python 2.2(2001 年 12 月发布)修正了这个问题,但是博客圈的固有印象不会轻易转变。自此之后,仅仅由于句法上的局限,lambda一直处于尴尬的境地。Python 装饰器和装饰器设计模式
Python 函数装饰器符合 Gamma 等人在《设计模式:可复用面向对象软件的基础》一书中对“装饰器”模式的一般描述:“动态地给一个对象添加一些额外的职责。就扩展功能而言,装饰器模式比子类化更灵活。”
在实现层面,Python 装饰器与“装饰器”设计模式不同,但是有些相似之处。
在设计模式中,
Decorator和Component是抽象类。为了给具体组件添加行为,具体装饰器的实例要包装具体组件的实例。《设计模式:可复用面向对象软件的基础》一书是这样说的:装饰器与它所装饰的组件接口一致,因此它对使用该组件的客户透明。它将客户请求转发给该组件,并且可能在转发前后执行一些额外的操作(例如绘制一个边框)。透明性使得你可以递归嵌套多个装饰器,从而可以添加任意多的功能。(第 115 页)
在 Python 中,装饰器函数相当于
Decorator的具体子类,而装饰器返回的内部函数相当于装饰器实例。返回的函数包装了被装饰的函数,这相当于“装饰器”设计模式中的组件。返回的函数是透明的,因为它接受相同的参数,符合组件的接口。返回的函数把调用转发给组件,可以在转发前后执行额外的操作。因此,前面引用那段话的最后一句可以改成:“透明性使得你可以递归嵌套多个装饰器,从而可以添加任意多的行为。”这就是叠放装饰器的理论基础。注意,我不是建议在 Python 程序中使用函数装饰器实现“装饰器”模式。在特定情况下确实可以这么做,但是一般来说,实现“装饰器”模式时最好使用类表示装饰器和要包装的组件。