Функциональное программирование в Python [2.4]
Понятно и на примерах разбираем: что такое lambda, какие аргументы в неё попадают, кто её вызывает (мы сами, map, filter, sorted), как работают замыкания, а также как использовать map(), filter(), enumerate() и zip() без типичных ошибок.
Главная идея ФП: стремимся писать код так, чтобы результат зависел только от входных данных. Такой код легче тестировать, отлаживать и переиспользовать.
Содержание
- 1. Введение: что такое ФП и зачем оно в Python
- 2. lambda: что это, какие аргументы передаются и как она работает
- 3. map() и filter(): преобразование и фильтрация
- 4. enumerate() и zip(): индексы и параллельные итерации
- 5. Мини‑тесты (как в учебнике) + разбор
- 6. Практика: задачи для закрепления
- 7. Чек‑лист самопроверки (обязательный)
- 8. Примечания и важные моменты (HTML‑структура и классы)
1. Введение: что такое функциональное программирование (ФП)
Идея “как в математике”
В математике функция — это однозначное отображение: для одинаковых аргументов результат всегда одинаков. В функциональном программировании к этому стремятся в коде: выход зависит только от входа.
Принципы ФП
- Иммутабельность (неизменяемость данных): вместо изменения объекта создаём новый (или ведём себя так, будто данные неизменяемы).
- Отсутствие побочных эффектов: функция не должна “тихо” печатать, писать в файл, менять глобальные переменные, менять входные данные.
- Функции как объекты: функцию можно присвоить переменной, передать как аргумент, вернуть как результат.
- Анонимные функции: маленькие функции “на месте” через
lambda. - Композиция: сложные операции строим из простых функций, соединяя их в цепочки.
- Ленивые вычисления: вычисляем только тогда, когда результат действительно нужен (часто через итераторы).
Преимущества ФП
ФП обычно даёт сразу несколько выгод: надёжность (меньше скрытых изменений), отладка проще (меньше состояния), иногда выше производительность (ленивость, меньше лишних промежуточных структур, использование встроенных функций).
Чистая функция: что это такое
Чистая функция — это функция, которая: (1) не имеет побочных эффектов и (2) возвращает одинаковый результат для одинаковых аргументов.
Пример чистой функции
def add(a, b):
return a + b
print(add(2, 3)) # 5
Пример “нечистой” функции
total = 0
def add_to_total(x):
global total
total += x # меняем внешнее состояние (side effect)
return total
Здесь результат зависит от истории вызовов, а не только от аргумента x.
2. lambda: что это, какие аргументы передаются и как она работает
Определение и синтаксис
lambda — это способ создать объект‑функцию “на месте”, обычно очень короткую. По смыслу это та же функция, что и через def, только записанная в одну строку.
lambda аргументы: выражениеСлева от
: — параметры, справа — одно выражение, значение которого и будет возвращено.# lambda-функция: принимает x и возвращает x * 2
doubler = lambda x: x * 2
print(doubler(10)) # 20
Эквивалент через def:
def doubler(x):
return x * 2
print(doubler(10)) # 20
Что передаётся в качестве аргумента: кто вызывает lambda
Вопрос “что попадает в аргумент x?” решается так: посмотрите, кто вызывает вашу lambda.
| Где используется lambda | Кто вызывает | Что передаётся в аргументы |
|---|---|---|
f = lambda x: ... и f(123) |
Вы | То, что вы передали в скобках (здесь 123) |
map(lambda x: ..., iterable) |
map |
Очередной элемент из iterable |
filter(lambda x: ..., iterable) |
filter |
Очередной элемент из iterable (ожидается True/False) |
sorted(iterable, key=lambda x: ...) |
sorted |
Очередной элемент, для которого вычисляется ключ сортировки |
Примеры: “вручную”, внутри map/filter/sorted
Пример 1 — вы вызываете lambda сами
adder = lambda a, b: a + b
print(adder(2, 3)) # a=2, b=3 => 5
Пример 2 — map() вызывает lambda сам и подставляет элементы
numbers = [1, 2, 3, 4, 5]
result = list(map(lambda x: x * 2, numbers))
print(result)
# [2, 4, 6, 8, 10]
Здесь x последовательно принимает значения 1, 2, 3, 4, 5.
Пример 3 — filter() вызывает lambda и ждёт True/False
numbers = [1, 2, 3, 4, 5]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)
# [2, 4]
Пример 4 — sorted(key=...) вызывает lambda, чтобы получить “ключ”
words = ["python", "java", "kotlin", "go"]
sorted_words = sorted(words, key=lambda s: len(s))
print(sorted_words)
# ['go', 'java', 'python', 'kotlin']
Замыкания: функция, которая “помнит” окружение
Замыкание — это когда функция использует переменные из внешней области видимости и “помнит” их значения. Пример “умножителя”:
def make_multiplier(n):
return lambda a: a * n
doubler = make_multiplier(2)
tripler = make_multiplier(3)
print(doubler(11)) # 22
print(tripler(11)) # 33
lambda a: a * n использует n из внешней функции. Поэтому после make_multiplier(2) получается функция, которая “закрепила” n = 2.Типичные ошибки lambda (и как исправлять)
Ошибка 1. Слишком сложная lambda вместо читаемого def
Если логика не умещается “в голову за 1–2 секунды”, лучше написать def. Lambda хороша как “маленький переходник”, а не как мини‑программа.
# Плохо: тяжело читать
score = lambda x: 10 if x > 90 else (5 if x > 70 else (2 if x > 50 else 0))
# Лучше: читаемо и легко расширять
def score(x):
if x > 90:
return 10
if x > 70:
return 5
if x > 50:
return 2
return 0
Ошибка 2. Late binding: lambda в цикле “все одинаковые”
Частая ловушка: вы создаёте несколько lambda в цикле и ожидаете, что каждая “запомнит” своё n. Но lambda захватывает переменную, а не её значение — и при вызове позже видит последнее значение.
funcs = []
for n in [1, 2, 3]:
funcs.append(lambda x: x * n)
print([f(10) for f in funcs])
# Неожиданно для новичка: [30, 30, 30]
Исправление: “заморозить” значение через аргумент по умолчанию.
funcs = []
for n in [1, 2, 3]:
funcs.append(lambda x, n=n: x * n)
print([f(10) for f in funcs])
# [10, 20, 30]
Ошибка 3. lambda с побочными эффектами
Если lambda только печатает (или пишет в файл) и ничего не возвращает, вы ломаете идею “чистых функций”, а в сочетании с map() получаете список None.
def show(x):
print(x) # side effect
# return нет => возвращается None
nums = [1, 2, 3]
res = list(map(show, nums))
print(res)
# 1
# 2
# 3
# [None, None, None]
map — про преобразование данных. Для действий (печать/запись) обычно честнее и понятнее обычный цикл.3. map() и filter(): преобразование и фильтрация
map(): “применить функцию к каждому элементу”
map(func, iterable) применяет func к каждому элементу iterable. В Python 3 map возвращает не список, а ленивый итератор (объект map).
numbers = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x**2, numbers))
print(squares)
# [1, 4, 9, 16, 25]
Можно передавать не lambda, а готовую функцию/метод:
text = ['python', 'java', 'kotlin', 'julia']
upper_text = list(map(str.upper, text))
print(upper_text)
# ['PYTHON', 'JAVA', 'KOTLIN', 'JULIA']
filter(): “оставить элементы по условию”
filter(predicate, iterable) оставляет только те элементы, для которых predicate(x) истинно. Как и map, filter возвращает итератор.
text = ['python', 'java', 'kotlin', 'julia']
short_words = list(filter(lambda s: len(s) <= 5, text))
print(short_words)
# ['java', 'julia']
Ленивые вычисления: почему map/filter возвращают итератор
Ленивость означает: элементы считаются “по требованию”. Это может экономить память и ускорять обработку больших данных. Но важно помнить: итератор можно “исчерпать” — второй раз он не выдаст элементы.
nums = [1, 2, 3, 4]
it = filter(lambda x: x % 2 == 0, nums)
print(list(it)) # первый проход
print(list(it)) # второй проход
# [2, 4]
# []
map/filter vs списковые включения
В Python списковые включения часто читаются проще. Однако map/filter хороши, когда: (1) хочется явно передать функцию как объект или (2) важна ленивость (вы не обязаны сразу создавать список).
| Задача | map/filter | Списковое включение |
|---|---|---|
| Квадраты | list(map(lambda x: x**2, nums)) |
[x**2 for x in nums] |
| Чётные | list(filter(lambda x: x%2==0, nums)) |
[x for x in nums if x%2==0] |
Типичные ошибки map/filter
Ошибка 1. Ожидали список, а получили объект map/filter
nums = [1, 2, 3]
res = map(lambda x: x * 2, nums)
print(res)
#
Решение: материализовать список: list(res).
Ошибка 2. filter с предикатом, который возвращает не bool (может быть сюрприз)
filter принимает “истинность” любого значения. Это иногда удобно, но может быть неожиданно.
items = ["", "hello", "", "world"]
res = list(filter(lambda s: s, items))
print(res)
# ['hello', 'world']
Пустая строка — “ложная”, непустая — “истинная”. Поэтому пустые строки отфильтровались.
Ошибка 3. map для “действия”, а не для преобразования
Если ваша функция ничего не возвращает (возвращает None), map построит итератор из None. Для действий используйте цикл.
4. enumerate() и zip(): индексы и параллельные итерации
enumerate(): индекс + значение
Когда нужен и индекс, и элемент, используйте enumerate вместо range(len(...)). Обычно это читается лучше и менее ошибочно.
array = ['a', 'b', 'c', 'd']
for i, val in enumerate(array):
print(i, val)
# 0 a
# 1 b
# 2 c
# 3 d
Можно начать индексацию с 1:
for i, val in enumerate(array, start=1):
print(i, val)
# 1 a
# 2 b
# 3 c
# 4 d
zip(): пары по индексам, обрезка по короткому
zip(a, b) “склеивает” элементы по одинаковым индексам. Количество пар равно длине самой короткой последовательности.
letters = ['a', 'b', 'c', 'd']
keywords = ['and', 'bool', 'class', 'def', 'eval']
for letter, word in zip(letters, keywords):
print(letter, word)
# a and
# b bool
# c class
# d def
letters короче (4 элемента), а zip останавливается на конце самого короткого.Типичные ошибки enumerate/zip
Ошибка 1. Ожидали, что zip обработает “хвост” длинного списка
Если хвост важен, используйте itertools.zip_longest (заполнит пропуски fillvalue).
from itertools import zip_longest
a = [1, 2, 3, 4]
b = ["one", "two"]
print(list(zip_longest(a, b, fillvalue="(нет)")))
# [(1, 'one'), (2, 'two'), (3, '(нет)'), (4, '(нет)')]
Ошибка 2. Изменять список при обходе enumerate()
Если удалять/вставлять элементы во время обхода, индексы и элементы могут “съехать”. Обычно безопаснее построить новый список (фильтрацией).
arr = [1, 2, 3, 4, 5]
arr2 = [x for x in arr if x % 2 != 0]
print(arr2)
# [1, 3, 5]
5. Мини‑тесты (как в учебнике) + разбор
Вопрос 1
Какие преимущества имеет функциональное программирование?
- Увеличение производительности программы
- Упрощение и ускорение процесса отладки
- Увеличение надёжности программы
- Все вышеперечисленные преимущества
Вопрос 2
Что такое чистая функция?
- Функция без побочных эффектов и с одинаковым результатом для одинаковых аргументов
- Функция без аргументов и всегда разный результат
- Функция, которая может менять состояние программы
- Функция, которая может вызывать другие функции
Вопрос 3
Что такое анонимная функция в Python?
- Функция без имени, определяемая через ключевое слово lambda
- Функция без параметров и со случайным результатом
- Функция, которую нельзя вызвать из других функций
- Функция, которую нельзя использовать вместе с циклами
Вопрос 4
Какой будет результат?
def my_func(n):
return lambda a: a * n
my_doubler = my_func(2)
print(my_doubler(11))
Ответ: 22
my_func(2) возвращает функцию, “помнящую” n=2, поэтому 11 * 2.Вопрос 5
Какой будет результат?
my_list = [1, 2, 3, 4, 5]
result = list(map(lambda x: x * 2, my_list))
print(result)
Ответ: [2, 4, 6, 8, 10]
Вопрос 6
Какой будет результат?
my_list = [1, 2, 3, 4, 5]
result = list(filter(lambda x: x % 2 == 0, my_list))
print(result)
Ответ: [2, 4]
Вопрос 7
Что возвращает zip(), если передать две последовательности разной длины?
- Все возможные пары
- Пары, пока не закончится самая короткая последовательность
- Только элементы первого объекта
- Только элементы второго объекта
zip “обрезает” результат по минимальной длине входов.6. Практика: задачи для закрепления
Задачи на map()
Задача 1. Дан список чисел. Получите список кубов.
nums = [1, 2, 3, 4]
cubes = list(map(lambda x: x**3, nums))
print(cubes)
# [1, 8, 27, 64]
Задача 2. Дан список строк. Превратите каждую строку в “очищенную” и разбитую на слова.
strip_and_split = lambda s: s.strip().split()
data = [" Hello, world! ", " Python is fun "]
res = list(map(strip_and_split, data))
print(res)
# [['Hello,', 'world!'], ['Python', 'is', 'fun']]
Задачи на filter()
Задача 3. Оставьте только строки длиной не более 5.
text = ["python", "java", "kotlin", "julia"]
res = list(filter(lambda s: len(s) <= 5, text))
print(res)
# ['java', 'julia']
Задача 4. Оставьте только слова, содержащие букву "a".
words = ["apple", "banana", "cherry"]
res = list(filter(lambda s: "a" in s, words))
print(res)
# ['apple', 'banana']
Задачи на zip() и enumerate()
Задача 5. Выведите элементы с индексами, начиная с 1.
arr = ["x", "y", "z"]
for i, v in enumerate(arr, start=1):
print(i, v)
# 1 x
# 2 y
# 3 z
Задача 6. Склейте два списка в пары и объясните, почему результат именно такой длины.
a = [1, 2, 3, 4]
b = ["one", "two"]
print(list(zip(a, b)))
# [(1, 'one'), (2, 'two')]
↑ К оглавлению7. Чек‑лист самопроверки знаний
Отметьте только то, что вы действительно понимаете и можете объяснить/написать без подсказок.
| ✓ | Я знаю / умею | Проверка (что я могу сделать) |
|---|---|---|
| Определить функциональное программирование своими словами | Объяснить “выход зависит только от входа” | |
| Отличить чистую функцию от нечистой | Привести пример side effects: print, запись в файл, изменение глобальной переменной |
|
Понимать синтаксис lambda |
Написать lambda x: x**2 и вызвать её |
|
| Понимать, кто передаёт аргументы в lambda | Объяснить, что в map(lambda x: ..., nums) значение x — это очередной элемент nums |
|
| Понимать замыкания | Написать make_multiplier(n), который возвращает функцию‑умножитель |
|
Использовать map() |
Преобразовать список строк в верхний регистр через map(str.upper, ...) |
|
Использовать filter() |
Отобрать чётные числа или строки по условию | |
Понимать ленивость (итераторы) в map/filter/zip |
Объяснить, почему “второй list(it) пустой” после первого прохода |
|
Использовать enumerate() |
Напечатать индекс и значение без range(len(...)) |
|
Использовать zip() и понимать обрезку |
Объяснить, почему zip([1,2,3],["a"]) даёт одну пару |
|
| Знать типичные ошибки lambda | Исправить late binding через lambda x, n=n: ... |