Классы в Python и углубленное изучение декораторов

Этот курс охватывает фундаментальные принципы объектно-ориентированного программирования в Python с особым акцентом на механизме декораторов. Вы пройдете путь от создания простых классов до написания сложных декораторов для методов и самих классов.

1. Основы ООП: создание классов, работа с атрибутами и методы экземпляра

Основы ООП: создание классов, работа с атрибутами и методы экземпляра

Добро пожаловать в курс «Классы в Python и углубленное изучение декораторов». Мы начинаем наше путешествие с фундаментальной темы, которая лежит в основе большинства современных программных систем — Объектно-Ориентированного Программирования (ООП).

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

Что такое ООП и зачем оно нужно?

Представьте, что вы пишете программу для управления автопарком. Если использовать только функции и переменные (процедурный стиль), вам придется создавать множество списков или словарей для хранения данных о каждой машине: марка, модель, год выпуска, пробег. Затем вам понадобятся отдельные функции, которые принимают эти данные и что-то с ними делают.

С ростом программы код превратится в запутанный клубок данных и функций. ООП предлагает другое решение: объединить данные (характеристики) и функции (действия) в единую сущность.

Аналогия с чертежом и домом

Чтобы понять разницу между классом и объектом (экземпляром), давайте используем классическую аналогию.

!Класс — это чертеж, описывающий общую структуру. Объекты — это конкретные автомобили, созданные по этому чертежу.

* Класс — это чертеж, шаблон или инструкция. Он описывает, какими свойствами должен обладать объект и что он должен уметь делать. Сам по себе класс не содержит конкретных данных (например, конкретного пробега). * Объект (или экземпляр класса) — это конкретная реализация этого чертежа. У него есть свои уникальные значения свойств.

В Python всё является объектом: строки, числа, списки. Когда вы создаете строку s = "Привет", вы создаете объект класса str.

Создание первого класса

В Python создание класса начинается с ключевого слова class. По общепринятому соглашению (PEP 8), имена классов пишутся в стиле CapWords (каждое слово с большой буквы, без подчеркиваний).

Здесь мы создали простейший класс Car. Оператор pass используется как заглушка, так как тело класса не может быть пустым.

Теперь мы можем создать экземпляры (объекты) этого класса:

Результат будет примерно таким: <__main__.Car object at 0x7f...> <__main__.Car object at 0x7f...>

Мы видим, что это два разных объекта, находящихся в разных ячейках памяти.

Атрибуты: наделяем объекты данными

Пустые объекты бесполезны. Нам нужно, чтобы у каждой машины были свои характеристики: цвет, марка, скорость. В терминологии ООП переменные, принадлежащие объекту, называются атрибутами.

Мы могли бы добавлять атрибуты вручную после создания объекта:

Но это плохой подход. Он не гарантирует, что у всех машин будут одинаковые наборы характеристик. Правильный способ — инициализировать атрибуты при создании объекта.

Магический метод __init__

Для начальной настройки объекта в Python используется специальный метод __init__. Его часто называют конструктором (хотя технически это инициализатор).

Этот метод запускается автоматически в момент создания нового экземпляра класса.

Давайте разберем этот код детально.

  • def __init__: Имя метода строго фиксировано. Двойные подчеркивания с обеих сторон указывают на то, что это «магический» метод Python.
  • (self, brand, color): В скобках мы указываем параметры, которые нужно передать при создании объекта.
  • self.brand = brand: Это самая важная часть. Мы берем значение аргумента brand и сохраняем его внутри объекта в атрибут с именем brand.
  • Теперь создание машин выглядит так:

    Загадочный self

    Вы наверняка заметили слово self, которое стоит первым аргументом в методе __init__ и используется внутри него. Это часто сбивает с толку новичков.

    self — это ссылка на конкретный объект, с которым мы сейчас работаем.

    Когда вы пишете toyota = Car("Toyota", "White"), Python под капотом делает примерно следующее:

  • Создает пустой объект в памяти.
  • Вызывает метод __init__, передавая этот новый пустой объект в качестве первого аргумента — self.
  • То есть: * Внутри объекта toyota переменная self ссылается на toyota. * Внутри объекта bmw переменная self ссылается на bmw.

    Благодаря self код класса знает, какому именно объекту нужно присвоить цвет «White», а какому — «Black».

    > Важно: Имя self — это просто соглашение. Вы могли бы назвать этот аргумент this или me, и код бы работал. Однако использование любого другого имени, кроме self, считается грубым нарушением этикета Python-разработчика.

    Методы экземпляра: учим объекты действовать

    Атрибуты описывают состояние объекта. Методы описывают его поведение. Технически, методы — это функции, объявленные внутри класса.

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

    Добавим нашей машине возможность разгоняться и тормозить.

    Вызов методов

    Обратите внимание: при объявлении метода мы пишем def accelerate(self, value), но при вызове мы не передаем self вручную.

    Вывод:

    Если вы забудете указать self в определении метода (def accelerate(value):), то при попытке вызова my_car.accelerate(20) получите ошибку TypeError. Python попытается передать объект my_car первым аргументом, но функция его не ждет.

    Взаимодействие объектов

    Сила ООП раскрывается, когда объекты начинают взаимодействовать друг с другом. Давайте создадим класс Driver (Водитель) и научим его взаимодействовать с Car.

    Здесь метод drive принимает не просто число или строку, а целый объект машины (car).

    Результат:

    В этом примере объект driver использует публичные методы и атрибуты объекта my_car, чтобы изменить его состояние. Это демонстрирует, как сложные системы строятся из простых, взаимодействующих блоков.

    Инкапсуляция (краткое введение)

    В наших примерах мы свободно обращались к атрибутам car.speed или car.brand напрямую. В серьезном программировании часто требуется защитить данные от некорректного изменения извне (например, чтобы нельзя было установить отрицательную скорость).

    Хотя Python не имеет строгого запрета на доступ к данным (как private в Java или C++), существует соглашение: если имя атрибута начинается с нижнего подчеркивания (например, _engine_status), это сигнал для программиста: «Не трогай это снаружи класса, это для внутреннего использования».

    Более подробно о защите данных и свойствах (property) мы поговорим в следующих статьях курса.

    Резюме

    Сегодня мы заложили первый камень в фундамент вашего понимания ООП в Python.

  • Класс — это шаблон, описывающий структуру и поведение.
  • Объект (экземпляр) — это конкретная сущность, созданная по шаблону класса.
  • Атрибуты — это переменные, привязанные к объекту (хранят данные). Мы инициализируем их в методе __init__.
  • Методы — это функции внутри класса, определяющие поведение объекта.
  • self — это ссылка на текущий объект, позволяющая методам работать с данными именно этого экземпляра.
  • В следующей статье мы углубимся в работу с атрибутами класса, статическими методами и начнем подбираться к теме наследования.

    2. Введение в декораторы: замыкания, синтаксис и создание простых оберток

    Введение в декораторы: замыкания, синтаксис и создание простых оберток

    В предыдущей статье мы погрузились в мир Объектно-Ориентированного Программирования, научились создавать классы Car и Driver, и поняли, как объекты хранят данные и взаимодействуют друг с другом.

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

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

    Но прежде чем писать @decorator, нам нужно разобраться с тремя фундаментальными концепциями: функции как объекты первого класса, вложенные функции и замыкания.

    Функции — это объекты

    В Python функции — это не просто инструкции для процессора. Это полноправные объекты, такие же как строки, списки или экземпляры класса Car.

    Это означает, что вы можете:

  • Присвоить функцию переменной.
  • Передать функцию как аргумент другой функции.
  • Вернуть функцию из другой функции.
  • Рассмотрим пример:

    Обратите внимание: когда мы пишем yell = shout, мы не ставим скобки после shout. Мы не вызываем функцию, мы берем саму «инструкцию» и даем ей новое имя.

    Вложенные функции

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

    Здесь whisper существует только внутри speak. Если вы попытаетесь вызвать whisper из основной программы, получите ошибку NameError.

    Замыкания (Closures)

    Теперь мы подходим к магии, на которой строятся декораторы. Что произойдет, если внешняя функция вернет внутреннюю функцию, а не результат её вызова?

    Результат:

    Давайте разберем, что здесь странного.

  • Мы вызвали outer_function с аргументом "Привет, Python!".
  • Она завершила свою работу и вернула inner_function.
  • Обычно, когда функция завершается, её локальные переменные (включая аргумент message) уничтожаются и стираются из памяти.
  • Однако, когда мы вызываем my_func() позже, она всё равно «помнит» значение message.
  • Это и есть замыкание.

    !Замыкание похоже на путешественника (внутренняя функция), который уносит с собой сувениры (данные) из места, которого больше не существует (внешняя функция).

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

    Создание первого декоратора вручную

    Декоратор — это просто функция, которая принимает другую функцию в качестве аргумента, добавляет к ней какое-то поведение и возвращает новую функцию.

    Представьте, что у нас есть простая функция:

    Мы хотим добавить вывод сообщений до и после её выполнения, не меняя код самой simple_function. Создадим «обертку»:

    Теперь применим декоратор:

    Вывод:

    Мы успешно «декорировали» функцию вручную.

    Синтаксический сахар: символ @

    В Python есть специальный синтаксис, который делает процесс декорирования красивым и лаконичным. Вместо того чтобы писать func = decorator(func), мы ставим @decorator прямо над определением функции.

    Этот код делает абсолютно то же самое, что и предыдущий пример, но выглядит гораздо чище. Символ @ — это просто «синтаксический сахар».

    Декораторы для функций с аргументами

    Наш предыдущий декоратор my_decorator имеет серьезный недостаток. Он работает только с функциями, которые не принимают аргументов.

    Если мы попробуем применить его к функции add(a, b), произойдет ошибка, так как внутренняя функция wrapper() не принимает аргументов, а мы попытаемся их передать.

    Чтобы сделать декоратор универсальным, мы используем args и *kwargs.

    Вывод:

    Разбор универсального шаблона

    Это стандартный шаблон для написания простых декораторов. Давайте разберем его ключевые элементы:

  • def wrapper(args, kwargs):* — Обертка готова принять любое количество позиционных и именованных аргументов.
  • result = func(args, kwargs)* — Мы «пробрасываем» эти аргументы в оригинальную функцию. Это гарантирует, что логика работы оригинала не сломается.
  • return result — Это критически важно. Если обертка не вернет значение, которое вернула оригинальная функция, то результат вызова потеряется (станет None).
  • Практический пример: Измерение времени выполнения

    Один из самых частых сценариев использования декораторов — измерение производительности кода.

    Резюме

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

  • Функции — это объекты. Их можно передавать и возвращать.
  • Замыкание позволяет внутренней функции запоминать данные из внешней функции даже после завершения работы внешней.
  • Декоратор — это функция, которая принимает другую функцию и возвращает её обертку.
  • Синтаксис @name эквивалентен записи func = name(func).
  • Чтобы декоратор работал с любыми функциями, используйте args и kwargs* и обязательно возвращайте результат выполнения оригинальной функции.
  • В следующей статье мы углубимся в тему и узнаем, как передавать аргументы самим декораторам (например, @repeat(3)), и как декораторы взаимодействуют с методами классов.

    3. Встроенные декораторы в классах: @property, @staticmethod и @classmethod

    Встроенные декораторы в классах: @property, @staticmethod и @classmethod

    Добро пожаловать обратно! В прошлых модулях мы построили прочный фундамент: вы уже умеете создавать классы, наделять их атрибутами и методами, а также понимаете, как работают декораторы «под капотом» (замыкания и функции-обертки).

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

    Речь пойдет о @property, @staticmethod и @classmethod.

    @property: Умный доступ к атрибутам

    Вспомните наш класс Car из первой лекции. У нас был атрибут speed (скорость). Проблема прямого доступа к атрибутам заключается в том, что мы не можем контролировать, какие данные туда попадают.

    В таких языках, как Java или C++, принято делать все переменные приватными и писать методы get_speed() и set_speed(value). В Python это считается избыточным. Вместо этого мы используем декоратор @property.

    Превращаем метод в атрибут

    Декоратор @property позволяет объявить метод, но обращаться к нему как к обычному атрибуту (без скобок). Это идеальный способ реализовать «геттер» (получение значения).

    Обратите внимание: метод называется speed, а переменная, где реально хранится значение — _speed. Если бы мы назвали их одинаково, произошла бы бесконечная рекурсия.

    Валидация данных через сеттер

    Самое интересное начинается, когда мы хотим изменить значение. Для этого используется декоратор @имя_свойства.setter.

    !Иллюстрация принципа работы сеттера: контроль входящих данных перед записью в атрибут.

    Давайте запретим отрицательную скорость:

    Теперь интерфейс нашего класса выглядит чистым (my_car.speed = 100), но под капотом работает полноценная логика проверки.

    @staticmethod: Функции внутри классов

    Иногда нам нужно создать функцию, которая логически связана с классом, но ей не нужны данные конкретного экземпляра (она не использует self).

    Например, мы хотим добавить в класс Car метод для конвертации скорости из километров в час в мили в час. Эта формула универсальна и не зависит от того, какая именно у нас машина — «Лада» или «Феррари».

    Для расчета мы используем следующую формулу:

    Где — скорость в милях в час, — скорость в километрах в час, а — коэффициент перевода.

    Если мы объявим обычный метод, Python будет требовать передать self, который нам не нужен. Здесь на помощь приходит @staticmethod.

    Ключевые особенности @staticmethod: * Не принимает self первым аргументом. * Не имеет доступа к атрибутам экземпляра (self.model) или класса. * По сути, это обычная функция, которую мы поместили внутрь класса для удобства организации кода (чтобы не «мусорить» в глобальном пространстве имен).

    @classmethod: Работа с классом, а не с объектом

    Третий декоратор — @classmethod — часто путают со статическим методом, но между ними есть фундаментальная разница.

    Метод класса принимает первым аргументом не экземпляр (self), а сам класс. По общепринятому соглашению этот аргумент называют cls.

    Зачем это нужно? Альтернативные конструкторы

    Самое частое применение @classmethod — создание альтернативных конструкторов. Стандартный __init__ позволяет создать объект одним способом. Но что, если данные приходят в другом формате?

    Представьте, что мы получаем информацию о машине в виде строки "Toyota-Red-2020", и нам нужно создать объект на основе этой строки.

    !Визуализация паттерна "Фабричный метод": создание объекта из нестандартных входных данных.

    Использование cls вместо жесткого указания имени класса Car важно для наследования. Если вы создадите подкласс SuperCar(Car), метод from_string корректно создаст экземпляр именно SuperCar.

    Сводная таблица различий

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

    | Тип метода | Декоратор | Первый аргумент | Доступ | Для чего используется | | :--- | :--- | :--- | :--- | :--- | | Instance Method | (нет) | self | К экземпляру и классу | Обычное поведение объекта (ехать, гудеть) | | Static Method | @staticmethod | (нет) | Нет доступа | Утилиты, вспомогательные расчеты, не зависящие от состояния | | Class Method | @classmethod | cls | Только к классу | Альтернативные конструкторы, изменение настроек всего класса |

    Резюме

    Сегодня мы значительно расширили наш инструментарий работы с классами:

  • @property позволяет скрыть логику получения и изменения атрибутов за красивым фасадом, сохраняя синтаксис простого доступа (obj.x). Это основа инкапсуляции в Python.
  • @staticmethod используется для функций, которые логически относятся к классу, но не работают с его состоянием. Это вопрос чистоты и организации кода.
  • @classmethod получает ссылку на сам класс (cls) и идеально подходит для создания фабричных методов (альтернативных конструкторов).
  • В следующей статье мы разберем одну из самых сложных, но интересных тем ООП — Наследование и полиморфизм, где увидим, как классы могут передавать свои свойства потомкам и видоизменять их поведение.

    4. Продвинутые техники: декораторы с аргументами и декорирование методов класса

    Продвинутые техники: декораторы с аргументами и декорирование методов класса

    Мы продолжаем наш курс «Классы в Python и углубленное изучение декораторов». В прошлых статьях мы разобрали фундамент ООП, научились создавать классы и познакомились с концепцией декораторов — функций, которые оборачивают другие функции. Мы также рассмотрели встроенные декораторы @property, @staticmethod и @classmethod, которые меняют поведение методов класса.

    Но что, если нам нужно больше контроля? Что, если мы хотим не просто замерить время выполнения функции, а передать декоратору параметры? Например, сказать: «Повтори эту функцию 5 раз» или «Жди 2 секунды перед запуском».

    Сегодня мы перейдем на новый уровень абстракции. Мы разберем, как создавать декораторы, принимающие аргументы, и узнаем, как правильно применять самописные декораторы к методам классов, не ломая логику self.

    Декораторы с аргументами: тройная вложенность

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

    Это эквивалентно записи func = my_decorator(func).

    Но когда мы видим код вида @repeat(3), Python интерпретирует это иначе. Сначала вызывается функция repeat(3), и то, что она вернет, должно стать декоратором. То есть, repeat — это не сам декоратор, а фабрика декораторов.

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

    !Структура вложенности функций для создания декоратора с параметрами.

    Реализация декоратора @repeat

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

    Посмотрите внимательно на структуру:

  • repeat(num_times) — принимает настройки (число 3). Возвращает actual_decorator.
  • actual_decorator(func) — принимает саму функцию greet. Возвращает wrapper.
  • wrapper(args, *kwargs) — заменяет собой оригинальную функцию и содержит логику повторения.
  • Применим его:

    Что происходит под капотом?

    Когда Python видит @repeat(3), он выполняет цепочку вызовов:

  • Вызывается repeat(3). Переменная num_times становится равной 3. Функция возвращает actual_decorator.
  • Этот actual_decorator немедленно применяется к функции greet.
  • greet заменяется на wrapper, который «помнит» и func (оригинал), и num_times (настройку) благодаря механизму замыканий.
  • Проблема потери метаданных и functools.wraps

    Когда мы оборачиваем функцию в декоратор, мы подменяем оригинальный объект функции на функцию wrapper. Это приводит к побочному эффекту: теряется имя функции и её документация.

    Это может сильно усложнить отладку, особенно если вы используете библиотеки, которые полагаются на интроспекцию (анализ кода во время выполнения).

    Чтобы исправить это, в стандартной библиотеке Python есть модуль functools и специальный декоратор @wraps. Он копирует метаданные (имя, строку документации, аннотации) из оригинальной функции в обертку.

    Правильный шаблон для любого профессионального декоратора выглядит так:

    Всегда используйте @functools.wraps, если вы пишете код для реальных проектов.

    Декорирование методов класса

    Теперь, когда мы освоили сложные декораторы, давайте вернемся к ООП. Можно ли применять наши самописные декораторы к методам внутри класса?

    Да, можно. Но нужно помнить про self.

    Вспомним наш класс Car. Допустим, мы хотим логировать каждый вызов метода drive.

    Вывод будет следующим:

    Обратите внимание на кортеж аргументов: (<__main__.Car object...>, 100).

    Первым аргументом в args прилетел сам объект экземпляра класса! Это и есть наш self. Поскольку мы использовали конструкцию args, наш декоратор автоматически «проглотил» self вместе с остальными аргументами и передал его дальше в оригинальную функцию.

    Доступ к атрибутам экземпляра внутри декоратора

    Иногда нам нужно, чтобы декоратор не просто передавал self дальше, а использовал данные из объекта. Например, мы хотим разрешать выполнение метода только если у пользователя есть права администратора.

    Предположим, у нас есть класс User.

    Здесь мы явно извлекаем args[0], понимая, что при вызове метода первым позиционным аргументом всегда идет экземпляр класса. Это мощная техника, но она делает декоратор зависимым от того, к чему он применяется (он сломается, если применить его к обычной функции без аргументов).

    Практический пример: Декоратор с повторными попытками (Retry)

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

    Мы хотим использовать его так:

    Реализация:

    Теперь применим его к методу класса, который имитирует нестабильное соединение.

    Этот пример демонстрирует всю мощь декораторов:

  • Аргументы: Мы настраиваем количество попыток и задержку.
  • Классы: Декоратор корректно работает с методом класса (аргумент self проходит через *args).
  • Универсальность: Логика повторных попыток отделена от бизнес-логики отправки данных.
  • Резюме

    Сегодня мы разобрали продвинутые техники работы с декораторами:

  • Декораторы с аргументами требуют трехуровневой вложенности функций: Фабрика -> Декоратор -> Обертка.
  • @functools.wraps — обязательный инструмент для сохранения имени и документации оригинальной функции.
  • При декорировании методов класса первый аргумент в *args — это всегда self (экземпляр класса). Это позволяет декоратору взаимодействовать с состоянием объекта.
  • В следующей статье мы отойдем от декораторов и вернемся к архитектуре классов, чтобы разобрать одну из самых важных тем ООП: Наследование и полиморфизм.

    5. Декораторы классов и использование классов в качестве декораторов

    Декораторы классов и использование классов в качестве декораторов

    Добро пожаловать на очередной этап нашего курса «Классы в Python и углубленное изучение декораторов». В предыдущих статьях мы проделали большой путь: от создания простых классов до написания сложных вложенных декораторов с аргументами.

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

  • Декораторы классов: когда мы применяем декоратор к целому классу (пишем @decorator над class MyClass).
  • Классы в качестве декораторов: когда мы используем сам класс (его механизм создания экземпляров) вместо функции для декорирования чего-либо.
  • Эти техники открывают двери к мощным архитектурным паттернам, таким как Синглтон (Singleton), регистрация плагинов и продвинутое логирование с сохранением состояния.

    Часть 1: Декораторы классов

    Вы уже привыкли видеть @decorator над определением функции. Но что произойдет, если поместить эту конструкцию над определением класса?

    Механика остается прежней. Эта запись эквивалентна следующему коду:

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

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

    Пример 1: Добавление функциональности

    Представьте, что у вас есть множество классов, и вы хотите добавить к каждому из них метод __str__, который выводит все атрибуты объекта в формате JSON. Писать этот метод в каждом классе — нарушение принципа DRY (Don't Repeat Yourself).

    Давайте напишем декоратор, который сделает это за нас.

    Результат: {"name": "Алекс", "age": 30}

    Мы успешно «привили» новое поведение классу User, не меняя его исходный код внутри.

    Пример 2: Паттерн Одиночка (Singleton)

    Один из самых классических примеров использования декораторов классов — реализация паттерна Singleton. Этот паттерн гарантирует, что у класса будет создан только один экземпляр.

    Вывод:

    Сообщение «Создание соединения...» вывелось только один раз. Переменные db1 и db2 ссылаются на один и тот же объект в памяти. Декоратор singleton перехватил вызов конструктора класса и подменил его своей логикой управления экземплярами.

    Часть 2: Классы в качестве декораторов

    Теперь перевернем ситуацию. Мы хотим декорировать обычную функцию, но использовать для этого не другую функцию (как мы делали раньше), а класс.

    Зачем это нужно? Обычные декораторы-функции используют замыкания (closures) для хранения состояния (например, счетчика вызовов). Это может выглядеть запутанно из-за ключевого слова nonlocal. Классы же созданы для хранения состояния. Использование атрибутов self делает код чище и понятнее.

    Магический метод __call__

    Чтобы экземпляр класса можно было вызвать как функцию (поставить скобки ()), в классе должен быть реализован метод __call__.

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

    Декоратор без аргументов

    Если декоратор не принимает параметров (просто @my_decorator), то:

  • Метод __init__ принимает декорируемую функцию.
  • Метод __call__ срабатывает при каждом вызове этой функции.
  • Давайте напишем декоратор, который считает, сколько раз была вызвана функция.

    Результат:

    Когда Python видит @CountCalls, он выполняет say_hello = CountCalls(say_hello). Теперь say_hello — это экземпляр класса CountCalls, а не функция.

    Декоратор с аргументами

    Здесь структура меняется, и это важно запомнить. Если мы пишем @MyDecorator(arg), то:

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

    Реализуем декоратор, который повторяет выполнение функции раз.

    Результат:

    Сравнение подходов

    | Подход | Преимущества | Недостатки | | :--- | :--- | :--- | | Функции-декораторы | Лаконичность, привычный синтаксис, поддержка методов классов (descriptors) «из коробки». | Сложно хранить состояние (нужен nonlocal), громоздкая вложенность при наличии аргументов. | | Классы-декораторы | Отличная читаемость, легкое управление состоянием через self, возможность наследования логики декораторов. | Могут возникнуть сложности при декорировании методов внутри других классов (теряется self декорируемого метода, если не использовать дескрипторы). |

    > Важное замечание: Использование классов-декораторов (как в примере CountCalls) для декорирования методов других классов требует осторожности. Обычный __call__ перехватит self метода как первый аргумент, и это может сломать логику. Для методов лучше использовать функции-декораторы или реализовывать протокол дескрипторов (__get__), о котором мы поговорим в будущих модулях.

    Пример с математикой: Кэширование

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

    Допустим, у нас есть функция вычисления кинетической энергии:

    Где — кинетическая энергия, — масса тела, а — его скорость.

    Использование класса здесь оправдано, так как self.cache является естественным местом для хранения истории вызовов.

    Резюме

    Сегодня мы значительно расширили наш арсенал:

  • Декораторы классов (@deco class A) позволяют менять поведение или структуру целого класса при его создании. Это основа для паттернов вроде Singleton.
  • Классы как декораторы используют метод __call__, чтобы вести себя как функции. Это идеальный выбор, когда декоратору нужно хранить сложное состояние (кэш, счетчики, настройки).
  • Выбор между функцией и классом зависит от задачи: для простых оберток функции удобнее, для сложных систем с состоянием — классы.
  • В следующей статье мы погрузимся в одну из самых загадочных тем Python — Метаклассы, и узнаем, кто создает сами классы.