'Decorator factory returning lambda
I came across the following code (this is a simplification of the real code) which combines decorators factory and lambda usage:
from functools import wraps
def decorator_factory(arg1):
def decorator(arg2):
return lambda func: real_decorator(arg1, arg2, func)
return decorator
def real_decorator(prefix: str, perm: str, func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
my_decorator_instance = decorator_factory("r")
class MyClass():
def __init__(self, *args, **kwargs):
pass
@my_decorator_instance('decorator args')
def method_1(self):
print("method_1")
m = MyClass()
m.method_1()
The code works great but I don't really understand the mechanics of the same. Especially how is the lambda value used when returned as a decorator. Please notice that in the decorator_factory there's no func argument. the func is the arg of lambda.
Solution 1:[1]
It's just returning an anonymous function instead of using a def statement. The following is entirely equivalent:
def decorator_factory(arg1):
def decorator(arg2):
def _(func):
return real_decorator(arg1, arg2, func)
return _
return decorator
decorator_factory("r") returns a function bound to the name my_decorator_instance; my_decorator_instance('decorator_args') returns a function that gets applied to method_1.
Just to get a better feel for how decorators work (they are just functions applied to other things), you could also have written
class MyClass():
def __init__(self, *args, **kwargs):
pass
@decorator_factory("r")('decorator args')
def method_1(self):
print("method_1")
or even
class MyClass():
def __init__(self, *args, **kwargs):
pass
method_1 = decorator_factory("r")('decorator args')(lambda self: print("method_1"))
Solution 2:[2]
To get a better feeling for how the various nested functions come into effect, you can imagine them being "unwrapped" on each function call (with the corresponding function parameter being replaced by whatever the function argument was). So your example code is roughly equivalent to the following:
def decorator_factory(arg1):
def decorator(arg2):
return lambda func: real_decorator(arg1, arg2, func)
return decorator
# The following function call effectively "unwraps" the `decorator_factory` function:
# `my_decorator_instance = decorator_factory("r")`
# is equivalent to (`arg1` being replaced by "r"):
def my_decorator_instance(arg2):
return lambda func: real_decorator("r", arg2, func)
# Now unwrapping `my_decorator_instance`:
# `@my_decorator_instance("decorator args")`
# is equivalent to (`arg2` being replaced by "decorator args"):
@(lambda func: real_decorator("r", "decorator args", func))
For the last step, i.e. where the decorator is applied, the lambda function captures the already-specified parameters ("r" and "decorator args") and it also takes care of putting the func argument at the correct position (third) for the function call to real_decorator.
Something similar can be achieved by using functools.partial with real_decorator (i.e. partial takes on the job from decorator_factory):
from functools import partial
[...]
my_decorator_instance = partial(real_decorator, "r")
class MyClass:
@partial(my_decorator_instance, "decorator args")
def method_1(self):
pass
Solution 3:[3]
One possible cause for confusion is that the decorator_factory function is not a decorator factory, but a (decorator factory) factory.
A decorator factory is a function that returns a decorator (See this answer by Martijn Pieters). For example, functools.lru_cache is a decorator factory because it returns a decorator:
import functools
decorator = functools.lru_cache(maxsize=69)
@decorator
def compute_something_expensive(foo, bar):
...
But your function returns a decorator factory, not a decorator. You can refactor it like this:
def decorator_factory_factory(arg1):
def decorator_factory(arg2):
def decorator(func):
return real_decorator(arg1, arg2, func)
return decorator
return decorator_factory
You can't use the function like this because it's not a decorator factory:
# will not really work:
@decorator_factory_factory('foo')
def method_1(self):
print("method_1")
You have to use it like so:
@decorator_factory_factory('foo')('bar')
def method_1(self):
print("method_1")
# alternatively:
decorator_factory = decorator_factory_factory('foo')
@decorator_factory('bar')
def method_1(self):
print("method_1")
Solution 4:[4]
let's focus on this part of code:
my_decorator_instance = decorator_factory("r")
class MyClass():
def __init__(self, *args, **kwargs):
pass
@my_decorator_instance('decorator args')
def method_1(self):
print("method_1")
m = MyClass()
m.method_1()
the first line (function call) is going to apply "r" on arg1, again, the decorator is being used for the method with value "decorator args", this means this value is going to be applied on arg2, then the method is going be applied on func argument
Remember, you can use chaining in function calls in python, so the code above is equivalent to:
decorator_factory("r")("decorator args")(method_1)
Now for lambda part inside decorator, to ease it up for yourself, try convert the lambda to function, it becomes:
def function(func):
return real_decorator(arg1,arg2,func)
so there is nothing magical about it.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | chepner |
| Solution 2 | a_guest |
| Solution 3 | |
| Solution 4 | Ghazi |
