装饰器基础知识
- 装饰器是一个可调用对象,其参数是另一个函数
- 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或者可调用对象。
一个例子
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 """
series
是 make_average
函数的局部变量,因为那个函数的定义体中初始化了 series: series = []
.可是,调用 avg(10)
时, make_average
函数已经返回了,而它的本地作用域也一去不复返了。
在 average
函数中,series
是自由变量(free variable)。 这是一个技术术语,指未在本地作用域中绑定的变量。
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()