Функциональное программирование
Python — мультипарадигменный язык, который позаимствовал функциональные инструменты, но не функциональную семантику. Здесь есть функции первого класса, замыкания, lambda, map, filter, functools, itertools — и при этом изменяемые объекты, повсеместные побочные эффекты, жадное вычисление по умолчанию и полное отсутствие оптимизации хвостовых вызовов. Правильная формулировка на собеседовании звучит именно так — «Python поддерживает функциональный стиль частично», — а не «Python функциональный язык» и не «в Python нет функционального программирования».
Из этой половинчатости растут все ловушки темы, и почти все они специфичны для Python 3. map и filter перестали возвращать список и отдают ленивый одноразовый итератор, пустеющий после первого прохода. reduce намеренно вынесен из встроенных имён в functools, потому что свёртка почти всегда читается хуже явного цикла. functools.lru_cache требует хэшируемых аргументов и без maxsize растёт неограниченно. А хвостовая рекурсия в CPython не даёт ровно ничего — стек растёт так же, и на глубине около тысячи кадров прилетает RecursionError. Ниже каждый механизм разобран отдельно.
Карта темы
- Функциональный стиль в Python — чистота, неизменяемость и то, где Python осознанно отказался от функциональной модели.
- Функции высшего порядка — функции как объекты первого класса, приём и возврат функции,
key=вsorted. - map, filter и reduce — ленивые итераторы вместо списков,
reduceвfunctoolsи почему comprehension обычно лучше. - Модуль operator — именованные функции-операторы,
itemgetter,attrgetter,methodcallerвместо мелких лямбд. - Каррирование и partial — цепочка одноаргументных функций против частичного применения, которое реально используют в Python.
- Строительные блоки itertools — ленивые и бесконечные итераторы,
chain,islice,groupbyи комбинаторика. - Мемоизация — lru_cache — кэш по аргументам, требование хэшируемости, вытеснение и утечки памяти.
- Рекурсия — базовый и рекурсивный случай, кадры стека,
RecursionErrorи экспоненциальный наивный Фибоначчи. - Хвостовая рекурсия и отсутствие TCO — аккумулятор, почему CPython не схлопывает кадры и чем это лечится.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Называть Python чисто функциональным языком | Ответ сразу неверен — в Python есть изменяемое состояние, жадное вычисление и нет гарантий чистоты |
Ждать от map/filter список | В Python 3 это ленивые одноразовые итераторы — повторный list() вернёт пустоту, а len() вообще не сработает |
Звать reduce без импорта | NameError — из встроенных имён reduce убран намеренно, он живёт в functools |
Сводить функциональный стиль к lambda | Теряются чистота, неизменяемость и comprehension — а именно они и делают код функциональным |
Кэшировать через lru_cache функцию со списком или словарём в аргументах | TypeError — unhashable type на первом же вызове, потому что ключом кэша служит кортеж аргументов |
Ставить lru_cache на метод или на нечистую функцию | Кэш удерживает self и течёт, а нечистая функция начинает возвращать устаревшие данные |
Звать list() на count() или ждать от groupby глобальной группировки | Бесконечный итератор съедает всю память, а группы рвутся на каждой смене ключа — вход обязан быть отсортирован |
| Считать, что хвостовая рекурсия защищает от переполнения | CPython не делает TCO — кадры копятся, и на глубине около 1000 прилетает RecursionError |
Значение для собеседований
Тему почти никогда не спрашивают как теорию ради теории — это быстрый тест на то, читали ли вы про Python 3 глубже туториала. Классический заход — «что вернёт map»: правильный ответ «объект map, ленивый итератор» и следом «поэтому его можно пройти один раз». Дальше просят переписать пример через comprehension и смотрят, скажете ли вы вслух, что в Python comprehension обычно предпочтительнее пары map+filter с лямбдами. Отдельный маркер уровня — знать, что reduce вынесли из builtins сознательно, а не по недосмотру.
Вторая половина беседы уходит в кэширование и рекурсию. По lru_cache спрашивают три вещи — что служит ключом, почему аргументы обязаны быть хэшируемыми и что делает maxsize; ответ «кэширует по порядку вызовов» проваливает вопрос мгновенно. По рекурсии просят назвать два случая и — на senior-грейде — ответить, оптимизирует ли CPython хвостовые вызовы. Правильный ответ «нет, и это осознанное решение ради полных трейсбеков», типичная ошибка — сказать «да» или решить, что sys.setrecursionlimit включает оптимизацию. Практическое продолжение всегда одно — переписать рекурсию циклом.