Трек «Инженерное программирование» · Модуль 2 · Неделя 8

Идиомы Python и антипаттерны

dataclasses · ABC и Protocol · context manager · descriptors и метаклассы · семь антипаттернов

2 ч теории. Финальная неделя модуля: сдача проекта «Платёжная система».

Недели 5–7 дали словарь паттернов GoF — а что язык даёт вам готовым, без классов и интерфейсов?

Финальный этап «Платёжной системы»: mypy, тесты ≥ 70%, README с обоснованием

Сегодняшние идиомы (Protocol, context manager) и аудит антипаттернов — прямо то, что нужно для «качества кода» и «обоснования выбора» в рубрике проекта.

Пункты рубрики «качество кода» и «уместность паттернов» — что именно закроет сегодняшняя лекция?

Не открывая конспект: по одному паттерну каждого класса GoF

Назовите по одному паттерну каждого класса GoF (порождающие, структурные, поведенческие).

Каков критерий уместности паттерна?

Пауза 10–15 секунд. Варианты: Factory Method, Adapter, Strategy. Критерий: «код с паттерном проще менять, чем код без него». Как тот же критерий применим к антипаттернам — наоборот?

К концу лекции вы сможете…

Это последняя лекция модуля — после неё вы сдаёте проект. Какие из шести навыков лягут в финальную полировку «Платёжной системы»?

Идиома — характерный способ выразить типовое решение; многие идиомы заменяют паттерны GoF

Вы уже писали @dataclass — это идиома. Почему это работает и когда её не применять?

@dataclass — «данные без шума»: __init__, __repr__, __eq__ автоматически

from dataclasses import dataclass, field @dataclass class Student: name: str group: str = "101" grades: list[int] = field(default_factory=list) s = Student("Иван") print(s) s.grades.append(5) print(s.grades) Student(name='Иван', group='101', grades=[]) [5]

Правило: @dataclass генерирует __init__, __repr__, __eq__; поля с аннотациями; frozen=True — неизменяемость; field(default_factory=list) — изменяемые значения по умолчанию.

Предскажите: что будет, если написать grades: list[int] = []? (общий список на все экземпляры)

dataclass vs NamedTuple; frozen=True; field(default_factory=list)

Когда применять

  • данные с поведением — класс, мутация;
  • нужна неизменяемость — frozen=True;
  • изменяемые дефолты — default_factory.

Когда не применять

  • 2–3 поля без поведения — кортеж или NamedTuple проще;
  • dataclass ради dataclass — шум.
Данные без поведения — что выбрать? (NamedTuple); данные с поведением — dataclass.

Два способа задать контракт: ABC — наследование, Protocol — структурная типизация

class Payable(Protocol): def pay(self, amount: float) > bool: ...

Правило: «Protocol — если не хотим привязывать чужие классы к нашей иерархии; ABC — если иерархия своя и нужен общий код».

Помните FormatFactory с недели 5? Почему там Protocol, а не ABC? (чтобы OfflineFactory не наследовал нашу иерархию)
Предскажите результат

Protocol + @runtime_checkable: чужой класс подходит без наследования

from typing import Protocol, runtime_checkable @runtime_checkable class Payable(Protocol): def pay(self, amount: float) > bool: ... class CardPayment: # чужой класс, не наследует Payable def pay(self, amount: float) > bool: return True def process(p: Payable) > bool: return p.pay(100.0) print(process(CardPayment())) print(isinstance(CardPayment(), Payable)) True True

Правило: CardPayment подходит под Payable, потому что имеет метод pay — наследование не нужно; @runtime_checkable включает isinstance. Без него isinstance с Protocol падает с TypeError.

Предскажите: что вернёт isinstance(CardPayment(), Payable)? Что будет без @runtime_checkable? (True; TypeError)

Когда выбирать Protocol, когда ABC; типичная ошибка — ABC привязывает чужие классы

Когда Protocol

  • границы модулей, внешние/чужие классы, duck typing;
  • контракт без наследования.

Когда ABC

  • своя иерархия, нужен общий код (реализация в базе);
  • обязательная реализация методов.

Типичная ошибка: используют ABC, хотя нужен Protocol (ABC привязывает чужие классы к иерархии); Protocol там, где нужен общий код (в Protocol нет общей реализации).

Интерфейс хранилища, под который вы подгоняете «старое» хранилище (неделя 6) — ABC или Protocol? (Protocol — чужой класс не должен наследовать нашу иерархию)

Context manager — паттерн управления ресурсами и транзакциями

from contextlib import contextmanager @contextmanager def payment_transaction(ledger): print("открыта транзакция") try: yield ledger print("commit") except Exception: print("rollback") raise

Правило: try/finally вокруг yield обязателен: без него код после yield не выполнится при исключении — ресурс не освободится.

Вы писали with open(...) — это и есть паттерн управления ресурсами. Что менеджер транзакции гарантирует при исключении? (очистку/rollback)
Предскажите результат

payment_transaction: commit при успехе, rollback при исключении

from contextlib import contextmanager @contextmanager def payment_transaction(ledger): print("открыта транзакция") try: yield ledger print("commit") except Exception: print("rollback") raise with payment_transaction("ledger") as lg: print("работаем с", lg) открыта транзакция работаем с ledger commit
Предскажите: что напечатается, если внутри with бросить исключение? (открыта транзакция → rollback → исключение уходит дальше; «commit» не печатается)

Descriptors (идея): __get__/__set__ внутри @property — переиспользуемая логика доступа

Каждый раз, когда вы пишете @property, вы используете дескриптор. Какую переиспользуемую логику доступа это даёт? (валидация, кэш, вычисление)

Метаклассы (идея): type — класс классов; где применяются: ABC, ORM, dataclasses

class CounterMeta(type): instances = 0 def __call__(cls, *args, **kwargs): CounterMeta.instances += 1 return super().__call__(*args, **kwargs) class Point(metaclass=CounterMeta): def __init__(self, x: int) > None: self.x = x a = Point(1) b = Point(2) print(CounterMeta.instances) 2

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

Вы уже пользуетесь метаклассами, когда пишете class X(ABC) или @dataclass — они «под капотом». Кто управляет созданием класса? (метакласс, type по умолчанию)
Разбор ошибок

Типичные ошибки идиом

  1. Используют ABC, хотя нужен Protocol (или наоборот): ABC привязывает чужие классы к иерархии; Protocol там, где нужен общий код.
  2. dataclass с мутируемым значением по умолчанию без default_factory — общий список на все экземпляры.
  3. Контекст-менеджер без try/finally вокруг yield — ресурс не освободится при исключении.
  4. Пытаются писать метаклассы/дескрипторы в учебных задачах — сложность без выгоды.
Отметьте знакомые ошибки в своём коде «Университета»: ABC вместо Protocol, [] вместо default_factory, yield без try/finally?

Антипаттерн — не любое несовершенство: нужны признаки и стоимость; лечение — по правилам

Признаки как распознать Как лечить план действий тесты сначала
Для каждого антипаттерна: признаки → как лечить; любое лечение — поверх тестов как страховки.
Аудит — по чеклисту, с таблицей «проблема → признак → паттерн/идиома → ожидаемый эффект». Почему нельзя лечить «большим рефакторингом» без тестов? (тесты — страховка)

Класс-гигант: класс делает всё; схема «до/после» — декомпозиция по SRP

class University: def __init__(self) > None: self.students: list = [] self.courses: list = [] self.schedule: dict = {} self.budget: float = 0.0 def enroll_student(self, ...): ... def build_schedule(self, ...): ... def pay_salary(self, ...): ... def send_email(self, ...): ... # и так далее — всё в одном классе

Признаки: класс > 300–400 строк; поля разных предметных областей; метод меняется по любой причине; десятки несвязанных полей.

Лечение: декомпозиция по SRP, выделение сервисов, фасадов.

до University всё в одном классе students courses schedule budget email salary после University делегирует StudentService ScheduleService FinanceService делегирует делегирует делегирует
Декомпозиция по SRP: одна ответственность на класс — University делегирует студентам (StudentService), расписанию (ScheduleService) и финансам (FinanceService).
Ведите пальцем по схеме: один класс тянет шесть ответственностей. Какой сервис был бы в вашем «Университете»?

Spaghetti Code: флаги, break/continue в глубине; лечение — функции и слои

def process(data): ok = False for row in data: for cell in row: if cell is None: ok = True break ... if ok: break

Лечение: извлечение функций, слои, рефакторинг по Фаулеру (чеклист ревью, §3).

Что делает функция process? Если не можете ответить за 5 секунд — это и есть признак Spaghetti.

Copy-Paste: правка бага в N местах; лечение — DRY и «правило трёх»

«Правило трёх»: первое повторение терпим, второе присматриваемся, третье — абстракция. Когда копирование ветвлений по типу — сигнал к Strategy?

Golden Hammer: всё молотком; лечение — критерии применимости и ревью

Код с паттерном должен быть проще менять, чем без него. Если это не так — какой антипаттерн вы обнаружили? (Golden Hammer)

Premature Optimization: оптимизация до профилирования; лечение — сначала ясность, потом cProfile

Почему кэш «на всякий случай» — антипаттерн, а не забота о скорости? (стоимость сложности без измерения выгоды)

Boat Anchor: мёртвый код и библиотеки «на всякий случай»; лечение — удаление

Якорь на дне лодки не спасает, а тянет ко дну — как и неиспользуемый код. Чем контролировать его удаление? (покрытие и ревью)

Lava Flow: код, который никто не трогает; лечение — тесты как страховка, постепенное удаление

Что общего у Boat Anchor и Lava Flow? (мёртвый код; разница — в Lava Flow он ещё и непонятен, его страшно удалять)

Семь антипаттернов: признак → лечение

АнтипаттернПризнакиКак лечить
Класс-гиганткласс > 300–400 строк; поля разных областей; метод меняется по любой причинедекомпозиция по SRP, сервисы, фасады
Spaghetti Codeфлаги, break/continue в глубине; функции без ответственностиизвлечение функций, слои, рефакторинг по Фаулеру
Copy-Pasteодинаковые блоки в N местах; правка бага в N местахизвлечение, DRY, «правило трёх»
Golden Hammerодин инструмент на все задачи (Singleton везде, наследование везде)критерии применимости, альтернативы, ревью
Premature Optimizationмикро-оптимизации, кэши «на всякий случай» до профилированиясначала ясность, потом cProfile и замеры
Boat Anchorнеиспользуемый код/библиотеки «на всякий случай»удаление; контроль через покрытие и ревью
Lava Flowкод, который никто не понимает и не трогает; документации неттесты как страховка, постепенное удаление, ADR (М5)
Признак — то, что видите в коде; лечение — первый шаг. Какие три антипаттерна вы найдёте в своём «Университете»?
Разбор ошибки

Context manager без try/finally: ресурс не освободится при исключении

from contextlib import contextmanager @contextmanager def payment_transaction(ledger): print("открыта транзакция") yield ledger # ошибка: нет try/finally print("commit") try: with payment_transaction("ledger"): raise RuntimeError("сбой") except RuntimeError: print("исключение обработано") открыта транзакция исключение обработано

Разбор: «commit» не печатается — код после yield не выполнился при исключении; но и rollback некому сделать — ресурс/транзакция «повисли». Traceback показывает только RuntimeError, а проблема — в менеджере.

Правило: try/finally (или except) вокруг yield обязателен: гарантия очистки — смысл паттерна.

Найдите баг в парах: что должен проверять тест менеджера транзакций? (что при исключении вызывается rollback)
Live-coding

Live-coding: Протокол вместо ABC — переписываем интерфейс «Университета»

Задача: взять один интерфейс «Университета» (например, Storage с save/load), написать его как ABC и как Protocol; показать, что внешний класс (JSON-модуль) подходит под Protocol без наследования; сравнить гибкость.

Ожидаемый результат: isinstance(json_storage, StorageProtocol) — True при @runtime_checkable; с ABC — потребовалось бы наследование.

  1. ABC-вариант;
  2. Protocol-вариант;
  3. чужой класс без наследования;
  4. вывод «Protocol — для границ модулей».
Повторите: зачем нужен @runtime_checkable? Что случится с isinstance без него? (TypeError)
Live-coding

Live-coding: транзакция через context manager (commit/rollback)

Что должен проверить тест транзакции? (commit при успехе, rollback при исключении)
Проверь себя

Проверь себя: 4 вопроса перед семинаром

  1. Чем ABC отличается от Protocol и когда что выбирать?
  2. Назовите три антипаттерна из своего проекта «Университет» и как их лечить.
  3. Почему преждевременная оптимизация — антипаттерн, а не «забота о скорости»?
  4. Что общего у Boat Anchor и Lava Flow?
2 минуты письменно, затем сверка с залом; слабые места — на семинаре.

Типичные ошибки антипаттернов

  1. Называют антипаттерном любое несовершенство — важно указывать конкретные признаки и стоимость.
  2. Лечат антипаттерн «большим рефакторингом» без тестов — тесты сначала.
  3. Не замечают антипаттерны в своём коде, но легко находят в чужом — аудит по чеклисту, а не «на глаз».
Таблица «проблема → признак → паттерн/идиома → ожидаемый эффект» — и ни одного лечения без тестов. Так и будете работать на семинаре?

Связка с проектом: финальная полировка «Платёжной системы»

Проект станет объектом рефакторинга в М3 — паттерны модуля 2 — подготовка к слоям.

Чек-лист сдачи: python -m payments, mypy ., pytest --cov; README с обоснованием. Готовы к финальной полировке?

Сегодня вы научились… + итог модуля 2

Связка с целями модуля (R1, R2, R3, R5): всё, что обещали в начале (Слайд 4), — сделали?
Рефлексия

One-minute paper: главное + один вопрос

Напишите (1 минута, не подписывая):

  1. одно самое важное, что вы узнали за весь модуль 2;
  2. один вопрос, который остался неясным.
Соберите ответы: 2–3 вопроса разберём сразу или на консультации перед сдачей проекта.

Что дальше: семинар недели 8, сдача проекта, анонс М3

Спасибо за внимание — вопросы на семинаре. Откройте семинар-8.md: аудит «Университета» и план исправлений.