Decorators
Decorators provide a simple syntax for calling higher-order functions [GeirAH].
Important
There is some kind of misunderstanding in definitions.
Decorator is a function returning another function, usually applied
as a function transformation using the @wrapper
syntax
[docf].
However, that’s no quit enough to describe it. The more complete definition is:
Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors [ref].
Before you understand decorators, you must first understand how functions work.
First-class objects
In Python functions are first-class objects. Everything in Python is an object. Functions are objects too.
Inner functions
Functions can be nested. This means it is possible to define functions inside other functions.
def heap_sort(origin: List[int]) -> List[int]:
"""Return a sorted collection using the heap sort algorithm"""
def heapify(_ds: List[int], _size: int, _idx: int) -> List[int]:
...
...
for idx in range(size, -1, -1):
heapify(result, size, idx)
...
The order in which inner functions are defined no matters. The function
definition does not execute the function body; this gets executed only when
the function is called. Furthermore, the inner functions are not defined until
the parent function is called. They are locally scoped to their parent. Trying
to call heapify
function outside of heap_sort
will cause NameError
exception.
Functions are objects
This means functions can be passed around and used as arguments, just like any other object (e.g. int, str etc.).
from typing import Callable
def say_hello(name: str) -> str:
return f"Hello, {name}!"
def be_awesome(name: str) -> str:
return f"Yo, {name}!"
def greet_serhii(greeting_func: Callable) -> str:
return greeting_func("Serhii")
if __name__ == "__main__":
print(f"{greet_serhii(say_hello) = }")
print(f"{greet_serhii(be_awesome) = }")
Returning functions
Since function can be passed as an argument, it may be returned from another function.
from typing import Callable
def parent(idx: int) -> Callable:
def first_child():
return "this is the first child"
def second_child():
return "this is the second child"
return second if not num % 2 else first
first = parent(1)
second = parent(2)
Note
parent
returns functions themselves, there are no parentheses.
After running the code snippet above, first
refers the first_child
function from the inner parent
scope. From now it can be used to call
the target function it refers.
>>> first()
"this is the first child"
>>> second()
"this is the second child"
Simple decorators
Now you’re ready to move on and see the magical beast that is the Python decorators. Let’s start with a simple example:
def decorator(func: Callable) -> Callable:
def wrapper():
print(f"before {func.__name__} call")
func()
print(f"after {func.__name__} call")
return wrapper # no wrapper call, return reference to wrapper function
def say_hello():
print("Hello!")
say_hello_decorated = decorator(say_hello)
Running function:
>>> say_hello()
Hello!
>>> say_hello_decorated()
before say_hello call
Hello!
after say_hello call
The common way to use decorators is to replace the original function with a decorated one:
>>> say_hello = decorator(say_hello)
>>> say_hello()
before say_hello call
Hello!
after say_hello call
say_hello
function is the reference to the decorator.<locals>.wrapper
,
which itself is bound to the original say_hello
function. There is a
syntactic sugar to do this, called pie-syntax. The following example does
exact the same things as the first decorator example:
def decorator(func: Callable) -> Callable:
def wrapper():
print(f"before {func.__name__} call")
func()
print(f"after {func.__name__} call")
return wrapper # no wrapper call, return reference to wrapper function
@decorator
def say_hello():
print("Hello!")
Important
There is no way to undecorate object in Python. Once something is bound to the decorator’s wrapper - it is decorated forever.
A decorated function still remains a function. So, it can be decorated once more time again, and again, and again…
from typing import Callable
def bread(func: Callable) -> Callable:
def wrapper():
print("<--bread-->")
func()
print("<--bread-->")
return wrapper
def vegetables(func: Callable) -> Callable:
def wrapper():
print("~~~salad~~~")
print("***tomato***")
func()
return wrapper
def cheese(func: Callable) -> Callable:
def wrapper():
func()
print("---cheese---")
return wrapper
@bread
@vegetables
@cheese
def sandwich():
print("_sliced_meat_")
>>> sandwich()
<--bread-->
~~~salad~~~
***tomato***
_sliced_meat_
---cheese---
<--bread-->
Note
“Wrapper” is the alternative nickname for the Decorator pattern that clearly expresses the main idea of the pattern. A wrapper is an object that can be linked with some target object. The wrapper contains the same set of methods as the target and delegates to it all requests it receives. However, the wrapper may alter the result by doing something either before or after it passes the request to the target.
Passing arguments to the wrapper
Until now the examples use simple decorators. But what if the decorated
function gets some arguments? This will cause TypeError
exception that tell
that “arguments are missed”. This can be fixed with just passing arguments to
the wrapper
inner function.
import logging
from typing import Callable
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def log(func: Callable) -> Callable:
def wrapper(x, y):
logger.info("%s called", func.__name__)
return func(x, y)
return wrapper
@log
def sum_numbers(x, y):
return x + y
If case you’re trying to make a generic decorator, you may pass *args
and
**kwargs
instead of exact arguments:
from typing import Callable
def generic_decorator(func: Callable) -> Callable:
def wrapper(*args, **kwargs):
... # do something before
result = func(*args, **kwargs)
... # do something after
return result
return wrapper
Passing arguments to decorator
At last it’s time to know how to pass arguments to the decorator.
Here is a simple implementation of defer
decorator which deferred
the function execution for some time:
import time
from typing import Callable
def slow_down(seconds: int = 3) -> Callable:
def decorator(func: Callable) -> Callable:
def wrapper(*args, **kwargs):
started_at = time.perf_counter()
time.sleep(seconds)
result = func(*args, **kwargs)
completed_in = round(time.perf_counter() - started_at, 2)
print("Completed in %.2f" % completed_in)
return result
return wrapper
return decorator
@slow_down()
def function_a():
return 42
@slow_down(10)
def function_b():
return 24
function_a()
Completed in 3.00
function_b()
Completed in 10.00
Class decorators
There are some pre-defined decorators exists for usage together with classes. They are:
classmethod
staticmethod
property
If you develop an intuitive understanding for their differences you’ll be able to write object-oriented Python that communicates its intent more clearly and will be easier to maintain in the long run [DanBader].
Class methods
Instead of accepting a self
parameter, class methods take a cls
parameter that points to the class — and not the object instance — when
the method is called.
Because the class method only has access to this cls
argument, it can’t
modify object instance state. That would require access to self
.
However, class methods can still modify class state that applies across all
instances of the class.
The common usage for classmethod
is provide alternative initializers.
Static methods
This type of method takes neither a self
nor a cls
parameter (but of
course it’s free to accept an arbitrary number of other parameters).
Therefore a static method can neither modify object state nor class state. Static methods are restricted in what data they can access - and they’re primarily a way to namespace your methods.
It’s tricky to explain staticmethod
usage. Almost always you can create
a dedicated function instead of static method. But sometimes you need to
bind some logic independent from class itself or its instances to a class -
it common to encapsulate it with staticmethod
.
Properties
It’s a way to bind a method name to access it as an attribute. Properties are
read-only by default. This means a value cannot be assigned to property
member
.
Some examples
import datetime
class Person:
"""Person class implementation"""
def __init__(self, first_name: str, last_name: str) -> None:
"""Initialize instance"""
self.first_name = first_name
self.last_name = last_name
@classmethod
def from_fullname(cls, name: str) -> "Person":
"""Return a person instance"""
first_name, last_name = name.rsplit(" ", 1)
return cls(first_name, last_name)
@staticmethod
def format_date(date: datetime.date) -> str:
"""Return a formatted date as string"""
return date.strftime("%d-%m-%Y")
@property
def fullname(self) -> str:
"""Return person's fullname"""
return " ".join([self.first_name, self.last_name])
>>> sh = Person("Serhii", "Horodilov")
>>> vp = Person.from_fullname("Vladyslav Ponomaryov")
>>> sh.fullname
'Serhii Horodilov'
>>> vp.fullname
'Vladyslav Ponomaryov'