Агрегатные операции
Агрегатная операция превращает всю коллекцию в одно значение. Например, это количество элементов, их сумма, наибольший элемент или все элементы, объединённые в один результат.
Самые распространённые из них есть в Python сразу, в виде встроенных функций. len() считает элементы, min() и max() находят наименьший и наибольший элемент, а sum() складывает их.
Будьте осторожны: min() и max() вызывают ValueError для пустого списка, и программа останавливается. Если список может быть пустым, сначала проверьте len(s) > 0.
Для объединения элементов по одному есть functools.reduce(). Она хранит промежуточное значение — аккумулятор — и по очереди вызывает вашу функцию с аккумулятором и каждым элементом. Одна функция покрывает то, что во многих языках разделено на reduce и fold: без третьего аргумента она начинает с первого элемента, а с ним — с этого начального значения.
Основные агрегаты
Самые распространённые агрегаты — количество, минимум, максимум, сумма и среднее.
len(numbers)даёт количество.min(numbers)иmax(numbers)дают наименьший и наибольший элемент. Они работают с числами и со строками.sum(numbers)складывает все элементы. Среднее — это сумма, делённая на количество; деление с помощью/всегда сохраняет дробную часть.
Иногда наименьший или наибольший элемент нужно выбрать по правилу, а не по самому значению. Для этого min() и max() принимают функцию key=: побеждает элемент с наименьшим или наибольшим ключом, и возвращается сам элемент.
Иногда каждый элемент нужно сначала преобразовать функцией, а затем сложить результаты. sum() принимает выражение-генератор, поэтому sum(f(n) for n in numbers) делает это в одну строку.
Количество, минимум, максимум, сумма и среднее
Для чисел 6, 42, 10 и 4 выведите количество с помощью len(), наибольший и наименьший элемент с помощью max() и min() и сумму с помощью sum(). Среднее равно 62 / 4 = 15.5.
Вывод: полная программа на Python
numbers = [6, 42, 10, 4]
total = sum(numbers)
print("Количество:", len(numbers))
print("Максимум:", max(numbers))
print("Минимум:", min(numbers))
print("Среднее:", total / len(numbers))
print("Сумма:", total)
Минимум и максимум по правилу
min() с key=lambda n: n % 3 сравнивает остатки. Остатки от 5, 42, 10 и 4 равны 2, 0, 1 и 1, поэтому побеждает 42. max() с key=len сравнивает длины строк и находит "three".
Вывод: полная программа на Python
numbers = [5, 42, 10, 4]
words = ["one", "two", "three", "four"]
# Элемент с наименьшим остатком от деления на 3.
print(min(numbers, key=lambda n: n % 3))
# Самое длинное слово, сравнение по длине.
print(max(words, key=len))
Сумма функции от каждого элемента
sum() складывает всё, что выдаёт выражение-генератор. Удвоенные 5, 42, 10 и 4 дают 122. Делённые пополам с помощью / — 30.5.
Вывод: полная программа на Python
numbers = [5, 42, 10, 4]
# Сначала преобразуем каждый элемент, затем складываем результаты.
print(sum(n * 2 for n in numbers))
print(sum(n / 2 for n in numbers))
Fold и Reduce
Fold и reduce шаг за шагом объединяют все элементы в одно значение. Они хранят промежуточное значение, которое называется аккумулятором. Для каждого элемента они вызывают функцию с аккумулятором и элементом, и функция возвращает новый аккумулятор.
Разница — в начальном значении, и в Python это просто третий аргумент functools.reduce():
reduce(f, items, initial)— это fold: он начинает с начального значения, которое вы передаёте. Функция выполняется один раз для каждого элемента.reduce(f, items)начинает с первого элемента. Функция выполняется начиная со второго элемента, поэтому первый элемент никогда через неё не проходит.
Это важно, когда функция изменяет элемент. Если она удваивает каждый элемент, reduce() без начального значения оставляет первый элемент как есть.
В Python нет reduce справа. Чтобы выполнить fold справа, запустите reduce() для reversed(items): элементы приходят от последнего к первому, а функция по-прежнему получает первым аккумулятор, а вторым — элемент.
Reduce
reduce() без начального значения берёт 5 как начало и прибавляет к нему 2, 10 и 4. Результат — 21.
Вывод: полная программа на Python
from functools import reduce
numbers = [5, 2, 10, 4]
# Без начального значения reduce начинает с первого элемента.
total = reduce(lambda acc, element: acc + element, numbers)
print(total)
Fold
С начальным значением 0 reduce() начинает с 0 и прибавляет каждый элемент, умноженный на 2. Каждый элемент удваивается: 10 + 4 + 20 + 8 = 42.
Вывод: полная программа на Python
from functools import reduce
numbers = [5, 2, 10, 4]
# Третий аргумент — начальное значение: каждый элемент удваивается.
sum_doubled = reduce(lambda acc, element: acc + element * 2, numbers, 0)
print(sum_doubled)
Fold в сравнении с Reduce
Оба вызова используют одну и ту же функцию: аккумулятор плюс элемент, умноженный на 2. Без начального значения результат — 37, потому что первый элемент 5 становится началом и не удваивается. С начальным значением 0 результат — 42.
Вывод: полная программа на Python
from functools import reduce
def add_doubled(acc, element):
return acc + element * 2
numbers = [5, 2, 10, 4]
# Здесь 5 — начальное значение, поэтому оно не удваивается: 5 + 4 + 20 + 8
print("Reduce:", reduce(add_doubled, numbers))
# Каждый элемент удваивается: 10 + 4 + 20 + 8
print("Fold:", reduce(add_doubled, numbers, 0))
Fold справа
reversed(numbers) передаёт элементы в reduce() от последнего к первому. Для сложения порядок не важен, поэтому результат снова 42.
Вывод: полная программа на Python
from functools import reduce
numbers = [5, 2, 10, 4]
# reversed() передаёт элементы от последнего к первому.
sum_doubled_right = reduce(lambda acc, element: acc + element * 2,
reversed(numbers), 0)
print(sum_doubled_right)
Fold с индексом
Иногда важна позиция элемента, а не только его значение.
В Python индекс даёт обычный цикл с enumerate(). for idx, element in enumerate(numbers) проходит с начала. for idx, element in reversed(list(enumerate(numbers))) проходит те же пары индекс-элемент с конца. Внутри цикла if проверяет индекс и решает, прибавлять ли элемент.
Fold с индексом
Складываются только элементы с чётными индексами: по индексу 0 находится 5, по индексу 2 — 10, поэтому сумма равна 15.
Вывод: полная программа на Python
numbers = [5, 2, 10, 4]
# enumerate() даёт ещё и индекс, поэтому условие может его использовать.
sum_even = 0
for idx, element in enumerate(numbers):
if idx % 2 == 0:
sum_even += element
print(sum_even)
Fold с индексом справа
То же правило, но цикл начинается с индекса 3 и идёт вниз до 0. Результат по-прежнему 15.
Вывод: полная программа на Python
numbers = [5, 2, 10, 4]
# Проходим пары индекс-элемент от последней вниз до индекса 0.
sum_even = 0
for idx, element in reversed(list(enumerate(numbers))):
if idx % 2 == 0:
sum_even += element
print(sum_even)
Безопасный Reduce
reduce() нужен первый элемент, с которого можно начать. Для пустого списка она вызывает TypeError, и программа останавливается.
В Python способ сказать «результата нет» — это None. Небольшая вспомогательная функция сначала проверяет список: если он пуст, она возвращает None вместо вызова reduce(). Затем вызывающий код проверяет результат на None, так же как с dict.get() для отсутствующего ключа.
Reduce или None
reduce_or_none возвращает 21 для чисел 5, 2, 10 и 4 и None для пустого списка.
Вывод: полная программа на Python
from functools import reduce
def reduce_or_none(items, f):
# reduce() вызывает TypeError для пустого списка; вместо этого возвращаем None.
if not items:
return None
return reduce(f, items)
def add(acc, element):
return acc + element
numbers = [5, 2, 10, 4]
empty = []
print(reduce_or_none(numbers, add))
print(reduce_or_none(empty, add))
Накопительные Fold и Reduce
Накопительный fold или reduce не отбрасывает промежуточные шаги. Он сохраняет аккумулятор после каждого элемента, поэтому результат — это список всех промежуточных значений.
В Python это встроено в itertools.accumulate(). По умолчанию она складывает и выдаёт каждую промежуточную сумму. Для накопительного reduce результат начинается с первого элемента. С ключевым аргументом initial= это становится накопительным fold: результат начинается с начального значения и содержит на один элемент больше, чем входные данные.
Накопительный reduce
Числа от 0 до 5 складываются по одному. accumulate() сохраняет каждую сумму по ходу: [0, 1, 3, 6, 10, 15].
Вывод: полная программа на Python
from itertools import accumulate
numbers = [0, 1, 2, 3, 4, 5]
# accumulate() сохраняет каждую промежуточную сумму, а не только последнюю.
print(list(accumulate(numbers)))
Накопительный fold
То же самое с initial=10. Результат начинается с самого 10 и содержит на один элемент больше, чем входные данные: [10, 10, 11, 13, 16, 20, 25].
Вывод: полная программа на Python
from itertools import accumulate
numbers = [0, 1, 2, 3, 4, 5]
# initial=10 ставит начальное значение перед промежуточными суммами.
print(list(accumulate(numbers, initial=10)))