Python Decorator 的一些小细节/坑

Decorator 的本质是什么?

decorator 本质就是一个接收对象对象(对,是个对象,而不是大多数人认为的函数),更多的资料可以参照 理解Python的装饰器 | Darkof

1
2
3
4
5
final_func = decorator(wrapped_function) # 与注释部分的实质是一致的。

@decorator
def wrapped_func(*args, **kwargs):
pass

被装饰的函数与之前相比,改变了什么?

行为

这个是最显而易见的,装饰器可以在原函数执行之前或之后添加额外的行为

函数本身的属性

如果你简单的实现了下面的 decorator 会改变什么呢

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
30
def foo(func):
def wrapped(*args, **kwargs):
print "in decorator"
return func(*args, **kwargs)
return wrapped

class bar(object): # 原谅我用小写的 bar :)
def __init__(self, func):
self.func= func
def __call__(self, *args, **kwargs):
print "in decorator"
return self.func(*args, **kwargs)

@foo
def f1(a):
print a

print f1.__name__
#输出: 'wrapped'

@bar
def f2(a):
print a

print f2.__name__
# AttributeError Traceback (most recent call last)
# <ipython-input-9-dff5600c49e8> in <module>()
# ----> 1 f2.__name__
#
# AttributeError: 'bar' object has no attribute '__name__'

从上面我们看出来,函数的 __name__ 属性也发生了变化,这也是为什么我们推荐装饰器的时候使用 fucntools.wraps

1
2
3
4
5
6
7
8
import functools

def foo(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
print "in decorator"
return func(*args, **kwargs)
return wrapped

wraps 会把原函数的属性赋给新的 wrapped 这个函数(主要会同步的属性为 __name__, __module__, __doc__, __dict__, 当然,你也可以添加你希望同步的属性)

函数的参数

是的,很少有人会注意到被装饰过会,函数接收的参数也会产生变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
inspect.getargspec(f1)
# 输出 ArgSpec(args=[], varargs='args', keywords='kwargs', defaults=None)

inspect.getargspec(f2)
# TypeError Traceback (most recent call last)
# <ipython-input-16-bff760b02fba> in <module>()
# ----> 1 inspect.getargspec(f2)

# /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/inspect.pyc in getargspec(func)
# 814 func = func.im_func
# 815 if not isfunction(func):
# --> 816 raise TypeError('{!r} is not a Python function'.format(func))
# 817 args, varargs, varkw = getargs(func.func_code)
# 818 return ArgSpec(args, varargs, varkw, func.func_defaults)

# TypeError: <__main__.bar object at 0x108729f50> is not a Python function

f1 接收的函数名从 a 变成了 args 和 kwargs, f2 干脆就拿不到了,这也意味着其实装饰器并不能做到 works anywhere(毕竟有很多装饰器会通过参数来判断这是不是一个 classmethod,然后两个装饰器混用可能会导致其中一个失效)

装饰 classmethod/staticmethod

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

def foo(func):
def wrapped(*args, **kwargs):
print "in decorator"
return func(*args, **kwargs)
return wrapped

class Bar(object):
@foo
@classmethod
def duck(cls):
print 'Yooooooooooo!'

Bar.duck()
# ---------------------------------------------------------------------------
# TypeError Traceback (most recent call last)
# <ipython-input-19-ddf54c241cc4> in <module>()
# ----> 1 Bar.duck()
#
# TypeError: unbound method wrapped() must be called with Bar instance as first argument (got nothing instead)

当我们尝试用之前实现的 decorator 来装饰 classmethod 的时候,会遇到 TypeError,原因是 classmethod/staticmethod 本质是一个 Descriptor 而非 function,我们在一开始提到这样一句话:

decorator 本质就是一个接收对象对象

在前面也聊到了,decorator 是个对象,可能是个 class 或是 function, 那么他接收的是什么呢,很多人认为 decorator 接受的是函数,然而严格来说,decorator 接收的是一个对象

当我们知道 classmethod/staticmehtod 是 Descriptor 之后就很容易的知道如何写一个装饰 Descriptor(当然,你需要先了解什么是 Descriptor)

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class bar(object):  # 原谅我用小写的 bar :)
def __init__(self, func):
self.func= func
def __call__(self, *args, **kwargs):
print "in decorator"
return self.func(*args, **kwargs)

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

def __get__(self, instance, owner):
func = self.func.__get__(instance, owner)
return bar(func)

def __call__(self, *args, **kwargs):
return bar(self.func)(*args, **kwargs)

class Duck(object):
@foo
@staticmethod
def fly():
print 'fly'

@foo
@classmethod
def run(cls):
print 'run'

@foo
def stop(self):
print 'stop'

Duck.fly()
# 输出
# in decorator
# fly

Duck.run()
# 输出
# in decorator
# run

Duck().stop()
# 输出
# in decorator
# stop