【python基础】python装饰器(mmdetection的注册机制)
首先我们需要知道,python 中一切皆对象,函数也是,只不过函数实现了__call__方法,使得它可以被调用 (callable)。所以,函数既然是对象,我们不妨就把函数称为函数对象,函数对象可以作为参数传入另一个函数中。在上面的例子中,我们定义了一个 calcuelate,它接收三个参数,然后返回func(a, b),所以 func 必须是 callable,也就是它得是函数,或者一个实现了_
最近在阅读 mmdetection 源码时,发现了大量的装饰器的用法,而且发现当被装饰的类被导入时,装饰器的效果就会被自动触发。于是查阅了相关资料,梳理了一下装饰器的运行逻辑,并在文章最后解释一下mmdetection的注册机制。
前言
首先我们需要知道,python 中一切皆对象,函数也是,只不过函数实现了 __call__ 方法,使得它可以被调用 (callable)。
所以,函数既然是对象,我们不妨就把函数称为函数对象,函数对象可以作为参数传入另一个函数中。同样,函数也可以返回一个函数对象,比如下面的例子:
def calculate(func, a, b):
return func(a, b)
def my_sum(a, b):
return a + b
result = calculate(my_sum, 1, 2)
print(result) # 3
在上面的例子中,我们定义了一个 calcuelate,它接收三个参数,然后返回 func(a, b),所以 func 必须是 callable,也就是它得是函数,或者一个实现了 __call__ 的对象。
然后定义了一个简单的函数 my_sum,它接收两个参数,求和并返回。
最后通过 calculate(my_sum, 1, 2),把 my_sum,1,2 传入到 calculate 中,calculate 返回 my_sum(1, 2),my_sum(1, 2) 再返回求和结果 3。
一、函数的装饰器
装饰器是 python 的一种语法糖(帮助我们以少量的代码方便地实现某种功能)。
直接看下面的例子,可以看到在 def my_func(x): 上方有一个 @timeit 的字段:
import time
def timeit(f):
def wrapper(x):
start = time.time()
ret = f(x)
print(time.time() - start)
return ret
return wrapper
@timeit
def my_func(x):
time.sleep(x)
return x
x = my_func(1)
print(f'x = {x}')
这个@timeit 的字段就是装饰器,我们目前看不懂它,但可以通过以下方式把他转化为我们看得懂的样子,
所以,上面的代码等效于下面的代码:
import time
def timeit(f):
def wrapper(x):
start = time.time()
ret = f(x)
print(time.time() - start)
return ret
return wrapper
def my_func(x):
time.sleep(x)
return x
my_func = timeit(my_func) # 等价于这个
x = my_func(1)
print(f'x = {x}')
所以,装饰器的作用就是,把装饰器字段下方的函数名,作为参数传到装饰器字段所指的函数中,再把返回值保存回原来的函数名(还是看前面的图比较好理解,语言描述总是很绕…)。
那么这段代码运行后会发生什么呢?我们只需要把 my_func 传入 timeit 然后一步一步看就好了:
可以看出,wrapper 打印了 my_func(1) 执行所花费的时间,my_func(1) 的返回值作为 wrapper 的返回值被返回。
所以输出是这样的:
1.0006966590881348
x = 1
另外,结合 用*args和**kwargs来接收任意数量的参数 和 python 装饰器,我们可以计算任何函数所花费的时间了:
import time
def timeit(f):
def wrapper(*args, **kwargs):
start = time.time()
ret = f(*args, **kwargs)
print(time.time() - start)
return ret
return wrapper
# 把@timeit添加到函数前,当该函数被执行时,将打印执行花费的时间
二、类的装饰器
前面讲的装饰器涉及到两个函数 (function),也就是 @ 后的字段是一个函数的名字,其实 @ 后也可以跟一个类的名字,只要这个类实现了 __call__ 方法,因为实现了 __call__ 方法的类,可以被当作一个函数来调用,所以原理和前一节是一样的。
那么如果,装饰器装饰的是一个类呢?像下面这样:
@xxx
class my_class(object):
...
通常用一个函数来修饰类是为了修改类的属性,所以这个函数通常以一个类作为参数,修改类的属性后再返回这个类,例如:
def modify(cls):
cls.species = cls.__name__ # .__name__将获得cls的名称
return cls
@modify
class dog(object):
pass
按照上一节的解释,上面的代码就等价于下面的代码:
def modify(cls):
cls.species = cls.__name__ # .__name__将获得cls的名称
return cls
class dog(object):
pass
dog = modify(dog) # 装饰器的效果等价于这一行
然后我们可以实例化出一个对象:
my_dog = dog() # 等价于 my_dog = modify(dog)()
print(my_dog.species)
输出:
dog
三、装饰器的触发时机
另外,需要注意装饰器被触发的时机。
从前面两节的例子可以看出,无论装饰器装饰的是函数还是类,它们都等价于在函数/类下方,加上一句 函数名/类名 = 装饰器名(函数名/类名),只要这个被装饰的函数/类所在的文件被执行时,装饰器的效果就会被触发。
如果我们把代码都写在一个文件里,也许不会注意到这个事情,因为在运行代码的时候,装饰器的效果就顺带执行了。
但是,在 python 中,当我们从一个文件中导入函数或类时,该文件也会被执行!所以从一个文件导入一个被装饰的函数/类时,装饰器也就会被触发了。而这往往容易被忽视。
3.1 当一个“被装饰的函数”被导入时
举个例子,新建一个 test.py,填入以下代码:
# test.py
def debug(func):
print(f'DEBUG {func.__name__}') # .__name__将获得func的名称
return func
@debug
def say():
print("hello, this say")
上面的代码中,debug 装饰了 say 函数,其效果是在执行 say 函数前,会先打印 DEBUG say。
然后在同级目录新建一个 main.py,并从 test.py 导入 say:
# main.py
from test import say
最后,直接运行这个只包含一行代码的 main.py 文件,可以发现有输出:
DEBUG say
这说明,装饰器已经产生效果了,因为test.py 完全可以换成以下内容,输出还是一样的,我们也就能够理解为什么导入时就会触发装饰器了:
def debug(func):
print(f'DEBUG {func.__name__}') # .__name__将获得func的名称
return func
def say():
print("hello, this say")
say = debug(say) # 装饰器的效果等效于这一行,会导致输出 `DEBUG say`
因为当我们 from test import say,test.py 就会自动被执行,通常我们会把模块中的代码放在 if __name__ == '__main__' 下面来避免被其他文件导入时而执行,但从上面的代码可以看出,装饰器产生的效果是无法放到 if __name__ == '__main__' 下的,所以上面就会导致输出 DEBUG say 了。
3.2 当一个“被装饰的类”被导入时(mmdetection)
被装饰的类也是同理的。不举其他例子了,直接用 mmdetection 作为示例。
mmdetection 中的注册机制就是利用装饰器实现的。我写了一个简化版的代码用于解释其逻辑,代码层级结构是这样的:
mmdetection/
├── my_module
│ ├── __init__.py
│ ├── registry.py
│ └── datasets.py
└── main.py
__init__.py:
from .datasets import CocoDataset
registry.py:
class Registry(object):
def __init__(self, name):
self._name = name # 初始化_name为name
self._module_dict = dict() # 初始化_module_dict 为空的字典
def __repr__(self):
format_str = f'items={list(self._module_dict.keys())}' # 格式化打印 _module_dict字典中的所有key
return format_str
def register_module(self, cls):
self._module_dict[cls.__name__] = True # 增加一条字典元素, key是cls的名称,value是布尔值True
return cls # 返回 cls
DATASETS = Registry('dataset')
datasets.py:
from my_module.registry import DATASETS
@DATASETS.register_module
class CocoDataset(object):
def __init__(self):
self.name = 'coco'
main.py:
from my_module.registry import DATASETS
print(DATASETS)
当我们运行 main.py 会发现输出是:
items=['CocoDataset']
我们梳理一下代码的运行逻辑,首先当我们运行 main.py 时,首先执行 from my_module.registry import DATASETS,也就是从 my_module 这个包 (package) 中的 registry 模块中导入 DATASETS。
而根据导入机制,在真正导入 DATASETS 前,会先执行 my_module 包下的 __init__ 文件(参看python init.py的讲解),也就是这句代码被运行了:
from .datasets import CocoDataset
我们前往 datasets.py 查看发现 CocoDataset 是一个被 @DATASETS.register_module 装饰的类,因为前面说过导入一个被装饰的函数/类时,装饰器就会被触发,也就是等价于执行这个操作:
DATASETS.register_module(CocoDataset)
这会发生什么呢?查看一下 registry.py,不难理解 DATASETS 的成员 _module_dict(字典类型),会增加一条元素:key = CocoDataset,value = True。
至此,main.py 的第一行代码执行完毕,然后 print(DATASETS):
items=['CocoDataset']
打印出 _module_dict 所包含的 key 值,也就是 ‘CocoDataset’。
所以当我们使用 mmdetection 时,要多关注一下装饰器修饰了哪些东西,以及再导入包时多查看一下 __init__ 文件的内容,就不难理解所谓的“注册”了。
四、总结
- 装饰器的效果,等效于去掉装饰器的字段后,在函数和类的定义下面加上:
函数名/类名 = 装饰器名(函数名/类名)。 - 因为装饰器的效果等效于一句可执行的代码,所以注意文件被执行的时机,特别是导入文件中的函数/类时,文件就会被执行一次,这一点很容易被忽视。
- mmdetection 利用了装饰器机制,实现了在导入一些类时,自动将类的名称(以及一些其他信息)保存到另一个类的成员中,并把这个过程称为“注册”。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)