简体   繁体   中英

creating decorator out of another decorator (python)

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

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) .

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, eg

@permission_required(Permission.DESTRUCTIVE)
def erase_the_database():
    raise NotImplemented # 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM