Перейти к содержимому

Функциональное программирование в Python [2.4]

Функциональное программирование в Python [2.4]

Понятно и на примерах разбираем: что такое lambda, какие аргументы в неё попадают, кто её вызывает (мы сами, map, filter, sorted), как работают замыкания, а также как использовать map(), filter(), enumerate() и zip() без типичных ошибок.

Главная идея ФП: стремимся писать код так, чтобы результат зависел только от входных данных. Такой код легче тестировать, отлаживать и переиспользовать.

Содержание

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
Почему “eval” не вывелся? Потому что 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

Что такое чистая функция?

  • Функция без побочных эффектов и с одинаковым результатом для одинаковых аргументов
  • Функция без аргументов и всегда разный результат
  • Функция, которая может менять состояние программы
  • Функция, которая может вызывать другие функции
Разбор: “чистота” — про предсказуемость (нет зависимости от внешнего состояния) и отсутствие side effects.

Вопрос 3

Что такое анонимная функция в Python?

  • Функция без имени, определяемая через ключевое слово lambda
  • Функция без параметров и со случайным результатом
  • Функция, которую нельзя вызвать из других функций
  • Функция, которую нельзя использовать вместе с циклами
Важно: у 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: ...
↑ К оглавлению
Вторник, 10 февраля 2026
Функциональное программирование в Python [2.4]