blog posts

Decorators In Python And How To Implement Them

Decorators In Python And How To Implement Them

Decorators In Python Are A Type Of Functions That Can Be Used To Change The Behavior Of A Function Or Class.

These changes can include adding functionality to a function, changing a process’s input or output, or changing a class’s behavior. Decorators are applied to a part or type using the @ character.

A decorated function can be executed again with a different name. This feature makes decorators helpful in changing the behavior of many tasks in a program.

In Python, various valuable decorators can be used to change the behavior of functions and classes. These decorators are @staticmethod, @classmethod, @property, @abstractmethod, @asyncio.coroutine and…

When are decorators used in Python?

As mentioned, decorators in Python are used to change the behavior of functions and classes. For example, decorators can add properties to a position that apply to all calls to that function. For example, the `@staticmethod` decorator can be used to define a static function that can be called on its calls like a regular function without creating an instance of the class.

Also, decorators can help define automatic functions. For example, the `@property` decorator can be used to express a computed property (Computed property) that, by calling it, will calculate a value and return it as output.

Also, decorators can be applied to check the inputs and outputs of a function. For example, the @typing decorator can specify the type of inputs and outputs of a process.

In general, decorators are used to change the behavior of functions and classes in Python. They can be used to define automatic functions, determine the type of inputs and outputs, check information and results, and determine the access level of operations.

What capabilities do decorators provide to programmers?

Decorators provide programmers with various high-level functionality, some of which are as follows:

  • Changing the behavior of functions: Decorators allow programmers to change the behavior of functions. For example, using the `@staticmethod` decorator, you can define a static function that will be called in its calls, like a regular function, without creating an instance of the class.
  • Defining automatic properties: Decorators allow programmers to limit mechanical properties. For example, by using the `@property` decorator, you can specify a calculated property (Computed property) that a value will be calculated and returned as output by calling it.
  • Determining the types of inputs and outputs: Decorators allow programmers to specify the inputs and outputs of functions. For example, by using the `@typing` decorator, you can select the type of inputs and outputs of a process.
  • Inspection of inputs and outputs: Decorators allow programmers to inspect the inputs and outputs of functions. For example, using `@check_arguments` and `@check_return` decorators, you can check the inputs and outputs of a process.
  • Determining the access level of functions: Decorators allow programmers to choose the access level of operations. For example, using the `@staticmethod` decorator, you can define a static function that can be called without creating an instance of the class.

In general, decorators allow programmers to change the behavior of functions and classes, define automatic properties, specify the type of inputs and outputs, inspect inputs and outputs, and specify the access level of operations.

An example of how to use decorators in Python

An example of how to use decorators in Python is the “@timing” decorator, which calculates the execution time of a function. For this purpose, the `@timing` decorator can be applied to the process. For example, the following code shows an example of this usage:

import time

def timing_decorator(func):

def wrapper(*args, **kwargs):

start_time = time.time()

result = func(*args, **kwargs)

end_time = time.time()

print(f”Elapsed time: {end_time – start_time}”)

return result

return wrapper

@timing_decorator

def my_function():

time.sleep(2)

my_function()

In this example, the `@timing_decorator` decorator is defined and applied to the `my_function` function. This decorator records the start time of the function execution and, after the execution of the process, records the end time of its execution and calculates the elapsed time between these two times. Then, it prints this time as output.

In this example, by using the `@timing_decorator` decorator, there is no need to change the code of the `my_function` function, and by applying this decorator, you can easily calculate its execution time.

Are there other decorators in Python?

Yes, there are various valuable decorators in Python. In the following, I mention some of these decorators:

  • `@staticmethod`: This decorator defines static functions that can be called independently of an instance of the class.
  •  `@classmethod`: This decorator defines class functions that are useful for performing operations related to the class.
  • `@property`: This decorator defines automatic properties (computed properties) that, when called, calculate a value and return as output.
  • `@abstractmethod`: This decorator defines abstract methods in classes that must be implemented in subclasses.
  • `@staticmethod`: This decorator defines static functions that can be called independently of an instance of the course.
  • `@typing`: This decorator is used to specify the type of inputs and outputs of functions.
  • `@wraps`: This decorator keeps information such as function name and documentation inside another part.
  • `@lru_cache`: This decorator is used to save the arithmetic results of the process and prevent them from being recalculated in subsequent calls.
  • @asyncio: This decorator defines asynchronous operations widely used in small and large programs.
  • `@retry` decorator: This decorator is used to repeat the execution of a process in case of an error. Using this decorator, the execution of the process can be repeated a specified number of times to retry in case of a mistake.
  • `@cache` decorator: This decorator is used to save the arithmetic results of the function and prevent them from being recalculated in subsequent calls. By using this decorator, you can improve the performance of the program and reduce its execution time.
  • `@login_required` decorator: This decorator protects content that requires a user login. Using this decorator, it is possible to allow access to specific pages only to users who are logged in.
  • `@validate_args` decorator: This decorator is used to validate function inputs. Using this decorator, you can verify that the function inputs are correct and complete and return an error if there is a problem.
  • `@synchronized` decorator: This decorator locks access to a code section. Using this decorator, avoiding concurrency in the code and ensuring that only one thread has access to one part of the code is possible.
  • `@log` decorator: This decorator registers code execution logs. Using this decorator, the activities performed in the program can be recorded and checked if needed.
  • `@suppress` decorator: This decorator is used to silence errors. Using this decorator, you can ignore these errors and continue running the program from the error to the information level.

These are just some examples of decorators in Python. Using decorators, code can be made simpler and more readable, improve program performance, and reduce errors.

Can decorators be combined?

Yes, decorators can be combined in Python. This helps to make decorators more useful and expand their capabilities.

To combine two decorators, they can be placed back to back. For example, if we want to define a function as a class with the `@staticmethod` decorator and also use the `@cache` decorator to store arithmetic results, we can do as follows:

Class MyClass:

@staticmethod

@cache

def my_static_method(x):

# This function is defined as a static function, and when called, its calculation result is stored using the cache decorator.

return x ** 2

With this, the “my_static_method” function is defined as a static function, and when called, its arithmetic result, if any, is stored in memory and is not recalculated in subsequent calls.

Also, it is possible to specify more than two decorators for a function and combine them appropriately. This can be done directly by using decorators.

How to combine decorators with functions?

In Python, decorators can be combined with other functions. Decorators are considered first-type functions in Python so that they can be used as arguments for different functions.

For example, suppose we have a function that receives a number as input, and its output is the sum of odd numbers more minor than that. Now we want to define this function as a function with two decorators, “@memoize” and “@timed”, so that in case of subsequent calls, its arithmetic result will be stored in memory, and its execution time will be accurately recorded.

We can do as follows:

import time

def memoize(func):

cache = {}

def wrapper(*args):

if args in cache:

return cache[args]

Otherwise:

result = func(*args)

cache[args] = result

return result

return wrapper

def timed(func):

def wrapper(*args):

start = time.time()

result = func(*args)

end = time.time()

print(f”Elapsed time: {end-start:.6f} seconds”)

return result

return wrapper

@memoize

@timed

def sum_of_odd_numbers(n):

return sum(filter(lambda x: x % 2 == 1, range(n)))

print(sum_of_odd_numbers(1000000))

print(sum_of_odd_numbers(1000000))

In this code, the “memoize” function is used to save the process’s arithmetic results, and the “timed” function is used to record the execution time of the process in each call. Then, using `memoize` and `timed` decorators, the `sum_of_odd_numbers` function is defined. When calling this function, its arithmetic result, if any, is stored in memory and is not recalculated in subsequent calls. Also, the execution time of the function is recorded and printed in each call using the `timed’ decorator. This way, you can add more functionality to your functions by combining decorators with other parts.

How to combine decorators with classes?

In Python, decorators can also be combined with classes. Decorators defined for functions are the most supported form of decorators, but decorators can also be defined for types. For example, suppose we have a style that stores the characteristics of a person, and we want to use the decorators `@property` and `@classmethod` to define the properties of the course.

We can do as follows:

Class Person:

def __init__(self, name, age):

self._name = name

self._age = age

@property

def name(self):

return self._name

@property

def age(self):

return self._age

@classmethod

def from_birth_year(cls, name, birth_year):

age = cls.calculate_age(birth_year)

return cls(name, age)

@staticmethod

def calculate_age(birth_year):

return 2023 – birth_year

In this code, `@property` defines the `name` and `age` properties. Also, `@classmethod` represents the `from_birth_year` function, which takes a person’s birth year and returns their name and age as an object from the class. Also, the “calculate_age” function is defined as a static function used to calculate age. In this way, you can add more functionality by combining decorators with styles.

Can other functions be used as decorators for classes?

Yes, other functions can be used in Python as decorators for classes. Any function that takes a class as input and returns a new class can be used as a decorator for classes. This helps to make classes more practical and expand their capabilities.

For example, suppose we have a class that receives a number as input, and its output is the sum of odd numbers more minor than that number. Now we want to define this class as a class with two decorators, “@memoize” and “@timed” so that in case of subsequent calls, its arithmetic result will be stored in memory, and its execution time will be accurately recorded.

We can do as follows:

import time

def memoize(cls):

cache = {}

init = cls.__init__

def wrapper(self, n):

if n in cache:

return cache[n]

Otherwise:

instance = cls.__new__(cls)

init(instance, n)

result = instance.sum_of_odd_numbers()

cache[n] = result

return result

cls.__init__ = wrapper

return cls

def timed(cls):

old_sum_of_odd_numbers = cls.sum_of_odd_numbers

def new_sum_of_odd_numbers(self):

start = time.time()

result = old_sum_of_odd_numbers(self)

end = time.time()

print(f”Elapsed time: {end-start:.6f} seconds”)

return result

CLS.sum_of_odd_numbers = new_sum_of_odd_numbers

return cls

@memoize

@timed

Class SumOfOddNumbers:

def __init__(self, n):

self.n = n

def sum_of_odd_numbers(self):

return sum(filter(lambda x: x % 2 == 1, range(self.n)))

s = SumOfOddNumbers(1000000)

print(s.sum_of_odd_numbers())

print(s.sum_of_odd_numbers())

In this code, the “memoize” function is used to save the process’s arithmetic results, and the “timed” function is used to record the execution time of the process in each call. Then, using `memoize` and `timed` decorators, the `SumOfOddNumbers` class is defined. When calling the `sum_of_odd_numbers’ function in this class, its arithmetic result, if any, is stored in memory and is not recalculated in subsequent calls. Also, the execution time of the function is recorded and printed in each call using the `timed’ decorator.

In this way, combining other functions with classes as decorators allows you to add more features to types and design them more smartly and efficiently.

Are there other decorators to define automatic properties?

Yes, there are other decorators in Python to define automatic properties. For example, the `@property’ decorator describes readable properties, `@classmethod’ describes a class method, and `@staticmethod’ defines a static function. Also, `@setter` and `@deleter` decorators portray mutable and deleted properties.

For example, suppose we want to define a class to display a person’s date of birth. First, we can add a readable property to set the date of birth and then define two properties that are changeable and delete to change and delete the date of delivery.

We can do as follows:

Birthday class:

def __init__(self, year, month, day):

self._year = year

self._month = month

self._day = day

@property

def year(self):

return self._year

@property

def month(self):

return self._month

@property

def day(self):

return self._day

@year.setter

def year(self, value):

self._year = value

@month.setter

def month(self, value):

self._month = value

@day.setter

def day(self, value):

self._day = value

@year.deleter

def year(self):

del self._year

@month.deleter

def month(self):

del self._month

@day.deleter

def day(self):

del self._day

def __str__(self):

return f”{self._year}/{self._month}/{self._day}”

In this code, the decorator `@property` defines the readable properties `year,` `month,` and `day.` Also, the `@setter` decorator is used to determine the mutable attributes `year,` `month,` and `day,` and the `@deleter` decorator defines the delectable attributes `year,` `month,` and `day.` Finally, by specifying the `__str__` function, the date of birth is printed as a string in the form of year/month/day.

In this way, by using `@property,` `@setter,` and `@deleter` decorators, we can add automatic properties to classes and design them more smartly and efficiently.