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 相关文章推荐
wxpython中利用线程防止假死的实现方法
Aug 11 Python
Python的Flask框架应用调用Redis队列数据的方法
Jun 06 Python
Python3使用PyQt5制作简单的画板/手写板实例
Oct 19 Python
python爬虫_微信公众号推送信息爬取的实例
Oct 23 Python
python+selenium实现163邮箱自动登陆的方法
Dec 31 Python
在Python中合并字典模块ChainMap的隐藏坑【推荐】
Jun 27 Python
浅谈pytorch、cuda、python的版本对齐问题
Jan 15 Python
python使用python-pptx删除ppt某页实例
Feb 14 Python
浅谈Python3中print函数的换行
Aug 05 Python
Restful_framework视图组件代码实例解析
Nov 17 Python
python实战之用emoji表情生成文字
May 08 Python
浅谈Python实现opencv之图片色素的数值运算和逻辑运算
Jun 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缓存类实例
2014/07/18 PHP
浅谈json_encode用法
2015/03/05 PHP
PHP二维关联数组的遍历方式(实例讲解)
2017/10/18 PHP
PHP开发实现微信退款功能示例
2017/11/25 PHP
PHP7新功能总结
2019/04/14 PHP
javascript 对象的定义方法
2007/01/10 Javascript
IE8 引入跨站数据获取功能说明
2008/07/22 Javascript
分页栏的web标准实现
2011/11/01 Javascript
javascript面向对象编程代码
2011/12/19 Javascript
javascript trim函数在IE下不能用的解决方法
2014/09/12 Javascript
jquery和js实现对div的隐藏和显示方法
2014/09/26 Javascript
javascript实现tab切换的四种方法
2015/11/05 Javascript
JavaScript职责链模式概述
2016/09/17 Javascript
学习 NodeJS 第八天:Socket 通讯实例
2016/12/21 NodeJs
js通过Date对象实现倒计时动画效果
2017/10/27 Javascript
基于mpvue搭建微信小程序项目框架的教程详解
2019/04/10 Javascript
layui的布局和表格的渲染以及动态生成表格的方法
2019/09/18 Javascript
vue分页插件的使用方法
2019/12/25 Javascript
何时/使用 Vue3 render 函数的教程详解
2020/07/25 Javascript
[04:51]TI10典藏宝瓶Ⅱ外观视频展示
2020/08/15 DOTA
Python中文件遍历的两种方法
2014/06/16 Python
Python基础语言学习笔记总结(精华)
2017/11/14 Python
python实现猜单词小游戏
2020/05/22 Python
Python中GeoJson和bokeh-1的使用讲解
2019/01/03 Python
python3.6根据m3u8下载mp4视频
2019/06/17 Python
python爬虫 urllib模块url编码处理详解
2019/08/20 Python
Django用户身份验证完成示例代码
2020/04/03 Python
在keras中实现查看其训练loss值
2020/06/16 Python
python 多线程中join()的作用
2020/10/29 Python
如何用Matlab和Python读取Netcdf文件
2021/02/19 Python
实现CSS3中的border-radius(边框圆角)示例代码
2013/07/19 HTML / CSS
党员服务承诺书
2014/05/28 职场文书
天堂的孩子观后感
2015/06/11 职场文书
爱心捐款倡议书:点燃希望,传递温暖
2019/11/04 职场文书
PyTorch 如何自动计算梯度
2021/05/23 Python
Redis调用Lua脚本及使用场景快速掌握
2022/03/16 Redis