dataclasses · ABC и Protocol · context manager · descriptors и метаклассы · семь антипаттернов
2 ч теории. Финальная неделя модуля: сдача проекта «Платёжная система».
Недели 5–7 дали словарь паттернов GoF — а что язык даёт вам готовым, без классов и интерфейсов?
Финальный этап «Платёжной системы»: mypy, тесты ≥ 70%, README с обоснованием
Этап недели 8: доделать недостающие паттерны, кастомные исключения, довести тесты до ≥ 70% покрытия, прогнать mypy, написать README с обоснованием выбора каждого паттерна.
Требования проекта: минимум 3 паттерна; кастомная иерархия исключений (PaymentError); type hints; mypy без ошибок; pytest ≥ 70%; осмысленные коммиты.
Сегодняшние идиомы (Protocol, context manager) и аудит антипаттернов — прямо то, что нужно для «качества кода» и «обоснования выбора» в рубрике проекта.
Пункты рубрики «качество кода» и «уместность паттернов» — что именно закроет сегодняшняя лекция?
Не открывая конспект: по одному паттерну каждого класса GoF
Назовите по одному паттерну каждого класса GoF (порождающие, структурные, поведенческие).
Каков критерий уместности паттерна?
Пауза 10–15 секунд. Варианты: Factory Method, Adapter, Strategy. Критерий: «код с паттерном проще менять, чем код без него». Как тот же критерий применим к антипаттернам — наоборот?
Акцент: идиомы — не «магия», а осознанные решения с известными компромиссами.
Вы уже писали @dataclass — это идиома. Почему это работает и когда её не применять?
@dataclass — «данные без шума»: __init__, __repr__, __eq__ автоматически
from dataclasses import dataclass, field
@dataclassclass 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)
NamedTuple — неизменяемый именованный кортеж: структура данных с именами, без методов и гибкости.
dataclass — класс с поведением и мутабельностью: можно добавлять методы, frozen=True — неизменяемость по желанию.
frozen=True — для неизменяемых объектов (ключ в dict/set, защита от случайной мутации).
field(default_factory=list) — для изменяемых значений по умолчанию.
Когда применять
данные с поведением — класс, мутация;
нужна неизменяемость — frozen=True;
изменяемые дефолты — default_factory.
Когда не применять
2–3 поля без поведения — кортеж или NamedTuple проще;
dataclass ради dataclass — шум.
Данные без поведения — что выбрать? (NamedTuple); данные с поведением — dataclass.
ABC/@abstractmethod — классическое наследование с обязательной реализацией: подходит, когда иерархия своя.
Protocol — структурная типизация: «объект подходит, если имеет нужный метод», без наследования; @runtime_checkable для isinstance. Подходит для границ модулей и внешних объектов.
class Payable(Protocol):
defpay(self, amount: float) > bool: ...
Правило: «Protocol — если не хотим привязывать чужие классы к нашей иерархии; ABC — если иерархия своя и нужен общий код».
Помните FormatFactory с недели 5? Почему там Protocol, а не ABC? (чтобы OfflineFactory не наследовал нашу иерархию)
Предскажите результат
Protocol + @runtime_checkable: чужой класс подходит без наследования
Правило: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 — паттерн управления ресурсами и транзакциями
Вы уже писали __enter__/__exit__ и @contextmanager; сегодня — как паттерн управления ресурсами и транзакциями: гарантированная очистка, транзакция «зафиксировать/откатить».
contextlib.ExitStack — для нескольких ресурсов (обзор).
class CounterMeta(type):
instances = 0
def__call__(cls, *args, **kwargs):
CounterMeta.instances += 1
returnsuper().__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 по умолчанию)
Разбор ошибок
Типичные ошибки идиом
Используют ABC, хотя нужен Protocol (или наоборот): ABC привязывает чужие классы к иерархии; Protocol там, где нужен общий код.
dataclass с мутируемым значением по умолчанию без default_factory — общий список на все экземпляры.
Контекст-менеджер без try/finally вокруг yield — ресурс не освободится при исключении.
Пытаются писать метаклассы/дескрипторы в учебных задачах — сложность без выгоды.
Отметьте знакомые ошибки в своём коде «Университета»: 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
defenroll_student(self, ...): ...
defbuild_schedule(self, ...): ...
defpay_salary(self, ...): ...
defsend_email(self, ...): ...
# и так далее — всё в одном классе
Признаки: класс > 300–400 строк; поля разных предметных областей; метод меняется по любой причине; десятки несвязанных полей.
Лечение: декомпозиция по SRP, выделение сервисов, фасадов.
Декомпозиция по SRP: одна ответственность на класс — University делегирует студентам (StudentService), расписанию (ScheduleService) и финансам (FinanceService).
Ведите пальцем по схеме: один класс тянет шесть ответственностей. Какой сервис был бы в вашем «Университете»?
Spaghetti Code: флаги, break/continue в глубине; лечение — функции и слои
Признаки: перемешанные потоки управления; флаги для выхода из циклов; break/continue в глубине; функции без понятной ответственности.
defprocess(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 и «правило трёх»
Признаки: одинаковые блоки в трёх местах с мелкой правкой; правка бага требует правки в N местах.
Пример признака: один и тот же расчёт скидки скопирован в три метода с разными именами переменных.
Лечение: извлечение функции/класса, DRY, «правило трёх» — третье повторение повод для абстракции.
Связь с неделей 7: если копируются ветвления по типу — это сигнал к Strategy.
«Правило трёх»: первое повторение терпим, второе присматриваемся, третье — абстракция. Когда копирование ветвлений по типу — сигнал к Strategy?
Golden Hammer: всё молотком; лечение — критерии применимости и ревью
Признаки: «всё молотком» — один инструмент на все задачи: Singleton везде, наследование везде, один паттерн на все случаи.
Пример: после недели 5 студент вставляет CourseFactory даже туда, где один класс без вариантов.
Лечение: явные критерии применимости паттерна (разделы «когда не применять» недель 5–7), обсуждение альтернатив, ревью.
Код с паттерном должен быть проще менять, чем без него. Если это не так — какой антипаттерн вы обнаружили? (Golden Hammer)
Premature Optimization: оптимизация до профилирования; лечение — сначала ясность, потом cProfile
Признаки: микро-оптимизации, кэши «на всякий случай», усложнение ради скорости до того, как скорость стала проблемой.
Пример: рукописный кэш в Proxy там, где репозиторий вызывается раз в минуту.
Лечение: сначала ясность и корректность, потом cProfile и только по замерам; правило «сначала измерь».
Связь: «Premature optimization is the root of all evil» — но с оговоркой: не оптимизировать до профилирования.
Почему кэш «на всякий случай» — антипаттерн, а не забота о скорости? (стоимость сложности без измерения выгоды)
Boat Anchor: мёртвый код и библиотеки «на всякий случай»; лечение — удаление
Признаки: неиспользуемый код, библиотеки и комментарии «на всякий случай»; импорты без использования; функции, которые никто не зовёт.
Пример: import numpy в проекте, где NumPy не используется, «вдруг понадобится».
Лечение: удаление; контроль через покрытие и ревью; «мёртвый код — это ложь, а не страховка».
Якорь на дне лодки не спасает, а тянет ко дну — как и неиспользуемый код. Чем контролировать его удаление? (покрытие и ревью)
Lava Flow: код, который никто не трогает; лечение — тесты как страховка, постепенное удаление
Признаки: код, который никто не понимает и не трогает, потому что страшно сломать; накапливается годами; документации нет.
Лечение: тесты как страховка (покрыть поведение), постепенное удаление/замена, документация решений (ADR — в М5).
Связь с модулем: «тесты сначала» — тот же принцип, что в лечении класса-гиганта.
Что общего у 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
@contextmanagerdefpayment_transaction(ledger):
print("открыта транзакция")
yield ledger # ошибка: нет try/finallyprint("commit")
try:
withpayment_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 — потребовалось бы наследование.
ABC-вариант;
Protocol-вариант;
чужой класс без наследования;
вывод «Protocol — для границ модулей».
Повторите: зачем нужен @runtime_checkable? Что случится с isinstance без него? (TypeError)
Live-coding
Live-coding: транзакция через context manager (commit/rollback)
Пишем с нуля: @contextmanager payment_transaction(ledger) с try/except (rollback + raise).
Демонстрируем успешный случай (commit) и случай с исключением (rollback).
Показываем, что без try/finally очистка не гарантирована.
Что должен проверить тест транзакции? (commit при успехе, rollback при исключении)
Проверь себя
Проверь себя: 4 вопроса перед семинаром
Чем ABC отличается от Protocol и когда что выбирать?
Назовите три антипаттерна из своего проекта «Университет» и как их лечить.
Почему преждевременная оптимизация — антипаттерн, а не «забота о скорости»?
Что общего у Boat Anchor и Lava Flow?
2 минуты письменно, затем сверка с залом; слабые места — на семинаре.
Типичные ошибки антипаттернов
Называют антипаттерном любое несовершенство — важно указывать конкретные признаки и стоимость.
Лечат антипаттерн «большим рефакторингом» без тестов — тесты сначала.
Не замечают антипаттерны в своём коде, но легко находят в чужом — аудит по чеклисту, а не «на глаз».
Таблица «проблема → признак → паттерн/идиома → ожидаемый эффект» — и ни одного лечения без тестов. Так и будете работать на семинаре?
Связка с проектом: финальная полировка «Платёжной системы»
Этап недели 8: доделать недостающие паттерны, кастомные исключения, тесты ≥ 70%, mypy без ошибок, README с обоснованием каждого паттерна.
Идиомы недели в проекте: Protocol для контрактов (PaymentFactory, Storage), context manager для транзакций (payment_transaction), dataclass для моделей платежей.
Аудит перед сдачей: прогнать проект по таблице антипаттернов (Слайд 24); проверить mypy . и pytest --cov.
Проект станет объектом рефакторинга в М3 — паттерны модуля 2 — подготовка к слоям.
Чек-лист сдачи: python -m payments, mypy ., pytest --cov; README с обоснованием. Готовы к финальной полировке?
Сегодня вы научились… + итог модуля 2
неделя 8: применять dataclasses, ABC/Protocol, context manager; объяснять идею дескрипторов и метаклассов; распознавать семь антипаттернов и составлять план лечения;
весь модуль: порождающие (нед. 5), структурные (нед. 6), поведенческие (нед. 7) паттерны; идиомы и антипаттерны (нед. 8); обоснование выбора.
Связка с целями модуля (R1, R2, R3, R5): всё, что обещали в начале (Слайд 4), — сделали?
Рефлексия
One-minute paper: главное + один вопрос
Напишите (1 минута, не подписывая):
одно самое важное, что вы узнали за весь модуль 2;
один вопрос, который остался неясным.
Соберите ответы: 2–3 вопроса разберём сразу или на консультации перед сдачей проекта.
Что дальше: семинар недели 8, сдача проекта, анонс М3
Семинар недели 8 (семинар-8.md): аудит «Университета», план исправлений, протокол вместо ABC, транзакция через context manager, разбор чужого кода.
ДЗ недели 8 — финальный этап проекта «Платёжная система» (сдача к концу недели 8; рубрика 09-критерии-оценивания.md §4).
Анонс М3 «Архитектура кода»: слои, внедрение зависимостей, рефакторинг — «Платёжная система» станет объектом архитектурного рефакторинга.
Спасибо за внимание — вопросы на семинаре. Откройте семинар-8.md: аудит «Университета» и план исправлений.