您的位置:首页 > 编程语言 > Python开发

《流畅的Python》读书笔记——Python函数装饰器和闭包

2020-04-01 18:29 741 查看

引言

函数装饰器(不是设计模式中的装饰器模式)用于在源码中“标记”函数,以某种方式增强函数的行为。这是一项强大的功能,但是若想掌握,必须理解闭包。

除了在装饰器中有用处之外,闭包还是回调式异步编程和函数式编程风
格的基础。
首先看一下装饰器基础知识

装饰器基础知识

装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。 装
饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个
函数或可调用对象。

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

上述代码的效果与下面写法一样:

def target()
print('running target()')

target = decorate(target)

上述两个代码片段执行完毕后得到的

target
不一定是原来那个 target 函数,而是
decorate(target)
返回的函数。

装饰器通常把函数替换成另一个函数:

In [1]: def deco(func):
...:     def inner():
...:         print('running inner()')
...:     return inner  #deco返回inner函数对象
...:

In [2]: @deco  #使用deco装饰target
...: def target():
...:     print('running target()')
...:

In [3]: target()    #其实会运行inner
running inner()

In [4]: target  #target是inner的引用
Out[4]: <function __main__.deco.<locals>.inner()>

综上,装饰器的一大特性是,能把被装饰的函数替换成其他函数。第二
个特性是,装饰器在加载模块时立即执行。

Python何时执行装饰器

装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 Python 加载模块时)

#registration.py
# -*- coding: utf-8 -*
registry = [] #保存被 @register 装饰的函数引用
def register(func): #参数func是一个函数
print('running register(%s)' % func) #打印被装饰的函数
registry.append(func)#存入registry
return func #必须返回函数,这里返回的就是被装饰的函数

#f1和f2被装饰
@register
def f1():
print('running f1()')

@register
def f2():
print('running f2()')

#f3没有被装饰
def f3():
print('running f3()')

def main():
print('running main()')
print('registry ->',registry)
f1()
f2()
f3()

if __name__ == '__main__':
main()

运行该脚本,输出为:

running register(<function f1 at 0x0000015C8077E730>) #register在main运行之前运行
running register(<function f2 at 0x0000015C8077E7B8>)
running main()
registry -> [<function f1 at 0x0000015C8077E730>, <function f2 at 0x0000015C8077E7B8>]
running f1()
running f2()
running f3()

如果导入

registration.py
模块,输出如下:

>>> import registration
running register(<function f1 at 0x0000022BB52DE840>)
running register(<function f2 at 0x0000022BB52DE8C8>)

证明了函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。

使用装饰器改进策略模式

from collections import namedtuple

Customer = namedtuple('Customer','name fidelity')

class LineItem:
def __init__(self,product,quantity,price):
self._product = product
self._quantity = quantity
self._price = price

def total(self):
return self._price * self._quantity

class Order: #上下文,也就是客户端代码
def __init__(self,customer,cart,promotion = None):
self._customer = customer
self._cart = list(cart)
self._promotion = promotion

def total(self):
if not hasattr(self, '__total'):
self.__total = sum(item.total() for item in self._cart)
return self.__total

def due(self):
"""折扣"""
if self._promotion is None:
discount = 0
else:
discount = self._promotion(self) #只需调用self.promotion()函数
return self.total() - discount

def __repr__(self):
fmt = '<Order total: {:.2f} due: {:.2f}>'
return fmt.format(self.total(), self.due())

promos = [] #列表初始为空

def promotion(promo_func):
promos.append(promo_func) #添加到promos列表
return promo_func#原封不动将其返回

@promotion#通过@promotion装饰的函数都会添加到promos列表中
def fidelity_promo(order):
"""为积分为1000或以上的顾客提供5%折扣"""
return order.total() * 0.05 if order._customer.fidelity >= 1000 else 0

@promotion
def bulk_item_promo(order):
"""单个商品为20个或以上时提供10%折扣"""
discount = 0
for item in order._cart:
if item._quantity >= 20:
discount += item.total() * .1
return discount

@promotion
def large_order_promo(order):
"""订单中的不同商品达到10个或以上时提供7%折扣"""
distinct_items = {item._product for item in order._cart}
if len(distinct_items) >= 10:
return order.total() * 0.07
return 0

def best_promo(order):
"""选择可用的最佳折扣"""
return max(promo(order) for promo in promos)

Python使用一等函数实现设计模式给出的方案相比,该方案有几个优点:

  • 促销函数无需使用特殊的名称
  • @promotion装饰器突出了被装饰的函数的作用,便于临时禁用某个促销策略:只需把装饰器注释掉
  • 促销折扣策略可以在其他模块中定义,只要使用@promotion装饰即可

不过,多数装饰器会修改被装饰的函数。通常,它们会定义一个内部函
数,然后将其返回,替换被装饰的函数。使用内部函数的代码几乎都要
靠闭包才能正确运作。为了理解闭包,我们要退后一步,先了解 Python
中的变量作用域。

变量作用域规则

一个函数,读取一个局部变量和一个全局变量:

In [1]: def f1(a):
...:     print(a)
...:     print(b)
...:

In [2]: f1(3)
3
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-2-db0f80b394ed> in <module>
----> 1 f1(3)

<ipython-input-1-c1318c6d0711> in f1(a)
1 def f1(a):
2     print(a)
----> 3     print(b)
4

NameError: name 'b' is not defined

如果先给全局变量 b 赋值,然后再调用 f,那就不会出错:

In [3]: b = 6
In [5]: f1(3)

看一下下面示例中的 f2 函数。前两行代码与前面的 f1 一样,然
后为 b 赋值,再打印它的值。可是,在赋值之前,第二个 print 失败了。

In [8]: b = 6
In [9]: def f2(a):
...:     print(a)
...:     print(b)
...:     b = 9
...:
In [10]: f2(3)
3
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-10-ddde86392cb4> in <module>
----> 1 f2(3)

<ipython-input-9-2304a03d7bfd> in f2(a)
1 def f2(a):
2     print(a)
----> 3     print(b)
4     b = 9
5

UnboundLocalError: local variable 'b' referenced before assignment

从上面的报错信息可知,是在

print(b)
处报的错,是不是很吃惊,我也以为会打印6,因为有个全局变量b。

可事实是,Python 编译函数的定义体时,它判断 b 是局部变量,因为在
函数中给它赋值了。生成的字节码证实了这种判断,Python 会尝试从本
地环境获取 b。后面调用

f2(3)
时,
f2
的定义体会获取并打印局部变
量 a 的值,但是尝试获取局部变量 b 的值时,发现 b 没有绑定值。

这不是缺陷,而是设计选择:Python 不要求声明变量,但是假定在函数
定义体中赋值的变量是局部变量。

如果在函数中赋值时想让解释器把 b 当成全局变量,要使用 global 声明:

In [11]: b = 6
In [12]: def f3(a):
...:     global b  #使用global 声明
...:     print(a)
...:     print(b)
...:     b = 9
...:

In [13]: f3(3)
3
6
In [14]: b
Out[14]: 9
In [16]: f3(3)
3
9
In [17]: b = 30
In [18]: b
Out[18]: 30

闭包

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

假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均
值;例如,整个历史中某个商品的平均收盘价。每天都会增加新价格,
因此平均值要考虑至目前为止所有的价格。

起初,avg 是这样使用的:

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

avg 从何而来,它又在哪里保存历史值呢?
我们通过函数式来实现:

# -*- coding: utf-8 -*
def make_averager():
series = []#历史值

def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)

return averager

if __name__ == '__main__':
avg = make_averager()
print(avg(10))
print(avg(11))
print(avg(12))

输出为:

10.0
10.5
11.0

注意,

series
make_averager
函数的局部变量,在函数的定义体重初始化了
series: series = []
。但是,调用
avg(10)
时,
make_averager
函数已经返回了,而它的本地作用域也一去不复返了。

averager
函数中,
series
是自由变量(free variable)。这是一
个技术术语,指未在本地作用域中绑定的变量。


averager
的闭包延伸到那个函数的作用域之外,包含自由变量
series
的绑定。

print(avg.__code__.co_varnames) #('new_value', 'total')
print(avg.__code__.co_freevars) #('series',)
print(avg.__closure__) #(<cell at 0x00000261E22D8498: list object at 0x00000261E2371308>,)
print(avg.__closure__[0].cell_contents) #[10, 11, 12],就是series保存的值

avg.__closure__
中的各个元素对应于
avg.__code__.co_freevars
中的一个名称。这些元素是
cell
对象,有个
cell_contents
属性,保存着真正的值。

闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,
这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。

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

nonlocal声明

前面实现

make_averager
函数的方法效率不高。我们把所有值存储在历史数列中,然后在每次调用
averager
时使用
sum
求和。更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个数计算均值。

def make_averager():
count = 0
total = 0

def averager(new_value):
count += 1
total += new_value
return total/count

return averager

当我敲完这些代码后,编译器有提示报错,我尝试运行一下,看是什么错误:

UnboundLocalError: local variable 'count' referenced before assignment

count
是数字或任何不可变类型时,
count += 1
语句的
作用其实与
count = count + 1
一样。因此,我们在
averager

定义体中
count
赋值了,这会把
count
变成局部变量
total

量也是一样。

在之前的示例没遇到这个问题,因为我们没有给

series
赋值,只是调用了
append
方法。也就是说,利用了列表时可变的对象这一事实。

也就是说,重新绑定(赋值)会隐式创建局部变量,就不是自由变量了,因此不会保存在闭包中。

Python 3 引入了

nonlocal
声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。

def make_averager():
count = 0
total = 0

def averager(new_value):
nonlocal  count,total #和之前global的用法类似
count += 1
total += new_value
return total/count

return averager

实现一个简单的装饰器

一个简单的装饰器,输出函数的运行时间:

# -*- coding: utf-8 -*
import time

def clock(func):
def clocked(*args): #接受任意个定位参数
t0 = time.perf_counter()
result = func(*args)#clocked的闭包中包含自由变量func
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args) #参数
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked#返回内部函数,取代被装饰的函数

@clock
def snooze(seconds):
time.sleep(seconds)

@clock
def factorial(n):
return 1 if n < 2 else n * factorial(n - 1)
if __name__ == '__main__':
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))

运行输出为:

**************************************** Calling snooze(.123)
[0.12056875s] snooze(0.123) -> None
**************************************** Calling factorial(6)
[0.00000079s] factorial(1) -> 1
[0.00002686s] factorial(2) -> 2
[0.00003832s] factorial(3) -> 6
[0.00004899s] factorial(4) -> 24
[0.00005926s] factorial(5) -> 120
[0.00007111s] factorial(6) -> 720
6! = 720

如下代码:

@clock
def factorial(n):
return 1 if n < 2 else n * factorial(n - 1)

等价于:

def factorial(n):
return 1 if n < 2 else n*factorial(n-1)

factorial = clock(factorial)

factorial
会作为
func
参数传给
clock
,然后,
clock
函数会返回
clocked
函数,python在背后会把
clocked
赋值给
factorial
。可以查看
factorial
__name__
属性:

print(factorial.__name__) #clocked

现在

factorial
保存的是
clocked
函数的引用。

每次调用

factorial(n)
,执行的都是
clocked(n)
clocked
大致
做了下面几件事。

  1. 记录初始时间 t0。
  2. 调用原来的
    factorial
    函数,保存结果。
  3. 计算经过的时间。
  4. 格式化收集的数据,然后打印出来。
  5. 返回第 2 步保存的结果。

这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同
的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做些
额外操作。

上面实现的

clock
装饰器有几个缺点:不支持关键字参数,而
且覆盖了被装饰函数的
__name__
__doc__
属性。
在下面的示例中使用
functools.wraps
装饰器把相关的属性从
func
复制到
clocked
中。此外,这个新版还能正确处理关键字参数。

import functools

def clock(func):
@functools.wraps(func)
def clocked(*args,**kwargs): #接受任意个定位参数和关键字参数
t0 = time.time()
result = func(*args,**kwargs)#clocked的闭包中包含自由变量func
elapsed = time.time() - t0
name = func.__name__
arg_lst = []
if args:
arg_lst.append(', '.join(repr(arg) for arg in args))
if kwargs:
pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
arg_lst.append(', '.join(pairs))

arg_str = ', '.join(arg_lst)

print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked#返回内部函数,取代被装饰的函数

functools.wraps
只是标准库中拿来即用的装饰器之一。

标准库中的装饰器

标准库中最值得关注的两个装饰器是

lru_cache
和全新的
singledispatch
(Python 3.4 新增)。这两个装饰器都在
functools
模块中定义。

使用functools.lru_cache做备忘

functools.lru_cache
是非常实用的装饰器,它实现了备忘
(memoization)功能。把耗时的函数的结果保存
起来,避免传入相同的参数时重复计算。LRU 三个字母是“Least
Recently Used”的缩写,表明缓存不会无限制增长,一段时间不用的缓存
条目会被扔掉。

生成第 n 个斐波纳契数这种慢速递归函数适合使用

lru_cache
:

from clock_deco import clock

@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)

if __name__ == '__main__':
print(fibonacci(6))

输出:

[0.00000119s] fibonacci(0) -> 0
[0.00000079s] fibonacci(1) -> 1
[0.00008020s] fibonacci(2) -> 1
[0.00000040s] fibonacci(1) -> 1
[0.00000040s] fibonacci(0) -> 0
[0.00000040s] fibonacci(1) -> 1
[0.00002252s] fibonacci(2) -> 1
[0.00004425s] fibonacci(3) -> 2
[0.00014736s] fibonacci(4) -> 3
[0.00000040s] fibonacci(1) -> 1
[0.00000040s] fibonacci(0) -> 0
[0.00000079s] fibonacci(1) -> 1
[0.00002094s] fibonacci(2) -> 1
[0.00004227s] fibonacci(3) -> 2
[0.00000040s] fibonacci(0) -> 0
[0.00000040s] fibonacci(1) -> 1
[0.00002094s] fibonacci(2) -> 1
[0.00000040s] fibonacci(1) -> 1
[0.00000040s] fibonacci(0) -> 0
[0.00000079s] fibonacci(1) -> 1
[0.00002291s] fibonacci(2) -> 1
[0.00004267s] fibonacci(3) -> 2
[0.00008889s] fibonacci(4) -> 3
[0.00015289s] fibonacci(5) -> 5
[0.00032118s] fibonacci(6) -> 8
8

浪费时间的地方很明显:

fibonacci(1)
调用了 8次,
fibonacci(2)
调用了 5 次……但是,如果增加两行代码,使用
lru_cache
,性能会显著改善:

from clock_deco import clock
import functools

@functools.lru_cache() #像常规函数那样调用,因为可以接收参数
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)

if __name__ == '__main__':
print(fibonacci(6))

输出:

[0.00000040s] fibonacci(0) -> 0
[0.00000040s] fibonacci(1) -> 1
[0.00008573s] fibonacci(2) -> 1
[0.00000079s] fibonacci(3) -> 2
[0.00010509s] fibonacci(4) -> 3
[0.00000079s] fibonacci(5) -> 5
[0.00012168s] fibonacci(6) -> 8
8

每个值只调用了一次,执行时间优化了一半多。

lru_cache
可以使用两个可选的参数来配置。它的签名是:
functools.lru_cache(maxsize=128, typed=False)

  • maxsize 知道缓存多少个调用结果
  • typed 参数如果设为 True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区分开。

单分派泛函数

Python 3.4 新增的

functools.singledispatch
装饰器可以把整体
方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。使
@singledispatch
装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数。

singledispatch
创建一个自定义的htmlize.register 装饰器,把多个函数绑在一起组成一个泛函数:

from functools import singledispatch
from collections import abc
import numbers
import html

@singledispatch #@singledispatch 标记处理 object 类型的基函数。
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)

@htmlize.register(str) #各个专门函数使用@«base_function».register(«type») 装饰
def _(text): #专门函数的名称无关紧要
content = html.escape(text).replace('\n', '<br>\n')
return '<p>{0}</p>'.format(content)

@htmlize.register(numbers.Integral) #为每个需要特殊处理的类型注册一个函数。numbers.Integral 是int 的虚拟超类
def _(n):
return '<pre>{0} (0x{0:x})</pre>'.format(n)
@htmlize.register(tuple) #可以叠放多个 register 装饰器,让同一个函数支持不同类型
@htmlize.register(abc.MutableSequence)
def _(seq):
inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n</ul>'

只要可能,注册的专门函数应该处理抽象基类(如

numbers.Integral
abc.MutableSequence
),不要处理具体
实现(如
int
list
)。这样,代码支持的兼容类型更广泛。

装饰器是函数,因此可以组合起来使用(即,可以在已经被装饰的函数上应用装饰器)

叠放装饰器

@d1
@d2
两个装饰器按顺序应用到 f 函数上,作用相当于
f =d1(d2(f))

@d1
@d2
def f():
print('f')

等同于:

def f():
print('f')
f = d1(d2(f)) #注意顺序

参数化装饰器

如何让装饰器接受其他参数呢?答案是:创建一个装饰器
工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰
的函数上。

我们用

registration.py
模块的删减版为例:

registry = []
def register(func):
print('running register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('running f1()')

print('running main()')
print('registry ->', registry)
f1()

一个参数化的注册装饰器

为了便于启用或禁用 register 执行的函数注册功能,我们为它提供
一个可选的 active 参数,设为 False 时,不注册被装饰的函数:

# -*- coding: utf-8 -*
registry = set() #set对象,使得添加和删除函数的速度更快
def register(active=True): #接收一个可选的关键字参数
def decorate(func): #decorate这个内部函数才是真正的装饰器,参数是一个函数
print('running register(active=%s)->decorate(%s)' % (active, func))
if active: #只有active参数的值(从闭包中获得)是True时才注册func
registry.add(func)#存入registry
else:
registry.discard(func) #实现反向注册(注销)功能
return func #decorate 是装饰器,必须返回一个函数
return decorate#register 是装饰器工厂函数,返回 decorate

#@register工厂函数必须作为函数调用,并传入所需的参数
@register(active=False)
def f1():
print('running f1()')

#哪怕不传参数,也要作为函数调用
@register()
def f2():
print('running f2()')

def f3():
print('running f3()')

从概念上看,这个新的 register 函数不是装
饰器,而是装饰器工厂函数。调用它会返回真正的装饰器,这才是应用
到目标函数上的装饰器。

如果导入上面的模块,得到的结果如下:

>>> import registration
running register(active=False)->decorate(<function f1 at 0x000001CEA7FBE8C8>)
running register(active=True)->decorate(<function f2 at 0x000001CEA7FBE950>)
>>>registration.registry
{<function f2 at 0x0000026C7B43E950>}

如果不使用

@
句法,那就要像常规函数那样使用
register
;若想把 f
添加到
registry
中,则装饰 f 函数的句法是
register()(f)
;不
想添加(或把它删除)的话,句法是
register(active=False)(f)

下面演示了如何把函数添加到
registry
中,以及如何从中删除函数:

>>> from registration import *
running register(active=False)->decorate(<function f1 at 0x0000028C925CE8C8>)
running register(active=True)->decorate(<function f2 at 0x0000028C925CE950>)
>>> registry #导入模块时,只有f2在registry中
{<function f2 at 0x0000028C925CE950>}
>>> register()(f3) #register() 表达式返回 decorate,然后把它应用到 f3 上
running register(active=True)->decorate(<function f3 at 0x0000028C925CE840>)
<function f3 at 0x0000028C925CE840>
>>> registry#上面把f3注册到了registry中
{<function f3 at 0x0000028C925CE840>, <function f2 at 0x0000028C925CE950>}
>>> register(active=False)(f2)#将f2删除
running register(active=False)->decorate(<function f2 at 0x0000028C925CE950>)
<function f2 at 0x0000028C925CE950>
>>> registry#确认f2已经删除
{<function f3 at 0x0000028C925CE840>}
  • 点赞 2
  • 收藏
  • 分享
  • 文章举报
愤怒的可乐 博客专家 发布了151 篇原创文章 · 获赞 186 · 访问量 14万+ 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: