Python装饰器

Python装饰器用法:

闭包

​ 首先要了解一个概念,闭包一句话说就是,在函数中再嵌套一个函数,并且引用外部函数的变量,这就是一个闭包了:

函数装饰器

​ 装饰器就是一个闭包,装饰器是闭包的一种应用。python装饰器就是接受待装饰的函数的函数,用于拓展原来函数功能的一种函数(给函数加上装饰),这个函数的特殊之处在于它的返回值也是一个函数,使用python装饰器的好处就是在不用更改原函数的代码前提下给函数增加新的功能。使用时,再需要的函数前加上@demo即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def debug(func):
def wrapper():
print("[DEBUG]: enter {}()".format(func.__name__))
return func()
return wrapper

@debug
def hello():
print("hello")

hello()
-----------------------------
>>>[DEBUG]: enter hello()
>>>hello

​ 例子中的装饰器给函数加上一个进入函数的debug模式,这时候调用 hello() 完全等价于没有装饰器时候使用 bebug(hello),不用修改原函数代码就完成了这个功能,可以说是很方便了。

带参数的装饰器

​ 装饰器可以通过加一些参数拓展功能,这时候还需要再在外层加装一层函数以接受装饰器的参数,第二层函数才接受被装饰的函数,即装饰器共有三层函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def logging(level):
def outwrapper(func):
def wrapper(*args, **kwargs):
print("[{0}]: enter {1}()".format(level, func.__name__))
return func(*args, **kwargs)
return wrapper
return outwrapper

@logging(level="INFO")
def hello(a, b, c):
print(a, b, c)

hello("hello,","good","morning")
-----------------------------
>>>[INFO]: enter hello()
>>>hello, good morning

​ 上面例子中,装饰器中可以传入参数,先形成一个完整的装饰器,然后再来装饰函数,当然函数如果需要传入参数也是可以的,用不定长参数符号就可以接收。这里调用 hello() 等价于使用没有装饰器时的 outwrapper = logging("hello"), wrapper = outwrapper("outwrapper") hello = wrapper("good","morning")

装饰器类

​ 装饰器类仍然是以函数为参数的闭包,仍然可以用传入函数参数来理解:

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

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

def __call__(self, *args, **kwargs):
start = time.time()
ret = self.func(*args, **kwargs)
print(f"Time:{time.time() - start}")
return ret

@Timer
def add(a, b):
return a + b
# 等价于
add = Timer(add)
print(add(2, 3))
-----------------------------
>>>Time: 1.192e-6
>>>5

就等价于通过 add 实例化了一个 Timer 对象

进阶:带参数的装饰器类

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

class Timer(object):
def __init__(self, prefix):
self.prefix = prefix

def __call__(self, func):
def wrapper(*args, **kwargs):
start = time.time()
ret = func(*args, **kwargs)
print(f"Time:{time.time() - start}")
return ret
return wrapper

@Timer(prefix="curr_time: ")
def add(a, b):
return a + b
# 等价于
add = Timer(prefix="curr_time: ")(add)
print(add(2, 3))

​ 此时仍然可以类比上面的带参数的函数装饰器,也可以视为三层函数,第一层函数由 __init__ 函数承担

类装饰器

​ 为什么类还能装饰函数?从Python底层来看,函数和类都是对象,函数既然能修饰类,类就能修饰函数(不过这个用得少,看看就行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def add_str(cls):
def __str__(self):
return str(self.__dict__)
# 把cls的__str__给替换为了__str__
cls.__str__ = __str__
return cls

@add_str
class MyObject(object):
def __init__(self, a, b):
self.a = a
self.b = b

# 等价于
MyObject = add_str(MyObject)

o = MyObject(1, 2)
print(o) #自动调用__str__魔术方法

上面的代码本质就是重载了一下 __str__ 函数

类里面的装饰器

​ 把装饰器挪到类里面,本质上就相当于把一个函数变成类的方法,最容易想到的类里面的装饰器如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Decorators(object):
def log_function(self, func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper

d = Decorators()
@d.log_function
def fib(n):
if n<=1:
return 0
return fib(n-1) + fib(n-2)

fib(3)

但这样写我们是很不爽的,首先,为了调用这个装饰器我们还需要实例化这个类,这样写太麻烦了;其次,装饰器里面的 self 参数我们压根就没有用到它。于是Python有了一个 classmethod,把只能用对象调用的方法变成了类可以调用的方法,代码可改为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Decorators(object):
@classmethod
def log_function(cls, func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper

@Decorators.log_function
def fib(n):
if n<=1:
return 0
return fib(n-1) + fib(n-2)

fib(3)

但是这个 classmethod 里面第一个参数 cls 我们依然没有使用,如果我们只想把装饰器放在类里面封装,而和类和对象都无关的时候,装饰器可以使用 staticmethod,直接把第一个参数删掉,其他都不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Decorators(object):
@staticmethod
def log_function(func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper

@Decorators.log_function
def fib(n):
if n<=1:
return 0
return fib(n-1) + fib(n-2)

fib(3)

一个新的问题

​ 如果我们想用类里面的装饰器去装饰类里面的方法怎么办?我们的自然想法是:

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
class Decorators(object):
@staticmethod
def log_function(func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper

@log_function
def fib(self, n):
if n<=1:
return 0
return self.fib(n-1) + self.fib(n-2)
d = Decorators()
d.fib(3)

function start!
args: (<__main__.Decorators object at 0x000002132CEE6320>, 3)
function start!
args: (<__main__.Decorators object at 0x000002132CEE6320>, 2)
function start!
args: (<__main__.Decorators object at 0x000002132CEE6320>, 1)
function end!
function start!
args: (<__main__.Decorators object at 0x000002132CEE6320>, 0)
function end!
function end!
function start!
args: (<__main__.Decorators object at 0x000002132CEE6320>, 1)
function end!
function end!

​ 虽然这样确实能够正常运行,但没有人在Python中这样写代码,我们非常不习惯在 class 内部有着没有 self 参数的函数

​ 值得一提的是,在 class 之外,我们不能通过 d.log_function() 调用函数,而只能使用Decorators.log_function() 调用(原因很底层,我不知道)

最终问题是:我们想把这个装饰器和方法全部在类里面封装,即装饰器只能在类里面正常使用,而在类之外,只能通过类和对象正常使用怎么办?答案是在 log_function 后加上 log_function = staticmethod(log_function) 这一行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Decorators(object):
@staticmethod
def log_function(func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper

@log_function
def fib(self, n):
if n<=1:
return 0
return self.fib(n-1) + self.fib(n-2)
log_function = staticmethod(log_function)

补充:classmethod和staticmethod区别

声明时:

  • classmethod的第一个参数为类本身(cls),正如实例方法的第一个参数为对象本身(self);
  • staticmethod第一个参数不需要传入cls或self,故staticmethod中是无法访问类和对象的数据的。

调用时:

  • 都可用类名直接调用
  • 也可用实例对象调用(不推荐,没必要)