本文介绍 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__) return func() return wrapper def foo (): print ('I am foo' ) foo = use_print(foo) 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 loggingdef 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 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' )
类装饰器也一样:
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' )
利用这样的功能,我们就可以使用装饰器来做函数的注册,甚至把装饰器装饰的函数放到__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 ): 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): return add_register_item(target.__name__, target) else : return lambda x : add_register_item(target, x) def __call__ (self, 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 loggingdef 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 value if callable (target): return add_register_item(target.__name__, target) else : return lambda x : add_register_item(target, x) ...
以上是我的理解,可能有不正确的地方,但应该没啥大问题
一个使用装饰器的实例 目的为做出一个可交互的 Bot,用户可以编写插件实现新的功能,下面是一个插件示例:
1 2 3 4 5 6 7 from plugin.handle import Botfrom 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 函数进行消息输出,插件的加载逻辑如下:
使用importlib.import_module
导入插件名
目录
自动执行__init__.py
,在on_keyword
函数中将插件函数注册到相应的Plugins
类中
plugins = [kw_plugins, fm_plugins, sw_plugins, ew_plugins]
,plugins 包含了所有的插件函数
运行的逻辑如下:
Bot 阻塞在接受消息函数
接收到消息,依次遍历插件函数,调用插件类的get_match
函数判断是否触发响应规则
如果满足,yield 出相应插件,调用插件的 handle 函数,处理消息
根据 block 判断是否将消息继续匹配
源码可以在这里找到:plugin-bot-demo ,参考了 Nonebot2 的功能(但此 demo 不能完全代表 Nonebot2 的工作原理,它们只是在表现上比较相似)
本文参考链接
【python】装饰器超详细教学,用尽毕生所学给你解释清楚,以后再也不迷茫了!
Python进阶笔记(一)装饰器实现函数/类的注册