当前位置: 代码迷 >> 综合 >> Fluent Python - Part7 函数装饰器和闭包
  详细解决方案

Fluent Python - Part7 函数装饰器和闭包

热度:21   发布时间:2023-10-21 05:22:39.0

装饰器基础知识

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

一个例子

def deco(func):def inner():print("running inner()")return inner@deco
def target():print("running target()")target()
print(target)""" output: running inner() <function deco.<locals>.inner at 0x103fda550> """

Python 何时执行装饰器

  • 装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。即在模块被导入时就运行了。

变量作用域规则

首先看一个例子

>>> b = 6
>>> def f2(a):
...     print(a)
...     print(b)
...     b = 9
...
>>> 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

奇怪的地方在这里,当执行到print(b)语句时,明明有一个全局变量b,但仍然没有输出它,而是直接抛出异常。造成这种现象的原因是,Python 编译函数的定义体时,它判断 b 是局部变量,因为在函数中给它赋值了。当运行到 print(b) 时,Python 会尝试从本地环境获取 b。但是尝试获取局部变量 b 的值时,发现 b 没有绑定值。

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

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

>>> b = 6
>>> def f2(a):
...     global b
...     print(a)
...     print(b)
...     b = 9
...
>>> f2(3)
3
6

闭包

闭包指延伸了作用域的函数,其中包含了定义体中引用,但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。

一个例子

def make_average():series = []def average(new_value):series.append(new_value)total = sum(series)return total/len(series)return averageavg = make_average()
print(avg(10))
print(avg(11))
print(avg(12))""" output: 10.0 10.5 11.0 """

seriesmake_average 函数的局部变量,因为那个函数的定义体中初始化了 series: series = [].可是,调用 avg(10) 时, make_average 函数已经返回了,而它的本地作用域也一去不复返了。

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

Fluent Python - Part7 函数装饰器和闭包

print(avg.__code__.co_varnames)
print(avg.__code__.co_freevars)
""" output ('new_value', 'total') ('series',) """

series 的绑定在返回的 avg 函数中的 __closure__ 属性中。 avg.__closure__ 中的各个元素对应于 avg.__code__.co_freevars 中的一个名称。这些元素是 cell 对象,cell 对象有个 cell_contents 属性,保存着真正的值。这些属性的值如下所示:

print(avg.__closure__)
print(avg.__closure__[0].cell_contents)
""" output: (<cell at 0x108ebf160: list object at 0x108eb3c00>,) [10, 11, 12] """

nonlocal 声明

一个例子

def make_averager():count = 0total = 0def averager(new_value):count += 1total += new_valuereturn total / countreturn averageravg = make_averager()
print(avg(10))
""" output: Traceback (most recent call last):File "/Users/cguo/conan/b.py", line 13, in <module>print(avg(10))File "/Users/cguo/conan/b.py", line 6, in averagercount += 1 UnboundLocalError: local variable 'count' referenced before assignment """

问题是,当 count 是数字或任何不可变类型时, count += 1 语句的作用其实与 count = count + 1 一样。因此,我们在 averager 的定义体中为 count 赋值了,这会把 count 变成局部变量。
为了解决这个问题, Python3 引入了 nonlocal 声明。 它的作用是把变量标记为自由变量, 即使在函数中为变脸赋予新值了,也会变成自由变量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。

def make_averager():count = 0total = 0def averager(new_value):nonlocal count, totalcount += 1total += new_valuereturn total / countreturn averageravg = make_averager()
print(avg(10))
  • python2 怎么办呢?我们其实可以比较trick的实现,就是将这些变量存储到可变对象当中(如字典或简单的实例)的元素或属性当中,然后把这个对象绑定给一个自由变量。

标准库中的装饰器

使用 functools.lru_cache 做备忘

functools.lru_cache 是非常实用的装饰器,它实现了备忘功能。它有两个可选参数来配置,它的签名是:

functools.lru_cache(maxsize=128, typed=False)

maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize 应该设为2的幂。 typed 参数如果设为 True, 把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0) 区分开。另外,因为 lru_cache 实用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被 lru_cache 装饰的函数,它的所有参数都必须是可散列的。

单分派泛函数

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

一个例子

from functools import singledispatch
from collections import abc
import numbers@singledispatch
def Print(obj):print("hello obj")@Print.register(str)
def _(text):print("{} is str".format(text))@Print.register(numbers.Integral)
def _(n):print("{} is number".format(n))@Print.register(tuple)
def _(seq):print("{} is tuple".format(seq))Print([1,2,3])
Print(1)
Print("1")
Print((1,2,3))""" output: hello obj 1 is number 1 is str (1, 2, 3) is tuple """

参数化装饰器

解析源码中的装饰器时,Python把被装饰的函数作为第一个参数传给装饰器函数。那怎么让装饰器接受其他参数呢?答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。

一个例子

def my_decorator(word):def decorate(func):print(word)def f(*args, **kwargs):return func(*args, **kwargs)return freturn decorate@my_decorator("balabala")
def f1():print("hello world")f1()