toggling decorators

后端 未结 7 1890
伪装坚强ぢ
伪装坚强ぢ 2021-01-04 18:37

What\'s the best way to toggle decorators on and off, without actually going to each decoration and commenting it out? Say you have a benchmarking decorator:



        
7条回答
  •  挽巷
    挽巷 (楼主)
    2021-01-04 18:40

    I think you should use a decorator a to decorate the decorator b, which let you switch the decorator b on or off with the help of a decision function.

    This sounds complex, but the idea is rather simple.

    So let's say you have a decorator logger:

    from functools import wraps 
    def logger(f):
        @wraps(f)
        def innerdecorator(*args, **kwargs):
            print (args, kwargs)
            res = f(*args, **kwargs)
            print res
            return res
        return innerdecorator
    

    This is a very boring decorator and I have a dozen or so of these, cachers, loggers, things which inject stuff, benchmarking etc. I could easily extend it with an if statement, but this seems to be a bad choice; because then I have to change a dozen of decorators, which is not fun at all.

    So what to do? Let's step one level higher. Say we have a decorator, which can decorate a decorator? This decorator would look like this:

    @point_cut_decorator(logger)
    def my_oddly_behaving_function
    

    This decorator accepts logger, which is not a very interesting fact. But it also has enough power to choose if the logger should be applied or not to my_oddly_behaving_function. I called it point_cut_decorator, because it has some aspects of aspect oriented programming. A point cut is a set of locations, where some code (advice) has to be interwoven with the execution flow. The definitions of point cuts is usually in one place. This technique seems to be very similar.

    How can we implement it decision logic. Well I have chosen to make a function, which accepts the decoratee, the decorator, file and name, which can only say if a decorator should be applied or not. These are the coordinates, which are good enough to pinpoint the location very precisely.

    This is the implementation of point_cut_decorator, I have chosen to implement the decision function as a simple function, you could extend it to let it decide from your settings or configuration, if you use regexes for all 4 coordinates, you will end up with something very powerful:

    from functools import wraps
    

    myselector is the decision function, on true a decorator is applied on false it is not applied. Parameters are the filename, the module name, the decorated object and finally the decorator. This allows us to switch of behaviour in a fine grained manner.

    def myselector(fname, name, decoratee, decorator):
        print fname
    
        if decoratee.__name__ == "test" and fname == "decorated.py" and decorator.__name__ == "logger":
            return True
        return False 
    

    This decorates a function, checks myselector and if myselector says go on, it will apply the decorator to the function.

    def point_cut_decorator(d):
        def innerdecorator(f):
            @wraps(f)
            def wrapper(*args, **kwargs):
                if myselector(__file__, __name__, f, d):
                    ps = d(f)
                    return ps(*args, **kwargs)
                else:
                    return f(*args, **kwargs)
            return wrapper
        return innerdecorator
    
    
    def logger(f):
        @wraps(f)
        def innerdecorator(*args, **kwargs):
            print (args, kwargs)
            res = f(*args, **kwargs)
            print res
            return res
        return innerdecorator
    

    And this is how you use it:

    @point_cut_decorator(logger)
    def test(a):
        print "hello"
        return "world"
    
    test(1)
    

    EDIT:

    This is the regular expression approach I talked about:

    from functools import wraps
    import re
    

    As you can see, I can specify somewhere a couple of rules, which decides a decorator should be applied or not:

    rules = [{
        "file": "decorated.py",
        "module": ".*",
        "decoratee": ".*test.*",
        "decorator": "logger"
    }]
    

    Then I loop over all rules and return True if a rule matches or false if a rule doesn't matches. By making rules empty in production, this will not slow down your application too much:

    def myselector(fname, name, decoratee, decorator):
        for rule in rules:
            file_rule, module_rule, decoratee_rule, decorator_rule = rule["file"], rule["module"], rule["decoratee"], rule["decorator"]
            if (
                re.match(file_rule, fname)
                and re.match(module_rule, name)
                and re.match(decoratee_rule, decoratee.__name__)
                and re.match(decorator_rule, decorator.__name__)
            ):
                return True
        return False
    

提交回复
热议问题