Decorators aren't always implemented using functions.
It's helpful to think of a decorator as a function that accepts a function and returns a function to replace our original function with:
from functools import wraps
def log_me(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Calling with", args, kwargs)
return_value = func(*args, **kwargs)
print("Returning", return_value)
return return_value
return wrapper
But decorators aren't necessarily implemented using functions. A decorator can be any callable that accepts a function and returns a replacement object of some sort.
But what would happen if we implemented a decorator using a class?
class log_me:
def __init__(self, func):
self.func = func
When we call a function, we get its return value. When we call a decorator function we get a replacement function.
But our decorator isn't a function here, it's a class.
So what do you think will happen when we use this class as a decorator?
What will greet
be?
>>> @log_me
... def greet(name):
... """Greet a user."""
... print("Hello", name)
When we call a class we get an instance of that class.
So our greet
"function" is now an instance of the log_me
class:
>>> greet
<__main__.log_me object at 0x7f35d8feb2b0>
If our class is a decorator it will be called with a function and it needs to return a replacement function. That means our class instances need to act like replacement functions. So we somehow need to make our class instances act like functions.
Normally, instances of classes aren't callable.
But they can be if we implement a __call__
method:
class log_me:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("Calling with", args, kwargs)
return_value = self.func(*args, **kwargs)
print("Returning", return_value)
return return_value
We've now implemented a decorator using a class.
We can use this log_me
class as a decorator:
>>> @log_me
... def greet(name):
... """Greet a user."""
... print("Hello", name)
And when we call our decorated function, it prints out the arguments that we've called the function with and it will, it does whatever the original function did, and then it prints out the return value:
>>> greet("Trey")
Calling with ('Trey',) {}
Hello Trey
Returning None
But this greet
function isn't actually a function; it's an instance of this log_me
class:
>>> greet
<__main__.log_me object at 0x7f0d346db250>
There is a very big downside to implementing a decorator with a class.
When we ask for help on a decorated function, we normally expect to see:
And in the case of our decorator function because we're using functools.wraps
that is what we see:
>>> help(greet)
Help on function greet in module __main__:
greet(name)
Greet a user.
Note: if you're unfamiliar with functools.wraps
, see making a well-behaved decorator function.
When you implement a decorator using a class, it can't use functools.wrap
to borrow the name and type signature because we can't change that information on class instances.
So if we decorate our greet
function with our log_me
class:
>>> log_me
<class '__main__.log_me'>
>>> @log_me
... def greet(name):
... """Greet a user."""
... print("Hello", name)
And then we ask for help on this new greet
"function", it tells us information about log_me
objects (which isn't very helpful):
>>> help(greet)
Help on log_me in module __main__ object:
class log_me(builtins.object)
| log_me(func)
|
| Methods defined here:
|
| __call__(self, *args, **kwargs)
| Call self as a function.
|
| __init__(self, func)
| Initialize self. See help(type(self)) for accurate signature.
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
It's not usually recommended implementing a decorator using a class, unless you were making a decorator that accepts arguments.
You can think of a decorator that accepts arguments as a function that accepts arguments that returns a decorator.
Here we have a restrict_return
decorator which accepts one argument (the expected return value):
from functools import wraps
def restrict_return(expected):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
value = func(*args, **kwargs)
if value is not expected:
raise ValueError(f"Returned {value!r}, not {expected!r}")
return value
return wrapper
return decorator
We can use this restrict_return
decorator by giving it our expected return value and then using the decorator we get back to decorate our function:
>>> @restrict_return(True)
... def is_odd(n):
... """Return True if n is odd."""
... return n % 2 == 1
When we call this restrict_return
-decorated function, if it returns the expected value of True
, then we'll get that return value:
>>> is_odd(3)
True
But whenever this function returns something that isn't True
, our function raises a ValueError
instead:
>>> is_odd(4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/trey/restrict_return_func.py", line 9, in wrapper
raise ValueError(f"Returned {value!r}, not {expected!r}")
ValueError: Returned False, not True
So if we were to use a class to implement this decorator that accepts arguments, we would need to make a class that gives us back a decorator when we call it. That means our class instances need to be decorators.
Here we have a restrict_return
class that accepts a value:
from functools import wraps
class restrict_return:
def __init__(self, value):
self.value = value
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
value = func(*args, **kwargs)
if value is not self.value:
raise ValueError(f"Returned {value!r}, not {self.value!r}")
return value
return wrapper
And instances of this class are callable (due to __call__
): we can call them with a function (func
) and we'll give back a replacement function (wrapper
).
So class instances act as a decorator here, not the class itself.
Since we're returning a function (wrapper
), we can use functools.wraps
to borrow the original information from our original function.
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
...
return wrapper
If we decorate our is_odd
function just like before:
>>> @restrict_return(True)
... def is_odd(n):
... """Return True if n is odd."""
... return n % 2 == 1
The is_odd
function will have the correct function name, even though we're using a class to implement the restrict_return
decorator:
>>> is_odd
<function is_odd at 0x7f9072f77040>
And if we ask for help on is_odd
, we'll see that it knows its type signature as well:
>>> help(is_odd)
Help on function is_odd in module __main__:
is_odd(n)
Return True if n is odd.
Decorators are not necessarily implemented using functions. You could also make a decorator using a class or an instance of a class.
A decorator is really a callable that accepts a function or a class, and returns some object to replace that function or class with.
Need to fill-in gaps in your Python skills?
Sign up for my Python newsletter where I share one of my favorite Python tips every week.
Sign in to your Python Morsels account to track your progress.
Don't have an account yet? Sign up here.