"Простое лучше сложного".

Лучшая функция Python, которая применяет эту философию из "дзен Python", - это декоратор.

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

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

Болтать не буду. Давайте посмотрим на отобранные мной 6 декораторов, которые покажут вам, насколько элегантен 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")
# The execution time: 0.18129450 seconds

Приведенная выше программа вычисляет 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")
# The execution time: 0.00002990 seconds

Как видно из приведенного выше кода, после использования декоратора @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!')
# The file is opening...
# The file is closing...

4. @property: Настраиваем геттеры и сеттеры для классов

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

Для каждой переменной экземпляра класса метод 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: The score must be between 0 ~ 100!

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

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

5. @cached_property: Кешируем результат функции как атрибут

В Python 3.8 в модуле functools появился новый мощный декоратор - @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)
# prints 314.0
print(circle.area)
# returns the cached result (314.0) directly

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

6. @atexit.register: Объявляем функцию которая вызывается при выходе из программы

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

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

Вот пример:

import atexit

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

print("Hello Yang!")

На выходе получаем

Hello Yang!
Bye bye!

Еще больше примеров использования Python и Machine Learning в современных сервисах можно посмотреть в моем телеграм канале. Я пишу про разработку, ML, стартапы и релокацию в UK для IT специалистов.

Комментарии (14)


  1. firegurafiku
    06.01.2023 18:33
    +6

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

    Какой-то супернеудачный перевод. Ну не придумывайте вы своих терминов: во всём гугле нет ни одного результата по запросу "стратегия наименее использованного результата" (в кавычках).


  1. vkni
    06.01.2023 21:46
    +1

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


    1. a-tk
      06.01.2023 21:48
      +15

      Это просто синтаксический сахар поверх ФВП


    1. ris58h
      07.01.2023 17:15
      +2

      Удобство. Есть у вас функция foo и теперь вы её каждый вызов затрейсить хотите. Навесили декоратор и готово. А так вам придётся fooWithoutTracing писать и её уже из foo вызывать обёрнутую в функцию трассировки. Ну или везде в коде foo() на trace(foo()) заменять, что может быть совсем неудобно.


    1. funca
      08.01.2023 00:57

      Просто функциональщина в питоне это моветон, а декораторы они в своё время сделали под впечатлением от аннотаций в java.


      1. ValeryIvanov
        09.01.2023 11:12

        Так аннотации java и декораторы python же не имеют вообще ничего общего


  1. DeepFakescovery
    06.01.2023 22:25
    +3

    @atexit.register - этим только кормить антипаттерны


    1. Andrey_Solomatin
      07.01.2023 09:18
      +3

      Раскройте пожалуйста эту тему подробднее, какие именно антипаттерны и чем их кормить?


      1. kalombo
        07.01.2023 12:45
        +5

        Ну во-первых, для чего. Сможете придумать кейс? Гораздо прозрачнее в конце программы вызвать необходимую вам функцию, чем придумывать какие-то декораторы(хорошо хоть не целый фреймворк:)). А потом запрятать эту задекорированную функцию в дебрях приложения, забыть про нее и не понимать откуда у вас взялась логика по выходу из приложения. А ещё может быть так, что эта функция при одной логике будет импортироваться, при другой нет, соответственно, код при выходе из приложения то будет отрабатывать, то нет. В общем, "веселая" отладка вам будет обеспечена.


        1. tmaxx
          07.01.2023 15:08
          +3

          Выглядит как обертка над стандартной POSIX-функцией “atexit”

          А нужна она, очевидно, в случаях когда у вас нет доступа к main(). Например вы пишете библиотеку. Вам нужно освободить ресурсы при выходе (дописать в файлы итп). Как сделать так чтобы ваша библиотека нормально работала в программах, вызывающих exit()?


        1. Andrey_Solomatin
          07.01.2023 19:08

          Спасибо

          Гораздо прозрачнее в конце программы вызвать необходимую вам функцию



          Половина людей забудет туда try: finally добавить. Я бы смотрел в сторону контекстных менеджеров.

          Похожий подход используется в https://docs.python.org/3/library/signal.html?highlight=signal#signal.signal, там нет декоратора, но в общем смысл примерно тотже. Только вот через код это не обработаетшь.


        1. PaveTranquil
          09.01.2023 11:12

          Потому что, порой, программы завершаются через KeyboardInterrupt или, того хуже, через SystemExit не по воле разработчика, и вот тогда нужно выполнить ряд действий, чтобы закрылось всё без накладок. Наоборот очень полезная штука — взял себе на заметку.


  1. tenzink
    07.01.2023 02:20
    +3

    В примере для contextmanager, думаю, стоит добавить try...finally.


  1. iosuslov
    09.01.2023 11:13
    +1

    Про lru_cache: чтобы работало, нужно чтобы все аргументы функции были хэшируемыми.

    Про декоратор @property: его назначение не только для организации сеттеров и геттеров, также используется для получения доступа к методу класса как к атрибуту. Т.е. можно будет вызывать его в виде Class.property, без скобок и т.д. Иногда очень упрощает понимание/нейминг