函数装饰器和闭包
严格来说,装饰器只是语法糖。如前所示,装饰器可以像常规的可调用
对象那样调用,其参数是另一个函数。有时,这样做更方便,尤其是做元编程(在运行时改变程序的行为)时。Python何时执行装饰器
它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 Python 加载模块时)
registry = []def register(func): print('running register(%s)' % func) registry.append(func) return func@registerdef f1(): print('running f1()')@registerdef f2(): print('running f2()')def f3(): print('running f3()')def main(): print('running main()') print('registry ->', registry) f1() f2() f3()if __name__=='__main__': main()
把 registration.py 当作脚本运行得到的输出如下:
$ python3 registration.pyrunning register()running register( )running main()registry -> [ , ]running f1()running f2()running f3()
如果导入 registration.py 模块(不作为脚本运行),输出如下:
>>> import registrationrunning register()running register( )
此时查看 registry 的值,得到的输出如下:
>>> registration.registry[, ]
装饰器在真实代码中的常用方式
装饰器函数与被装饰的函数在同一个模块中定义。实际情况是,装
饰器通常在一个模块中定义,然后应用到其他模块中的函数上。使用装饰器改进“策略”模式
promos = []def promotion(promo_func): promos.append(promo_func) return@promotiondef fidelity(order): """为积分为1000或以上的顾客提供5%折扣""" return order.total() * .05 if order.customer.fidelity >= 1000 else 0@promotiondef bulk_item(order): """单个商品为20个或以上时提供10%折扣""" discount = 0 for item in order.cart: if item.quantity >= 20: discount += item.total() * .1 return discount@promotiondef large_order(order): """订单中的不同商品达到10个或以上时提供7%折扣""" distinct_items = {item.product for item in order.cart} if len(distinct_items) >= 10: return order.total() * .07 return 0def best_promo(order): """选择可用的最佳折扣""" return max(promo(order) for promo in promos)
- promotion 把 promo_func 添加到 promos 列表中,然后原封不动地将其返回。
- 被 @promotion 装饰的函数都会添加到 promos 列表中。
与 6.1 节给出的方案相比,这个方案有几个优点。
- 促销策略函数无需使用特殊的名称(即不用以 _promo 结尾)。
- @promotion 装饰器突出了被装饰的函数的作用,还便于临时禁用
- 某个促销策略:只需把装饰器注释掉。
- 促销折扣策略可以在其他模块中定义,在系统中的任何地方都行,只要使用 @promotion 装饰即可。
变量作用域规则
神奇的例子
>>> b = 6>>> def f2(a):... print(a)... print(b)... b = 9...>>> f2(3)3Traceback (most recent call last):File "", line 1, in File " ", line 3, in f2UnboundLocalError: local variable 'b' referenced before assignment
b = 9###### 它判断 b 是局部变量,因为在函数中给它赋值了
- 可事实是,Python 编译函数的定义体时,它判断 b 是局部变量,因为在函数中给它赋值了。
- 生成的字节码证实了这种判断,Python 会尝试从本地环境获取 b。
- 后面调用 f2(3) 时, f2 的定义体会获取并打印局部变量 a 的值,但是尝试获取局部变量 b 的值时,发现 b 没有绑定值。
为什么会这样
- 这不是缺陷,而是设计选择:Python 不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。
- 这比 JavaScript 的行为好多了,JavaScript 也不要求声明变量,但是如果忘记把变量声明为局部变量使用 var),可能会在不知情的情况下获取全局变量。
利用global就可以啦
>>> b = 6>>> def f3(a):... global b... print(a)... print(b)... b = 9...>>> f3(3)36
闭包
人们有时会把闭包和匿名函数弄混。
这是有历史原因的:在函数内部定义函数不常见,直到开始使用匿名函数才会这样做,注意:
- 函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
- 只有涉及嵌套函数时才有闭包问题。因此,很多人是同时知道这两个概念的。
案例
假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值;
初学者可能会这样
class Averager(): def __init__(self): self.series = [] def __call__(self, new_value): self.series.append(new_value) total = sum(self.series) return total / len(self.series)
>>> avg = Averager()>>> avg(10)10.0>>> avg(11)10.5>>> avg(12)11.0
函数式实现,使用高阶函数 make_averager。
def make_averager(): series = [] def averager(new_value): series.append(new_value) total = sum(series) return total / len(series) return averager
重要概念
在 averager 函数中,series 是自由变量(free variable)。这是一个
技术术语,指未在本地作用域中绑定的变量.
审查 make_averager(见示例 7-9)创建的函数
>>> avg.__code__.co_varnames('new_value', 'total')>>> avg.__code__.co_freevars('series',)
- series 的绑定在返回的 avg 函数的 closure 属性中。
- avg.__closure__ 中的各个元素对应于avg.__code__.co_freevars 中的一个名称。
- 这些元素是 cell 对象,有个 cell_contents 属性,保存着真正的值。
>>> avg.__code__.co_freevars('series',)>>> avg.__closure__(,)>>> avg.__closure__[0].cell_contents[10, 11, 12] |
小总结
- 综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,
- 这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
- 注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。
nonlocal声明
计算移动平均值的高阶函数,不保存所有历史值,但有 缺陷
def make_averager(): count = 0 total = 0 def averager(new_value): count += 1 total += new_value return total / count return averager
- 问题是,当 count 是数字或任何不可变类型时
count += 1 语句的作用其实与 count = count + 1 一样。 因此,我们在 averager 的定义体中为 count 赋值了,这会把 count 变成局部变量。
注意
示例 7-9 没遇到这个问题,因为我们没有给 series 赋值,我们只是调 用 series.append,并把它传给 sum 和 len。也就是说,我们利用了 列表是可变的对象这一事实。但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。
如果尝试重新绑定,例如 count = count + 1,其实会隐式创建局部变量 count。这样,count 就不是自由变量了,因此不会保存在闭包中。
解决这个问题
为了解决这个问题,Python 3 引入了 nonlocal 声明。它的作用是把变
量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。def make_averager(): count = 0 total = 0 def averager(new_value): nonlocal count,total count += 1 total += new_value return total / count return averager
对付没有 nonlocal 的 Python 2
基本上,这种处理方式是把内部函数需要修改 的变量(如 count 和 total)存储为可变对象(如字典或简单的 实例)的元素或属性,并且把那个对象绑定给一个自由变量。
实现一个简单的装饰器
import timedef clock(func): def clocked(*args): t0 = time.perf_counter() result = func(*args) elapsed = time.perf_counter() - t0 name = func.__name__ args_str = ''.join(repr(arg) for arg in args) print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, args_str, result)) return result return clocked@clockdef snooze(seconds): time.sleep(seconds)@clockdef factorial(n): return 1 if n < 2 else n * factorial(n - 1)if __name__ == "__main__": print("*" * 40) snooze(0.123) print("*" * 40) factorial(6) ## 这里的函数对象变成了从clocked print(factorial.__name__)
这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,而且(通
常)返回被装饰的函数本该返回的值,同时还会做些额外操作。
上面实现的 clock 装饰器有几个缺点:不支持关键字参数,而且遮盖了被装饰函
数的 name 和 doc 属性。使用 functools.wraps 装饰器把相关的属性从 func 复制到 clocked 中。此外,这个新版还能正确处理关键字参数。
import timeimport functoolsdef clock(func): @functools.wraps(func) ###这里 保留__name__ 和 __doc__ 属性 def clocked(*args, **kwargs): t0 = time.time() result = func(*args, **kwargs) 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.lru_cache 是非常实用的装饰器,它实现了备忘(memoization)功能。
就是更加利用缓存干活import timeimport functoolsdef clock(func): @functools.wraps(func) def clocked(*args, **kwargs): t0 = time.time() result = func(*args, **kwargs) 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@clockdef fibonacci(n): if n < 2: return n return fibonacci(n - 2) + fibonacci(n - 1)if __name__ == '__main__': print(fibonacci(6))
浪费时间的地方很明显:fibonacci(1) 调用了 8 次,fibonacci(2) 调用了 5 次……但是,如果增加两行代码,使用 lru_cache,性能会显著改善,
import timeimport functoolsdef clock(func): @functools.wraps(func) def clocked(*args, **kwargs): t0 = time.time() result = func(*args, **kwargs) 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.lru_cache() # @clock #def fibonacci(n): if n < 2: return n return fibonacci(n - 2) + fibonacci(n - 1)if __name__ == '__main__': print(fibonacci(6))
❶ 注意,必须像常规函数那样调用 lru_cache。这一行中 有一对括 号:@functools.lru_cache()。这么做的原因是,lru_cache 可以接受配置参数,稍 后说明。
lru_cache 可以使用两个可选的参数来配置。
functools.lru_cache(maxsize=128, typed=False)
- maxsize 参数指定存储多少个调用的结果。
- 缓存满了之后,旧的结果会被扔掉,腾出空间。
- 为了得到最佳性能,maxsize 应该设为 2 的幂。typed 参数如果设为 True,把不同
- 参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区
分开。
functools.singledispatch 装饰器 让Python强行支持重载方法
因为 Python 不支持重载方法或函数,所以我们不能使用不同的签名定义 htmlize 的变体,也无法使用不同的方式处理不同的数据类型。
使用 @singledispatch 装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数。
让Python强行支持重载方法,不再使用一串 if/elif/elif,调用专门的函数
singledispatch 创建一个自定义的 htmlize.register 装饰器,把多
个函数绑在一起组成一个泛函数
from functools import singledispatchfrom collections import abcimport numbersimport html@singledispatchdef htmlize(obj): content = html.escape(repr(obj)) return '{}'.format(content)@htmlize.register(str)def _(text): content = html.escape(text).replace('\n', '\n') return '{0}
'.format(content)@htmlize.register(numbers.Integral)def _(n): return '{0} (0x{0:x})'.format(n)@htmlize.register(tuple)@htmlize.register(abc.MutableSequence)def _(seq): inner = '\n
- \n
- ' + inner + ' \n
❷ 各个专门函数使用 @«base_function».register(«type») 装饰。
❸ 专门函数的名称无关紧要;_ 是个不错的选择,简单明了。为每个需要特殊处理的类型注册一个函数。numbers.Integral 是 int 的虚拟超类。❺ 可以叠放多个 register 装饰器,让同一个函数支持不同类型。只要可能,注册的专门函数应该处理抽象基类(如 numbers.Integral 和
abc.MutableSequence),不要处理具体实现(如 int 和 list)。这样,代码支持的兼容类型更广泛。例如,Python 扩展可以子类化 numbers.Integral,使用固定的位数实 现 int 类型。
注意:
- @singledispatch 不是为了把 Java 的那种方法重载带入 Python。在一个类中 为同一个方法定义多个重载变体,
- @singledispath 的优点是支持模块化扩展:各个模块可以为它支持的各个类型注册一个专门函数。
叠放装饰器
@d1@d2def f(): print('f')
等同于
def f(): print('f')f = d1(d2(f))
一个参数化的注册装饰器
为了便于启用或禁用 register 执行的函数注册功能,我们为它提供一个可选的 active
参数,设为 False 时,不注册被装饰的函数。registry = set()def register(active=True): def decorate(func): print('running register(active=%s)->decorate(%s)' % (active, func)) if active: registry.add(func) else: registry.discard(func) return func return decorate@register(active=False)def f1(): print('running f1()')@register()def f2(): print('running f2()')def f3(): print('running f3()')if __name__ =="__main__": print(registry)
参数化装饰器通常会把被装饰的函数替换掉,而且结构上需要多一层嵌套。
参数化clock装饰器
import timeDEFAULT_FMT = '花费时间:[{elapsed:0.5f}s] 程序名:{name} 参数:({args}) -> 结果:{result}'def clock(fmt=DEFAULT_FMT): def decorate(func): def clocked(*_args): t0 = time.time() _result = func(*_args) ### locals() 局部变量 elapsed = time.time() - t0 name = func.__name__ args = ', '.join(repr(arg) for arg in _args) result = repr(_result) # 这里不知道他为什么这么能用 print(fmt.format(**locals())) return _result return clocked return decorateif __name__ == '__main__': # ## 第一种情况 # @clock() # def snooze(seconds): # time.sleep(seconds) ## 第二种情况 # @clock('程序名:{name}: 花费时间:{elapsed}s') # def snooze(seconds): # time.sleep(seconds) ## 第三种情况 @clock('程序名:{name} 参数:({args}) 花费时间:dt={elapsed:0.3f}s') def snooze(seconds): time.sleep(seconds) snooze(0.123)
clock 是参数化装饰器工厂函数
❷ decorate 是真正的装饰器。❸ clocked 包装被装饰的函数。❹ _result 是被装饰的函数返回的真正结果这里的locals()是啥不知道
def runnoob(arg:'int'): z = 1 print(arg + 1) # 返回字典类型的局部变量。 print('==='*30) print(locals()) # 返回字典类型的全部变量。 print('=' * 50) print(globals())num = 8runnoob(num)
小总结
- 严格来说,装饰器只是语法糖。
- 它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 Python 加载模块时)
- 装饰器改进了策略模式
闭包
- 闭包是一种函数,它会保留定义函数时存在的自由变量的绑定
- Python 3 引入了 nonlocal 声明。它的作用是把变
量标记为自由变量
- functools.wraps 装饰器把相关的属性从 func 复制到 clocked 中。此外,这个新版还能正确处理关键字参数
functools.lru_cache 是非常实用的装饰器,它实现了备忘(memoization)功能。就是更加利用缓存干活
functools.singledispatch 装饰器 让Python强行支持重载方法
locals() globals()
- *locals
- ** locals()