Python Middle: Углубленная разработка и архитектура приложений

Курс предназначен для перехода от базовых знаний к уровню Middle, охватывая продвинутые аспекты языка, асинхронность и работу с данными [practicum.yandex.ru](https://practicum.yandex.ru/middle-python/). Вы научитесь проектировать сложные веб-сервисы, писать тесты и использовать современные инструменты разработки [smotriuchis.ru](https://smotriuchis.ru/midl-python-razrabotchik-yandeks-praktikum).

1. Углубленный Python: ООП, декораторы и метаклассы

Углубленный Python: ООП, декораторы и метаклассы

Добро пожаловать на курс уровня Middle. Переход от Junior к Middle — это не просто изучение новых библиотек, а понимание того, как работает инструмент изнутри. Мы начнем с фундаментальных механизмов Python: жизненного цикла объектов, магии декораторов и метаклассов.

Многие считают эти темы «черной магией», но на самом деле это строгая логика, на которой строятся такие фреймворки, как Django, FastAPI и SQLAlchemy.

1. Объектно-ориентированное программирование: за пределами __init__

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

1.1. __new__ против __init__

В Python процесс рождения объекта состоит из двух этапов:

  • Аллокация (создание): Метод __new__ выделяет память и возвращает экземпляр класса.
  • Инициализация (настройка): Метод __init__ получает уже созданный экземпляр и заполняет его атрибутами.
  • Метод __new__ — это статический метод (даже без декоратора @staticmethod), который принимает класс cls первым аргументом. Он обязан вернуть экземпляр, иначе __init__ никогда не запустится.

    Когда это нужно?

    * Паттерн Singleton: Гарантия того, что у класса есть только один экземпляр. * Наследование от неизменяемых типов: Вы не можете изменить int или str в __init__, так как они уже созданы. Это можно сделать только в __new__.

    Пример реализации Singleton через __new__:

    1.2. MRO и super()

    В Python поддерживается множественное наследование. Чтобы избежать путаницы (например, «проблемы ромба»), язык использует алгоритм C3-линеаризации для выстраивания порядка разрешения методов (MRO — Method Resolution Order).

    Функция super() не просто возвращает родительский класс. Она возвращает прокси-объект, который делегирует вызовы следующему классу в цепочке MRO.

    Посмотреть порядок наследования можно через атрибут __mro__:

    2. Декораторы: анатомия замыканий

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

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

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

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

    Важные детали:

  • @functools.wraps(func): Обязателен для использования. Без него декорированная функция потеряет свое имя (__name__) и строку документации (__doc__), что усложнит отладку и работу автодокументаторов.
  • Вложенность: Здесь три уровня вложенности. Внешняя функция принимает аргументы (num_times), средняя принимает функцию (func), внутренняя (wrapper) принимает аргументы вызова (*args).
  • 2.2. Декораторы классов

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

    Это мощная альтернатива наследованию или миксинам для добавления функциональности.

    3. Метаклассы: фабрики классов

    Если объект — это экземпляр класса, то класс — это экземпляр метакласса. Метаклассы позволяют перехватить момент создания самого класса (а не его экземпляра).

    Согласно ru.hexlet.io, метакласс — это «класс классов». По умолчанию в Python все классы создаются метаклассом type.

    3.1. Динамическое создание классов через type

    Мы привыкли использовать class Name: ..., но это лишь синтаксический сахар. Класс можно создать динамически, используя функцию type с тремя аргументами.

    Как отмечается в proproprogs.ru, сигнатура вызова выглядит так:

    Где: * 'MyClass' — имя создаваемого класса. * (BaseClass,) — кортеж родительских классов (bases). * {'x': 10, ...} — словарь атрибутов и методов класса (dict).

    3.2. Написание собственного метакласса

    Чтобы создать свой метакласс, нужно наследоваться от type. Основной метод, который мы переопределяем — __new__ (или __init__, но реже).

    Практический пример: Автоматическая регистрация плагинов.

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

    В этом примере, как только интерпретатор Python считывает определение класса AudioPlugin, метакласс RegistryMeta автоматически добавляет его в словарь registry. Это позволяет строить гибкие архитектуры без явного связывания компонентов.

    3.3. Когда (не) использовать метаклассы

    Метаклассы — инструмент огромной силы, но и высокой сложности. Согласно habr.com, для подавляющего большинства задач использование метаклассов является избыточным. Часто задачу можно решить проще с помощью:

    * Декораторов классов. * Метода __init_subclass__ (появился в Python 3.6).

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

    Итоги

    Мы рассмотрели инструменты, которые превращают Python из простого скриптового языка в мощную платформу для построения архитектуры.

  • __new__ позволяет управлять созданием объектов до их инициализации, что критично для неизменяемых типов и паттерна Singleton.
  • Декораторы — это способ обернуть логику функций или классов, используя замыкания. Всегда используйте functools.wraps для сохранения метаданных.
  • Метаклассы управляют созданием самих классов. Это основа для создания ORM и систем плагинов, но в прикладном коде их часто можно заменить на __init_subclass__.
  • type — это не только функция проверки типа, но и конструктор классов при вызове с тремя аргументами.
  • 2. Асинхронное программирование (AsyncIO) и многопоточность

    Асинхронное программирование (AsyncIO) и многопоточность

    В предыдущем модуле мы разобрали, как структурировать данные и логику с помощью ООП, декораторов и метаклассов. Теперь пришло время поговорить о том, как структурировать время выполнения программы.

    Переход к уровню Middle требует четкого понимания того, как заставить Python делать несколько вещей одновременно (или хотя бы создавать видимость этого). Мы разберем три кита конкурентности: threading, multiprocessing и asyncio.

    1. Конкурентность против Параллелизма

    Прежде чем писать код, необходимо разграничить понятия, которые часто путают.

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

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

    Проблема GIL

    В CPython (стандартной реализации Python) существует GIL (Global Interpreter Lock) — глобальная блокировка интерпретатора. Это мьютекс, который предотвращает одновременное выполнение байт-кода Python несколькими потоками.

    Это означает, что даже если вы создадите 10 потоков на 10-ядерном процессоре, Python будет выполнять их по очереди на одном ядре. GIL защищает внутреннюю память интерпретатора от повреждений, но делает многопоточность бесполезной для вычислений (CPU-bound задач).

    2. Многопоточность (threading)

    Модуль threading позволяет создавать потоки. Потоки в Python — это потоки операционной системы (OS threads). Переключением между ними управляет планировщик ОС (преемтивная многозадачность).

    Когда использовать?

    Потоки идеально подходят для I/O-bound задач (операции ввода-вывода). Это задачи, где процессор большую часть времени простаивает, ожидая ответа от сети, диска или базы данных.

    > Потоки могут переключаться часто... но в каждый момент времени только один из них исполняет байткод. По этой причине многопоточность не улучшает производительность для CPU-bound задач. > > habr.com

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

    Пример использования threading

    Проблемы многопоточности

    Главная проблема — Race Condition (состояние гонки). Поскольку потоки делят одну и ту же память, они могут попытаться изменить одни и те же данные одновременно, что приведет к непредсказуемым результатам. Для защиты данных приходится использовать примитивы синхронизации, такие как Lock, RLock или Semaphore.

    3. Многопроцессорность (multiprocessing)

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

    Он создает отдельные процессы ОС. У каждого процесса свой собственный экземпляр Python-интерпретатора и своя память. GIL им не мешает, так как у каждого процесса он свой.

    Оценка эффективности (Закон Амдала)

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

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

    Если (90% кода можно выполнить параллельно) и у нас ядра, то:

    Это означает, что даже при 4 ядрах мы получим ускорение лишь в 3 раза, а не в 4, из-за накладных расходов и последовательной части кода.

    Пример multiprocessing

    Минусы: * Высокое потребление памяти (копируется весь процесс). * Сложная коммуникация между процессами (нужны очереди Queue или каналы Pipe).

    4. AsyncIO: Кооперативная многозадачность

    AsyncIO (асинхронный ввод-вывод) — это современный подход к конкурентности, добавленный в Python 3.4+. В отличие от потоков, где переключением управляет ОС, здесь переключением управляет сам код (кооперативная многозадачность).

    Как это работает?

    В основе лежит Event Loop (цикл событий). Это бесконечный цикл, который следит за задачами. Если задача ожидает ввода-вывода (например, ответа от базы данных), она добровольно отдает управление (await), и Event Loop переключается на другую задачу.

    > Асинхронность не создаёт новые потоки — вместо этого она переключает выполнение между задачами внутри одного потока, когда текущая задача ожидает результат ввода-вывода. > > tretyakov.net

    Ключевые слова

  • async def: Определяет корутину (coroutine). Вызов такой функции не выполняет её код сразу, а возвращает объект корутины.
  • await: Маркер точки переключения. Говорит интерпретатору: «Здесь я буду ждать, можешь пока заняться чем-то другим».
  • Пример на asyncio

    В этом примере, несмотря на то, что суммарное время ожидания 2+1+3 = 6 секунд, программа выполнится примерно за 3 секунды (время самой долгой задачи), так как ожидания происходят параллельно.

    AsyncIO vs Threading

    | Характеристика | Threading | AsyncIO | | :--- | :--- | :--- | | Модель | Преемтивная (ОС решает, когда переключить) | Кооперативная (код решает, когда переключить) | | Параллелизм | Нет (из-за GIL) | Нет (один поток) | | Сложность | Race conditions, блокировки | Проще (нет гонки за память в точках без await) | | Масштабируемость | Сотни потоков (тяжелые) | Тысячи/десятки тысяч корутин (легкие) | | Применение | Простой I/O, легаси код | Высоконагруженный I/O (веб-серверы, чаты) |

    Согласно slurm.io, асинхронность идеально подходит для сценариев, где сервис должен обрабатывать тысячи запросов, например, веб-серверы или микросервисы.

    5. Смешанный подход

    Иногда в асинхронном приложении нужно выполнить тяжелую CPU-задачу или использовать блокирующую библиотеку (которая не поддерживает await). Если вызвать такую функцию внутри async def, она заблокирует весь Event Loop, и все остальные клиенты «повиснут».

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

    Итоги

  • GIL ограничивает выполнение Python-кода одним потоком одновременно, делая threading бесполезным для ускорения вычислений на CPU.
  • threading используйте для простых I/O задач (работа с файлами, простые сетевые скрипты), где не требуется держать тысячи соединений.
  • multiprocessing — единственный способ получить истинный параллелизм в Python для CPU-bound задач (математика, ML), но ценой повышенного расхода памяти.
  • asyncio — стандарт де-факто для написания высоконагруженных сетевых приложений. Он использует один поток и цикл событий для эффективного управления тысячами соединений.
  • Используйте await только с объектами, поддерживающими асинхронность. Для интеграции синхронного кода в асинхронный используйте экзекьюторы.