“Лучше проще, чем сложнее”  —  оптимальным примером использования этого философского положения “Python-дзена” являются декораторы.

Декораторы позволяют писать более короткий и простой код для реализации сложной логики и ее повторного использования.

Важно помнить, что существует много функциональных встроенных декораторов Python, которые значительно облегчают жизнь разработчику. С их помощью можно использовать всего одну строку кода для добавления сложных функций к существующим функциям и классам.

Представляю вашему вниманию топ-9 декораторов, которые покажут, насколько элегантным может быть Python.

1. @lru_cache: ускорение работы программ с помощью кэширования

Самый простой способ ускорить работу функций Python посредством кэширования  —  использовать декоратор @lru_cache.

Его можно применять для кэширования результатов функции, чтобы последующие вызовы функции с теми же аргументами не выполнялись повторно.

Это особенно полезно в случае функций, которые требуют больших вычислительных затрат или часто вызываются с одними и теми же аргументами.

Рассмотрим интуитивно понятный пример:

import time


def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)


start_time = time.perf_counter()
print(fibonacci(30))
end_time = time.perf_counter()
print(f"The execution time: {end_time - start_time:.8f} seconds")
# Время выполнения: 0,18129450 секунды

Приведенная выше программа вычисляет N-ое число Фибоначчи с помощью функции Python. Эта процедура отнимает много времени, поскольку при вычислении fibonacci(30) многие предыдущие числа Фибоначчи неоднократно вычисляются в процессе рекурсии.

Теперь ускорим работу с помощью декоратора @lru_cache:

from functools import lru_cache
import time


@lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)


start_time = time.perf_counter()
print(fibonacci(30))
end_time = time.perf_counter()
print(f"The execution time: {end_time - start_time:.8f} seconds")
# Время выполнения: 0,00002990 секунды

Как видно из приведенного выше кода, после использования декоратора @lru_cache мы можем получить тот же результат за 0,00002990 секунды, что намного быстрее, чем предыдущий показатель  —  0,18129450 секунды.

В декораторе @lru_cache есть параметр maxsize, который задает максимальное количество результатов для хранения в кэше. Когда кэш переполнен и необходимо сохранить новый результат, из кэша удаляется наиболее давно использовавшийся результат, чтобы освободить место для нового. Это называется стратегией наиболее давно использовавшегося результата (LRU).

По умолчанию параметр maxsize установлен на 128. Если он установлен на None, как в нашем примере, функции LRU отключены, и кэш может расти без каких-либо ограничений.

2. @total_ordering: декоратор класса, заполняющий недостающие методы упорядочивания

Декоратор @total_ordering из модуля functools используется для генерации недостающих методов сравнения для класса Python на основе тех, которые уже определены.

Пример:

from functools import total_ordering


@total_ordering
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade

def __eq__(self, other):
return self.grade == other.grade

def __lt__(self, other):
return self.grade < other.grade


student1 = Student("Alice", 85)
student2 = Student("Bob", 75)
student3 = Student("Charlie", 85)

print(student1 < student2) # False
print(student1 > student2) # True
print(student1 == student3) # True
print(student1 <= student3) # True
print(student3 >= student2) # True

Как видно из приведенного выше кода, в классе Student нет определений для методов __ge__, __gt__ и __le__ Однако благодаря декоратору @total_ordering все результаты сравнений между различными экземплярами корректны.

Преимущества этого декоратора очевидны.

  • Он делает код чище и экономит время, поскольку не нужно писать все методы сравнения.
  • Некоторые устаревшие классы могут не определять достаточное количество методов сравнения. Безопаснее будет добавить к классу декоратор @total_ordering для дальнейшего его использования.

3.@contextmanager: создание пользовательского контекстного менеджера

Python снабжен механизмом контекстного менеджера, который поможет правильно управлять ресурсами.

В большинстве случаев нужно просто использовать операторы with:

with open("test.txt",'w') as f:
f.write("Yang is writing!")

Как показано в приведенном выше коде, мы можем открыть файл с помощью оператора with, и он закроется автоматически после записи. Нам не нужно явно вызывать функцию f.close(), чтобы закрыть файл.

Иногда приходится определять пользовательский контекстный менеджер под какие-либо особые требования. В этом случае выручит декоратор @contextmanager.

Например, следующий код реализует простой пользовательский контекстный менеджер, который может выводить соответствующую информацию при открытии и закрытии файла.

from contextlib import contextmanager

@contextmanager
def file_manager(filename, mode):
print("The file is opening...")
file = open(filename,mode)
yield file
print("The file is closing...")
file.close()

with file_manager('test.txt', 'w') as f:
f.write('Yang is writing!')
# Файл открывается...
# Файл закрывается...

4. @property: настройка геттеров и сеттеров для классов Python

Геттеры и сеттеры  —  важные понятия в объектно-ориентированном программировании (ООП).

Для каждой переменной экземпляра класса метод getter возвращает ее значение, а setter устанавливает или обновляет ее значение. Поэтому геттеры и сеттеры также называют аксессорами и мутаторами.

Они используются для защиты данных от прямого и нежелательного доступа и изменений.

Разные языки ООП снабжены различными механизмами для определения геттеров и сеттеров. В языке Python мы можем просто использовать декоратор @property.

class Student:
def __init__(self):
self._score = 0

@property
def score(self):
return self._score

@score.setter
def score(self, s):
if 0 <= s <= 100:
self._score = s
else:
raise ValueError('The score must be between 0 ~ 100!')

Yang = Student()

Yang.score=99
print(Yang.score)
# 99

Yang.score = 999
# ValueError: Оценка должна быть в диапазоне 0 ~ 100!

Как видно из приведенного выше примера, переменная score не может быть установлена как 999 (это бессмысленное число). Причина в том, что мы ограничили ее допустимый диапазон внутри функции setter с помощью декоратора @property.

Без сомнения, добавление setter поможет избежать неожиданных ошибок и результатов.

5. @cached_property: кэширование результата метода в качестве атрибута

В Python 3.8 в модуле functool появился новый мощный декоратор —@cached_property. Он может преобразовать метод класса в свойство, значение которого вычисляется один раз, а затем кэшируется как обычный атрибут на весь срок существования экземпляра.

Пример:

from functools import cached_property


class Circle:
def __init__(self, radius):
self.radius = radius
@cached_property
def area(self):
return 3.14 * self.radius ** 2


circle = Circle(10)
print(circle.area)
# выводит 314.0
print(circle.area)
# непосредственно возвращает кэшированный результат (314.0)

В приведенном выше коде мы декорировали метод area посредством @cached_property. Таким образом, мы не сталкиваемся с повторными вычислениями для circle.area одного и того же неизменного экземпляра.

6. @classmethod: определение методов класса в классе Python

Внутри класса Python существует 3 возможных типа методов.

  • Методы экземпляра. Это методы, которые привязаны к экземпляру. Они могут обращаться к данным экземпляра и изменять их. Метод экземпляра вызывается в экземпляре класса и может получать доступ к данным экземпляра через параметр self.
  • Методы класса. Методы, которые привязаны к классу. Они не могут изменять данные экземпляра. Метод класса вызывается в самом классе и получает класс в качестве первого параметра, который условно называется cls.
  • Статические методы. Методы, которые не привязаны ни к экземпляру, ни к классу.

Методы экземпляра можно определить как обычные функции Python в том случае, если первым параметром является self. Однако, чтобы определить метод класса, нужно использовать декоратор @classmethod.

Приведем небольшой пример. Ниже показано определение метода класса, который может быть использован для получения экземпляра Circle посредством diameter:

class Circle:
def __init__(self, radius):
self.radius = radius

@classmethod
def from_diameter(cls, diameter):
return cls(diameter / 2)

@property
def diameter(self):
return self.radius * 2

@diameter.setter
def diameter(self, diameter):
self.radius = diameter / 2


c = Circle.from_diameter(8)
print(c.radius) # 4.0
print(c.diameter) # 8.0

7. @staticmethod: определение статических методов в классе Python

Как уже говорилось, статические методы не привязаны к экземпляру или классу. Они включаются в класс просто потому, что по логике принадлежат ему.

Статические методы обычно используются во вспомогательных классах, которые выполняют группу связанных задач, например математические вычисления. Если сгруппировать связанные функции в статические методы внутри класса, код станет более организованным и понятным.

Чтобы определить статический метод, достаточно использовать декоратор @staticmethod. Рассмотрим пример:

class Student:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
self.nickname = None

def set_nickname(self, name):
self.nickname = name

@staticmethod
def suitable_age(age):
return 6 <= age <= 70


print(Student.suitable_age(99)) # False
print(Student.suitable_age(27)) # True
print(Student('yang', 'zhou').suitable_age(27)) # True

8. @dataclass: определение специальных классов с помощью меньшего количества кода

Декоратор @dataclass (появился в версии Python 3.7) может автоматически генерировать несколько специальных методов для класса, таких как __init__, __repr__, __eq__, __lt__ и др.

Таким образом, он экономит много времени на написание этих базовых методов. Если класс в основном используется для хранения данных, декоратор @dataclass  —  лучший помощник.

В следующем примере определяются всего два поля данных класса с именем Point. Благодаря декоратору @dataclass этого вполне достаточно для работы:

from dataclasses import dataclass

@dataclass
class Point:
x: float
y: float

point = Point(1.0, 2.0)
print(point)
# Point(x=1.0, y=2.0)

9. @atexit.register: регистрация функции, которая будет выполняться во время обычного завершения программы

Декоратор @register из модуля atexit позволяет выполнить функцию при завершении работы интерпретатора Python.

Этот декоратор очень полезен для осуществления заключительных задач, таких как освобождение ресурсов и просто прощание! 

Пример:

import atexit

@atexit.register
def goodbye():
print("Bye bye!")

print("Hello Yang!")

На выходе имеем:

Hello Yang!
Bye bye!

Как показано в примере, благодаря использованию декоратора @register терминал выведет “Bye bye!”, даже если мы не будем вызывать функцию goodbye явно.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Yang Zhou: 9 Python Built-In Decorators That Optimize Your Code Significantly

Предыдущая статьяЭти маленькие сниппеты кода изменили мир
Следующая статьяReact: плюсы и минусы популярной библиотеки JavaScript