函数装饰器和闭包
目录
函数装饰器: 用于在源码中“标记”函数,以某种方式增强函数的行为 闭包: 函数装饰器,回调式异步编程,函数式编程风格的基础
1. 装饰器基础
1.1 装饰器简介
语法:
- 装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)
作用:
- 可能会处理被装饰的函数,然后把它返回
- 或者将其替换成另一个函数或可调用对象
使用:
- 装饰器通常在一个模块中定义,然后应用到其他模块中的函数上
- 大多数装饰器会在内部定义一个函数,然后将其返回
|
|
1.2 装饰器执行
装饰器: 通常是在导入时,在被装饰的函数定义之后立即运行 – 导入时
被装饰函数: 只在明确调用时运行 – 运行时
1.3 使用装饰器改进“策略”模式
|
|
2. 变量作用域
|
|
作用域确定:
- Python 编译函数的定义体时,会将 b 判定为局部变量,因为在它在函数体中赋值
- 这不是缺陷,而是设计选择: Python 不要求声明变量,但假定在函数定义体中赋值的变量是局部变量
dis 模块
- 作用: 为反汇编 Python 函数字节码提供了简单的方式
- 文档: http://docs.python.org/3/library/dis.html
3. 闭包
|
|
自由变量(free variable):
- 指未在本地作用域中绑定的变量
闭包:
- 定义:
- 闭包指延伸了作用域的函数,其中包含在函数定义体中引用、但是不在定义体中定义的非全局变量
- 函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量
- 原理:
- 闭包是一种函数,它会保留定义函数时,存在的自由变量的绑定
- 这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定
- __code__ 属性: 表示编译后的函数定义体
- __code__.co_freevars: 保存着自由变量的名称
- __closure__属性: cell 对象的列表,表示自由变量,一一对应于 co_freevars
- cell.co_freevars: 保存着自由变量真正的值
4. 作用域转换
global var: 把局部变量转换为全局变量
nonlocal var: 把变量标记为自由变量
|
|
说明:
- 当 count 是数字或任何不可变类型时, (count += 1) == (count =count + 1)
- count=count+1,会隐式创建局部变量 count, count 就不是自由变量,因此不会保存在闭包中
- 为了解决这个问题, Python 3 引入了 nonlocal 声明。它的作用是把变量标记为自由变量
- Python 2处理方式: PEP 3104—Access to Names in Outer Scopes
5. 装饰器进阶
装饰器典型行为:
- 把被装饰的函数替换成新函数,二者接受相同的参数,
- 通常返回被装饰的函数本该返回的值,同时还会做些额外操作
- 即动态地给一个对象添加一些额外的职责
装饰器实现:
- 函数装饰器
- 通过实现 __call__ 方法的类实现 – 最佳方式
- 构建工业级装饰器的技术, 参见Graham Dumpleton 的博客和 wrapt 模块
装饰器扩展模块
- wrapt:
- 文档: http://wrapt.readthedocs.org/en/latest/
- 作用:
- 简化装饰器和动态函数包装器的实现,即使多层装饰也支持内省,
- 而且行为正确,既可以应用到方法上,也可以作为描述符使用
- decorator
- 文档: http://pypi.python.org/pypi/decorator
- 作用: 简化普通程序员使用装饰器的方式,并且通过各种复杂的示例推广装饰器
装饰器用法:
- Graham Dumpleton
- 写了一系列博客文章,深入剖析了如何实现行为良好的装饰器 http://github.com/GrahamDumpleton/wrapt/blob/develop/blog/README.md
- Python Decorator Library 维基页面
- Guido van Rossum
- Five-Minute Multimethods in Python
- 详细说明了如何使用装饰器实现泛函数(也叫多方法)
- http://www.artima.com/weblogs/viewpost.jsp?thread=101605
5.1 简单装饰器
|
|
functools.wraps 装饰器
- 作用: 把 func 的 __name__ 和 __doc__ 等相关属性复制到 clocked 中
5.2 标准库中的装饰器
functools.lru_cache(maxsize, typed):
- 作用: 备忘(memoization)功能,把耗时的函数的结果保存起来,避免传入相同的参数时重复计算
- lru: Least Recently Used 的缩写,表明缓存不会无限增长,一段时间不用的缓存条目会被扔掉
- 参数:
- maxsize: 指定存储多少个调用的结果,为了得到最佳性能, maxsize 应该设为 2 的幂
- typed:=True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数区分开
- 应用:
- 优化递归算法
- 在从 Web 中获取信息的应用中也能发挥巨大作用
- 附注: 因为 lru_cache 使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建, 所以被 lru_cache 装饰的函数,它的所有参数都必须是可散列的
|
|
functools.singledispatch:
- 作用: 将普通函数变成泛函数(generic function),
- 泛函数: 根据第一个参数的类型,以不同方式执行相同操作的一组函数
- 特性:
- 可以在系统的任何地方和任何模块中注册专门函数
- 如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处理那个类型
- 还可以为不是自己编写的或者不能修改的类添加自定义函数
- 文档: http://www.python.org/dev/peps/pep-0443/
- 版本:
- python34: functools.singledispatch
- python<34: singledispatch包
- 特性对比:
- Python 不支持 重载方法或函数,所以不能使用不同的签名定义函数的变体, 也无法使用不同的方式处理不同的数据类型
- @singledispatch 不是为了把 Java 的那种方法重载带入 Python
- 在一个类中为同一个方法定义多个重载变体,比在一个函数中使用一长串 if/elif/elif/elif 块要更好
- 但是这两种方案都有缺陷,因为它们让代码单元(类或函数)承担的职责太多
- @singledispath 的优点是支持模块化扩展: 各个模块可以为它支持的各个类型注册一个专门函数
|
|
分析:
- ➊ @singledispatch 标记处理 object 类型的基函数
- ➋ 各个专门函数使用 @«base_function».register(«type») 装饰
- ➍ 为每个需要特殊处理的类型注册一个函数, numbers.Integral 是 int 的虚拟超类
- ➎ 可以叠放多个 register 装饰器,让同一个函数支持不同类型
- 只要可能,注册的专门函数应该处理抽象基类(如 numbers.Integral,abc.MutableSequence) 不要处理具体实现(如 int 和 list)。这样,代码支持的兼容类型更广泛
5.3 叠放装饰器
|
|
5.4 参数化装饰器
实现方式:
- 创建一个 装饰器工厂函数,把参数传给它,
- 返回一个装饰器,然后再把它应用到要装饰的函数上
示例1: 一个参数化的注册装饰器
|
|
示例2: 参数化clock装饰器
|
|
示例3: 通过实现__call__方法的类实现装饰器
|
|
6. 泛函数用法
6.1 单分派泛函数
原理解析:
- 文档: http://www.python.org/dev/peps/pep-0443/
- 文档说明:对单分派泛函数的基本原理和细节做了说明
使用示例:
- Guido van Rossum 写的博客 Five-Minute Multimethods in Python, 详细说明了如何使用装饰器实现泛函数(也叫多方法)
- http://www.artima.com/weblogs/viewpost.jsp?thread=101605
6.2 泛函数扩展模块
Reg
- 文档: http://reg.readthedocs.io/en/latest/
- 作用: 使用现代的技术实现多分派泛函数,并支持在生产环境中使用
延伸阅读
Python:
wrapt:
- 文档: http://wrapt.readthedocs.org/en/latest/
- 作用:
- 简化装饰器和动态函数包装器的实现,即使多层装饰也支持内省,
- 而且行为正确,既可以应用到方法上,也可以作为描述符使用
decorator
- 文档: http://pypi.python.org/pypi/decorator
- 作用: 简化普通程序员使用装饰器的方式,并且通过各种复杂的示例推广装饰器
单分派泛函数
- 文档: http://www.python.org/dev/peps/pep-0443/
- 文档说明:对单分派泛函数的基本原理和细节做了说明
Reg
- 文档: http://reg.readthedocs.io/en/latest/
- 作用: 使用现代的技术实现多分派泛函数,并支持在生产环境中使用
nonlocal 声明
- 文档: 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 中闭包的基本原理和实现方式的选择
blog:
Graham Dumpleton
- 写了一系列博客文章,深入剖析了如何实现行为良好的装饰器
- http://github.com/GrahamDumpleton/wrapt/blob/develop/blog/README.md
Python Decorator Library 维基页面
Guido van Rossum
- Five-Minute Multimethods in Python
- 详细说明了如何使用装饰器实现泛函数(也叫多方法)
- http://www.artima.com/weblogs/viewpost.jsp?thread=101605
Fredrik Lundh
- Closures in Python
- 解说了闭包这个术语
- http://effbot.org/zone/closure.htm
实用工具
Morepath
- 作用: 模型驱动型 REST 式 Web 框架
- http://morepath.readthedocs.org/en/latest/
书籍:
《 Python Cookbook(第 3 版)中文版》:
- 第 9 章“元编程”有几个诀窍构建了基本的装饰器和特别复杂的装饰器
附注
一等函数
- 任何把函数当作一等对象的语言,它的设计者都要面对一个问题: 作为一等对象的函 数在某个作用域中定义,但是可能会在其他作用域中调用
- 问题是,如何计算自由变量?
如何计算自由变量
- 动态作用域: - 定义: 根据函数调用所在的环境计算自由变量 - 缺点: 对动态作用域来说,如果函数使用自由变量,程序员必须知道函数的内部细节, 这样才能搭建正确运行所需的环境 - 优点: 动态作用域易于实现 - 应用: Lisp
- 词法作用域: - 定义: 根据定义函数的环境计算自由变量。 - 缺点: 词法作用域让人更难实现支持一等函数的语言,因为需要支持闭包 - 优点: 词法作用域让代码更易于阅读 - 应用: Algol 之后出现的语言大都使用词法作用域
Python 装饰器和装饰器设计模式
- 功能: Python 函数装饰器符合 Gamma 等人在《设计模式: 可复用面向对象软件的基础》一 书中对“装饰器”模式的一般描述: “动态地给一个对象添加一些额外的职责。就扩展 功能而言,装饰器模式比子类化更灵活
- 实现: Python 装饰器与“装饰器”设计模式不同
- 在设计模式中:
- Decorator 和 Component 是抽象类。
- 为了给具体组件添加行为,具体装饰器的实例要包装具体组件的实例
- 在 Python 中:
- 装饰器函数相当于 Decorator 的具体子类,
- 而装饰器返回的内部函数相当于装饰器实例。
- 返回的函数包装了被装饰的函数,这相当于“装饰器”设计模式中的组件。
- 返回的函数是透明的,因为它接受相同的参数,符合组件的接口
- 返回的函数把调用转发给组件,可以在转发前后执行额外的操作
- 不是建议在 Python 程序中使用函数装饰器实现“装饰器”模式。在特定情况下确实可以这么做, 但是一般来说,实现“装饰器”模式时最好 使用类表示装饰器和要包装的组件
- 在设计模式中: