《流畅的Python》(7)—— 装饰器
Python装饰器是笔者至少学习过三遍知识点,通过《Fluent Python》算是基本上弄清楚装饰器的基础了,写下一些心得体会与思考。
包括的内容有:
- Python如何计算装饰器句法
- Python如何判断变量是不是局部的
- 闭包存在的原因和工作原理
- nonlocal解决问题
1. 基础知识
装饰器是可调用的对象,
输入:函数
输出:函数
1.1句法
假设有一个装饰器decorator,将函数target装饰的句法如下:
@decorator
def target():
print("running target")
上述代码等价于:
def target():
print("running target")
target = decorator(target)
这样操作后原本target的功能没有改变,但是可以在target函数的上下文增加功能,类似叠汉堡,肉饼是点好不变的,可以加不同的蔬菜和沙司。
1.2 装饰器运行阶段:导入模块时即运行
1.3 函数的作用域
函数的作用域对任何语言都很重要,在C语言中,函数的作用域就是{}之内,如果想调用{}之外的变量,那必须是全局变量。而在Python中,情况有所不同,需要点出来:
- 函数是由解释器成块编译的,而不是一行行的编译并运行
- 如果函数使用的变量名在块内没有定义过,那么它会向上一个块寻找该变量名
一个问题: 请问下列代码运行结果是什么?
b = 6
def f2(a):
print(a)
print(b)
b = 9
f2(3)
结果是:报错,b变量在声明前就使用,原因是:编译是按块编译,由于b=9在块中出现,使得解释器认为b是个局部变量,而不是全局遍历,而print(b)在b=9之前,所以造成错误。
以上两点结论可以通过查看函数的字节码中获得。
那么该如何解决这个问题呢?在这个问题中用global关键字解决,申明变量b为全局变量
下面我们看闭包。
2. 闭包(closure)
2.1闭包的基础
闭包:是引用了自由变量的函数。那么自由变量是什么呢?
自由变量:未在本地作用域中绑定的变量。
以一个计算滑动平均值的例子来说:
def make_average():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return averager
avg = make_average()
这里averager是函数闭包函数,因为它调用了series这个自由变量。series是自由变量的原因是:series在averager的作用域中没有声明或定义。
那么series不绑定在averager的本地作用域那么它应该存在哪里呢?
首先一个函数被定义或导入时,就会在内存中存储。其次查看make_average中的局部变量
In [11]: make_average.__code__.co_varnames
Out[11]: ('averager',)
从make_average.code .co_varnames()(函数的局部变量),可以看到只有average这一个局部变量,而series不在make_average中,那series就应该在averager中,最后的答案是在avg.__closure __.cell_content中保存,说明装饰器的运行由闭包属性支撑。
2.2 再论函数作用域
在闭包中也会出现1.3的问题,比如2.1中的计算滑动平滑的方法效率很低,为了计算平均值我们并不需要记录所有的值,只需要记录数据总和与数据个数两个量,直觉上修改为。
def make_average():
total = 0
count = 0
def averager(new_value):
total += new_value
count += 1
return total / count
return averager
avg = make_average()
avg(1)
出现同样的错误:
UnboundLocalError: local variable 'total' referenced before assignment
这是因为 total += new_value 等价于 total = total + new_value ,还是在同样的原因:在total没有定义时就在等式右侧使用了它。
解决方案:用nonlocal关键字声明两个变量为自由变量。
def make_average():
total = 0
count = 0
def averager(new_value):
nonlocal total, count
total += new_value
count += 1
return total / count
return averager
avg = make_average()
avg(1)
avg(2)
这时程序运行正常。
3. 装饰器
第二节我们说了很多关于闭包的知识,那么这和我们的装饰器的关系是什么?
3.1 装饰器与闭包关系
从例子入手,一个可以给任何函数打印函数名的装饰器:
def print_name(func):
def wrapper(*args, **kw):
print("Now %s is running."%func.__name__)
res = func(*args, **kw)
return res
return wrapper
# example
@print_name
def add(a, b):
return a + b
add(1, 2)
在这个例子中,print_name就是一个装饰器,里面的函数为闭包,因为它调用自由变量 func,func不在wrapper中声明而是在print_name中,所以这是一个自由变量。装饰器必须要用到闭包的特性来实现。
3.2 标准库中的装饰器
Python内置的装饰器有:property、classmethod和staticmethod,这些将在面向对象环节中再阐述。此外,一个常见的装饰器为:functools.lru_cache做备忘,可以缓存函数输出的结果。
unctools.lru_cache做备忘
可能会使用在一些运算时间久,但是输入情况有限的情况,比如阶乘。
import time
import functools
def clock(func):
def clocked(*args, **kw):
t0 = time.perf_counter()
result = func(*args, **kw)
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
@functools.lru_cache() # 由于lru_cache是接受参数的装饰器,所以要加括号
@clock
def factorial(n):
return 1 if n < 2 else n*factorial(n-1)
factorial(31)
单分配泛函数functools.singledispatch
用于解决Python没有方法或函数重载的问题(Python中只有重写函数和方法没有重载)。
from functools import singledispatch
from collections import abc
import numbers
import html
@singledispatch ➊
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
@htmlize.register(str) ➋
def _(text): ➌
content = html.escape(text).replace('\n', '<br>\n')
return '<p>{0}</p>'.format(content)
@htmlize.register(numbers.Integral) ➍
def _(n):
return '<pre>{0} (0x{0:x})</pre>'.format(n)
@htmlize.register(tuple) ➎
@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>'