Python 装饰器

本文介绍 Python 装饰器的使用和原理,并且编写了一个插件式聊天 Bot 框架(一个 demo)

装饰器是什么

装饰器本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数或类对象

它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

通过参数调用函数

由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数

1
2
3
4
5
6
7
def foo():
print("foo")

def bar(func):
func()

bar(foo)

简单的装饰器

use_print 就是一个装饰器,它一个普通的函数,它把执行真正业务逻辑的函数 func 包裹在其中,看起来像 foo 被 use_print 装饰了一样,use_print 返回的也是一个函数,这个函数的名字叫 wrapper。在这个例子中,函数进入和退出时 ,被称为一个横切面,这种编程方式被称为面向切面的编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def use_print(func):

def wrapper():
print("Function %s is running" % func.__name__)
# 把 foo 当做参数传递进来时,执行 func() 就相当于执行 foo()
return func()
return wrapper

def foo():
print('I am foo')

# 因为装饰器 use_print(foo) 返回的时函数对象 wrapper,这条语句相当于 foo = wrapper
foo = use_print(foo)
# 执行 foo() 就相当于执行 wrapper()
foo()

语法糖 @

@ 符号就是装饰器的语法糖,它放在函数开始定义的地方,这样就可以省略最后一步再次赋值的操作

1
2
3
4
5
6
7
8
9
10
11
12
def use_print(func):

def wrapper():
print("Function %s is running" % func.__name__)
return func()
return wrapper

@use_print
def foo():
print('I am foo')

foo()

如上所示,有了 @ ,我们就可以省去foo = use_print(foo)这一句了,直接调用 foo() 即可得到想要的结果。foo() 函数不需要做任何修改,只需在定义的地方加上装饰器,调用的时候还是和以前一样,如果我们有其他的类似函数,我们可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性

装饰器在 Python 使用如此方便都要归因于 Python 的函数能像普通的对象一样能作为参数传递给其他函数,可以被赋值给其他变量,可以作为返回值,可以被定义在另外一个函数内

传入参数

我们可以在定义 wrapper 函数的时候指定参数:

1
2
3
4
5
6
7
8
9
10
11
12
def use_print(func):

def wrapper(name):
print("Function %s is running" % func.__name__)
return func(name)
return wrapper

@use_print
def foo(name):
print('I am {0}'.format(name))

foo("Apple")

多个参数可以在wrapper 函数里使用*args, **kwargs来收集参数

带参数的装饰器

装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数就是执行业务的函数 foo 。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。比如,我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的。

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

def use_logging(level):
def decorator(func):
def wrapper(*args, **kwargs):
if level == "warn":
logging.warn("%s is running" % func.__name__)
elif level == "info":
logging.info("%s is running" % func.__name__)
return func(*args)
return wrapper

return decorator

@use_logging(level="warn")
def foo(name='foo'):
print("i am %s" % name)

foo()

上面的 use_logging 是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。当我们使用@use_logging(level="warn")调用的时候,Python 能够发现这一层的封装,并把参数传递到装饰器的环境中。@use_logging(level="warn")等价于@decorator

在我理解,@use_logging(level="warn")可以看作调用use_logging(level="warn"),它的返回值是一个装饰器,即相当于直接使用@decorator

装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的__call__方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo(object):
def __init__(self, func):
self._func = func

def __call__(self):
print('class decorator runing')
self._func()
print('class decorator ending')

@Foo
def bar():
print('bar')

bar()

装饰器顺序

一个函数还可以同时定义多个装饰器,比如:

1
2
3
4
5
@a
@b
@c
def f ():
pass

它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于

1
f = a(b(c(f)))

装饰器的初始化

装饰器装饰的函数即使不调用,装饰器的某些部分也会执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
def use_print(func):
def wrapper():
print("Function %s is running" % func.__name__)
return func()
print("Invoke")
return wrapper

@use_print
def foo():
print('I am foo')

# foo()
# 会输出 Invoke

类装饰器也一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Foo(object):
def __init__(self, func):
self._func = func
print("Invoke")

def __call__(self):
print('class decorator runing')
self._func()
print('class decorator ending')

@Foo
def bar():
print('bar')

# bar()
# 会输出 Invoke

利用这样的功能,我们就可以使用装饰器来做函数的注册,甚至把装饰器装饰的函数放到__init__.py实现模块的自动注册

函数注册

某些场景下我们可能需要这样的功能:需要收集不确定的函数到容器(如列表、字典)中,那么就可以使用装饰器来注册函数:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Register(dict):

def __init__(self, *args, **kwargs):
super(Register, self).__init__(*args, **kwargs)
self._dict = {}

def register(self, target):
def add_register_item(key, value):
# key 为我们的函数名,value 为我们的函数对象
if not callable(value):
raise Exception(f"register object must be callable! But receive: {value} is not callable!")
if key in self._dict:
print(f"warning: \033[33m {value.__name__} has been registered before, so we will overriden it\033[0m")
self[key] = value

# 最终返回的函数
return value

if callable(target):
# 如果 target 可以被调用,说明它是一个函数
# 也就是说我们没有自定义函数名,即:@register_functions
# 直接调用 add_register_item 返回其返回值
return add_register_item(target.__name__, target)
else:
# 如果 target 不可以被调用,说明它可能是字符串
# 也就是说我们自定义了函数名,即:@register_functions("my_multiply")
# 返回一个 lambda 函数,这个函数需要再次被调用才可以做到和上面相同的结果
# x 为需要的参数,应该为我们的函数对象
# ------
# 这里 x 是被传进去的函数对象,原理其实和函数中带参数的装饰器有很大的相似之处
# 都是在最外层函数返回真正的装饰器之后,把函数对象作为参数传入真正的装饰器内
# 从而达到传入函数对象 x,调用 add_register_item 的效果
# 这里应该和 Python 实现装饰器的原理相关了
return lambda x : add_register_item(target, x)

def __call__(self, target):
# 这里是装饰器运行的起点
# target 的值是自定义的函数名,如果自定义的函数名没填
# 那么就是我们的函数对象
return self.register(target)

def __setitem__(self, key, value):
self._dict[key] = value

def __getitem__(self, key):
return self._dict[key]

def __contains__(self, key):
return key in self._dict

def __str__(self):
return str(self._dict)

def keys(self):
return self._dict.keys()

def get(self, key):
return self._dict.get(key)

def values(self):
return self._dict.values()

def items(self):
return self._dict.items()


register_functions = Register()

@register_functions
def add(a: int, b: int):
return a + b

@register_functions("my_multiply")
def multiply(a: int, b: int):
return a * b

print(register_functions.get("add")(1, 1))
print(register_functions.get("my_multiply")(2, 2))

在其中,自定义函数名的那一部分可能比较难以理解,可以和下面的例子作对照(就是上文举得例子):

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

def use_logging(level):
def decorator(func):
def wrapper(*args, **kwargs):
if level == "warn":
logging.warn("%s is running" % func.__name__)
elif level == "info":
logging.info("%s is running" % func.__name__)
return func(*args)
return wrapper

return decorator

@use_logging(level="warn")
def foo(name='foo'):
print("i am %s" % name)

foo()

对照如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Register(dict):
...
def register(self, target):
def add_register_item(key, value):
if not callable(value):
raise Exception(f"register object must be callable! But receive: {value} is not callable!")
if key in self._dict:
print(f"warning: \033[33m {value.__name__} has been registered before, so we will overriden it\033[0m")
self[key] = value
# 然后调用到这里,返回值仍然是一个函数
# 相当于上面代码的 return wrapper
return value

if callable(target):
return add_register_item(target.__name__, target)
else:
# 返回一个函数参数是我们需要加装饰的函数
# 相当于上面代码的 return decorator
return lambda x : add_register_item(target, x)
...

以上是我的理解,可能有不正确的地方,但应该没啥大问题

一个使用装饰器的实例

目的为做出一个可交互的 Bot,用户可以编写插件实现新的功能,下面是一个插件示例:

1
2
3
4
5
6
7
from plugin.handle import Bot
from plugin.plugin import on_keyword


@on_keyword(match=["hello"], priority=1, block=True)
def helloworld(bot: Bot, _):
bot.send_msg("hello")

保存在plugins/插件名/__init__.py中,装饰器 on_keyword 表明这个插件函数是基于关键字触发的,match 为触发的关键字,priority 为插件函数的优先级,不同插件函数有不同的优先级,block 表明该处理函数结束后是否让别的处理函数继续处理,此处阻止了后续插件的处理

在函数部分,使用预留的 send_msg 函数进行消息输出,插件的加载逻辑如下:

  1. 使用importlib.import_module导入插件名目录
  2. 自动执行__init__.py,在on_keyword函数中将插件函数注册到相应的Plugins类中
  3. plugins = [kw_plugins, fm_plugins, sw_plugins, ew_plugins],plugins 包含了所有的插件函数

运行的逻辑如下:

  1. Bot 阻塞在接受消息函数
  2. 接收到消息,依次遍历插件函数,调用插件类的get_match函数判断是否触发响应规则
  3. 如果满足,yield 出相应插件,调用插件的 handle 函数,处理消息
  4. 根据 block 判断是否将消息继续匹配

源码可以在这里找到:plugin-bot-demo,参考了 Nonebot2 的功能(但此 demo 不能完全代表 Nonebot2 的工作原理,它们只是在表现上比较相似)


本文参考链接

【python】装饰器超详细教学,用尽毕生所学给你解释清楚,以后再也不迷茫了!

Python进阶笔记(一)装饰器实现函数/类的注册