0%

Python装饰器详解

装饰器应该是Python最富有表现力的语法结构之一了,基于装饰器很多功能可以实现得比较优雅。
Python中的装饰器,来源于设计模式中的装饰器模式。顾名思义,所谓装饰器就是对原有的对象做一些装饰,也就是给已有的对象添加一些功能。

简易装饰器

装饰器本质上是函数替换. 装饰器被调用会返回一个函数, 被装饰函数会被返回的这个函数替换.
要使用装饰器,先得定义一个装饰器函数,然后在需要装饰的函数的前一行使用@符号加上装饰器名称。
下面是一个简单是例子, hello函数被running装饰器装饰, running返回了fuck函数, 此时调用hello就变成了调用fuck, 实现了函数功能的改变.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def fuck(who='nobody'):
return "fuck, %s!" % who

def running(function):
print('replace %r to %r' % (function, fuck))
return fuck

@running
def hello(who='nobody'):
return "hello, %s!" % who

print('--- before call hello ---')
print(hello())
print(hello('foo'))

output

1
2
3
4
replace <function hello at 0x1052e98c8> to <function fuck at 0x104d15f28>
--- before call hello ---
fuck, nobody!
fuck, foo!

装饰器在这里的效果等效于函数嵌套,不过看起来有点别扭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def fuck(who='nobody'):
return "fuck, %s!" % who

def running(function):
print('replace %r to %r' % (function, fuck))
return fuck

def hello(who='nobody'):
return "hello, %s!" % who


hello = running(hello)
print(hello())
print(hello('foo'))

注意:一旦通过@running装饰了函数,不管被装饰函数是否运行,python解释器都会执行一遍running函数。

普通装饰器

前面这个装饰器将hello函数替换成fuck函数之外就没有别的功能了, 只是为了演示装饰器的原理, 并没有什么实际用处

现在我们写一个计时器装饰器.
还是将hello函数替换成fuck, 这里我们将fuck函数的定义移动到runing内部, fuck函数在调用hello的同时, 实现计时功能.
这样有一个好处, 这个fuck函数就不是全局可见的, 不会污染全局环境, 还可以用到闭包的一些特性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import time

def running(func):
def fuck(*args, **kwargs):
start = time.time()
print('`%s` is running...' % func.__name__)
_result = func(*args, **kwargs)
print('run `%s` takes %s seconds' % (func.__name__, time.time()-start))
return _result
return fuck

@running
def hello(who='nobody'):
return "hello, %s!" % who


print(hello('foo'))
print(hello('bar'))
print(hello.__name__)

输出

1
2
3
4
5
6
7
`hello` is running...
run `hello` takes 3.981590270996094e-05 seconds
hello, foo!
`hello` is running...
run `hello` takes 3.814697265625e-06 seconds
hello, bar!
fuck

消除装饰器的副作用

上一个装饰器也有一个问题,因为装饰器本质上是函数替换. 就是经过装饰的函数一些属性变了, 比如hello.__name__变成了fuck

所以我们要将这些属性复制到新函数上, 同时由于fuck函数已经没有fuck功能了, 我们将它重命名为wrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time

def running(func):
def wrapper(*args, **kwargs):
start = time.time()
print('`%s` is running...' % func.__name__)
_result = func(*args, **kwargs)
print('run `%s` takes %s seconds' % (func.__name__, time.time()-start))
return _result

wrapper.__name__ = func.__name__
return wrapper

@running
def hello(who='nobody'):
return "hello, %s!" % who

print(hello.__name__)

output

1
hello

这个问题也可以通过python内置的functools.wraps装饰器解决,这个装饰器对原函数的一些属性进行了复制。

1
2
3
4
5
6
7
8
9
10
11
12
import time
import functools

def running(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
print('`%s` is running...' % func.__name__)
_result = func(*args, **kwargs)
print('run `%s` takes %s seconds' % (func.__name__, time.time()-start))
return _result
return wrapper

带参数的装饰器

上一个装饰器还有一个缺点是, 装饰器不能接受参数, 现在我们来实现带参数的装饰器

带参数的装饰器其实就是多嵌套一层函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import time
import functools

def running(system):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
print('`%s` is running at %s' % (func.__name__, system))
_result = func(*args, **kwargs)
print('run `%s` takes %s seconds' % (func.__name__, time.time()-start))
return _result
return wrapper
return decorator

@running('mac')
def hello(who='nobody'):
return "hello, %s!" % who

print(hello('foo'))

output

1
2
3
`hello` is running at mac
run `hello` takes 3.910064697265625e-05 seconds
hello, foo!

可选带参数的装饰器

有时我们希望装饰器更为通用, 适用于带参数和不带参数的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import time
import functools

def running(*running_args, system='mac', **running_kwargs):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
print('`%s` is running at %s' % (func.__name__, system))
_result = func(*args, **kwargs)
print('run `%s` takes %s seconds' % (func.__name__, time.time()-start))
return _result
return wrapper

if len(running_args) == 1 and callable(running_args[0]):
return decorator(running_args[0])
else:
return decorator

@running(system='linux')
def hello(who='nobody'):
return "hello, %s!" % who

@running
def hello2(who='nobody'):
return "hello, %s!" % who

print(hello('foo'))
print(hello2('bar'))

output

1
2
3
4
5
6
`hello` is running at linux
run `hello` takes 2.5033950805664062e-05 seconds
hello, foo!
`hello2` is running at mac
run `hello2` takes 3.814697265625e-06 seconds
hello, bar!