Reputation: 791
After spending several hours on the topic of decorators in python, I still have two issues.
First; if you have decorator without argument, sytntax is like this:
@decorator
def bye():
return "bye"
which is just a syntactic sugar and is same as this
bye = decorator(bye)
but if I have a decorator with argument:
@decorator(*args)
def bye():
return "bye"
How does "no-sugar" version looks like? Is the function passed inside as one of the arguments?
bye = decorator("argument", bye)
Second issue(which is related to the first, yet more practical example);
def permission_required(permission):
def wrap(function):
@functools.wraps(function)
def wrapped_func(*args, **kwargs):
if not current_user.can(permission):
abort(403)
return function(*args, **kwargs)
return wrapped_function
return wrap
def admin_required(f):
return permission_required(Permission.ADMINISTER)(f)
Here permission_required decorator is passed to a return statement of newly created decorator named admin_required. I have no idea how this works. Mainly the return statement where we returning original decorator + the function(in strange syntax). Can someone elaborate on this? - details are extremely welcome
Upvotes: 6
Views: 6987
Reputation: 140559
When arguments are given in decorator notation,
@decorator(a, b, c)
def function(): pass
it is syntactic sugar for writing
def function(): pass
function = decorator(a, b, c)(function)
That is, decorator
is called with arguments a, b, c, and then the object it returns is called with sole argument function
.
It is easiest to understand how that makes sense when the decorator is a class. I'm going to use your permission_required
decorator for a running example. It could have been written thus:
class permission_required:
def __init__(self, permission):
self.permission = permission
def __call__(self, function):
@functools.wraps(function)
def wrapped_func(*args, **kwargs):
if not current_user.can(permission):
abort(403)
return function(*args, **kwargs)
return wrapped_func
admin_required = permission_required(Permission.ADMINISTER)
When you use the decorator, e.g.
@permission_required(Permission.DESTRUCTIVE)
def erase_the_database():
raise NotImplementedError # TBD: should we even have this?
you instantiate the class first, passing Permission.DESTRUCTIVE
to __init__
, and then you call the instance as a function with erase_the_database
as an argument, which invokes the __call__
method, which constructs the wrapped function and returns it.
Thinking about it this way, admin_required
should be easier to understand: it's an instance of the permission_required
class, that hasn't yet been called. It's basically for shorthand:
@admin_required
def add_user(...): ...
instead of typing out
@permission_required(Permission.ADMINISTER)
def add_user(...): ...
Now, the way you had it...
def permission_required(permission):
def wrap(function):
@functools.wraps(function)
def wrapped_func(*args, **kwargs):
if not current_user.can(permission):
abort(403)
return function(*args, **kwargs)
return wrapped_func
return wrap
is really just another way of writing the same thing. Returning wrap
from permission_required
implicitly creates a closure object. It can be called like a function, and when you do it calls wrap
. It remembers the value of permission
passed to permission_required
so that wrap
can use it. That's exactly what the class I showed above does. (In fact, compiled languages like C++ and Rust often implement closures by desugaring them into class definitions like the one I showed.)
Notice that wrap
itself does the same thing! We could expand it out even further...
class permission_check_wrapper:
def __init__(self, function, permission):
self.function = function
self.permission = permission
functools.update_wrapper(self, function)
def __call__(self, *args, **kwargs):
if not current_user.can(permission):
abort(403)
return function(*args, **kwargs)
class permission_required:
def __init__(self, permission):
self.permission = permission
def __call__(self, function):
return permission_check_wrapper(self.permission, function)
Or we could do the entire job with functools.partial
:
def permission_check_wrapper(*args, function, permission, **kwargs):
if not current_user.can(permission):
abort(403)
return function(*args, **kwargs)
def wrap_fn_with_permission_check(function, *, permission):
return functools.update_wrapper(
functools.partial(permission_check_wrapper,
function=function,
permission=permission),
wrapped=function)
def permission_required(permission):
return functools.partial(wrap_fn_with_permission_check,
permission=permission)
The beauty of defining @decorator(a,b,c) def foo
to desugar to foo = decorator(a,b,c)(foo)
is that the language doesn't care which of these several implementation techniques you pick.
Upvotes: 10
Reputation: 9597
A decorator with an argument is simply called (with that argument), to produce another decorator. That decorator is then called with the decorated function as its argument, as usual. So the translation of:
@decorator(*args)
def bye():
return "bye"
would be:
bye = decorator(*args)(bye)
Or maybe you'd find that clearer as:
temp = decorator(*args)
bye = temp(bye)
(except that no temp
variable is actually created, of course.)
In your second issue, @admin_required
is being defined as a shortcut for @permission_required(Permission.ADMINISTER)
.
Upvotes: 3