美文网首页
python通过闭包来理解装饰器

python通过闭包来理解装饰器

作者: BourneKing | 来源:发表于2018-11-30 18:16 被阅读1次

    前言

    装饰器作为Python语言很重要的一个特性,在实际开发中,我们都经常用到,包括面试的时候也会拿出来问,所以想总结一下。
    在讲装饰器之前,我想讲一下闭包这个概念。在任何开发的语言中,我们都会接触到闭包这个概念,也比较难理解。

    基本了解

    闭包是什么?
    闭包指嵌套在非全局作用域里面的函数能够记住它在定义时候,它所处的封闭命名空间.
    听起来是不是很难理解,不用担心,下面可以看看一个例子:

    def outer():
         x = 1
         def inner():
             print x 
         return inner
    
    foo = outer()#2
    foo() #1
    

    我定义一个outer函数,里面包含一个本地变量x,和一个定义的函数inner,最后outer函数返回值为inner函数本身。#1处,当我们执行foo()时,结果为1。这是为什么呢?
    我们知道从变量的生存周期来看,变量x作为函数outer的一个本地变量,这意味着只有当函数outer正在运行也就是#2处的时候才会存在,inner函数中才能调用x并打印。根据我们已知的python运行模式,我们没法在函数outer返回之后继续调用函数inner,在#1处执行foo()时,函数inner被调用的时候,变量x早已不复存在,理论上会发生一个运行时错误。然而,这时候Python会支持一种叫做函数闭包的特性:

    foo.__closure__ 
    (<cell at 0x: int object at 0x>,)
    

    也就是foo作为一个函数,包含了一个__closure__(python2是func_closure )的属性,用来记录封闭作用域里面的值(只会包含被捕捉到的值(或者叫被引用的值),比如变量x。当每次函数foo被调用时,函数inner都会被重新定义,变量x的值是不会变化。
    接下来,对outer函数稍微改动一下:

    def outer(x):
         def inner():
             print x # 1
         return inner
    print1 = outer(1)
    print2 = outer(2)
    print1()
    1
    print2()
    2
    

    从这个改动后的outer函数中,我们可以看到闭包--被函数记住的封闭作用域,能够被用来创建自定义的函数,本质上来说是一个硬编码的参数,不管调用多少次,函数都会记住调用时对应赋予x的值。

    闭包单独拿出来就是一个非常强大的功能, 在某些方面,你也许会把它当做一个类似于面向对象的技术:outer像是给inner服务的构造器,x像一个私有变量。
    总结:实际上创建一个闭包必须满足以下几点:
    1.必须有一个内嵌函数
    2.内嵌函数必须要引进外部函数的变量
    3.外部函数的返回值必须是内嵌函数

    重点是函数运行后并不会被撤销,就像上面的例子中的outer函数,当函数运行完后,参数x并不被销毁,而是继续留在内存空间里.这个功能类似类里的类变量,只不过迁移到了函数上.

    装饰器

    接着上面的outer函数来进行修改:

    def outer(func):
        def inner():
            print("before func")
            ret = func()
            return ret + 1
        return inner
    def foo():
        return 1
    
    decorated = outer(foo)
    decorated()
    
    before func
    2
    
    

    仔细看看上面的代码,在outer函数里定义一个嵌套函数inner,inner函数会先打印一串字符串,然后在调用传进来的函数func并执行它得到它的返回值,然后inner返回func()+1,所以通过函数作为参数传到decorated,得到的结果为2并非为这个函数的返回值1。
    装饰器其实就是一个闭包,把一个函数作为参数然后返回一个替代版本的函数
    概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能
    因此,我们可以认为变量decorated是函数foo的一个装饰版本,一个加强版本。

    现在,写一个含有X和Y坐标的函数,并实现坐标数字运算:

    class Coordinate(object):
            def __init__(self, x, y):
                  self.x = x
                  self.y = y
            def __repr__(self):
                  return "coordinate:" + str(self.__dict__)
    def add(a, b):
         return Coordinate(a.x + b.x, a.y + b.y)
    def sub(a, b):
         return Coordinate(a.x - b.x, a.y - b.y)
    
    
    def wrapper(func):
         def checker(a, b): # 1
             if a.x < 0 or a.y < 0:
                 a = Coordinate(a.x if a.x > 0 else 0, a.y if a.y > 0 else 0)
             if b.x < 0 or b.y < 0:
                 b = Coordinate(b.x if b.x > 0 else 0, b.y if b.y > 0 else 0)
             ret = func(a, b)
             if ret.x < 0 or ret.y < 0:
                 ret = Coordinate(ret.x if ret.x > 0 else 0, ret.y if ret.y > 0 else 0)
             return ret
         return checker
    
    add = wrapper(add)
    sub = wrapper(sub)
    one = Coordinate(100, 200)
    two = Coordinate(300, 200)
    sub(one, two)
    Coord: {'y': 0, 'x': 0}
    add(one, three)
    Coord: {'y': 200, 'x': 100}
    

    在坐标进行数学运算时,需要检测传递的参数a,b的值为非负值,或者通过相加减后得出的值为非负值。
    这个装饰器就是将边界检查的逻辑隔离到单独的方法中,然后通过装饰器包装的方式应用到我们需要进行检查的地方

    使用 @ 标识符将装饰器应用到函数

    Python支持使用标识符@应用到函数上。只需要在函数定义之前加上@和装饰器函数名,上面例子可以写成:

    add = wrapper(add)
    sub = wrapper(sub)
    
    @wrapper
    def add(a, b):
         return Coordinate(a.x + b.x, a.y + b.y)
    @wrapper
    def sub(a, b):
         return Coordinate(a.x - b.x, a.y - b.y)
    

    需要明白的是,这样的做法和先前简单的用包装方法替代原有方法是一样的, python只是加了一些语法糖让装饰的行为更加的直接明确和优雅一点。

    更通用的装饰器

    在装饰器中使用*args, **kwargs来捕获任何用于被装饰的函数。也就是说,当定义函数用了任何参数,意味着那些通过位置传递的参数将会被放在带有前缀的变量中,先来个简单地把日志输出到界面的例子:

    def logger(func):
         def inner(*args, **kwargs): #1
             print "Arguments were: %s, %s" % (args, kwargs)
             return func(*args, **kwargs) #2
         return inner
    

    请注意我们的函数inner,它能够接受任意数量和类型的参数并把它们传递给被包装的方法,这让我们能够用这个装饰器来装饰任何方法。

    @logger
    def foo1(x, y=1):
         return x * y
    @logger
    def foo2():
         return 2
    
    
    foo1(5, 4)
    Arguments were: (5, 4), {}
    20
    
    foo1(1)
    Arguments were: (1,), {}
    1
    
    foo2()
    Arguments were: (), {}
    2
    

    总结

    通过对闭包的理解,加深了装饰器的认识,其实装饰器就是一种特殊的闭包,把一个函数当做参数然后返回一个替代版函数。对于Python初学者来说,感觉还是有一定的难度的,所以要多加应用。

    相关文章

      网友评论

          本文标题:python通过闭包来理解装饰器

          本文链接:https://www.haomeiwen.com/subject/qfnhcqtx.html