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实现文件助手中查看微信撤回消息
Apr 29 Python
python如何删除文件中重复的字段
Jul 16 Python
Python利用WMI实现ping命令的例子
Aug 14 Python
pytorch 实现在一个优化器中设置多个网络参数的例子
Feb 20 Python
Python自定义聚合函数merge与transform区别详解
May 26 Python
Python flask框架如何显示图像到web页面
Jun 03 Python
Python中对象的比较操作==和is区别详析
Feb 12 Python
conda安装tensorflow和conda常用命令小结
Feb 20 Python
python 第三方库paramiko的常用方式
Feb 20 Python
python SOCKET编程基础入门
Feb 27 Python
http通过StreamingHttpResponse完成连续的数据传输长链接方式
Feb 12 Python
python解析照片拍摄时间进行图片整理
Jul 23 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中的array数组类型分析说明
2010/07/27 PHP
PHP校验ISBN码的函数代码
2011/01/17 PHP
php表单处理操作
2017/11/16 PHP
PHP实现的mongoDB数据库操作类完整实例
2018/04/10 PHP
phpmyadmin在宝塔面板里进不去的解决方案
2020/07/06 PHP
js关闭子窗体刷新父窗体实现方法
2012/12/04 Javascript
js判断选择的时间是否大于今天的代码
2013/08/20 Javascript
javascript内存管理详细解析
2013/11/11 Javascript
JavaScript中的Math 使用介绍
2014/04/21 Javascript
NodeJS使用jQuery选择器操作DOM
2015/02/13 NodeJs
原创jQuery弹出层插件分享
2015/04/02 Javascript
Bootstrap作品展示站点实战项目2
2016/10/14 Javascript
实例详解display:none与visible:hidden的区别
2017/03/30 Javascript
使用Vue自定义指令实现Select组件
2018/05/24 Javascript
vue实现滑动超出指定距离回顶部功能
2019/07/31 Javascript
Vue自定义多选组件使用详解
2020/09/08 Javascript
OpenLayers3加载常用控件使用方法详解
2020/09/25 Javascript
Python实现新浪博客备份的方法
2016/04/27 Python
Python logging模块用法示例
2018/08/28 Python
详解Python正则表达式re模块
2019/03/19 Python
深入了解Python枚举类型的相关知识
2019/07/09 Python
Python中常用的高阶函数实例详解
2020/02/21 Python
Python+Opencv身份证号码区域提取及识别实现
2020/08/25 Python
CSS3 制作绽放的莲花采用效果叠加实现
2013/01/31 HTML / CSS
英国知名的护肤彩妆与时尚配饰大型综合零售电商:Unineed
2016/11/21 全球购物
韩国流行时尚女装网站:Dintchina(中文)
2018/07/19 全球购物
Java程序员常见面试题
2015/07/16 面试题
自我评价的正确写法
2013/09/19 职场文书
超市营业员岗位职责
2013/12/20 职场文书
大学生毕业求职信
2014/06/12 职场文书
韩语专业职业生涯规划范文:成功之路就在我们脚下
2014/09/11 职场文书
明确岗位职责
2015/02/14 职场文书
中学校园广播稿
2015/08/18 职场文书
安全生产标语口号
2015/12/26 职场文书
SQL Server数据定义——模式与基本表操作
2021/04/05 SQL Server
Python selenium的这三种等待方式一定要会!
2021/06/10 Python