All about Python Decorators

If you’re new to Python or never worked with traditional languages such as Java, at some point you’ve most likely opened a piece of Python code only to be bewildered by the strange “@” notation. This is the Python decorator syntax. In this article, I will demystify the Python decorator and explain its many use cases.

In brief, the decorator syntax allows the extension of a method with a decorator function without modifying the structure of the original function. Decorators can be as simple as a function that logs the input and output of a function to better facilitate debugging or a dominant feature of a Framework modifying how the functions themselves execute (such as in the Web Framework Flask).

How Do They Work?

Decorators essentially wrap your method inside another function, a very simple example would be if you had a function that returned a string and you needed that string to be upper case.

# takes in function to decorate
def uppercase_decorator(func):
  def wrapper():
    # Get the output of the original function
    output = func()
    # Modify the original output and return it
    return output.upper()
  # return the wrapper used to modify the decorated function
  return wrapper
def say_hello():
  return 'hello'
>>> say_hello()

How Are Function Arguments Passed?

Function arguments are passed directly to the wrapper function, if you need to use an unknown or varying number of arguments then *args and **kwargs can be used. For example, a generic logging decorator would need to accept a varying number of arguments. Below we print out the incoming arguments before calling the sum_values method, then store and print the output of the method.

def log_function(func):
  def wrapper(*args, **kwargs):
    print('Incoming arguments', args, kwargs)
    output = func(*args, **kwargs)
    print("Output", output)
  return wrapper

def sum_values(a, b, c, extra_kwarg=0):
  return a+b+c+extra_kwarg
>>> sum_values(1 ,2, 3, extra_kwarg=5)
Incoming arguments (1, 2, 3) {'extra_kwarg': 5}
Output 11

Getters and Setters

Getters and setters allow you to encapsulate the behaviour of properties within a class, allowing for additional functionality such as validation of a property on change. Traditionally this is done with two functions, normally named get_property and set_property. However, Python has an elegant solution to this, the property and setter decorators.

These two decorators allow for getter and setter functionality on a property while hiding the get and set methods. Instead, the getter and setter methods are named the desired property name and depending on how the property is used (get or set), the appropriate method is called.

In the example below of the class Heater, there is an internal property __temperature, which is encapsulated within the @property and @temperature.setter. If the property temperature is used in a read context then the @property method is called, and if it’s used in a write context then the @temperature.setter method is called. Here on set, we add extra validation so that the temperature cannot be set to a non-physical value.

class Heater:
    def __init__(self, temperature=0):
        self.__temperature = temperature

    def temperature(self):
        print("Getting temperature")
        return self.__temperature

    def temperature(self, new_temperature):
        if new_temperature < -273.15:
            raise ValueError("Temperature is outside the physical range")
        print("Setting temperature")
        self.__temperature = new_temperature
>>> heater = Heater()
>>> heater.temperature = -300
ValueError: Temperature is outside the physical range

>>> heater.temperature = 70
Setting temperature

>>> print(heater.temperature)
Getting temperature

Class Methods and Static Methods

Python has built-in decorators to support class methods and static methods. These allow access to methods with or without instantiation of the class. A class method receives the class as the first argument, just like a normal method within a class receives the instance (self) as the first argument. This is useful if you need to access the method without instantiating the class, but still need access to the class variables.

A static method is similar to a class method however, it has no access to the class or the instance and has no required arguments. This is useful if you need access to the method outside the class without instantiating it and have no requirement on any class properties.

Below is an example class containing a class method and a static method.

class DecoratorExamples:
    CLASS_VARIABLE = 'Class Method'

    def class_method(cls):
        # Has access to class variables, but not object instance

    def static_method():
        # No access to class variables or object instance
        print('Static Method')
>>> # Called without an instance
>>> DecoratorExamples().class_method()
Class Method

>>> DecoratorExamples().static_method()
Static Method

>>> # Called from an instance
>>> decorator_instance = DecoratorExamples()
>>> decorator_instance.class_method()
Class Method

>>> decorator_instance.static_method()
Static Method

Summary and Further Reading

Decorators allow you to modify the behaviour of a method without modifying its structure. This helps in keeping your code clean and easy to maintain without compromising on functionality.

They can greatly simplify the refactoring of legacy code, as you can quickly extend the functionality of the existing code, without needing to modify the methods themselves.

If you’re interested in the origin of decorators in Python PEP-0318 contains a discussion and introduction of decorators into the Python language.


Related Posts