Decorator with parameters

2019-07-17 06:21发布

问题:

Can you explain me how the following decorator works:

def set_ev_cls(ev_cls, dispatchers=None):
    def _set_ev_cls_dec(handler):
        if 'callers' not in dir(handler):
            handler.callers = {}
        for e in _listify(ev_cls):
            handler.callers[e] = _Caller(_listify(dispatchers), e.__module__)
        return handler
    return _set_ev_cls_dec


@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
def _switch_features_handler(self, ev):
    datapath = ev.msg.datapath
    ....

Please, don't go into details on what's going on inside the function. I'm interested in how the decorator with parameters wrap methods here. By the way, it's a code snippet from Ryu (event registration mechanism).

Thank you in advance

回答1:

First, a decorator is just a function that gets called with a function. In particular, the following are (almost) the same thing:

@spam
def eggs(arg): pass

def eggs(arg): pass
eggs = spam(eggs)

So, what happens when the decorator takes parameters? Same thing:

@spam(arg2)
def eggs(arg): pass

def eggs(arg): pass
eggs = spam(arg2)(eggs)

Now, notice that the function _set_ev_cls_dec, which is ultimately returned and used in place of _switch_features_handler, is a local function, defined inside the decorator. That means it can be a closure over variables from the outer function—including the parameters of the outer function. So, it can use the handler argument at call time, plus the ev_cls and dispatchers arguments that it got at decoration time.

So:

  • set_ev_cls_dev creates a local function and returns a closure around its ev_cls and dispatchers arguments, and returns that function.
  • That closure gets called with _switch_features_handler as its parameter, and it modifies and returns that parameter by adding a callers attribute, which is a dict of _Caller objects built from that closed-over dispatchers parameter and keyed off that closed-over ev_cls parameter.


回答2:

Explain how it works without detailing what's going on inside? That kind of sounds like "explain without explaining," but here's a rough walkthrough:

  1. Think of set_ev_cls as a factory for decorators. It's there to catch the arguments at the time the decorator is invoked:

    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    

    And return a function, _set_ev_cls_dec that has its variables bound to:

    ev_cls = ofp_event.EventOFPSwitchFeatures
    dispatchers = CONFIG_DISPATCHER
    

    Or put another way, you now have a 'customized' or 'parametrized' dispatcher that's logically equivalent to:

    def custom_decorator(handler):
        if 'callers' not in dir(handler):
            handler.callers = {}
        for e in _listify(ofp_event.EventOFPSwitchFeatures):
            handler.callers[e] = _Caller(_listify(CONFIG_DISPATCHER), e.__module__)
        return handler
    

    (If you captured the values of ofp_event.EventOFPSwitchFeatures and CONFIG_DISPATCHER at the moment the @set_ev_cls(...) was called).

  2. The custom_decorator of step 1 is applied to _switch_features_handleras a more traditional unparameterized decorator.