I'd like to create a Python decorator that can be used either with parameters:
@redirect_output("somewhere.log") def foo(): ....
or without them (for instance to redirect the output to stderr by default):
@redirect_output def foo(): ....
Is that at all possible?
Note that I'm not looking for a different solution to the problem of redirecting output, it's just an example of the syntax I'd like to achieve.
To create a decorator function in Python, I create an outer function that takes a function as an argument. There is also an inner function that wraps around the decorated function. To use a decorator ,you attach it to a function like you see in the code below.
The syntax for decorators with parameters :Inside the inner function, required operations are performed and the actual function reference is returned which will be assigned to func_name. Now, func_name() can be used to call the function with decorator applied on it.
Python decorator are the function that receive a function as an argument and return another function as return value. The assumption for a decorator is that we will pass a function as argument and the signature of the inner function in the decorator must match the function to decorate.
In fact, there are two types of decorators in Python — class decorators and function decorators — but I will focus on function decorators here. Before we get into the fun details of how a basic decorator works and how to implement your own decorators, let's see why we need them in the first place.
I know this question is old, but some of the comments are new, and while all of the viable solutions are essentially the same, most of them aren't very clean or easy to read.
Like thobe's answer says, the only way to handle both cases is to check for both scenarios. The easiest way is simply to check to see if there is a single argument and it is callabe (NOTE: extra checks will be necessary if your decorator only takes 1 argument and it happens to be a callable object):
def decorator(*args, **kwargs): if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): # called as @decorator else: # called as @decorator(*args, **kwargs)
In the first case, you do what any normal decorator does, return a modified or wrapped version of the passed in function.
In the second case, you return a 'new' decorator that somehow uses the information passed in with *args, **kwargs.
This is fine and all, but having to write it out for every decorator you make can be pretty annoying and not as clean. Instead, it would be nice to be able to automagically modify our decorators without having to re-write them... but that's what decorators are for!
Using the following decorator decorator, we can deocrate our decorators so that they can be used with or without arguments:
def doublewrap(f): ''' a decorator decorator, allowing the decorator to be used as: @decorator(with, arguments, and=kwargs) or @decorator ''' @wraps(f) def new_dec(*args, **kwargs): if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): # actual decorated function return f(args[0]) else: # decorator arguments return lambda realf: f(realf, *args, **kwargs) return new_dec
Now, we can decorate our decorators with @doublewrap, and they will work with and without arguments, with one caveat:
I noted above but should repeat here, the check in this decorator makes an assumption about the arguments that a decorator can receive (namely that it can't receive a single, callable argument). Since we are making it applicable to any generator now, it needs to be kept in mind, or modified if it will be contradicted.
The following demonstrates its use:
def test_doublewrap(): from util import doublewrap from functools import wraps @doublewrap def mult(f, factor=2): '''multiply a function's return value''' @wraps(f) def wrap(*args, **kwargs): return factor*f(*args,**kwargs) return wrap # try normal @mult def f(x, y): return x + y # try args @mult(3) def f2(x, y): return x*y # try kwargs @mult(factor=5) def f3(x, y): return x - y assert f(2,3) == 10 assert f2(2,5) == 30 assert f3(8,1) == 5*7
Using keyword arguments with default values (as suggested by kquinn) is a good idea, but will require you to include the parenthesis:
@redirect_output() def foo(): ...
If you would like a version that works without the parenthesis on the decorator you will have to account both scenarios in your decorator code.
If you were using Python 3.0 you could use keyword only arguments for this:
def redirect_output(fn=None,*,destination=None): destination = sys.stderr if destination is None else destination def wrapper(*args, **kwargs): ... # your code here if fn is None: def decorator(fn): return functools.update_wrapper(wrapper, fn) return decorator else: return functools.update_wrapper(wrapper, fn)
In Python 2.x this can be emulated with varargs tricks:
def redirected_output(*fn,**options): destination = options.pop('destination', sys.stderr) if options: raise TypeError("unsupported keyword arguments: %s" % ",".join(options.keys())) def wrapper(*args, **kwargs): ... # your code here if fn: return functools.update_wrapper(wrapper, fn[0]) else: def decorator(fn): return functools.update_wrapper(wrapper, fn) return decorator
Any of these versions would allow you to write code like this:
@redirected_output def foo(): ... @redirected_output(destination="somewhere.log") def bar(): ...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With