I\'d like to create a Python decorator that can be used either with parameters:
@redirect_output(\"somewhere.log\")
def foo():
....
or
Have you tried keyword arguments with default values? Something like
def decorate_something(foo=bar, baz=quux):
pass
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.
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)
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
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...
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 :)