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

Абстракция и декомпозиция. Чтение чужого кода

абстракция · интерфейс vs реализация · декомпозиция · модульность · чтение кода без исполнения

2 ч теории. Итог модуля — инженерный анализ вашей CLI-утилиты С1.

Почему это вообще нужно? — вопрос, с которого начнём.

На стажировке первая задача — понять существующий код

Кто может объяснить преподавателю, как устроена каждая функция его утилиты?

Функция на 100 строк: «простыня», которую нельзя протестировать

def process(path): # читает, парсит, считает И печатает — всё в одной функции text = open(path).read() # уровень: файл lines = text.strip().split("\n") # уровень: парсинг result = {} for line in lines: parts = line.split(",") cat = parts[0] result[cat] = result.get(cat, 0) + int(parts[1]) # уровень: расчёт for cat in result: print(cat, result[cat]) # уровень: отчёт return result

Вопрос под кодом: что здесь плохо и почему? (2–3 ответа от группы).

Какие действия смешаны в одной функции? (чтение, парсинг, расчёт, печать)

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

В конце — «Проверь себя»: это сигнал, где повторить, а не экзамен.

Абстракция — выделение существенного и отбрасывание несущественного

Что вы «забываете», когда вызываете sorted(data)?

Уровни абстракции: данные → функции → модули → программа

на каждом уровне мы забываем детали нижнего данные значения, переменные функции операции над данными — прячем детали данных модули группы функций — прячем детали функций программа связывает модули — прячет детали модулей sorted(data) — чёрный ящик сортировка внутри — неизвестно data отсортированный список
Пользователь уровня знает интерфейс, а не реализацию.
На каком уровне вы находитесь, когда чините баг в sorted()?

sorted(data) и print() — чёрный ящик: пользователь не знает деталей

data = [5, 2, 8, 1] print(sorted(data)) print(data) [1, 2, 5, 8] [5, 2, 8, 1]

Правило: sorted(data) возвращает новый список и не меняет исходный — это часть контракта; какая сортировка внутри (пузырёк, быстрая, Timsort) — пользователю безразлично.

Что вернёт sorted(data) для data = [3, 1, 2], и изменится ли data?

Интерфейс (что можно вызвать) vs реализация (как устроено внутри)

ИНТЕРФЕЙС что можно вызвать входы / выходы, контракт пример: max_of_two(a, b) → большее из a и b пользователь видит только левую сторону; правая может меняться без изменения левой РЕАЛИЗАЦИЯ как устроено внутри код функции def max_of_two(a, b): if a > b: return a return b
Читая код, сначала восстанавливаем контракт (по docstring, сигнатуре, именам), потом — реализацию.
Что изменится для пользователя, если поменять реализацию, но не контракт?

Интерфейс один, реализации две: max_of_two через if и через max

def max_of_two_if(a, b): if a > b: return a return b def max_of_two_builtin(a, b): return max(a, b) print(max_of_two_if(3, 7), max_of_two_builtin(3, 7)) 7 7

Правило: сигнатура и контракт одинаковы («вернуть большее из двух»); реализации разные; вызывающий код не меняется.

Какую реализацию выберете вы и почему? (критерий — читаемость)

Какая реализация «лучше»? — пользователю безразлично

print(max_of_two_if(10, 2)) print(max_of_two_builtin(10, 2)) print(max_of_two_if(-5, -1)) print(max_of_two_builtin(-5, -1))

Вопрос: не запуская — что напечатают все четыре строки? Чем отличаются результаты?

Предскажите вывод (пауза 10–15 секунд), затем раскроем.

Декомпозиция сверху вниз: задача → 3–5 подзадач → функции

задача отчёт по расходам из CSV прочитать данные отфильтровать и посчитать сохранить отчёт load_csv filter_rows total_by_category save_report разбиение сверху вниз: задача → подзадачи → функции; каждая функция делает одно действие
В вашей CLI-утилите С1 это уже есть: load / analyze / report — осталось назвать это декомпозицией.
Почему 3–5 подзадач, а не 15?

Критерии хорошей функции: имя отвечает на «что делает?», ≤ 20 строк, один уровень

def total_by_category(rows, limit): """Суммы расходов по категориям для записей с sum > limit.""" result = {} for r in rows: if r["sum"] > limit: cat = r["category"] result[cat] = result.get(cat, 0) + r["sum"] return result rows = [ {"category": "food", "sum": 1500}, {"category": "transport", "sum": 700}, {"category": "fun", "sum": 2000}, ] print(total_by_category(rows, 1000)) {'food': 1500, 'fun': 2000}

Критерии: имя отвечает на «что делает?»; длина ≤ 20 строк; один уровень абстракции; нет побочных эффектов там, где их не ждут.

Как переименовать rows в плохом коде, чтобы имя несло информацию?

Правило одной ответственности: «пропарсить И вывести» — одна причина для изменения

def parse_and_report(path): rows = [] for line in open(path): cat, amount = line.strip().split(",") rows.append((cat, int(amount))) for cat, amount in rows: print(cat, amount) # печать внутри функции return rows

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

Что придётся менять, если отчёт надо выводить в JSON?

Стоп-вопрос: зачем восстанавливать контракт до чтения реализации?

  1. Зачем при чтении кода сначала восстанавливать контракт (docstring, сигнатура, имена), а не читать реализацию?
  2. По каким признакам вы поймёте, что функция нарушает правило одной ответственности?

Эталон: контракт задаёт «что делает», реализация — «как»; нарушение — смешение действий (парсинг и печать), которые меняются по разным причинам.

Если правильных ответов меньше половины — вернёмся к схеме «Интерфейс vs реализация».

Live-reading: читаем чужой код вслух — точка входа → контракты → карта

  1. «Сначала ищу точку входа: main, if __name__ == "__main__":, диспетчер команд» — проговаривать, как нашёл;
  2. «Читаю сверху вниз, от вызовов к определениям»;
  3. «Восстанавливаю контракт каждой функции по имени, сигнатуре и docstring, не читая тело»;
  4. «Строю карту: функция → ответственность → вход/выход».

Фрагмент кода — из материалов семинара (Задача 1.1); готовится заранее и на слайды не выносится.

Повторите приём на своём проекте: найдите точку входа и контракт первой функции.

Модуль — граница ответственности: load / analyze / report в вашей CLI-утилите

main точка входа load.py чтение данных (CSV/JSON) analyze.py фильтрация и статистика report.py вывод отчёта
Модульность — это уже декомпозиция на уровне файлов: переиспользование, тестирование по частям, разделение труда, локализация изменений.
Что произойдёт, если формат CSV поменяется?

Импорт без побочных эффектов: верхний уровень — только константы и if __name__

# так нельзя: верхний уровень читает файл при импорте rows = open("data.csv").readlines() # выполнится при import
# module report.py def make_report(rows): return {"total": sum(r["sum"] for r in rows)} if __name__ == "__main__": print(make_report([{"sum": 10}, {"sum": 20}])) {'total': 30}

При запуске как скрипта — вывод выше. При импорте — ничего: код под if __name__ не выполняется.

Почему импорт с побочным эффектом плох?

Чтение кода — навык: чтения больше, чем написания; чужой код не «хуже», а «другой»

Что теряется при переписывании чужого кода?

Методика, шаг 1: найти точку входа (main, диспетчер, if __name__)

def main(): rows = load_csv("data.csv") stats = total_by_category(rows, 1000) save_report(stats, "out.csv") if __name__ == "__main__": main()

Правило: точка входа — main, диспетчер команд, if __name__ == "__main__":. Читаем от неё, а не с первой строки файла.

Почему: точка входа показывает, что вызывается и в каком порядке; без неё код читается «снизу вверх», и детали заслоняют структуру.

Что будет точкой входа в модуле, который вы разбираете на семинаре?

Методика, шаг 2: читать сверху вниз, от вызовов к определениям

def main(): rows = load_csv("data.csv") # вызов 1 stats = total_by_category(rows, 1000) # вызов 2 save_report(stats, "out.csv") # вызов 3 # теперь — определения вызванных функций, по порядку вызовов: def load_csv(path): return [dict] # тело читаем после восстановления контракта

Правило: встретили вызов — идём к его определению, потом возвращаемся. Чтение идёт сверху вниз: от вызовов к определениям, а не от определений «на будущее».

Акцент: не читать определения, которые ещё не вызваны, — они нужны позже, когда дойдёте до вызова.

Почему не читать определения сразу все подряд?

Методика, шаги 3–5: контракт → имена → структура данных

def load_csv(path: str) -> list[dict]: """Читает CSV с шапкой category,sum; возвращает список словарей.""" # контракт: путь → список словарей; реализацию читаем после return [] # пример тела — не читаем, пока не восстановлен контракт
Что предсказывает имя total_by_category?

Зачем читать без исполнения: «что на одном входе» vs «что на всех входах»

СпособЧто показываетГде применяется
Исполнение«что происходит на одном входе»быстрая проверка догадки
Чтение«что возможно на всех входах»ревью, аудит, рефакторинг
Почему на ревью нельзя «просто запустить»?

Так нельзя: читать снизу вверх, с первой строки, без точки входа

Антиприём: читать с первой строки, построчно, без точки входа. Комментарий вслух: «что-то про файл… а зачем это?» — и «тонешь» в деталях.

Разбор по методу недели: «где я потерялся? — в деталях, потому что не знал, кто и зачем вызывает a. Что сделать? — найти точку входа и читать сверху вниз».

Фрагмент — учебный модуль с запахами (из семинара-12.md); готовится заранее и на слайды не выносится.

Это нормальная ошибка — так читают все новички. Что сделать после блока?

Пять ошибок недели 12, которые вы совершите на семинаре

  1. «Мне проще переписать, чем понять» — переписывание теряет заложенные решения (граничные случаи, совместимость);
  2. монолитная функция на 100+ строк без разбиения — «простыня»;
  3. имена-«однофамильцы»: a, b, c, tmp, data — имя не несёт информации;
  4. смешение уровней абстракции: функция и парсит CSV, и печатает в консоль, и пишет JSON;
  5. попытка понять реализацию, не восстановив контракт, — чтение «снизу вверх», построчное, без точки входа.
Какая из пяти ошибок самая дорогая на ревью?

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

  1. В чём разница между интерфейсом функции и её реализацией? Приведите пример, где интерфейс один, а реализаций две.
  2. По каким признакам вы поймёте, что функция нарушает правило одной ответственности?
  3. Почему при чтении чужого кода сначала нужно найти точку входа, а не читать файл с первой строки?
  4. Что делает импорт модуля, если в нём на верхнем уровне стоит код, читающий файл? Почему это плохо?
2 минуты письменно, затем разбор; эталоны — в решения-12.md.

Проект модуля: карта декомпозиции вашей CLI-утилиты + фиксация запахов

Какой первый шаг карты вашего проекта?

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

Всё, что обещали в начале, — сделали?

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

Это сигнал для семинара: что разобрать подробнее.

Что дальше: семинар недели 12, ДЗ разбор-неделя12.md, анонс недели 13

Готовы? Откройте семинар-12.md, задачи 1.1–1.5 — по своему уровню.