Python修饰器(Decorator)备忘

什么是Python修饰器

修饰器是Python语言的一种重要特性,简单说它是一种能够修改其他函数功能的函数。熟练的使用可以是我们的代码更加简洁和更加Pythonic。更开始接触会觉得有点绕,但是理解了之后就显得很常规了。

闭包(Closure)

要谈修饰器,首先得提一下闭包。闭包也算是一种挺有用也比较神奇的语言特性,那什么是闭包呢?闭包其实是一种函数对象,它能够记住某个封闭作用域的变量,即便该变量已经不再内存中了。这听着就很神奇了。下面看个例子吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def counter():
i = 0
def increase():
nonlocal i
i+=1
return i
return increase

my_counter = counter()
for _ in range(10):
print(my_counter(), end=" ")

# output:
# 1 2 3 4 5 6 7 8 9 10

上面的程序看起来还是比较普通的,但是我们分析一下,就发现挺有意思的了。程序实现的是计数功能,我们定义了一个counter函数,里面嵌套了一个increase函数并且用到了外层函数的局部变量,执行自增1的操作并返回,大致实现的就是这么一个效果。

但是我们看一下调用,我们调用counter函数后返回一个函数对象,按道理,counter函数已经调用完,就应该清除了函数内的所有变量,也就是代码中的i变量理应没了。但是实际上显示的效果就是这个变量被返回的函数对象给记住了,因此上述代码就能够如预期结果那样执行起来了。

从技术层面来说(青涩难懂那种),闭包就是:

闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数+引用环境)(想想Erlang的外层函数传入一个参数a, 内层函数依旧传入一个参数b, 内层函数使用a和b, 最后返回内层函数)

修饰器

接下来开始进入主题,讲讲什么是修饰器,修饰器是Python的一种语法特征,简单说就是函数decorator接收参数func并返回一个参数与func函数参数一样的函数wrapper,我们就可以拿decorator去修饰参数与func函数参数一致的函数了。说得有点饶了,下面看看代码上的描述吧!

1
2
3
4
5
6
7
8
def decorator(func):
def wrapper(arg1, arg2):
pass
return wrapper

@decorator
def my_func(arg1, arg2):
pass

对着上面的代码理解就比较清晰了。实际上不单单是函数对象,只要是执行callable返回True的对象都可以作为修饰器或则被修饰。

这里接着上面的代码,我们调用一下my_func函数

1
2
3
my_func(10, 10)
# 等效为没有使用修饰器@decorator时的下面语句
# decorator(my_func)(10, 10)

到这里,修饰器的用法的介绍完了,完了,完了…其实很多使用方法归结起来就是看你如何构造成上面所描述的规则吧,下面看看一些常用的例子吧!

日志打引器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def logger(func):
print("[Called] %s" % func.__name__)
def wrapper(*args, **kwargs):
func(*args,**kwargs)
return wrapper

@logger
def my_func():
print("test")

my_func()
# output:
# [Called] my_func
# test

这里说一下的就是wrapper,也就是返回的函数,把参数写为*args, **kwargs表示可以接收任何参数。

带参数的修饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def pow(a):
def wrapper(func):
def exec(x):
return x**a
return exec
return wrapper

@pow(2)
def square(x):
pass

print(square(4))

# 等效为没加修饰器时的pow(2)(square)(4)
# output
# 16

上面代码时实现了一个求二次幂的函数,我们来分析一下,按前面所说的,@后面的是一个参数为函数的函数,我们可以看到,pow(2)返回的结果就是所有的那个函数,所以带参数的修饰器还是很好实现的。本人测试了一下,嵌套三层才返回就不行了。如下面的代码就会提示有错了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def pow(a):
def test():
def wrapper(func):
def exec(x):
return x**a
return exec
return wrapper
return test

@pow(2)()
def square(x):
pass

print(square(4))

上面代码运行的时候就会提示@pow(2)()这一句有语法错误了。但是我们可以改一下让它运行起来, 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def pow(a):
def test():
def wrapper(func):
def exec(x):
return x**a
return exec
return wrapper
return test
decorator = pow(2)()

@decorator
def square(x):
pass

print(square(4))

# output
# 16

用类作为修饰器

用类作为修饰器我们必须实现__init____call__这两个内置函数。我们先用下面代码来简单分析一下。

先看一下

1
2
3
4
5
6
7
8
9
class test:
pass

print(callable(test))
print(callable(test()))

# output
# True
# False

再看一下

1
2
3
4
5
6
7
8
9
10
11
class test:

def __call__(self):
pass

print(callable(test))
print(callable(test()))

# output
# True
# True

可以看到,类本身是一个callable对象,而实现了__call__的类的实例也是一个callable对象,这样我们就很好办了。在带调用的时候,以类test为例,test()传入的是__init__的参数,test()()后面括号传入的是__call__的参数。因此我们举几个例子。

不到参数的类修饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class logger:
def __init__(self, func):
self.__func = func
def __call__(self, *args, **kwargs):
print("[Called] %s" % self.__func.__name__)
self.__func(*args, **kwargs)

@logger
def my_func():
print("test")

my_func()

# output
# [Called] my_func
# test
# 等效为不带修饰器时的logger(my_func)()

带参数的类修饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class pow:
def __init__(self, a):
self.__a = a
def __call__(self,func):
def wrapper(x):
return x ** self.__a
return wrapper

@pow(2)
def square(x):
pass

print(square(4))

# output:
# 16

偏函数作为修饰器

什么是偏函数

个人理解,也数学上的偏函数理解有点像,就是固定某些变量的值,得到关于余下其他变量的函数,下面是例子。

1
2
3
4
5
6
7
8
import functools
def pow(x, a):
return x ** a

square = functools.partial(pow, a=2)
print(square(4))
# output:
# 16

偏函数修饰器

根据上面的分析,我们可以用偏函数得到一个返回值和参数都是函数的函数,再用这个函数去修饰其他函数,下面举个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
import functools
def pow(func, a):
def wrapper(*args, **kwargs):
return args[0] ** a
return wrapper

@functools.partial(pow, a=2)
def square(x):
pass

print(square(4)))
# output:
# 16

利用修饰器修饰类

我们上面已经知道,类其实是一个callable对象,按道理应该可以被修饰器来修饰。我们按着修饰器的要求,构造一个修饰类的修饰器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def decorator(cls):
print("decorator")
return cls

@decorator
class pow:
def __init__(self, x, a):
print("init")
self.__x = x
self.__a = a

pow(4, 2)
# output:
# decorator
# init
# 效果等效于不用修饰器时的decorator(pow)(4, 2)

哈哈,从上面结果看还是验证了我们的想法。下面我们构造一个带参数的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def pow(a):
print("decorator")
def wrapper(cls):
return cls(a)
return wrapper

@pow(2)
class square:
def __init__(self, x):
print("init")
self.__x = x

def __call__(self, a):
return self.__x ** a

print(square(4))
# output:
# decorator
# init
# 16
# 效果等效于不带修饰器时的pow(2)(square)(4)

看来掌握了修饰器的规则,那么万变就不离其宗了。

利用多个修饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def func1(func):
print("func1")
return func

def func2(func):
print("func2")
return func

def func3(func):
print("func3")
return func

@func1
@func2
@func3
def test():
print("test")

# output:
# func3
# func2
# func1
# test
# 效果等效为不用修饰器时的func1(func2(func3(test)))()

内置修饰器

wraps

我们先看一下前面的例子,经过修饰器修饰后的函数还是原来的函数吗?

1
2
3
4
5
6
7
8
9
10
11
12
def logger(func):
def wrapper(*args, **kwargs):
pass
return wrapper

@logger
def my_func():
pass

print("function name:", my_func.__name__)
# output:
# wrapper

我们可以看到,经过修饰器修饰后,函数名就变了,理解起来也就是,经过修饰器后,实际上发生了这么一个事,即被修饰的函数等同于my_func = logger(my_func),因此也就有了输出的函数名为wrapper,怎么解决这问题呢?也就是经过修饰器后函数名不变。看下面的例子,用到了内置修饰器wraps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from functools import wraps
def logger(func):
@wraps(func)
def wrapper(*args, **kwargs):
pass
return wrapper

@logger
def my_func():
pass

print("function name:", my_func.__name__)
# output:
# my_func

property

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
class Student:
def __init__(self, age):
self.__age = age

@property
def age(self):
return self.__age

@age.setter
def age(self, age):
self.__age = age

@age.deleter
def age(self):
del self.__age

s = Student(10)
print(s.age)
s.age = 20
print(s.age)
del s.age
# print(s.age) # 在删除之后再执行这句的话会报错,说Student没有age属性
# output:
# 10
# 20

可以看到,我们用@property修饰器修饰函数后,函数名会自动变成了类的属性,如上面代码所示,我们将类的变量设置为私有的,但是用了修饰器之后,我们可以像公有成员变量一样使用,同时我们可以在相应的函数中多变量的修改和访问进行限制,可以说是相当便利的。

staticmethod和classmethod

我们先看一下代码,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Student:
@staticmethod
def static_func():
print("static method")
@classmethod
def class_method(cls):
print(cls == Student)
print("class method")

Student.static_func()
Student.class_method()
# output:
# static method
# True
# class method

可以看到,用了修饰器@staticmethod@classmethod修饰的类函数属于类本身,可以直接利用类进行调用,而不需要进行实例化。另外,我们可以看到它们都不用传如参数self,但是区别在于classmethod要传入cls参数,也就是相应的类。我们从cls == Student的结果可以看到,cls其实就是Student

总结

一句话,函数decorator接收参数func并返回一个参数与func函数参数一样的函数wrapper,我们就可以拿decorator去修饰参数与func函数参数一致的函数了

参考

0%