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 相关文章推荐
用PyQt进行Python图形界面的程序的开发的入门指引
Apr 14 Python
python将文本转换成图片输出的方法
Apr 28 Python
Python的collections模块中的OrderedDict有序字典
Jul 07 Python
使用Python绘制图表大全总结
Feb 11 Python
关于pip的安装,更新,卸载模块以及使用方法(详解)
May 19 Python
python中模块的__all__属性详解
Oct 26 Python
Django权限机制实现代码详解
Feb 05 Python
Python多线程threading模块用法实例分析
May 22 Python
OpenCV+Python--RGB转HSI的实现
Nov 27 Python
python GUI库图形界面开发之PyQt5图片显示控件QPixmap详细使用方法与实例
Feb 27 Python
python 发送邮件的示例代码(Python2/3都可以直接使用)
Dec 03 Python
Python编写冷笑话生成器
Apr 20 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实现的策略模式简单示例
2017/08/25 PHP
javascript中直接写php代码的方法
2013/07/31 Javascript
用js通过url传参把数据从一个页面传到另一个页面
2014/09/01 Javascript
Javascript与jQuery方法的隐藏与显示
2015/01/19 Javascript
百度UEditor编辑器如何关闭抓取远程图片功能
2015/03/03 Javascript
JS+CSS实现DIV层的展开、收缩效果
2016/01/28 Javascript
Ztree新增角色和编辑角色回显问题的解决
2016/10/25 Javascript
js实现漫天星星效果
2017/01/19 Javascript
Javascript中 带名 匿名 箭头函数的重要区别(推荐)
2017/01/29 Javascript
js绑定事件和解绑事件
2017/04/27 Javascript
Js 利用正则表达式和replace函数获取string中所有被匹配到的文本(推荐)
2018/10/28 Javascript
JavaScript 中 JSON.parse 函数 和 JSON.stringify 函数
2018/12/05 Javascript
JS拖动选择table里的单元格完整实例【基于jQuery】
2019/05/28 jQuery
webpack 如何解析代码模块路径的实现
2019/09/04 Javascript
解决layer.open后laydate失效的问题
2019/09/06 Javascript
vue简单练习 桌面时钟的实现代码实例
2019/09/19 Javascript
JS实现轮播图效果
2020/01/11 Javascript
[05:08]第一届“网鱼杯”DOTA2比赛精彩集锦
2014/09/05 DOTA
跟老齐学Python之使用Python操作数据库(1)
2014/11/25 Python
各种Python库安装包下载地址与安装过程详细介绍(Windows版)
2016/11/02 Python
用生成器来改写直接返回列表的函数方法
2017/05/25 Python
Django组件content-type使用方法详解
2019/07/19 Python
Python中的单下划线和双下划线使用场景详解
2019/09/09 Python
浅析Python3 pip换源问题
2020/01/06 Python
Tensorflow tf.tile()的用法实例分析
2020/05/22 Python
记录一下scrapy中settings的一些配置小结
2020/09/28 Python
中专生求职自荐信范文
2013/12/22 职场文书
幼教毕业生自我鉴定
2014/01/12 职场文书
理发店策划方案
2014/06/05 职场文书
党员创先争优心得体会
2014/09/11 职场文书
幼儿园秋季开学通知
2015/07/16 职场文书
《折线统计图》教学反思
2016/02/22 职场文书
2016年“9.22”世界无车日活动小结
2016/04/05 职场文书
员工保密协议范本,您一定得收藏!很有用!
2019/08/08 职场文书
ORACLE数据库应用开发的三十个注意事项
2021/06/07 Oracle
Mysql忘记密码解决方法
2022/02/12 MySQL