Python @函数装饰器及用法

1. 问题1

在阅读detectron2代码时,经常能看到@符号,例如在init函数上搞了一个@configurable,实例化RetinaNet类时没有先执行init内的代码,而是直接进入了config.py的configurable函数,让人很是不解。

class RetinaNet(nn.Module):
    """
    Implement RetinaNet in :paper:`RetinaNet`.
    """

    @configurable
    def __init__(
        self,
        *,
        backbone: Backbone,
        head: nn.Module,
        head_in_features,
        anchor_generator,
        box2box_transform,
        anchor_matcher,
        num_classes,
        focal_loss_alpha=0.25,
        focal_loss_gamma=2.0,
        smooth_l1_beta=0.0,
        box_reg_loss_type="smooth_l1",
        test_score_thresh=0.05,
        test_topk_candidates=1000,
        test_nms_thresh=0.5,
        max_detections_per_image=100,
        pixel_mean,
        pixel_std,
        vis_period=0,
        input_format="BGR",
    ):

2. 函数装饰器

那么 @configurable 到低啥意思,有啥作用?这其实是函数装饰器🤣。
假设用 funA() 函数装饰器去装饰 funB() 函数,如下所示:

#funA 作为装饰器函数
def funA(fn):
    print("A_1")
    fn("B") # 执行传入的fn参数
    print("A_2")
    return "Result"
@funA
def funB(str):
    print(str)
print("-----")
print(funB)

使用函数装饰器 A() 去装饰另一个函数 B(),其底层执行了如下 2 步操作:

  • 将 B 作为参数传给 A() 函数
  • 将 A() 函数执行完成的返回值反馈回 B

故上述代码可以等价为

def funA(fn):
    print("A_1")
    fn("B") # 执行传入的fn参数
    print("A_2")
    return "Result"
def funB(str):
    print(str)
funB = funA(funB)
print("-----")
print(funB)

它们的输出均为

A_1
B
A_2
-----
Result

回到之前的问题,configurable函数去装饰init函数,所以init = configurabl(init)

3. 问题2

可是不对啊,为啥调试的时候直接跑到了configurable函数里的wrapped,这又是搞哪出?

def configurable(init_func=None, *, from_config=None):
    if init_func is not None:
        assert (
            inspect.isfunction(init_func)
            and from_config is None
            and init_func.__name__ == "__init__"
        ), "Incorrect use of @configurable. Check API documentation for examples."

        @functools.wraps(init_func)
        def wrapped(self, *args, **kwargs):
            try:  #直接跑这来了!
                from_config_func = type(self).from_config
            except AttributeError as e:
                raise AttributeError(
                    "Class with @configurable must have a 'from_config' classmethod."
                ) from e
            if not inspect.ismethod(from_config_func):
                raise TypeError("Class with @configurable must have a 'from_config' classmethod.")

            if _called_with_cfg(*args, **kwargs):
                explicit_args = _get_args_from_config(from_config_func, *args, **kwargs)
                init_func(self, **explicit_args)
            else:
                init_func(self, *args, **kwargs)
        return wrapped

    else:
        if from_config is None:
            return configurable
        assert inspect.isfunction(
            from_config
        ), "from_config argument of configurable must be a function!"

        def wrapper(orig_func):
            @functools.wraps(orig_func)
            def wrapped(*args, **kwargs):
                if _called_with_cfg(*args, **kwargs):
                    explicit_args = _get_args_from_config(from_config, *args, **kwargs)
                    return orig_func(**explicit_args)
                else:
                    return orig_func(*args, **kwargs)
            return wrapped
        return wrapper

4. 理解@functools.wraps()

这就涉及到另一个点了,Python装饰器(decorator)在实现的时候,被装饰后的函数其属性会发生改变
例如之前的funA、funB例子,funA在装饰funB之后,funB相当于一字符串

那么为了保证被装饰器装饰后的函数仍拥有原来的属性,Python的functools包中提供了一个叫wraps的装饰器(decorator)来消除这样的副作用。
对之前funA和funB示例进行小改,得到如下代码:

#funA 作为装饰器函数
import functools
def funA(func=None):
    print("A_1")
    if func is not None:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print('Calling decorated function...')
            func(*args, **kwargs)
        return wrapper
    print("A_2")
    return "Result"
@funA
def funB(str):
    print(str)
print("--分界线,开始执行funB---")
funB("hhhhh")

执行结果如下:

A_1
--分界线,开始执行funB---
Calling decorated function...
hhhhh

可见被装饰的funB仍旧保持函数性质可被执行,只不过此时的funB和原先不同,此时funB等价于

def funB(str):
    print('Calling decorated function...')
    print(str)

5. 总结

问题1中谈到实例化RetinaNet类时直接进入configurable函数的说法貌似不太准确,装饰器通常是在导入时(即 Python 加载模块时)便发挥作用,之后被装饰的函数其属性将发生变化。
问题2提到调试时直接进入configurable函数里的wrapped函数,这是因为其使用了wraps装饰器(decorator),所以init函数仍旧拥有原来的属性,只不过执行的内容由于装饰器的缘故而做了一定的补充。

参考链接

https://blog.csdn.net/liuzonghao88/article/details/103586634
https://www.cnblogs.com/hosseini/p/7814941.html

posted @ 2021-07-01 09:47  MorStar  阅读(751)  评论(2编辑  收藏  举报