10--Python 闭包与装饰器

@Author : Roger TX (425144880@qq.com)
@Link : https://github.com/paotong999

一、Python 闭包

1、函数作为返回值

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
我们来实现一个可变参数的求和。通常情况下,求和的函数是这样定义的:

def calc_sum(*args):
    ax = 0
    for n in args:
        ax = ax + n
    return ax

但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:

def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum

当我们调用 lazy_sum() 时,返回的并不是求和结果,而是求和函数。

在这里,我们在函数 lazy_sum中又定义了函数 sum,并且,内部函数 sum 可以引用外部函数 lazy_sum 的参数和局部变量,当 lazy_sum 返回函数 sum 时,相关参数和变量都保存在返回的函数中,这种称为 闭包(Closure) 的程序结构拥有极大的威力。

2、闭包

在一个外函数中定义了一个内函数,内函数里运用了外函数的临时变量,并且外函数的返回值是内函数的引用。

一般情况下,在我们认知当中,如果一个函数结束,函数的内部所有东西都会释放掉,还给内存,局部变量都会消失。但是闭包是一种特殊情况,如果外函数在结束的时候发现有自己的临时变量将来会在内部函数中用到,就把这个临时变量绑定给了内部函数,然后自己再结束。

闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数+引用环境)

函数:

  • 函数只是一段可执行代码,编译后“固化”,每个函数在内存中只有一份实例,得到函数的入口点便可以执行函数。

如果函数名后紧跟一对括号,相当于现在我就要调用这个函数
如果函数名后不跟括号,相当于只是一个函数的名字,里面存了函数所在位置的引用

  • 在函数式编程语言中,函数是一等公民,函数可以作为另一个函数的参数或返回值,可以赋给一个变量。

(First class value:第一类对象,我们不需要像命令式语言中那样借助函数指针,委托操作函数)

  • 函数可以嵌套定义,即在一个函数内部可以定义另一个函数,有了嵌套函数这种结构,便会产生闭包问题。

引用环境:

  • 当内嵌函数体内引用到体外的变量时,将会把定义时涉及到的引用环境和函数体打包成一个整体返回

注意事项:

  • 闭包中无法修改外部作用域的局部变量
def foo():
    m = 0
    def foo1(): 
        m = 1
        print (m)    # m = 1

    print (m)    # m = 0
    foo1()
    print (m)    # m = 0
 foo()
  • 在闭包中,所有在赋值语句左面的变量都是局部变量
def foo():
    a = 1
    def bar():
        a = a + 1
        return a
    return bar

运行时,会报错 UnboundLocalError: local variable 'a' referenced before assignment,变量a在赋值语句左边被认为是局部变量,会在 bar() 中去找在赋值语句右面的a的值,而找到上层的变量a是不允许在局部内被修改

  • 修改赋值和返回,将a改为b
  • 将a设定为一个容器,a = [1]a[0] = a[0] + 1
  • a = a + 1 之前,使用语句nonloacal a就可以指定a不是闭包的局部变量,需要向上一层变量空间找这个变量。
  • 返回闭包时,返回函数不要引用任何循环变量,或者后续会发生变化的变量
def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i*i
        fs.append(f)
    return fs

f1, f2, f3 = count()    # 调用f1(),f2()和f3()结果应该是1,4,9,但实际结果是9,9,9

闭包的应用:

  • 坐标连续移动
origin = [0, 0]  # 坐标系统原点
legal_x = [0, 50]  # x轴方向的合法坐标
legal_y = [0, 50]  # y轴方向的合法坐标
def create(pos=origin):
    def player(direction,step):
        # 这里应该首先判断参数direction,step的合法性,比如direction不能斜着走,step不能为负等
        # 然后还要对新生成的x,y坐标的合法性进行判断处理,这里主要是想介绍闭包,就不详细写了
        new_x = pos[0] + direction[0]*step
        new_y = pos[1] + direction[1]*step
        pos[0] = new_x
        pos[1] = new_y
        #注意!此处不能写成 pos = [new_x, new_y],原因在上文有说过
        return pos
    return player

player = create()  # 创建棋子player,起点为原点
print (player([1,0],10))  # 向x轴正方向移动10步,结果为[10, 0]
print (player([0,1],20))  # 向y轴正方向移动20步,结果为[10, 20]
print (player([-1,0],10))  # 向x轴负方向移动10步,结果为[0, 20]
  • 取得文件"result.txt"中含有"pass"关键字的行
def make_filter(keep):  
    def the_filter(file_name):  
        file = open(file_name)  
        lines = file.readlines()  
        file.close()  
        filter_doc = [i for i in lines if keep in i]  
        return filter_doc  
    return the_filter 

filter = make_filter("yield")  
filter_result = filter("example.py")  
print(filter_result)
  • 装饰器
    python中的使用@语法实现的单例模式就是利用闭包实现的,只不过用了@作为语法糖,使写法更简洁,闭包函数将函数的唯一实例保存在它内部的 __closure__ 属性中,在再次创建函数实例时,闭包检查该函数实例已存在自己的属性中,不会再让他创建新的实例,而是将现有的实例返给它。

__closure__ 属性返回的是一个元组对象,包含了闭包引用的外部变量。

  • 若主函数内的闭包不引用外部变量,就不存在闭包,主函数的 _closure__ 属性永远为None
  • 若主函数没有return子函数,就不存在闭包,主函数不存在 _closure__ 属性,抛出异常

二、Python 装饰器

装饰器分为:不带参数的装饰器 和 带参数的装饰器
本质上,decorator就是一个返回函数的高阶函数,实现了以下两步

  • 将被修饰的函数(函数 B)作为参数传给 @ 符号引用的函数(函数 A)
  • 将函数 B 替换(装饰)成第 1 步的返回值

1、不带参数的装饰器

@a_decorator
def f(...):

经过a_decorator后, 函数f就相当于以f为参数调用a_decorator返回结果

f = a_decorator(f)

def makeBold(fn):
    print("makeBold"*2)
    def wrapped1():   #注意为了演示结果这里讲wrapped函数,分为wrapped1,wrapped2
        print("makeBold"*3)
        print("asd" + fn() + "asd")
    return wrapped1
 
def makeItalic(fn):
    print("makeItalic"*2)
    def wrapped2():     #注意为了演示结果这里讲wrapped函数,分为wrapped1,wrapped2
        print("makeItalic" *3)
        return "zzz" + fn() + "zzz"
    return wrapped2
 
#使用两个装饰器同时装饰一个函数,可以三个,甚至多个。原理一样
@makeBold  #其效果等同于执行test_B_I=makeBold(makeItalic(test_B_I))
@makeItalic  #其效果等同于执行test_B_I=makeItalic(test_B_I)
def test_B_I():   
    print("test_B_I"*2)
    return "this is the test_B_I"

2、带参数的装饰器

这里就给一个式子, 剩下的问题可以自己去想

@decomaker(argA, argB, ...)
def func(arg1, arg2, ...):

这个式子相当于

func = decomaker(argA, argB, ...)(func)

def log(text):
    def decorator(func):
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

@log('execute')
def now():
    print('2015-3-25')

和两层嵌套的decorator相比,3层嵌套的效果是这样的:

now = log('execute')(now)

我们来剖析上面的语句,首先执行log('execute'),返回的是decorator函数,再调用返回的函数,参数是now函数,返回值最终是wrapper函数

>>> now()
execute now():
2015-3-25
>>> print(now.__name__)
'wrapper'

函数也是对象,它有 __name__ 等属性,但你去看经过decorator装饰之后的函数,它们的 __name__ 已经从原来的 now 变成了 wrapper

  • 需要把原始函数的 __name__ 等属性复制到 wrapper() 函数中,需要使用Python内置的 functools.wraps 装饰器
  • 需要导入 functools 模块。然后在定义 wrapper() 的前面加上 @functools.wraps(func) 即可

3、装饰器类

在Python中, 其实函数也是对象。 反过来, 对象其实也可以像函数一样调用,只要在类的方法中实现 __call__ 方法,就可以实现装饰器类。

装饰器要求接受一个callable对象,并返回一个callable对象。那么用类来实现也是也可以的。我们可以让类的构造函数 __init__() 接受一个函数,然后重载 __call__()并返回一个函数,也可以达到装饰器函数的效果。

class logging(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print "function: {func}()".format(func=self.func.__name__)
        return self.func(*args, **kwargs)

@logging
def say(something):
    print "say {}!".format(something)

如果需要通过类形式实现带参数的装饰器,那么会比前面的例子稍微复杂一点。那么在构造函数里接受的就不是一个函数,而是传入的参数。通过类把这些参数保存起来。然后在重载 __call__ 方法是就需要接受一个函数并返回一个函数。

class logging(object):
    def __init__(self, level='INFO'):
        self.level = level
        
    def __call__(self, func): # 接受函数
        def wrapper(*args, **kwargs):
            print "{level}: {func}()".format(level=self.level, func=func.__name__)
            func(*args, **kwargs)
        return wrapper  #返回函数

@logging(level='INFO')
def say(something):
    print "say {}!".format(something)

4、多个装饰器

关于多个装饰器修饰一个函数

  1. 当一个函数被多个装饰器装饰时,装饰器的加载顺序是从内到外的(从下往上的)。其实很好理解:装饰器是给函数装饰的,所以要从靠近函数的装饰器开始从内往外加载
  2. 外层的装饰器,是给里层装饰器装饰后的结果进行装饰。相当于外层的装饰器装饰的函数是里层装饰器的装饰原函数后的结果函数(装饰后的返回值函数)。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容