前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文读懂python装饰器由来(二)

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

作者头像
Python中文社区
发布2018-07-26 17:21:14
3870
发布2018-07-26 17:21:14
举报
文章被收录于专栏:Python中文社区Python中文社区

-- Illustrations by Charlie Davis --

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

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

下面我们逐个介绍:

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

a.在运行时创建;

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

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

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

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

代码语言:javascript
复制
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()

输出:

代码语言:javascript
复制
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 程序员所说的导入时和运行时之间的区别。 第三点,闭包可以说是行为良好的装饰器赖以生存的关键。闭包其实并不难以理解,因为它只存在于嵌套函数中。还是看例子:

代码语言:javascript
复制
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))

输出:

代码语言:javascript
复制
<function get_averager.<locals>.averager at 0x0000000000672AE8>
10.0
10.5
11.0

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

代码语言:javascript
复制
>>> 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 求和。更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个数计算均值。依照这个思路我们可以对代码进行优化,但是在此之前我们需要看一个简单的例子:

代码语言:javascript
复制
>>> b = 99
>>> def f(t):
    print(t)
    print(b)
    b = 2   
>>> f(10)

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

代码语言:javascript
复制
10
99

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

代码语言:javascript
复制
>>> 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 不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。了解了这一点,我们来优化一下之前计算平均值的例子:

代码语言:javascript
复制
def get_averager():
    count = 0
    total = 0
    def averager(new_value):
        count += 1
        total += new_value
        return total / count
    return averager

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

代码语言:javascript
复制
UnboundLocalError: local variable 'count' referenced before assignment

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

代码语言:javascript
复制
>>> 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自然语言处理分析倚天屠龙记

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-05-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Python中文社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
NLP 服务
NLP 服务(Natural Language Process,NLP)深度整合了腾讯内部的 NLP 技术,提供多项智能文本处理和文本生成能力,包括词法分析、相似词召回、词相似度、句子相似度、文本润色、句子纠错、文本补全、句子生成等。满足各行业的文本智能需求。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档