-- Illustrations by Charlie Davis --
上一篇文章主要以一步一步演进的方式介绍了装饰器的工作原理以及使用(没看的小伙伴可以关注一下 一文读懂Python装饰器由来(一)),其实只要认真学习上一篇文章,已经能够满足日常对装饰器的使用了。但是,若想真正理解装饰器,并进行更高阶的使用还要了解其他一些知识:
下面我们逐个介绍:
第一点,在 Python 中,函数是一等对象,这在上一篇其实已经提到了。“一等对象”满足下述条件:
a.在运行时创建;
b.能赋值给变量或数据结构中的元素;
c.能作为参数传给函数;
d.能作为函数的返回结果;
Python 中的整数、字符串和字典等都是一等对象,大家对比着理解一下,在此不再过多介绍。 第二点,函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。看下面的例子:
al = []
def deco(func):
print('running deco and parm is{}'.format(func))
al.append(func)
return func
@deco
def f1():
print('running f1()')
@deco
def f2():
print('running f2()')
def f3():
print('running f3()')
def main():
print('running main()')
print('al ->', al)
f1()
f2()
f3()
if __name__=='__main__':
main()
输出:
running deco and parm is<function f1 at 0x00000000006C2AE8>
running deco and parm is<function f2 at 0x00000000011E6510>
running main()
al -> [<function f1 at 0x00000000006C2AE8>, <function f2 at 0x00000000011E6510>]
running f1()
running f2()
running f3()
我们简单定义了一个装饰器,把传进来的参数(函数名)添加到列表,然后再返回该函数名。观察输出结果,在运行main函数之前,deco就已经运行了(输出了2次,因为f1和f2都用deco进行了装饰),之后对列表的输出也印证了这一点,而不管是被装饰的f1、f2还是未被装饰的f3都是在明确的调用之后才执行的。这就是Python 程序员所说的导入时和运行时之间的区别。 第三点,闭包可以说是行为良好的装饰器赖以生存的关键。闭包其实并不难以理解,因为它只存在于嵌套函数中。还是看例子:
def get_averager():
nums = []
def averager(new_value):
nums.append(new_value)
total = sum(nums)
return total/len(nums)
return averager
avg = get_averager()
print(avg)
print(avg(10))
print(avg(11))
print(avg(12))
输出:
<function get_averager.<locals>.averager at 0x0000000000672AE8>
10.0
10.5
11.0
定义一个嵌套函数,作用是计算累计传入参数的平均值。通过输出结果我们可以看到avg是getaverager()返回的averager,通过不断的调用avg(),返回当前的平均值。这里面有个问题是我们之前没有探讨的:nums是外层函数中的变量,那么在getaverager()返回完毕之后,它的本地作用域应该一并消失,那为什么avg中还可以使用呢?这就是闭包的作用了。其实,闭包就是指函数作用域延伸了(从外层函数延伸到内层函数)。延伸的值保存在内层函数的code属性中:
>>> def get_averager():
nums = []
def averager(new_value):
nums.append(new_value)
total = sum(nums)
return total/len(nums)
return averager
>>> avg = get_averager()
>>> avg.__code__.co_freevars
('nums',)
我们注意到上面这个例子把所有值存储在历史列表中,然后在每次调用 averager 时使用 sum 求和。更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个数计算均值。依照这个思路我们可以对代码进行优化,但是在此之前我们需要看一个简单的例子:
>>> b = 99
>>> def f(t):
print(t)
print(b)
b = 2
>>> f(10)
各位可以想象一下,这个输出会是什么?
10
99
是不是这个?其实不然,真实的结果是这样:
>>> f(10)
10
Traceback (most recent call last):
File "<pyshell#42>", line 1, in <module>
f(10)
File "<pyshell#41>", line 3, in f
print(b)
UnboundLocalError: local variable 'b' referenced before assignment
>>>
这个结果可能让你惊讶,但事实就是如此。因为Python 编译函数的定义体时,由于b在函数中给它赋值了,因此它判断 b 是局部变量。后面调用 f(10) 时, f 的定义体会获取并打印局部变量 b的值,但是尝试获取局部变量 b的值时,发现 b 没有绑定值。这不是缺陷,而是设计选择:Python 不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。了解了这一点,我们来优化一下之前计算平均值的例子:
def get_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
而优化前的例子没遇到这个问题,因为nums是列表,我们只是调用 nums.append,也就是说,我们利用了列表是可变的对象这一事实。但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如 count = count + 1,其实会隐式创建局部变量 count。 为了解决这个问题,Python 3 引入了 nonlocal 声明,如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。
>>> def get_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
>>> avg = get_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
以上三点就是对装饰器基础知识的补充,希望对大家有所帮助。
最近热门文章
用Python更加了解微信好友
如何用Python做一个骚气的程序员
用Python爬取陈奕迅新歌《我们》10万条评论的新发现
用Python分析苹果公司股价数据
Python自然语言处理分析倚天屠龙记