How to create a Python decorator that can be used either with or without parameters?

前端 未结 13 1485
死守一世寂寞
死守一世寂寞 2020-11-30 21:26

I\'d like to create a Python decorator that can be used either with parameters:

@redirect_output(\"somewhere.log\")
def foo():
    ....

or

相关标签:
13条回答
  • 2020-11-30 21:37

    Have you tried keyword arguments with default values? Something like

    def decorate_something(foo=bar, baz=quux):
        pass
    
    0 讨论(0)
  • 2020-11-30 21:40

    Generally you can give default arguments in Python...

    def redirect_output(fn, output = stderr):
        # whatever
    

    Not sure if that works with decorators as well, though. I don't know of any reason why it wouldn't.

    0 讨论(0)
  • 2020-11-30 21:41

    A python decorator is called in a fundamentally different way depending on whether you give it arguments or not. The decoration is actually just a (syntactically restricted) expression.

    In your first example:

    @redirect_output("somewhere.log")
    def foo():
        ....
    

    the function redirect_output is called with the given argument, which is expected to return a decorator function, which itself is called with foo as an argument, which (finally!) is expected to return the final decorated function.

    The equivalent code looks like this:

    def foo():
        ....
    d = redirect_output("somewhere.log")
    foo = d(foo)
    

    The equivalent code for your second example looks like:

    def foo():
        ....
    d = redirect_output
    foo = d(foo)
    

    So you can do what you'd like but not in a totally seamless way:

    import types
    def redirect_output(arg):
        def decorator(file, f):
            def df(*args, **kwargs):
                print 'redirecting to ', file
                return f(*args, **kwargs)
            return df
        if type(arg) is types.FunctionType:
            return decorator(sys.stderr, arg)
        return lambda f: decorator(arg, f)
    

    This should be ok unless you wish to use a function as an argument to your decorator, in which case the decorator will wrongly assume it has no arguments. It will also fail if this decoration is applied to another decoration that does not return a function type.

    An alternative method is just to require that the decorator function is always called, even if it is with no arguments. In this case, your second example would look like this:

    @redirect_output()
    def foo():
        ....
    

    The decorator function code would look like this:

    def redirect_output(file = sys.stderr):
        def decorator(file, f):
            def df(*args, **kwargs):
                print 'redirecting to ', file
                return f(*args, **kwargs)
            return df
        return lambda f: decorator(file, f)
    
    0 讨论(0)
  • 2020-11-30 21:41

    This does the job without no fuss:

    from functools import wraps
    
    def memoize(fn=None, hours=48.0):
      def deco(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
          return fn(*args, **kwargs)
        return wrapper
    
      if callable(fn): return deco(fn)
      return deco
    
    0 讨论(0)
  • 2020-11-30 21:44

    Building on vartec's answer:

    imports sys
    
    def redirect_output(func, output=None):
        if output is None:
            output = sys.stderr
        if isinstance(output, basestring):
            output = open(output, 'w') # etc...
        # everything else...
    
    0 讨论(0)
  • 2020-11-30 21:46

    Several answers here already address your problem nicely. With respect to style, however, I prefer solving this decorator predicament using functools.partial, as suggested in David Beazley's Python Cookbook 3:

    from functools import partial, wraps
    
    def decorator(func=None, foo='spam'):
        if func is None:
             return partial(decorator, foo=foo)
    
        @wraps(func)
        def wrapper(*args, **kwargs):
            # do something with `func` and `foo`, if you're so inclined
            pass
    
        return wrapper
    

    While yes, you can just do

    @decorator()
    def f(*args, **kwargs):
        pass
    

    without funky workarounds, I find it strange looking, and I like having the option of simply decorating with @decorator.

    As for the secondary mission objective, redirecting a function's output is addressed in this Stack Overflow post.


    If you want to dive deeper, check out Chapter 9 (Metaprogramming) in Python Cookbook 3, which is freely available to be read online.

    Some of that material is live demoed (plus more!) in Beazley's awesome YouTube video Python 3 Metaprogramming.

    Happy coding :)

    0 讨论(0)
提交回复
热议问题