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
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(self.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(self.permission):
abort(403)
return self.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.