返回介绍

7.12 延伸阅读

发布于 2024-02-05 21:59:47 字数 4636 浏览 0 评论 0 收藏 0

《Python Cookbook(第 3 版)中文版》(David Beazley 和 Brian K. Jones 著)的第 9 章“元编程”有几个诀窍构建了基本的装饰器和特别复杂的装饰器。其中,“9.6 定义一个能接收可选参数的装饰器”一节中的装饰器可以作为常规的装饰器调用,也可以作为装饰器工厂函数调用,例如 @clock 或 @clock()。

Graham Dumpleton 写了一系列博客文章,深入剖析了如何实现行为良好的装饰器,第一篇是“How You Implemented Your Python Decorator is Wrong”。他在这方面的深厚知识充分体现在在他编写的 wrapt 模块中。这个模块的作用是简化装饰器和动态函数包装器的实现,即使多层装饰也支持内省,而且行为正确,既可以应用到方法上,也可以作为描述符使用。(描述符在本书第 20 章讨论。)

Michele Simionato 开发了一个包,根据文档,它旨在“简化普通程序员使用装饰器的方式,并且通过各种复杂的示例推广装饰器”。这个包是 decorator,可通过 PyPI 安装。

Python Decorator Library 维基页面在 Python 刚添加装饰器这个特性时就创建了,里面有很多示例。由于那个页面是几年前开始编写的,有些技术已经过时了,不过仍是很棒的灵感来源。

PEP 443对单分派泛函数的基本原理和细节做了说明。Guido van Rossum 很久以前(2005 年 3 月)写的一篇博客文章“Five-Minute Multimethods in Python”详细说明了如何使用装饰器实现泛函数(也叫多方法)。他给出的代码支持多分派(即根据多个定位参数进行分派)。Guido 写的多方法代码很棒,但那只是教学示例。如果想使用现代的技术实现多分派泛函数,并支持在生产环境中使用,可以用 Martijn Faassen 开发的 Reg。Martijn 还是模型驱动型 REST 式 Web 框架 Morepath的开发者。

Fredrik Lundh 写的一篇短文“Closures in Python”解说了闭包这个术语。

PEP 3104—Access to Names in Outer Scopes”说明了引入 nonlocal 声明的原因:重新绑定既不在本地作用域中也不在全局作用域中的名称。这份 PEP 还概述了其他动态语言(Perl、Ruby、JavaScript,等等)解决这个问题的方式,以及 Python 中可用设计方案的优缺点。

PEP 227—Statically Nested Scopes”更偏重于理论,说明了 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”一文对 John McCarthy 关于 Lisp 语言那篇论文(“Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I”)做了通俗易懂的解说。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 程序中使用函数装饰器实现“装饰器”模式。在特定情况下确实可以这么做,但是一般来说,实现“装饰器”模式时最好使用类表示装饰器和要包装的组件。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文