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

-- Illustrations by Charlie Davis --

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

  1. python中,函数是一等对象;
  2. 区分导入时执行和运行时执行;
  3. 闭包和 nonlocal 声明;

下面我们逐个介绍:

第一点,在 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自然语言处理分析倚天屠龙记

原文发布于微信公众号 - Python中文社区(python-china)

原文发表时间:2018-05-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏chenjx85的技术专栏

leetcode-551-Student Attendance Record I(判断是否出现连续几个相同字符)

15360
来自专栏用户2442861的专栏

2014阿里巴巴 实习生电面题目:输出给定字符串的全部连续子串

转载请注明出处:http://blog.csdn.net/ns_code/article/details/21043665

12610
来自专栏开发技术

排序之希尔排序(shell sort)

本篇博客是在伍迷兄的博客基础上进行的,其博客地址点击就可以进去,里面好博客很多,我的排序算法都来自于此;一些数据结构方面的概念我就不多阐述了,伍迷兄的博客中都...

14830
来自专栏AzMark

Python函数

25170
来自专栏Leetcode名企之路

【Leetcode】58. 最后一个单词的长度

这个题比较水,主要是注意一下前后有空格这种情况。 如下代码用preLong记录截止到当前字符最后一个单词的长度.

11420
来自专栏大数据杂谈

从 Zero 到 Hero ,一文掌握 Python

21690
来自专栏AzMark

Python字符串

20350
来自专栏诸葛青云的专栏

C语言for语句用法详解

在C语言中,for语句使用最为灵活,它完全可以取代 while 语句。它的一般形式为:

10300
来自专栏用户画像

7.7.3 多路平衡归并与败者树

归并趟数S=[logm R](向下取整)。从而增加归并路数m可以减少归并趟数S,进而减少访问外存的次数(I/O次数)。然而,当增加归并路数m时,内部归并时间将增...

9120
来自专栏云霄雨霁

子字符串查找----Rabin-Karp算法(基于散列)

29400

扫码关注云+社区

领取腾讯云代金券