一文读懂python装饰器由来(二)

Python中文社区 全球Python中文开发者的 精神部落

--  Illustrations by Charlie Davis  --

上一篇文章主要以一步一步演进的方式介绍了装饰器的工作原理以及使用(没看的小伙伴可以关注一下 一文读懂Python装饰器由来(一)),其实只要认真学习上一篇文章,已经能够满足日常对装饰器的使用了。但是,若想真正理解装饰器,并进行更高阶的使用还要了解其他一些知识:

  1. python中,函数是一等对象;

  2. 区分导入时执行和运行时执行;

  3. 闭包和 nonlocal 声明;

下面我们逐个介绍:

第一点,在 Python 中,函数是一等对象,这在上一篇其实已经提到了。“一等对象”满足下述条件: 

a.在运行时创建; 

b.能赋值给变量或数据结构中的元素; 

c.能作为参数传给函数; 

d.能作为函数的返回结果;

Python 中的整数、字符串和字典等都是一等对象,大家对比着理解一下,在此不再过多介绍。 第二点,函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。看下面的例子:

 
 
  1. al = []

  2. def deco(func):

  3.    print('running deco and parm is{}'.format(func))

  4.    al.append(func)

  5.    return func

  6. @deco

  7. def f1():

  8.    print('running f1()')

  9. @deco

  10. def f2():

  11.    print('running f2()')

  12. def f3():

  13.    print('running f3()')

  14. def main():

  15.    print('running main()')

  16.    print('al ->', al)

  17.    f1()

  18.    f2()

  19.    f3()

  20. if __name__=='__main__':

  21.    main()

输出:

 
 
  1. running deco and parm is<function f1 at 0x00000000006C2AE8>

  2. running deco and parm is<function f2 at 0x00000000011E6510>

  3. running main()

  4. al -> [<function f1 at 0x00000000006C2AE8>, <function f2 at 0x00000000011E6510>]

  5. running f1()

  6. running f2()

  7. running f3()

我们简单定义了一个装饰器,把传进来的参数(函数名)添加到列表,然后再返回该函数名。观察输出结果,在运行main函数之前,deco就已经运行了(输出了2次,因为f1和f2都用deco进行了装饰),之后对列表的输出也印证了这一点,而不管是被装饰的f1、f2还是未被装饰的f3都是在明确的调用之后才执行的。这就是Python 程序员所说的导入时和运行时之间的区别。 第三点,闭包可以说是行为良好的装饰器赖以生存的关键。闭包其实并不难以理解,因为它只存在于嵌套函数中。还是看例子:

 
 
  1. def get_averager():

  2.    nums = []

  3.    def averager(new_value):

  4.        nums.append(new_value)

  5.        total = sum(nums)

  6.        return total/len(nums)

  7.    return averager

  8. avg = get_averager()

  9. print(avg)

  10. print(avg(10))

  11. print(avg(11))

  12. print(avg(12))

输出:

 
 
  1. <function get_averager.<locals>.averager at 0x0000000000672AE8>

  2. 10.0

  3. 10.5

  4. 11.0

定义一个嵌套函数,作用是计算累计传入参数的平均值。通过输出结果我们可以看到avg是getaverager()返回的averager,通过不断的调用avg(),返回当前的平均值。这里面有个问题是我们之前没有探讨的:nums是外层函数中的变量,那么在getaverager()返回完毕之后,它的本地作用域应该一并消失,那为什么avg中还可以使用呢?这就是闭包的作用了。其实,闭包就是指函数作用域延伸了(从外层函数延伸到内层函数)。延伸的值保存在内层函数的code属性中:

 
 
  1. >>> def get_averager():

  2.    nums = []

  3.    def averager(new_value):

  4.        nums.append(new_value)

  5.        total = sum(nums)

  6.        return total/len(nums)

  7.    return averager

  8. >>> avg = get_averager()

  9. >>> avg.__code__.co_freevars

  10. ('nums',)

我们注意到上面这个例子把所有值存储在历史列表中,然后在每次调用 averager 时使用 sum 求和。更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个数计算均值。依照这个思路我们可以对代码进行优化,但是在此之前我们需要看一个简单的例子:

 
 
  1. >>> b = 99

  2. >>> def f(t):

  3.    print(t)

  4.    print(b)

  5.    b = 2  

  6. >>> f(10)

各位可以想象一下,这个输出会是什么?

 
 
  1. 10

  2. 99

是不是这个?其实不然,真实的结果是这样:

 
 
  1. >>> f(10)

  2. 10

  3. Traceback (most recent call last):

  4.  File "<pyshell#42>", line 1, in <module>

  5.    f(10)

  6.  File "<pyshell#41>", line 3, in f

  7.    print(b)

  8. UnboundLocalError: local variable 'b' referenced before assignment

  9. >>>

这个结果可能让你惊讶,但事实就是如此。因为Python 编译函数的定义体时,由于b在函数中给它赋值了,因此它判断 b 是局部变量。后面调用 f(10) 时, f 的定义体会获取并打印局部变量 b的值,但是尝试获取局部变量 b的值时,发现 b 没有绑定值。这不是缺陷,而是设计选择:Python 不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。了解了这一点,我们来优化一下之前计算平均值的例子:

 
 
  1. def get_averager():

  2.    count = 0

  3.    total = 0

  4.    def averager(new_value):

  5.        count += 1

  6.        total += new_value

  7.        return total / count

  8.    return averager

逻辑上看没啥问题,但是有了之前的铺垫,你可能会发现一些问题:内层函数对外层函数中的变量进行了重新赋值。我们来运行一下代码,就会发现报错:

 
 
  1. UnboundLocalError: local variable 'count' referenced before assignment

而优化前的例子没遇到这个问题,因为nums是列表,我们只是调用 nums.append,也就是说,我们利用了列表是可变的对象这一事实。但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如 count = count + 1,其实会隐式创建局部变量 count。 为了解决这个问题,Python 3 引入了 nonlocal 声明,如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。

 
 
  1. >>> def get_averager():

  2.    count = 0

  3.    total = 0

  4.    def averager(new_value):

  5.        nonlocal count, total

  6.        count += 1

  7.        total += new_value

  8.        return total / count

  9.    return averager

  10. >>> avg = get_averager()

  11. >>> avg(10)

  12. 10.0

  13. >>> avg(11)

  14. 10.5

  15. >>> avg(12)

  16. 11.0

以上三点就是对装饰器基础知识的补充,希望对大家有所帮助。

赞赏作者

最近热门文章

用Python更加了解微信好友

如何用Python做一个骚气的程序员

用Python爬取陈奕迅新歌《我们》10万条评论的新发现

用Python分析苹果公司股价数据

Python自然语言处理分析倚天屠龙记

▼ 点击下方阅读原文免费成为社区会员

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值