前言
装饰器作为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初学者来说,感觉还是有一定的难度的,所以要多加应用。
网友评论