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

Агрегатные операции

Агрегатная операция превращает всю коллекцию в одно значение. Например, это количество элементов, их сумма, наибольший элемент или все элементы, объединённые в один результат.

Самые распространённые из них есть в 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.

Prompt
Для чисел 6, 42, 10 и 4 выведите количество с помощью len(), наибольший и наименьший элемент с помощью max() и min(), среднее как сумму, делённую на количество, и сумму с помощью sum(). Подпишите каждую строку, например «Количество:», «Максимум:» и так далее.
Вывод: полная программа на Python
Count, min, max, sum and average

Минимум и максимум по правилу

min() с key=lambda n: n % 3 сравнивает остатки. Остатки от 5, 42, 10 и 4 равны 2, 0, 1 и 1, поэтому побеждает 42. max() с key=len сравнивает длины строк и находит "three".

Prompt
Для чисел 5, 42, 10 и 4 выведите элемент с наименьшим остатком от деления на 3, используя min() с key=lambda n: n % 3. Затем для слов one, two, three, four выведите самое длинное, используя max() с key=len.
Вывод: полная программа на Python
Min and max by a rule

Сумма функции от каждого элемента

sum() складывает всё, что выдаёт выражение-генератор. Удвоенные 5, 42, 10 и 4 дают 122. Делённые пополам с помощью / — 30.5.

Prompt
Для чисел 5, 42, 10 и 4 выведите сумму всех элементов, умноженных на 2, затем сумму всех элементов, делённых пополам с помощью /, каждый раз используя sum() с выражением-генератором.
Вывод: полная программа на Python
Sum of a function of each element

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.

Prompt
С помощью functools.reduce без начального значения сложите числа 5, 2, 10 и 4, чтобы начальным значением был первый элемент. Выведите результат.
Вывод: полная программа на Python
Reduce

Fold

С начальным значением 0 reduce() начинает с 0 и прибавляет каждый элемент, умноженный на 2. Каждый элемент удваивается: 10 + 4 + 20 + 8 = 42.

Prompt
С помощью functools.reduce с начальным значением 0 прибавьте к аккумулятору каждое из чисел 5, 2, 10 и 4, умноженное на 2. Выведите результат.
Вывод: полная программа на Python
Fold

Fold в сравнении с Reduce

Оба вызова используют одну и ту же функцию: аккумулятор плюс элемент, умноженный на 2. Без начального значения результат — 37, потому что первый элемент 5 становится началом и не удваивается. С начальным значением 0 результат — 42.

Prompt
Определите функцию add_doubled(acc, element), которая возвращает acc + element * 2. Вызовите functools.reduce для чисел 5, 2, 10 и 4 один раз без начального значения и один раз с начальным значением 0 и выведите оба результата с подписями "Reduce:" и "Fold:".
Вывод: полная программа на Python
Fold versus Reduce

Fold справа

reversed(numbers) передаёт элементы в reduce() от последнего к первому. Для сложения порядок не важен, поэтому результат снова 42.

Prompt
С помощью functools.reduce с начальным значением 0 по reversed(numbers) прибавьте каждое из чисел 5, 2, 10 и 4, умноженное на 2, проходя от последнего элемента к первому. Выведите результат.
Вывод: полная программа на Python
Fold from the 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.

Prompt
Сложите только те элементы из 5, 2, 10, 4, которые стоят на чётном индексе. Используйте цикл for по enumerate(numbers) и if по индексу. Выведите сумму.
Вывод: полная программа на Python
Fold with index

Fold с индексом справа

То же правило, но цикл начинается с индекса 3 и идёт вниз до 0. Результат по-прежнему 15.

Prompt
Сложите только те элементы из 5, 2, 10, 4, которые стоят на чётном индексе, проходя от последнего индекса вниз до 0 с помощью reversed(list(enumerate(numbers))). Выведите сумму.
Вывод: полная программа на Python
Fold with index from the right

Безопасный Reduce

reduce() нужен первый элемент, с которого можно начать. Для пустого списка она вызывает TypeError, и программа останавливается.

В Python способ сказать «результата нет» — это None. Небольшая вспомогательная функция сначала проверяет список: если он пуст, она возвращает None вместо вызова reduce(). Затем вызывающий код проверяет результат на None, так же как с dict.get() для отсутствующего ключа.

Reduce или None

reduce_or_none возвращает 21 для чисел 5, 2, 10 и 4 и None для пустого списка.

Prompt
Напишите функцию reduce_or_none(items, f), которая возвращает None для пустого списка, а иначе — результат functools.reduce. Вызовите её с функцией сложения для чисел 5, 2, 10 и 4 и для пустого списка и выведите оба результата.
Вывод: полная программа на Python
Reduce or None

Накопительные Fold и Reduce

Накопительный fold или reduce не отбрасывает промежуточные шаги. Он сохраняет аккумулятор после каждого элемента, поэтому результат — это список всех промежуточных значений.

В Python это встроено в itertools.accumulate(). По умолчанию она складывает и выдаёт каждую промежуточную сумму. Для накопительного reduce результат начинается с первого элемента. С ключевым аргументом initial= это становится накопительным fold: результат начинается с начального значения и содержит на один элемент больше, чем входные данные.

Накопительный reduce

Числа от 0 до 5 складываются по одному. accumulate() сохраняет каждую сумму по ходу: [0, 1, 3, 6, 10, 15].

Prompt
С помощью itertools.accumulate сложите числа от 0 до 5 по одному и выведите список всех промежуточных сумм.
Вывод: полная программа на Python
Running reduce

Накопительный fold

То же самое с initial=10. Результат начинается с самого 10 и содержит на один элемент больше, чем входные данные: [10, 10, 11, 13, 16, 20, 25].

Prompt
Примените itertools.accumulate с initial=10 к числам от 0 до 5 и выведите список промежуточных сумм, который начинается с 10 и содержит на один элемент больше, чем входные данные.
Вывод: полная программа на Python
Running fold