Что здесь плохо? Кто сможет объяснить эту строку через месяц?
К концу лекции вы сможете…
объяснять, что такое парадигма, и различать три парадигмы;
отличать чистую функцию от функции с побочными эффектами;
применять map/filter/reduce и генераторы списков без мутации;
выбирать парадигму под задачу и обосновывать выбор;
называть оси компромиссов и приводить пример «время vs память»;
объяснять, почему «всё через классы» — ошибка.
В конце — «Проверь себя» (Слайд 26): это сигнал, где повторить, а не экзамен.
Парадигма — способ мышления о решении задачи, набор абстракций и правил
Парадигма программирования — способ мышления о решении задачи, набор базовых абстракций и правил.
Парадигма — не «мода», а инструмент: у каждой свои сильные стороны и своя цена.
Сегодня: процедурная (уже знаете из С1), ООП (напоминание), функциональная (новая).
Python — мультипарадигмальный язык: выбор парадигмы — инженерное решение, а не вкус (подробнее — Слайд 16).
Какую парадигму вы использовали в С1? (процедурную — неосознанно)
Процедурный стиль: последовательность шагов, явное состояние, порядок вызовов важен
rows = [{"s": 1500}, {"s": 700}, {"s": 2000}]
total = 0
for r in rows:
if r["s"] > 1000:
total += r["s"]
print(total)
3500
Свойства: программа — последовательность шагов и функций, изменяющих состояние (total); порядок вызовов важен; явная мутация.
Что здесь является состоянием? (переменная total)
Процедурный стиль: плюсы — простота, минусы — состояние «размазано»
Плюсы
Минусы
простота, прямой контроль
состояние «размазано» по функциям
естественен для коротких скриптов
сложно рассуждать о больших системах
порядок шагов очевиден
мутация усложняет тесты и параллельность
Вывод: процедурный стиль — база для коротких скриптов и последовательностей шагов; для больших систем состояние лучше организовывать (ООП) или убирать (функциональный стиль).
Для какой задачи процедурный стиль естественен, а для какой начинает мешать?
ООП (напоминание): состояние и поведение в объекте; полное изучение — С2
class ExpenseAnalyzer:
def__init__(self, rows):
self.rows = rows
deftotal_by_category(self, limit):
result = {}
for r in self.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(ExpenseAnalyzer(rows).total_by_category(1000))
{'food': 1500, 'fun': 2000}
Свойства: состояние (self.rows) и поведение (методы) собраны в объекте; инкапсуляция скрывает детали; переиспользование через наследование и полиморфизм — в С2, модуль 1.
Что здесь состояние, а что поведение? (состояние — self.rows; поведение — total_by_category)
Чистая функция: результат зависит только от аргументов, нет побочных эффектов
defadd_one(xs):
return [x + 1 for x in xs] # чистая: не меняет xsdefadd_one_bad(xs):
for i inrange(len(xs)):
xs[i] += 1 # побочный эффект: мутирует xsreturn xs
nums = [1, 2, 3]
print(add_one(nums))
print(nums)
nums2 = [1, 2, 3]
print(add_one_bad(nums2))
print(nums2)
[2, 3, 4]
[1, 2, 3]
[2, 3, 4]
[2, 3, 4]
Определение:чистая функция — результат зависит только от аргументов; нет побочных эффектов (не читает/пишет внешнее состояние).
Правило: чистые функции легче тестировать — для одного входа всегда один выход, не зависящий от порядка вызовов.
Почему чистую функцию легче тестировать? (нет скрытого состояния, детерминированный результат)
Неизменяемость: преобразования создают новые значения, данные не мутируют
Правило: в функциональном стиле данные не мутируют — преобразования создают новые значения (sorted вернул новый список, исходный не изменился).
Предскажите: «Что выведет print(nums) после sorted(nums)?» — пауза, затем вывод.
Какая функция из С1 мутирует список? (list.sort() — в отличие от sorted())
Функции высшего порядка: map/filter принимают функцию; lambda — короткая функция
prices = [100, 1500, 700, 2000]
big = list(filter(lambda p: p > 1000, prices))
print(big)
print(list(map(lambda p: p * 2, [1, 2, 3])))
[1500, 2000]
[2, 4, 6]
Определения:функции высшего порядка — принимают/возвращают функции; filter(f, xs) оставляет элементы, где f истинно; map(f, xs) преобразует каждый элемент; lambda — безымянная короткая функция.
Замечание:filter/map возвращают итераторы — оборачиваем в list() для печати.
Что вернёт filter(lambda x: x > 10, [5, 15, 20]) до list()? (итератор, не список)
Цикл с мутацией vs map+filter: тот же результат, другой стиль
# процедурно: цикл + append
big = []
for r in expenses:
if r["sum"] > 1000:
big.append(r)
# функционально: filter без мутации
big = list(filter(lambda r: r["sum"] > 1000, expenses))
Оба дают одинаковый результат ✓:[{'category': 'food', 'sum': 1500}, {'category': 'fun', 'sum': 2000}]
Вывод: результат один, но во втором варианте нет изменяемого состояния big и нет append — проще рассуждать и тестировать.
Какой вариант короче? Какой легче тестировать? (короче — правый; тестировать — оба, но правый детерминированнее)
Одна задача в трёх парадигмах: процедурно, ООП, функционально
Задача: «расходы больше 1000, сгруппировать по категории, сумма».
Результат одинаковый ({'food': 1500, 'fun': 2000}); различаются состояние, мутация и способ композиции.
Полные три решения — на семинаре недели 13 (Задача 02).
Какая строка самая короткая? Какая самая явная по состоянию? (функциональная короче; процедурная явнее)
reduce и генераторы списков — функциональные инструменты
from functools import reduce
nums = [1, 2, 3, 4]
print(reduce(lambda a, b: a + b, nums))
print([x * x for x in nums if x % 2 == 0])
10
[4, 16]
Правило:reduce(f, xs) сворачивает список в одно значение; генератор списков [expr for x in xs if cond] — декларативный фильтр+преобразование.
Предостережение:reduce с вложенными lambda быстро становится нечитаемым (Слайд 3) — используйте, когда действительно сворачиваете.
Что вернёт [x * x for x in range(5) if x % 2 == 1]? (ответ: [1, 9])
Стоп-вопрос: что такое чистая функция и почему её легче тестировать?
Что такое чистая функция? Почему чистые функции легче тестировать?
В чём разница между filter(lambda ...) и циклом с if и append? Когда какой вариант уместен?
Эталон: чистая — результат зависит только от аргументов, нет побочных эффектов; filter — декларативнее и без мутации, цикл — привычнее и понятнее новичку; уместен тот, что читается лучше в контексте.
Пауза 10–15 секунд. Если правильных ответов меньше половины — вернёмся к Слайду 9 (чистая функция).
Когда какая парадигма уместна; Python — мультипарадигмальный язык
Задача
Парадигма/стиль
обработка потоков данных, конвейеры, расчёты без состояния
функциональный
моделирование сущностей с инвариантами и поведением
ООП
короткие скрипты, последовательности шагов
процедурный
комбинирование подходов в одном проекте
мультипарадигмальный
Python — мультипарадигмальный: можно комбинировать в одном проекте.
Выбор парадигмы — инженерное решение, а не вкус: решение принимается под задачу и обосновывается.
Какая парадигма уместна для модели «Банковский счёт с инвариантом “сумма ≥ 0”»? (ООП — есть инвариант и поведение)
Live-coding: пишем функциональное решение с нуля — без мутации входных данных
Задача: дано expenses (список словарей), вернуть суммы по категориям для расходов > 1000, не мутируя вход.
Ожидаемый результат:
{'food': 1500, 'fun': 2000}
Порядок написания:
over = filter(lambda r: r["sum"] > 1000, expenses) — фильтрация;
by_cat = sorted(over, key=lambda r: r["category"]) — сортировка для groupby;
from itertools import groupby — группировка;
{cat: sum(r["sum"] for r in group) for cat, group in groupby(...)} — словарь.
Намеренная ошибка: забыть list() вокруг filter — получим итератор; разберём, почему печатается адрес объекта.
Повторите на своих данных: зачем нужна сортировка перед groupby?
Компромиссы: инженер никогда не получает «всё сразу»
Ось
Пример
время vs память
Counter (память) vs вложенный цикл (время)
читаемость vs производительность
оптимизированный, но непонятный код
простота vs гибкость
абстракции «на всякий случай»
скорость разработки vs качество
долг: тесты, ревью, документация
Вывод:компромисс — это осознанный выбор с обоснованием, а не случайность; инженер выбирает ось под контекст задачи.
Какой компромисс вы уже принимали в С1? (например, словарь вместо списка — память за скорость)
Время vs память: Counter вместо вложенного цикла — O(n) вместо O(n²)
from collections import Counter
words = ["a", "b", "a", "c", "a", "b"]
print(Counter(words))
counts = {}
for w in words:
counts[w] = counts.get(w, 0) + 1
print(counts)
Counter({'a': 3, 'b': 2, 'c': 1})
{'a': 3, 'b': 2, 'c': 1}
Big O:Counter — один проход + хэш-таблица, O(n) по времени и O(k) по памяти; наивный «сравни каждую пару» — O(n²) по времени. Counter платит памятью за скорость.
Правило: «O(n) по времени часто значит O(n) по памяти» — показывать на примере кэша и словаря.
Где здесь память, а где время? (словарь внутри Counter — память; быстрый поиск — время)
Читаемость vs производительность: измеряй timeit, не угадывай
import timeit
t1 = timeit.timeit("sum(x for x in range(1000))", number=10000)
t2 = timeit.timeit("sum(range(1000))", number=10000)
print("генератор:", round(t1, 4))
print("встроенный sum:", round(t2, 4))
генератор: 0.6686
встроенный sum: 0.1954
Вывод — на моей машине, Python 3.11.2; ориентировочно.
Правило: «оптимизированный, но непонятный код дороже в поддержке»; преждевременная оптимизация без timeit — ошибка; оптимизируем только там, где реально узкое место.
Что плохого в «оптимизации» кода, который работает мгновенно? (портят читаемость без выигрыша)
Простота vs гибкость: абстракции «на всякий случай» вредны
# антипример: класс без состояния — «набор функций в обёртке»class Calc:
defadd(self, a, b):
return a + b
defmul(self, a, b):
return a * b
Правило: добавлять абстракции заранее «на всякий случай» вредно; класс без состояния и поведения — это модуль с функциями, замаскированный под ООП. Гибкость нужна, когда она решает реальную задачу.
Что лучше: класс Calc или модуль с функциями add/mul? (модуль — проще и честнее)
Сначала корректность и читаемость, потом измеренная производительность; оптимизация только там, где реально узкое место.
Что вы делаете, если код корректен, но медленный? (сначала измеряем, потом оптимизируем)
Стоп-вопрос: какие компромиссы вы принимаете, выбирая словарь вместо списка?
Какие компромиссы вы принимаете, когда выбираете словарь вместо списка? Приведите пример «время vs память».
Почему «всё через классы» — это ошибка? Когда ООП действительно уместен?
Эталон: словарь — быстрый поиск по ключу (время), но больше памяти, чем список; «всё через классы» — ошибка, когда нет состояния и поведения; ООП уместен при сущностях с инвариантами.
Пауза 10–15 секунд. Если правильных ответов меньше половины — вернёмся к Слайду 18 (оси компромиссов).
Так нельзя: «всё через классы» — класс без состояния и поведения
# «всё через классы» — антипримерclass Stats:
def__init__(self, rows):
self.rows = rows
deffilter_big(self, limit):
return [r for r in self.rows if r["sum"] > limit]
deftotal(self, rows):
returnsum(r["sum"] for r in rows)
Разбор по методу «сначала нарушение»:
«Что здесь плохо?» — total не использует self — это не метод, а функция; класс нужен только из-за «модно»;
«Что лучше?» — модуль с функциями filter_big(rows, limit) и total(rows); класс уместен, когда есть инвариант (например, «сумма ≥ 0») и поведение вокруг него.
Модуль с функциями — 6 строк, класс — 9 строк без выигрыша.
Найдите в своём коде класс без состояния (если есть).
Пять ошибок недели 13, которые вы совершите на семинаре
«всё через классы, потому что модно» — классы без состояния и поведения, по сути набор функций;
злоупотребление однострочниками ради краткости — reduce(lambda ...) в три вложенных вызова, которые никто не читает;
преждевременная оптимизация — «оптимизируют» код, который работает мгновенно, не измеряя (timeit), и портят читаемость;
мутация входных данных в функции (побочный эффект) — тесты и вызовы зависят от порядка;
непонимание, что компромисс — осознанный выбор с обоснованием, а не случайность.
Какая из ошибок чаще всего ломает тесты? (№ 4 — мутация входных данных)
Проверь себя: 4 вопроса перед семинаром
Что такое чистая функция? Почему чистые функции легче тестировать?
В чём разница между filter(lambda ...) и циклом с if и append? Когда какой вариант уместен?
Какие компромиссы вы принимаете, когда выбираете словарь вместо списка? Приведите пример «время vs память».
Почему «всё через классы» — это ошибка? Когда ООП действительно уместен?
2 минуты письменно, затем короткий разбор; эталоны — в решения-13.md.
Проект модуля: план рефакторинга + рефакторинг одной функции в функциональном стиле
Этап недели 13 проекта: план рефакторинга 2–3 запахов (по карте недели 12) + рефакторинг одной функции в функциональном стиле + тесты.
На семинаре вы: решите одну задачу в трёх парадигмах, разберёте «пахнущие» решения, найдёте компромисс в своей утилите.
ДЗ недели: refactor-week13.py + сравнение-неделя13.md — сравнение «до/после» с честными выигрышами и потерями.
Какую функцию своей утилиты вы выберете для рефакторинга? (статистику или фильтрацию)
Сегодня вы научились…
различать три парадигмы и объяснять, чем чистая функция отличается от функции с побочными эффектами;
применять map/filter/reduce и генераторы списков без мутации;
выбирать парадигму под задачу и обосновывать выбор;
называть оси компромиссов и приводить пример «время vs память» (Counter vs вложенный цикл);
следовать порядку «корректность → читаемость → измерение → оптимизация».
Всё, что обещали в начале (Слайд 4), — сделали?
One-minute paper: главное + один вопрос
Напишите (1 минута, не подписывая):
самое главное про выбор парадигмы, что вы сегодня поняли;
один вопрос, который остался.
Соберите бумажки: преподаватель читает 2–3 анонимно и отвечает.
Это сигнал для семинара: что разобрать подробнее.
Что дальше: семинар недели 13, ДЗ refactor-week13.py, анонс недели 14
Семинар (семинар-13.md): одна задача в трёх парадигмах, разбор «пахнущих» решений, компромиссы на своём коде.
ДЗ: refactor-week13.py + сравнение-неделя13.md — рефакторинг одной функции в функциональном стиле, критерии 0–2.
Анонс недели 14: «Отладка как научный метод» — цикл «воспроизвести → гипотеза → эксперимент → фикс → проверка».
Готовы? Откройте семинар-13.md, задачи 2.1–2.5 — по своему уровню.