python的装饰器和闭包

装饰器的作用:函数装饰器用于在源码中“标记函数”,以某种方式增强函数的行为。

装饰器是可调用的对象,其参数是另一个函数。

如下:

def decorate(func):
    def inner():
        print("i'm king of inner")
    return inner
@decorate
def test():
    print("come baby!")
test()
i'm king of inner

你看test的输出被覆盖了,再看看修改后的

def decorate(func):
    def inner():
        func()
        print("i'm king of inner")
    return inner
@decorate
def test():
    print("come baby!")
test()

come baby!

i'm king of inner

再看看test这个对象

test
Out[7]: <function __main__.decorate.<locals>.inner>

实际上已经变成了inner的引用了

既然提到了闭包,势必会牵扯到变量作用域规则:

如下:

test(3)
3
Traceback (most recent call last):
  File "<ipython-input-9-6d5f8ea96ea8>", line 1, in <module>
    test(3)
  File "<ipython-input-8-b390258981fe>", line 3, in test
    print(b)
NameError: name 'b' is not defined

这个错误很容易理解,毕竟b没有定义,再看看下面这个:

b=2    
def test(a):
    print(a)
    print(b)
    b=9
test(3)
3
Traceback (most recent call last):
  File "<ipython-input-11-6d5f8ea96ea8>", line 1, in <module>
    test(3)
  File "<ipython-input-10-1a4f96c56ff7>", line 4, in test
    print(b)
UnboundLocalError: local variable 'b' referenced before assignment
b
Out[12]: 2

b明明定义了,为啥却依然报错?

因为在编译时,python会认为b是局部变量,这是python的一个设计选择,为了避免变量的污染,想一想。如果某人在函数内部改动了变量,你没有办法看到这个函数,

你再调用b时,你突然发现b变了,这个时候,你该去哪找改动源呢?

如果你非要改,python也提供了global关键字

b=2    
def test(a):
    global b
    print(a)
    print(b)
    b=9
test(3)
3
2
b
Out[15]: 9

但不建议在大型项目中这么玩,如果有这么玩的,请告诉我。

闭包:

定义如下:延伸了作用域的函数,其中包含函数定义体的引用、但是不在定义体中定义的非全局变量。核心在于它能访问定义体之外定义的非全局变量。

给个样例吧:

假设有一个

def avg():

"""

可以计算不断增加的平均值,也就是说,

 avg(20)
 --20
 avg(30)
 --25
 avg(10)
 --20

"""

按照之前的文章,可以使用可调用的对象实现:

class Avg():
    def __init__(self):
        self.items = []
    def __call__(self, item):
        self.items.append(item)
        total = sum(self.items)
        return total / len(self.items)
avg = Avg()
avg(20)
Out[18]: 20.0
avg(30)
Out[19]: 25.0
avg(10)
Out[20]: 20.0

是不是一点都不符合python的美感?

既然函数都是一等对象了,那试试函数?

def avg_closure():
    items = []
    def avgr(item):
        items.append(item)
        total = sum(items)
        return total / len(items)   
    return avgr
avg = avg_closure()
avg(20)
Out[23]: 20.0
avg(30)
Out[24]: 25.0
avg(10)
Out[25]: 20.0

再看看更简洁的写法:

def avg_closure(item):
    items = []
    def avgr(item):
        items.append(item)
        total = sum(items)
        return total / len(items)
    return avgr
@avg_closure
def avg1():
    pass
avg1(20)
Out[30]: 20.0
avg1(30)
Out[31]: 25.0
avg1(10)
Out[32]: 20.0

发现什么没有?其实这两个东西是同一个。。

用dis看看

dis(avg1)
  5           0 LOAD_DEREF               0 (items)
              3 LOAD_ATTR                0 (append)
              6 LOAD_FAST                0 (item)
              9 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             12 POP_TOP
  6          13 LOAD_GLOBAL              1 (sum)
             16 LOAD_DEREF               0 (items)
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 STORE_FAST               1 (total)
  7          25 LOAD_FAST                1 (total)
             28 LOAD_GLOBAL              2 (len)
             31 LOAD_DEREF               0 (items)
             34 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             37 BINARY_TRUE_DIVIDE
             38 RETURN_VALUE
dis(avg)
  5           0 LOAD_DEREF               0 (items)
              3 LOAD_ATTR                0 (append)
              6 LOAD_FAST                0 (item)
              9 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             12 POP_TOP
  6          13 LOAD_GLOBAL              1 (sum)
             16 LOAD_DEREF               0 (items)
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 STORE_FAST               1 (total)
  7          25 LOAD_FAST                1 (total)
             28 LOAD_GLOBAL              2 (len)
             31 LOAD_DEREF               0 (items)
             34 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             37 BINARY_TRUE_DIVIDE
             38 RETURN_VALUE

看出来了吗?

再回到之前的例子

def avg_closure():
    items = []
    def avgr(item):
        items.append(item)
        total = sum(items)
        return total / len(items)   
    return avgr

items引用了外部变量,在这个函数中,items是所谓的自由变量,未在本地作用域中绑定的作用域。

那是不是字符串,数值也是这样呢?

def avg_closure():
    items = 0
    count = 0
    def avgr(item):
        count += 1
        total += items
        return total / count
    return avgr
avg = avg_closure()
avg(10)
Traceback (most recent call last):
  File "<ipython-input-41-2b3d43cb065d>", line 1, in <module>
    avg(10)
  File "<ipython-input-39-e8c3b37544f4>", line 6, in avgr
    count += 1
UnboundLocalError: local variable 'count' referenced before assignment

这是什么鬼?刚才不是说了items是自由变量吗?且慢,根据python对于变量的定义,不可变类型只能读取,不能更新,如果更新的话,就会重新创建变量count,那这个就不是自由变量了。

这就是自由的含义,未在本地作用域绑定的变量。

有趣的装饰器:

一个是functools.lru_cache,用于将缓存结果保存起来,避免传入相同的参数重复计算,适用于递归函数。

先定义一个闭包,用来测量时间,不是我写的哈,抄的

import time
def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        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
@clock        
def f(n):
    if n <= 0:
        return n
    return f(n-1) + f(n-2)   
f(3)
[0.00000041s]f(0) -> 0
[0.00000041s]f(-1) -> -1
[0.00005005s]f(1) -> -1
[0.00000041s]f(0) -> 0
[0.00006646s]f(2) -> -1
[0.00000041s]f(0) -> 0
[0.00000000s]f(-1) -> -1
[0.00001518s]f(1) -> -1
[0.00009723s]f(3) -> -2
Out[69]: -2

ps:注意检查最大递归次数,否则就会报错

sys.getrecursionlimit()
Out[60]: 1000
@functools.lru_cache()
@clock        
def f(n):
    if n <= 0:
        return n
    return f(n-1) + f(n-2)
f(3)
[0.00000041s]f(0) -> 0
[0.00000082s]f(-1) -> -1
[0.00004923s]f(1) -> -1
[0.00005826s]f(2) -> -1
[0.00006810s]f(3) -> -2
Out[71]: -2

惊讶了吧,这仅仅只是添加了一个装饰器。

原文发布于微信公众号 - 鸿的学习笔记(shujuxuexizhilu)

原文发表时间:2017-08-11

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏屈定‘s Blog

(转)Java--栈与队列

Java中栈与队列相比集合来说不是很常用的数据结构,因此经常被忽略.个人觉得还是有必要掌握下,以备不时之需. Java中实际上提供了java.util.Stac...

66930
来自专栏一枝花算不算浪漫

一道笔试题来理顺Java中的值传递和引用传递

393110
来自专栏写代码的海盗

scala如何解决类型强转问题

scala如何解决类型强转问题   scala属于强类型语言,在指定变量类型时必须确定数据类型,即便scala拥有引以为傲的隐式推到,这某些场合也有些有心无力。...

36390
来自专栏数据结构与算法

BZOJ4939: [Ynoi2016]掉进兔子洞(莫队 bitset)

那么第$i$个询问的答案为$r1 - l1 + r2 - l2 + r3 - l3 + 3 - min(cnt1[x], cnt2[x], cnt3[x])$

9210
来自专栏web前端教室

重学javascript 红皮高程(4)

继续啊,顺着JS高程的目录往下走,今天是3.4.4 Boolean类型。 这个Boolean一般来说它只有二个值,true和false。但其实它还有第三种值, ...

24090
来自专栏乐享123

Python编程实战 - 笔记1

20290
来自专栏Golang语言社区

Golang语言断言

golang中的所有程序都实现了interface{}的接口,这意味着,所有的类型如string,int,int64甚至是自定义的struct类型都就此拥有了i...

326110
来自专栏机器学习入门

LWC 55:712. Minimum ASCII Delete Sum for Two Strings

LWC 55:712. Minimum ASCII Delete Sum for Two Strings 传送门:712. Minimum ASCII Dele...

23470
来自专栏武培轩的专栏

Leetcode#500. Keyboard Row(键盘行)

给定一个单词列表,只返回可以使用在键盘同一行的字母打印出来的单词。键盘如下图所示。

19530
来自专栏文武兼修ing——机器学习与IC设计

栈与栈的实现栈栈的基本操作栈的实现

栈 栈是一种基础的数据结构,只从一端读写数据。基本特点就”后进先出“,例如顺序入栈1,2,3,4,5,再顺序出栈是5,4,3,2,1 栈的基本操作 栈的基本操作...

34050

扫码关注云+社区

领取腾讯云代金券