Почему это вообще нужно? — вопрос, с которого начнём.
На стажировке первая задача — понять существующий код
На стажировке первая задача — не написать новое, а понять существующее: «разберись в коде, добавь фичу».
Чтение кода занимает в индустрии больше времени, чем написание; ревью и аудит — это и есть чтение.
Требование почти всех вакансий: «читаемость кода», «умение работать в чужом коде», «code review».
На 2–3 курсе: алгоритмы и структуры данных вы будете разбирать по чужим реализациям — по контрактам, а не построчно.
Кто может объяснить преподавателю, как устроена каждая функция его утилиты?
Функция на 100 строк: «простыня», которую нельзя протестировать
defprocess(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 ответа от группы).
Какие действия смешаны в одной функции? (чтение, парсинг, расчёт, печать)
К концу лекции вы сможете…
объяснять абстракцию и перечислять уровни абстракции;
различать интерфейс и реализацию (пример: два max_of_two);
декомпозировать задачу сверху вниз и применять критерии хорошей функции;
находить нарушение правила одной ответственности;
читать незнакомый код без исполнения: точка входа → контракты → карта;
объяснять, почему импорт модуля не должен иметь побочных эффектов.
В конце — «Проверь себя»: это сигнал, где повторить, а не экзамен.
Абстракция — выделение существенного и отбрасывание несущественного
Абстракция — выделение существенного и отбрасывание несущественного.
На каждом уровне мы «забываем» детали нижнего уровня.
Пример: вы пользуетесь print(), не зная, как устроены буферизация и кодировки; вы вызываете sorted(), не зная, какая сортировка внутри.
«Забыть детали» — это не лень, а способ думать: голова освобождается для задачи текущего уровня.
Что вы «забываете», когда вызываете sorted(data)?
Уровни абстракции: данные → функции → модули → программа
Пользователь уровня знает интерфейс, а не реализацию.
На каком уровне вы находитесь, когда чините баг в sorted()?
sorted(data) и print() — чёрный ящик: пользователь не знает деталей
Правило:sorted(data) возвращает новый список и не меняет исходный — это часть контракта; какая сортировка внутри (пузырёк, быстрая, Timsort) — пользователю безразлично.
Что вернёт sorted(data) для data = [3, 1, 2], и изменится ли data?
Интерфейс (что можно вызвать) vs реализация (как устроено внутри)
Читая код, сначала восстанавливаем контракт (по docstring, сигнатуре, именам), потом — реализацию.
Что изменится для пользователя, если поменять реализацию, но не контракт?
Интерфейс один, реализации две: max_of_two через if и через max
defmax_of_two_if(a, b):
if a > b:
return a
return b
defmax_of_two_builtin(a, b):
returnmax(a, b)
print(max_of_two_if(3, 7), max_of_two_builtin(3, 7))
7 7
Правило: сигнатура и контракт одинаковы («вернуть большее из двух»); реализации разные; вызывающий код не меняется.
Какую реализацию выберете вы и почему? (критерий — читаемость)
Вопрос: не запуская — что напечатают все четыре строки? Чем отличаются результаты?
Предскажите вывод (пауза 10–15 секунд), затем раскроем.
Декомпозиция сверху вниз: задача → 3–5 подзадач → функции
В вашей CLI-утилите С1 это уже есть: load / analyze / report — осталось назвать это декомпозицией.
Почему 3–5 подзадач, а не 15?
Критерии хорошей функции: имя отвечает на «что делает?», ≤ 20 строк, один уровень
deftotal_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 в плохом коде, чтобы имя несло информацию?
Правило одной ответственности: «пропарсить И вывести» — одна причина для изменения
defparse_and_report(path):
rows = []
for line inopen(path):
cat, amount = line.strip().split(",")
rows.append((cat, int(amount)))
for cat, amount in rows:
print(cat, amount) # печать внутри функцииreturn rows
Правило: функция должна иметь одну причину для изменения: если «пропарсить строку И вывести отчёт», изменение формата отчёта ломает парсинг.
Что придётся менять, если отчёт надо выводить в JSON?
Стоп-вопрос: зачем восстанавливать контракт до чтения реализации?
Зачем при чтении кода сначала восстанавливать контракт (docstring, сигнатура, имена), а не читать реализацию?
По каким признакам вы поймёте, что функция нарушает правило одной ответственности?
Эталон: контракт задаёт «что делает», реализация — «как»; нарушение — смешение действий (парсинг и печать), которые меняются по разным причинам.
Если правильных ответов меньше половины — вернёмся к схеме «Интерфейс vs реализация».
Live-reading: читаем чужой код вслух — точка входа → контракты → карта
«Сначала ищу точку входа: main, if __name__ == "__main__":, диспетчер команд» — проговаривать, как нашёл;
«Читаю сверху вниз, от вызовов к определениям»;
«Восстанавливаю контракт каждой функции по имени, сигнатуре и docstring, не читая тело»;
«Строю карту: функция → ответственность → вход/выход».
Фрагмент кода — из материалов семинара (Задача 1.1); готовится заранее и на слайды не выносится.
Повторите приём на своём проекте: найдите точку входа и контракт первой функции.
Модуль — граница ответственности: load / analyze / report в вашей CLI-утилите
Модульность — это уже декомпозиция на уровне файлов: переиспользование, тестирование по частям, разделение труда, локализация изменений.
Что произойдёт, если формат CSV поменяется?
Импорт без побочных эффектов: верхний уровень — только константы и if __name__
# так нельзя: верхний уровень читает файл при импорте
rows = open("data.csv").readlines() # выполнится при import
# module report.pydefmake_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__)
Правило: точка входа — main, диспетчер команд, if __name__ == "__main__":. Читаем от неё, а не с первой строки файла.
Почему: точка входа показывает, что вызывается и в каком порядке; без неё код читается «снизу вверх», и детали заслоняют структуру.
Что будет точкой входа в модуле, который вы разбираете на семинаре?
Методика, шаг 2: читать сверху вниз, от вызовов к определениям
defmain():
rows = load_csv("data.csv") # вызов 1
stats = total_by_category(rows, 1000) # вызов 2save_report(stats, "out.csv") # вызов 3# теперь — определения вызванных функций, по порядку вызовов:defload_csv(path):
return [dict] # тело читаем после восстановления контракта
Правило: встретили вызов — идём к его определению, потом возвращаемся. Чтение идёт сверху вниз: от вызовов к определениям, а не от определений «на будущее».
Акцент: не читать определения, которые ещё не вызваны, — они нужны позже, когда дойдёте до вызова.
Почему не читать определения сразу все подряд?
Методика, шаги 3–5: контракт → имена → структура данных
defload_csv(path: str) -> list[dict]:
"""Читает CSV с шапкой category,sum; возвращает список словарей."""# контракт: путь → список словарей; реализацию читаем послеreturn [] # пример тела — не читаем, пока не восстановлен контракт
Шаг 4 — имена: имя функции/переменной должно предсказывать поведение; a, b, c, tmp — имя не несёт информации.
Шаг 5 — структура данных: какие данные входят и выходят, как преобразуются (список → dict по категориям).
Правило: сначала контракт каждой функции, потом — реализация.
Что предсказывает имя total_by_category?
Зачем читать без исполнения: «что на одном входе» vs «что на всех входах»
Способ
Что показывает
Где применяется
Исполнение
«что происходит на одном входе»
быстрая проверка догадки
Чтение
«что возможно на всех входах»
ревью, аудит, рефакторинг
В ревью и аудите исполнять нельзя: вы должны понять код, не запуская его, — это и есть основная работа на code review.
Чтение по контрактам покрывает «все входы»: если контракт «путь → list[dict]», вы знаете поведение для любого корректного пути.
Почему на ревью нельзя «просто запустить»?
Так нельзя: читать снизу вверх, с первой строки, без точки входа
Антиприём: читать с первой строки, построчно, без точки входа. Комментарий вслух: «что-то про файл… а зачем это?» — и «тонешь» в деталях.
Разбор по методу недели: «где я потерялся? — в деталях, потому что не знал, кто и зачем вызывает a. Что сделать? — найти точку входа и читать сверху вниз».
Фрагмент — учебный модуль с запахами (из семинара-12.md); готовится заранее и на слайды не выносится.
Это нормальная ошибка — так читают все новички. Что сделать после блока?
Пять ошибок недели 12, которые вы совершите на семинаре
«Мне проще переписать, чем понять» — переписывание теряет заложенные решения (граничные случаи, совместимость);
монолитная функция на 100+ строк без разбиения — «простыня»;
имена-«однофамильцы»: a, b, c, tmp, data — имя не несёт информации;
смешение уровней абстракции: функция и парсит CSV, и печатает в консоль, и пишет JSON;
попытка понять реализацию, не восстановив контракт, — чтение «снизу вверх», построчное, без точки входа.
Какая из пяти ошибок самая дорогая на ревью?
Проверь себя: 4 вопроса перед семинаром
В чём разница между интерфейсом функции и её реализацией? Приведите пример, где интерфейс один, а реализаций две.
По каким признакам вы поймёте, что функция нарушает правило одной ответственности?
Почему при чтении чужого кода сначала нужно найти точку входа, а не читать файл с первой строки?
Что делает импорт модуля, если в нём на верхнем уровне стоит код, читающий файл? Почему это плохо?
2 минуты письменно, затем разбор; эталоны — в решения-12.md.
Проект модуля: карта декомпозиции вашей CLI-утилиты + фиксация запахов
Этап недели 12 проекта: карта декомпозиции проекта (модули/функции → ответственность → вход/выход) + фиксация запахов.
На семинаре вы: разберёте чужой код без исполнения, составите карту своего CLI, проведёте парное ревью по чеклисту.