Python 中的函数装饰器和闭包详解


Posted in Python onFebruary 06, 2021

函数装饰器可以被用于增强方法的某些行为,如果想自己实现装饰器,则必须了解闭包的概念。

装饰器的基本概念

装饰器是一个可调用对象,它的参数是另一个函数,称为被装饰函数。装饰器可以修改这个函数再将其返回,也可以将其替换为另一个函数或者可调用对象。

例如:有个名为 decorate 的装饰器:

@decorate
def target():
 print('running target()')

上述代码的写法和以下写法的效果是一样的:

def target():
 print('running target()')
 
target = decorate(target)

但是,它们返回的 target 不一定是原来的那个 target 函数,例如下面这个例子:

>>> def deco(func):
...  def inner():
...   print('running inner()')
...  return inner
...
>>> @deco
... def target():
...  print('running target()')
...
>>> target()
running inner()
>>> target
<function deco.<locals>.inner at 0x0000013D88563040>

可以看到,调用 target 函数执行的是 inner 函数,这里的 target 实际上是 inner 的引用。

何时执行装饰器

装饰器的另一个关键特性是,它们在被装饰函数定义时立即执行,这通常是发生在导入模块的时候。

例如下面的这个模块:registration.py

# 存储被装饰器 @register 装饰的函数
registry = []

# 装饰器
def register(func):
 print(f"注册函数 -> {func}")
 # 记录被装饰的函数
 registry.append(func)
 return func

@register
def f1():
 print("执行 f1()")

@register
def f2():
 print("执行 f2()")

def f3():
 print("执行 f3()")

if __name__ == "__main__":
 print("执行主函数")
 print("registry -> ", registry)
 f1()
 f2()
 f3()

现在我们在命令行执行这个脚本:

$ python registration.py
注册函数 -> <function f1 at 0x000001F6FC8320D0>
注册函数 -> <function f2 at 0x000001F6FC832160>
执行主函数
registry -> [<function f1 at 0x000001F6FC8320D0>, <function f2 at 0x000001F6FC832160>]
执行 f1()
执行 f2()
执行 f3()

这里我们可以看到,在主函数执行之前,register 已经执行了两次。加载模块后,registry 中已经有两个被装饰函数的引用:f1f2。不过这两个函数以及 f3 都是在脚本中明确调用后才开始执行的。

如果只是单纯的导入 registration.py 模块而不运行:

>>> import registration
注册函数 -> <function f1 at 0x0000022670012280>
注册函数 -> <function f2 at 0x0000022670012310>

查看 registry 中的值:

>>> registration.registry
[<function f1 at 0x0000022670012280>, <function f2 at 0x0000022670012310>]

这个例子主要说明:装饰器在导入模块时立即执行,而被装饰的函数只有在明确调用时才运行。这也突出了 Python 中导入时和运行时这个两个概念的区别。

在装饰器的实际使用中,有两点和示例是不同的:

  • 示例中装饰器和被装饰函数在同一个模块中。实际使用中,装饰器通常在一个单独的模块中定义,然后再应用到其它模块的函数上。
  • 示例中 register 装饰器返回的函数和传入的参数相同。实际使用中,装饰器会在内部定义一个新函数,然后将其返回。

装饰器内部定义并返回新函数的做法需要靠闭包才能正常运作。为了理解闭包,则必须先了解 Python 中的变量作用域。

变量作用域的规则

我们来看下面这个例子,一个函数读取一个局部变量 a,一个全局变量 b

>>> def f1(a):
...  print(a)
...  print(b)
...
>>> f1(3)
3
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 3, in f1
NameError: name 'b' is not defined

出现错误并不奇怪。如果我们先给 b 赋值,再调用 f1,那就不会出错了:

>>> b = 1
>>> f1(3)
3
1

现在,我们来看一个不寻常的例子:

>>> b = 1
>>> def f2(a):
...  print(a)
...  print(b)
...  b = 2
...
>>> f2(3)
3
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment

这里,f2 函数的前两行和 f1 相同,然后再给 b 赋值。可是,在赋值之前,第二个 print 失败了。这是因为Python 在编译函数的定义体时,发现在函数中有给 b 赋值的语句,因此判断它是局部变量。而在上述示例中,当我们打印局部变量 b 时,它并没有被绑定值,故而报错。

Python 不要求声明变量,但是会把在函数定义体中赋值的变量当成局部变量。

如果想把上述示例中的 b 看成全局变量,则需要使用 global 声明:

>>> b = 1
>>> def f3(a):
...  global b
...  print(a)
...  print(b)
...  b = 2
...
>>> f3(3)
3
1
>>> b
2
>>> f3(3)
3
2

闭包

闭包是指延伸了作用域的函数,其中包含了函数定义体中的引用,以及不在定义体中定义的非全局变量。

我们通过以下示例来理解这句话。

假设我们有这种需求,计算某个商品在整个历史中的平均收盘价格(商品每天的价格会变化)。例如:

>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

那么如何获取 avg 函数?历史收盘价格又是如何保存的?

我们可以用一个类来实现:

class Averager:
 def __init__(self):
  self.serial = []

 def __call__(self, price):
  self.serial.append(price)
  return sum(self.serial) / len(self.serial)

Averager 的实例是一个可调用对象。

>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

也可以使用一个函数来实现:

>>> def make_averager():
...  serial = []
...  def averager(price):
...   serial.append(price)
...   return sum(serial) / len(serial)
...  return averager
...
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

第一种写法很明显的可以看到,所有历史收盘价均保存在实例变量 self.serial 中。

第二种写法我们要好好的分析一下:serialmake_averager 的局部变量,但是当我们调用 avg(10) 时,make_averager 函数已经返回了,它的作用域不是应该消失了吗?

实际上,在 averager 函数中,serial 是自由变量(未在本地作用域中绑定的变量)。如下图所示:

Python 中的函数装饰器和闭包详解

我们可以在 averager 返回对象的 __code__ 属性中查看它的局部变量和自由变量的名字。

>>> avg.__code__.co_varnames
('price',)
>>> avg.__code__.co_freevars
('serial',)

自由变量 serial 绑定的值存放在 avg 对象的 __closure__ 属性中,它是一个元组,里面的元素是 cell 对象,它的 cell_contents 属性保存实际的值:

>>> avg.__closure__
(<cell at 0x000002266FF99430: list object at 0x00000226702841C0>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]

综上所述,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定值,这样在我们调用这个函数时,即使作用域不在了,仍然可以使用这些绑定的值。

注意:

只有嵌套在其它函数中的函数才可能需要处理不在全局作用域中的外部变量。

nonlocal 声明

前面的 make_averager 方法的效率并不高,我们可以只保存当前的总值和元素个数,再使用它们计算平均值。下面是我们更改后的函数体:

>>> def make_averager():
...  count = total = 0
...  def averager(price):
...   count += 1
...   total += price
...   return total / count
...  return averager

但是这个写法实际上是有问题的,我们先运行再分析:

>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 4, in averager
UnboundLocalError: local variable 'count' referenced before assignment

这里 count 被当成 averager 的局部变量,而不是我们期望的自由变量。这是因为 count += 1 相当于 count = count + 1。因此,我们在 averager 函数体中实际包含了给 count 赋值的操作,这就把 count 变成局部变量。total 也有这个问题。

为了解决这个问题,Python3 引入了 nonlocal 关键字,用于声明自由变量。使用 nonlocal 修改上述的例子:

>>> def make_averager():
...  count = total = 0
...  def averager(price):
...   nonlocal count, total
...   count += 1
...   total += price
...   return total / count
...  return averager
...
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

叠放装饰器

如果我们把 @d1@d2 两个装饰器应用到同一个函数 f() 上,实际相当于 f = d1(d2(f))

也就是说,下属代码:

@d1
@d2
def f():
	pass

等同于:

def f():
	pass
f = d1(d2(f))

参数化装饰器

Python 会把被装饰的参数作为第一个参数传递给装饰器函数,那么如何让装饰器接受其它的参数呢?这里我们需要定义一个装饰器工厂函数,返回真正的装饰器函数。

以本文开头的 register 装饰器为例,我们为它添加一个 active 参数,如果置为 False,那就不注册这个函数。

registry = []

def register(active=True):
 def decorate(func):
  if active:
   print(f"注册函数 -> {func}")
   # 记录被装饰的函数
   registry.append(func)
  return func

 return decorate

@register()
def f1():
 print("执行 f1")

@register(active=False)
def f2():
 print("执行 f2")

现在我们导入这个模块:

>>> import registration
注册函数 -> <function f1 at 0x0000016D80402280>

可以看到只注册了 f1 函数。

实现一个简单的装饰器

这里我们使用嵌套函数实现一个简单的装饰器:计算被装饰函数执行的耗时,并将函数名、参数和执行的结果打印出来。

import time
def clock(func):
 def clocked(*args):
  start_time = time.perf_counter()
  result = func(*args)
  cost = time.perf_counter() - start_time
  print(
   "[%.2f] %s(%s) -> %r" % (cost, func.__name__, list(map(repr, args)), result)
  )
  return result

 return clocked

下面我们来试试这个装饰器:

>>> @clock
... def factorial(n):
...  # 计算 n 的阶乘
...  return 1 if n < 2 else n * factorial(n - 1)
>>> 
>>> factorial(6)
[0.00] factorial(['1']) -> 1
[0.00] factorial(['2']) -> 2
[0.00] factorial(['3']) -> 6
[0.00] factorial(['4']) -> 24
[0.00] factorial(['5']) -> 120
[0.00] factorial(['6']) -> 720
720

具体来分析一下,这里 factorial 作为 func 参数传递给 clock 函数,然后 clock 函数返回 clocked 函数,Python 解释器会把 clocked 赋值给 factorial。所以,如果我们查看 factorial__name__ 属性,会发现它的值是 clocked 而不是 factorial

>>> factorial.__name__
'clocked'

所以,factorial 保存的是 clocked 的引用,每次调用 factorial 实际上都是在调用 clocked 函数。

我们也可以使用 functools.wraps 装饰器把 func 的一些属性复制到 clocked 函数上,例如:__name____doc__

def clock(func):
 @functools.wraps(func)
 def clocked(*args):
  start_time = time.perf_counter()
  result = func(*args)
  cost = time.perf_counter() - start_time
  print(
   "[%.2f] %s(%s) -> %r" % (cost, func.__name__, list(map(repr, args)), result)
  )
  return result

 return clocked
>>> 
>>> @clock
... def factorial(n):
...  return 1 if n < 2 else n * factorial(n - 1)
>>> 
>>> factorial.__name__
'factorial'

标准库中的装饰器

使用 functools.lru_cache 做备忘

functools.lru_cache 会把耗时的函数的结果保存起来,避免传入相同的参数时的重复计算。lru 的意思是 Least Recently Used,表示缓存不会无限增长,一段时间不用的缓存条目会被丢弃。

lru_cache 非常适合计算第 n 个斐波那契数这样的慢速递归函数。

我们来看看不使用 lru_cache 时的情况:

>>> @clock
... def fibonacci(n):
...  return n if n < 2 else fibonacci(n - 2) + fibonacci(n - 1)
...
>>> fibonacci(6)
[0.00000040] fibonacci(['0']) -> 0
[0.00000060] fibonacci(['1']) -> 1
[0.00030500] fibonacci(['2']) -> 1
[0.00000030] fibonacci(['1']) -> 1
[0.00000040] fibonacci(['0']) -> 0
[0.00000060] fibonacci(['1']) -> 1
[0.00042110] fibonacci(['2']) -> 1
[0.00074440] fibonacci(['3']) -> 2
[0.00128530] fibonacci(['4']) -> 3
[0.00000020] fibonacci(['1']) -> 1
[0.00000030] fibonacci(['0']) -> 0
[0.00000050] fibonacci(['1']) -> 1
[0.00035500] fibonacci(['2']) -> 1
[0.00055270] fibonacci(['3']) -> 2
[0.00000030] fibonacci(['0']) -> 0
[0.00000060] fibonacci(['1']) -> 1
[0.00041220] fibonacci(['2']) -> 1
[0.00000040] fibonacci(['1']) -> 1
[0.00000040] fibonacci(['0']) -> 0
[0.00000050] fibonacci(['1']) -> 1
[0.00032410] fibonacci(['2']) -> 1
[0.00061420] fibonacci(['3']) -> 2
[0.00122760] fibonacci(['4']) -> 3
[0.00206850] fibonacci(['5']) -> 5
[0.00352630] fibonacci(['6']) -> 8
8

这种方式有很多重复的计算,例如 fibonacci(['1']) 执行了 8 次,fibonacci(['2']) 执行了 5 次等等。

现在我们使用 functools.lru_cache 优化一下:

>>> @functools.lru_cache
... @clock
... def fibonacci(n):
...  return n if n < 2 else fibonacci(n - 2) + fibonacci(n - 1)
...
>>> fibonacci(6)
[0.00000060] fibonacci(['0']) -> 0
[0.00000070] fibonacci(['1']) -> 1
[0.00106320] fibonacci(['2']) -> 1
[0.00000080] fibonacci(['3']) -> 2
[0.00132790] fibonacci(['4']) -> 3
[0.00000060] fibonacci(['5']) -> 5
[0.00159670] fibonacci(['6']) -> 8
8

可以看到节省了一般的执行时间,并且 n 的每个值只调用了一次函数。

在执行 fibonacci(30) 时,如果使用未优化的版本需要 141 秒,使用优化后的版本只需要 0.002 秒。

除了优化递归算法之外,lru_cache 在从 WEB 获取信息的应用中也能发挥巨大作用。

lru_cache 还有两个可选参数:

def lru_cache(maxsize=128, typed=False):
  • maxsize:最多可存储的调用结果的个数。缓存满了之后,旧的结果被丢弃。为了获取最佳的性能,maxsize 应该设置为 2 的幂。
  • typed:如果置为 True,会把不同参数类型得到的结果分开保存。例如:f(3.0)f(3) 会被当成不同的调用。

单分派泛函数

假设我们现在开发一个调试 WEB 应用的工具:生成 HTML,显示不同类型的 Python 对象。

我们可以这样编写一个函数:

import html
def htmlize(obj):
 content = html.escape(repr(obj))
 return f"<pre>{content}</pre>"

现在我们需要做一些拓展,让它使用特别的方式显示某些特定类型:

  • str:把字符串内部的 \n 替换为 <br>\n,并且使用 <p> 替换 <pre>
  • int:以十进制和十六进制显示数字;
  • list:显示一个 HTML 列表,根据各个元素的类型格式化;

最常用的方式就是写 if...elif..else 判断:

import numbers
from collections.abc import MutableSequence

def htmlize(obj):
 if isinstance(obj, str):
  content = obj.replace("\n", "<br>\n")
  return f"<p>{content}</p>"
 elif isinstance(obj, numbers.Integral):
  content = f"{obj} ({hex(obj)})"
  return f"<pre>{content}</pre>"
 elif isinstance(obj, MutableSequence):
  content = "</li>\n<li>".join(htmlize(item) for item in obj)
  return "<ul>\n<li>" + content + "</li>\n</ul>"
 else:
  content = f"<pre>{obj}</pre>"
  return content

如果想添加新的类型判断,只会将函数越写越长,并且各个类型之间耦合度较高,不利于维护。

Python 3.4 新增的 functools.singledispatch 装饰器可以将整个方案拆分成多个模块。

import numbers
from collections.abc import MutableSequence
from functools import singledispatch

@singledispatch
def htmlize(obj):
 content = f"<pre>{obj}</pre>"
 return content

@htmlize.register(str)
def _(text):
 content = text.replace("\n", "<br>\n")
 return f"<p>{content}</p>"
@htmlize.register(numbers.Integral)
def _(num):
 content = f"{num} ({hex(num)})"
 return f"<pre>{content}</pre>"

@htmlize.register(MutableSequence)
def _(seq):
 content = "</li>\n<li>".join(htmlize(item) for item in seq)
 return "<ul>\n<li>" + content + "</li>\n</ul>"

这里我们为每一个需要特殊处理的类型都定义另一个专门的函数。

functools.singledispatch 的更详细的文档参考:https://www.python.org/dev/peps/pep-0443/。

到此这篇关于Python 中的函数装饰器和闭包详解的文章就介绍到这了,更多相关Python函数装饰器和闭包内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
python调用java的Webservice示例
Mar 10 Python
Python3实现连接SQLite数据库的方法
Aug 23 Python
浅析Python中的序列化存储的方法
Apr 28 Python
python使用reportlab实现图片转换成pdf的方法
May 22 Python
浅析Python的Django框架中的Memcached
Jul 23 Python
python读取oracle函数返回值
Jul 18 Python
python使用epoll实现服务端的方法
Oct 16 Python
浅谈Python在pycharm中的调试(debug)
Nov 29 Python
python opencv 批量改变图片的尺寸大小的方法
Jun 28 Python
PyCharm中代码字体大小调整方法
Jul 29 Python
Python3基于plotly模块保存图片表格
Aug 03 Python
pytorch实现加载保存查看checkpoint文件
Jul 15 Python
python 利用openpyxl读取Excel表格中指定的行或列教程
Feb 06 #Python
python 利用matplotlib在3D空间中绘制平面的案例
Feb 06 #Python
PyQt5通过信号实现MVC的示例
Feb 06 #Python
python 利用matplotlib在3D空间绘制二次抛物面的案例
Feb 06 #Python
python 利用panda 实现列联表(交叉表)
Feb 06 #Python
jupyter 添加不同内核的操作
Feb 06 #Python
解决import tensorflow导致jupyter内核死亡的问题
Feb 06 #Python
You might like
PHP通过反射动态加载第三方类和获得类源码的实例
2015/11/27 PHP
javascript页面上使用动态时间具体实现
2014/03/18 Javascript
js生成随机数的过程解析
2015/11/24 Javascript
JavaScript实现页面跳转的几种常用方式
2015/11/28 Javascript
利用jsonp跨域调用百度js实现搜索框智能提示
2016/08/24 Javascript
JavaScript的变量声明提升问题浅析(Hoisting)
2016/11/30 Javascript
vue实现简单实时汇率计算功能
2017/01/15 Javascript
jQuery实现动态控制页面元素的方法分析
2017/12/20 jQuery
JS基于设计模式中的单例模式(Singleton)实现封装对数据增删改查功能
2018/02/06 Javascript
babel的使用及安装配置教程
2018/02/22 Javascript
React 路由懒加载的几种实现方案
2018/10/23 Javascript
简单了解JavaScript中的执行上下文和堆栈
2019/06/24 Javascript
Vue 用Vant实现时间选择器的示例代码
2019/10/25 Javascript
关于vue组件事件属性穿透详解
2019/10/28 Javascript
JavaScript中的惰性载入函数及优势
2020/02/18 Javascript
JavaScript实现手机号码 3-4-4格式并控制新增和删除时光标的位置
2020/06/02 Javascript
Vue优化:常见会导致内存泄漏问题及优化详解
2020/08/04 Javascript
[06:11]2014DOTA2国际邀请赛 专访团结一心的VG战队
2014/07/21 DOTA
python黑魔法之参数传递
2016/02/12 Python
python 地图经纬度转换、纠偏的实例代码
2018/08/06 Python
对Python 两大环境管理神器 pyenv 和 virtualenv详解
2018/12/31 Python
Python实现监控Nginx配置文件的不同并发送邮件报警功能示例
2019/02/26 Python
django框架防止XSS注入的方法分析
2019/06/21 Python
Python自动化完成tb喵币任务的操作方法
2019/10/30 Python
Python 模拟生成动态产生验证码图片的方法
2020/02/01 Python
Python 读取有公式cell的结果内容实例方法
2020/02/17 Python
Pycharm 跳转回之前所在页面的操作
2021/02/05 Python
初中女生自我鉴定
2013/12/19 职场文书
优秀教师事迹简介
2014/02/02 职场文书
目标责任书范本
2014/04/16 职场文书
学校学雷锋活动总结
2014/06/26 职场文书
业绩倒数第一的检讨书
2014/09/24 职场文书
2015年教师节慰问信
2015/03/23 职场文书
酒店开业主持词
2015/07/02 职场文书
大学生创业计划书常用模板
2019/08/07 职场文书
动视暴雪取消疫苗禁令 让所有员工返回线下工作
2022/04/03 其他游戏