Decorators in Python

In this article I will explain what are the Decorators in Python. And I will also show how I create my own Decorator.

Content

  • Decorators Definition
  • @staticmethod
  • @classmethod
  • @property
  • My Custom Decorator
  • Custom Decorator with Params

You can find more details in the following video.

All the code used in the article is available in this repository.

Decorators Definition

A decorator is a function which will wrap another method or function. The decorator can modify the original behavior of the decorated method or function by altering the input parameter or output value, or it can just spy on it.

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def get_name(self):
        return self.name

    def get_age(self):
        return self.age

@staticmethod

Let’s see the first example. In object oriented programming, the methods inside a class always have the self input parameter. The self parameter allows me to access the instance values of the object. Let’s decorate a method with @staticmethod.

class Dog:
    ...
    @staticmethod
    def get_paws_number():
        return 4

The @staticmethod decorator allows me to not have the self input parameter. The decorator modified the method. As the method get_paws_number doesn’t need to read information from the instance object, I can declare it as a static method.

@classmethod

class Dog:
    PAWS_NUMBER = 4
    ...
    @classmethod
    def get_paws_number(cls):
        return cls.PAWS_NUMBER

An alternative is the @classmethod. This way, the method is also modified, no self input parameter, but this time, I have the cls input parameter. This new input parameter allows me to access the variables of the class, not the variables of the instance.

@property

Okay, @staticmethod and @classmethod are two built-in decorators. Let’s see the last one, @property.

class Dog:
    def __init__(self, birth_year):
        self.birth_year = birth_year

    @property
    def age(self):
        return date.today().year - self.birth_year

my_dog = Dog(2000)
print(my_dog.age)

This time the decorator not only modifies a method but the way a method is called. It allows me to access a method as it was a single property. And I can go further. The @property decorator has two more decorators inside.

class Dog:
    ...
    @property.setter
    def age(self, age):
        self.birth_year = date.today().year - age

    @property.deleter
    def age(self):
        del self.birth_year

my_dog = Dog(1900)
my_dog.age = 15

The main decorator allows me to read values as it was a property. The setter decorator allows me to have a method as a setter, but use it as a property. And the @property decorator also has the deleter decorator to remove the field and its value from the object. As before, all of this using it as a property.

My Custom Decorator

Ok, those were the main building the decorators used in Python. Those decorators modify the method where they are placed. But let’s see what else I can do with a decorator. Let’s see how to implement my own decorator. As I said at the beginning, a decorator is like a wrapper, it will surround the method where it’s placed. As my first example, I will calculate the time elapsed in a method using a decorator. As the decorator allows me to run some code before and after calling the method, I will save the start time and the end time and compute the difference.

def timed(func):

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        try:
            return func(*args, **kwargs)
        finally:
            LOGGER.debug(f"Method {func.__name__} took {time.time() - start}")

    return wrapper

Timed is the name of the decorator. I get inside, func, the decorated method as input parameter. The other decorator, functools.wraps, is useful to preserve the original name of the function when using some built-in properties. Then the wrapper accepts all the input parameters of the decorated method. And from line 5 to 9 is the logic of my decorator. I store the start time, call the function, and get the end time in the finally clause which will always be executed before leaving the block. Finally, return the wrapper at line 11 which will be used to call the method. Let’s see how to use it.

@timed
def some_method():
    # some code

Custom Decorator with Params

What if I want that my decorator to have a different behavior upon the method decorated? What if I need to parameterize my decorator with some input parameters? Let’s create another decorator which accepts input parameters. Maybe, print the time elapsed for each method can generate a lot of logs. What if my method is called hundreds of times per second? I will create another decorator which will calculate the average time elapsed of the decorated method. And as input value, it will accept the window size where to calculate the average.

TIMES_PER_FUNC: Dict[str, Dict[str, int]] = dict()
lock = threading.RLock()

def timed_windowed(period):

    def decorator_timed_windowed(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            start = time.time()
            try:
                return func(*args, **kwargs)
            finally:
                with lock:
                    times = TIMES_PER_FUNC.get(build_key(func), {"sum": 0, "count": 0, "first": start})
                time_elapsed = time.time() - start
                times["sum"] += time_elapsed
                times["count"] += 1
                if times["first"] + period < start:
                    avg = times["sum"] / times["count"]
                    LOGGER.debug(f"Method {build_key(func)} took {avg}")
                    with lock:
                        del TIMES_PER_FUNC[build_key(func)]
                else:
                    with lock:
                        TIMES_PER_FUNC[build_key(func)] = times

        return wrapper

    return decorator_timed_windowed

This one is a little bit harder. But let’s go step by step. I need to store the sum, the count and the initial time for each method I want to trace in a dictionary. Then, when the window ends, compute the average. At line 1 is the dictionary which will contain all of this.

The lock at line 2 is necessary as the dictionary will be updated from multiple methods at the same time. I must be sure that the writes are done one by one.

Now the decorator definition at line 4 is a little bit different. I need another level of definition, another level of declaration. The first one at line 4 will accept as input parameter the input value described. And the second definition at line 6 will accept the method decorated. The rest remains similar as before. Finally, between lines 9 and 25 is the logic to store the times and compute the average time. And at the end, return both wrappers. Let’s use it now.

@timed_windowed(60)
def some_method():
    # some code

This was a simple use case I can do with decorators. But I can perform some more interesting actions:

  • I can print the value of all the input parameters and the output value for debug purpose.
  • I can check the authentication when used in controllers, reading the value from the headers or the cookies.
  • I can create a retry decorator to run again the method if some error occurs.
  • I can modify the input values, add a new input value as the user object from the authenticated headers.
  • I can modify the output values, wrap the returned value into a richer object.
  • Or simply surround my method with a try-except-finally.

Conclusion

  • I’ve used the @staticmethod to identify a method of a class as a static with no self parameter.
  • I’ve used the @classmethod to identify a method of a class as belonging to the class with access to the class values with the cls parameter.
  • I’ve used the @property decorator to simulate a property but have a method to set, get and delete the values.
  • I’ve created my own decorator to obtain the time elapsed inside a single method.
  • And I’ve created another decorator which accepts input values to perform the average time elapsed given a window time.

References

Repository


Never Miss Another Tech Innovation

Concrete insights and actionable resources delivered straight to your inbox to boost your developer career.

My New ebook, Best Practices To Create A Backend With Spring Boot 3, is available now.

Best practices to create a backend with Spring Boot 3

Leave a comment

Discover more from The Dev World - Sergio Lema

Subscribe now to keep reading and get access to the full archive.

Continue reading