Python装饰器详解

1. 函数

在python中,函数通过def关键字、函数名和可选的参数列表定义。通过return关键字返回值。方法体是必须的,通过缩进来表示,在方法名的后面加上双括号()就能够调用函数。

def foo():
     return 1
print(foo())  # 1

2. 作用域

在python中,函数会创建一个新的作用域。也就是函数有自己的命名空间。函数内部碰到一个变量的时候,函数会优先在自己的命名空间里面去寻找。

a_string = "This is a global variable"
def foo():
     print(locals())
print(globals())  # {'a_string': 'This is a global variable'}
foo() # {}

内置的函数globals返回一个包含所有python解释器知道的变量名称的字典。调用函数 foo 把函数内部本地作用域里面的内容打印出来。能够看到函数foo有自己独立的命名空间,虽然暂时命名空间里面什么都还没有。

3. 变量解析规则

在python的作用域规则里面,创建变量一定会在当前作用域里创建一个变量;访问或者修改变量时会先在当前作用域查找变量,没有找到匹配变量的话会依次向上在闭合的作用域里面进行查找。所以如果我们修改函数foo的实现让它打印全局的作用域里的变量也是可以的:

a_string = "This is a global variable"
def foo():
     print(a_string)
foo()  # This is a global variable

但是,全局变量能够被访问到(如果是可变数据类型(list,dict等)甚至能够被更改),但是赋值不行:

a_string = "This is a global variable"
def foo():
     a_string = "test" # 位置1
     print(locals())
foo()  # {'a_string': 'test'}
print(a_string)  # This is a global variable

在函数内部的位置1处,我们实际上新创建了一个局部变量,隐藏全局作用域中的同名变量。

4. 变量生存周期

变量不仅是生存在一个个的命名空间内,他们都有自己的生存周期:

def foo():
     x = 1
foo()
print(x)  # Traceback (most recent call last): NameError: name 'x' is not defined

函数foo的命名空间随着函数调用开始而开始,结束而销毁。

5. 函数参数

向函数传递参数,参数会变成本地变量存在于函数内部:

def foo(x):
     print(locals())
foo(1)  # {'x': 1}

函数的参数可以有名称和位置。函数的参数可以是必须的位置参数或者是可选的命名,默认参数。如:

def foo(x, y=0):  # 位置参数x和命名参数y
    return x - y
print(foo(3, 1))  # 2
print(foo(3))  # 默认参数 3
# foo()  # 位置参数不能省 Traceback (most recent call last): TypeError: foo() takes at least 1 argument (0 given)
print(foo(y=1, x=3))  # 命名参数的顺序不重要 2

6. 嵌套函数

可以在函数里面定义函数,而且现有的作用域和变量生存周期依旧适用。

def outer():
     x = 1
     def inner():
         print(x)
     inner()
outer()  # 1

python解释器需找一个叫x的本地变量,查找失败之后会继续在上层的作用域里面寻找,这个上层的作用域定义在另外一个函数里面。

7. 函数是对象

在python世界里,函数和其他东西一样都是对象:

issubclass(int, object)  # True # all objects in Python inherit from a common baseclass(object)
def foo():
     pass
print(foo.__class__) # 1  # <type 'function'>
print(issubclass(foo.__class__, object))  # True

可以把函数像参数一样传递给其他的函数:

def add(x, y):
     return x + y
def sub(x, y):
     return x - y
def apply(func, x, y):
     return func(x, y) 
print(apply(add, 2, 1))  # 3
print(apply(sub, 2, 1))  # 1

可以从函数里面返回函数:

def outer():
    def inner():
        print("Inside inner")
    return inner
foo = outer()
print(foo)  # <function outer.<locals>.inner at 0x10284b400>
foo()  # Inside inner

还记得变量的生存周期吗?每次函数outer被调用的时候,函数inner都会被重新定义,如果它不被当做变量返回的话,每次执行过后它将不复存在。

8. 闭包

闭包(closure)就是能够读取其他函数内部变量的函数:

def outer():
    x = 1
    def inner():
        print(x)
    return inner
foo = outer()
foo()  # 1
print(foo.__closure__)  # (<cell at 0x10eba9738: int object at 0x10ea11560>,)

inner作为一个函数被outer返回,保存在一个变量foo,并且我们能够对它进行调用foo()。

  • 所有的东西都在python的作用域规则下进行工作:“x是函数outer里的一个局部变量。当函数inner在打印x的时候,python解释器会在inner内部查找相应的变量,当然会找不到,所以接着会到封闭作用域里面查找,并且会找到匹配。
  • 但是从变量的生存周期来看,该怎么理解呢?我们的变量x是函数outer的一个本地变量,这意味着只有当函数outer正在运行的时候才会存在。根据我们已知的python运行模式,我们没法在函数outer返回之后继续调用函数inner,在函数inner被调用的时候,变量x早已不复存在,可能会发生一个运行时错误。
  • 万万没想到,返回的函数inner居然能够正常工作。Python支持一个叫做函数闭包的特性:嵌套定义在非全局作用域里面的函数能够记住它在被定义的时候它所处的封闭命名空间。这能够通过查看函数的func_closure属性得出结论,这个属性里面包含封闭作用域里面的值(只会包含被捕捉到的值,比如x,如果在outer里面还定义了其他的值,封闭作用域里面是不会有的)。

每次函数outer被调用的时候,函数inner都会被重新定义:

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

从这个例子中你能够看到【闭包 – 被函数记住的封闭作用域 – 能够被用来创建自定义的函数】,本质上来说是一个硬编码的参数。事实上我们并不是传递参数1或者2给函数inner,我们实际上是创建了能够打印各种数字的各种自定义版本。闭包单独拿出来就是一个非常强大的功能,在某些方面,你也许会把它当做一个类似于面向对象的技术:outer像是给inner服务的构造器,x像一个私有变量。

9. 装饰器

装饰器其实就是一个闭包,把一个函数当做参数然后返回一个替代版函数。

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

我们可以认为变量decorated是函数foo的一个装饰版本,一个加强版本。

想象我们有一个库,这个库能够提供类似坐标的对象(x, y)。不过可惜的是这些坐标对象不支持数学运算符。为了做一系列的数学运算,我们想要能够对两个坐标对象进行合适加减运算的函数,如下:

class Coordinate(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
    # 把一个类的实例变成可打印的str:
    def __repr__(self):
        return "Coord: " + 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)
one = Coordinate(100, 200)
two = Coordinate(300, 200)
three = Coordinate(-100, -100)
print(add(one, two))  # Coord: {'y': 400, 'x': 400}
print(sub(one, two))  # Coord: {'x': -200, 'y': 0}
print(add(one, three))  # Coord: {'x': 0, 'y': 100}

如果要使我们的加减函数有一些边界检查的行为那该怎么办呢?可能你只能够对正的坐标对象进行加减操作,任何返回的值也都应该是正的坐标。但我们希望在不更改坐标对象one, two, three的前提下,one减去two的值是{x: 0, y: 0},one加上three的值是{x: 100, y: 200}。与其给每个方法都加上参数和返回值边界检查的逻辑,不如接着上一段代码来写一个边界检查的装饰器:

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)
print(sub(one, two))  # Coord: {'x': 0, 'y': 0}
print(add(one, three))  # Coord: {'x': 100, 'y': 200}

通过这样的方式,我们的代码变得更加简洁:将边界检查的逻辑隔离到单独的方法中,然后通过装饰器包装的方式应用到我们需要进行检查的地方。另外一种方式是在方法的开始处和返回值之前调用边界检查的函数。但是,使用装饰器能够减少代码量达到目的。

10. 有逼格的装饰器:@标识符

  • 法一、将原本的方法用装饰后的方法代替:
add = wrapper(add)
  • 法二、使用@对add方法进行装饰:
@wrapper
 def add(a, b):
     return Coordinate(a.x + b.x, a.y + b.y)

注意,法一和法二是一毛一样的, python只是加了一些语法糖让装饰的行为更加的直接和优雅。

11. *args 和 **kwargs

我们已经完成了一个有用的装饰器,但是由于硬编码的原因它只能应用在一类具体的方法上,这类方法接收两个参数,传递给闭包捕获的函数。如果我们想实现一个能够应用在任何方法上的装饰器要怎么做呢?再比如,如果我们要实现一个能应用在任何方法上的类似于计数器的装饰器,不需要改变原有方法的任何逻辑。这意味着装饰器能够接受拥有任何签名的函数作为自己的被装饰方法,同时能够用传递给它的参数对被装饰的方法进行调用。

当定义函数的时候使用了*变量,意味着那些通过位置传递的参数将会被放在带有*前缀的变量中:

def one(*args):
     print(args)
one()  # ()
one(1, 2, 3)  # (1, 2, 3)
def two(x, y, *args):
     print(x, y, args)
two('a', 'b', 'c')  # a b ('c',)

.
* 操作符在函数被调用的时候也能使用。意义基本是一样的。当调用一个函数的时候,变量里面的内容需要被提取出来当做位置参数被使用。如:

def add(x, y):
    return x + y
lst = [1, 2]
print(add(lst[0], lst[1]))  # 3 位置1
print(add(*lst))  # 3 位置2

位置1的代码和位置2的代码所做的事情其实是一样的,*args要么是表示调用方法的时候额外的参数可以从一个可迭代列表中取得,要么就是定义方法的时候标志这个方法能够接受任意的位置参数。

**代表着键值对的参数字典,和*所代表的意义相差无几:

def foo(**kwargs):
    print(kwargs)
foo()  # {}
foo(x=1, y=2)  # {'x': 1, 'y': 2}

.
当我们定义一个函数的时候,用**kwargs表明,所有未捕获的参数都应该存储在kwargs的字典中。args和kwargs并不是python语法的一部分,但在定义函数的时候,使用这样的变量名算是一个不成文的约定。和*一样,我们同样可以在定义或者调用函数的时候使用**:

dct = {'x': 1, 'y': 2}
def bar(x, y):
     return x + y
print(bar(**dct))  # 3

12. 通用装饰器

通过上一步,我们可以写一个能够记录下传递给函数参数的装饰器了。先来个简单地把日志输出到界面的例子:

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

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

@logger
def foo1(x, y=1):
    return x * y
@logger
def foo2():
    return 2
print(foo1(5, 4))  # Arguments were: (5, 4), {}
                   # 20
print(foo1(1))  # Arguments were: (1,), {}
                # 1
print(foo2())  # Arguments were: (), {}
               # 2
dct = {'x': 5, 'y': 2}  
print(foo1(**dct))  # Arguments were: (), {'x': 5, 'y': 2}
                    # 10
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值