Прочтите его бесплатно на https://melvinkoh.me.
Несмотря на то, что существует множество библиотек Python для манипулирования данными, они могут оказаться излишними для простого преобразования данных. Стандартная библиотека Python поставляется с функцией functools.reduce()
, одной из наиболее часто используемых функций в функциональном программировании, удобной для простого преобразования данных. Если вы слышали о reduce()
или функциональном программировании, но не знаете, что это такое на самом деле и как они могут помочь вам написать лучший код Python, эта статья для вас.
В этом руководстве вы:
- Понять основные принципы функционального программирования
- Понять, что такое функция Python
reduce()
- Узнайте, как функцию Python reduce можно использовать для обработки базовых данных и получения аналитической информации из данных.
- Познакомьтесь с
accumulate
, который предлагает функции, аналогичные функцииreduce()
- Напомним, что такое лямбда-функция и итерация.
После прочтения этого руководства вы сможете использовать Python reduce()
для извлечения полезной информации из необработанных данных и для выполнения преобразования данных. Вы также должны уметь определять различия и варианты использования reduce()
и accumulate
и использовать их соответственно.
Что такое функциональное программирование в Python?
Функциональное программирование - это парадигма программирования, которая фокусируется на написании декларативного кода и чистых функций без каких-либо побочных эффектов. Не углубляясь в философию и историю этого, при написании кода функционального стиля вы пишете функции, которые:
- Не изменяйте никакие входные аргументы или переменные в пределах его области видимости.
- Не делайте никаких сетевых запросов
- Не печатать ничего на консоль
- Не включайте случайность
- Всегда возвращать одно и то же значение, если входные аргументы остаются неизменными
Python - это многопарадигмальный язык, поддерживающий функциональное программирование. Он предлагает гораздо больше, чем reduce()
, с точки зрения функционального программирования. Чтобы узнать больше о функциональном программировании, вы можете прочитать статью о map () в Python и HOWTO-руководство в официальной документации.
Что такое Python Reduce?
Приступим к делу, что такое reduce()
в Python? Вы начнете с простого примера - умножения ряда целых чисел.
>>> # Using for loop, the imperative way >>> multipliers = [2, 10, 4, 16] >>> accumulation = 1
>>> for number in multipliers: ... accumulation *= number
>>> accumulation 1280
Из фрагмента кода вы просто использовали цикл for
для умножения всех чисел и сохранили результаты в переменной accumulation
.
Затем давайте посмотрим на альтернативное решение с reduce()
:
>>> # Using reduce(), a functional approach >>> from functools import reduce
>>> multipliers = [2, 10, 4, 16]
>>> accumulation = reduce( ... lambda acc, number: acc * number, ... multipliers ... )
>>> accumulation 1280
Из фрагмента кода вместо использования цикла for
вы импортировали reduce()
из functools
и использовали reduce()
для достижения цели умножения чисел и сохранили результаты в переменной accumulation
.
Это просто для того, чтобы дать вам краткое представление о том, как reduce()
можно использовать в качестве альтернативного решения для умножения целых чисел.
Фактически, начиная с Python 3.8, добавлена новая функция для выполнения умножения по итерируемым числам.
Цель приведенного выше фрагмента - показать вам альтернативу написанию императивного кода с for
циклами. Вы увидите, как reduce()
можно использовать в более сложных примерах в кратчайшие сроки.
Сигнатура функции Python Reduce
Давайте углубимся в сигнатуру функции reduce()
:
functools.reduce(function, iterable[, initializer])
reduce()
занимает:
function
: первый аргумент, определяющий применяемую функцию, также известный как предикат функции сокращения.- второй аргумент со значениями, передаваемыми в
function
initializer
: третий аргумент, с которого нужно начинать.
Обратите внимание, что аргументы в квадратных скобках [ ]
необязательны.
Итерируемый объект - это объект, способный возвращать по одному элементу за раз. Примеры итераций включают все типы последовательности, такие как
str
,list
,tuple
,set
, а также любые объекты, реализующие метод__iter__()
. Итерации можно использовать сfor
циклами и функциями, которые принимают последовательность. Чтобы узнать об итерации, вы можете прочитать эту статью о Real Python и этот глоссарий с полным определением.
Далее вы узнаете, как работает функция Python reduce()
. Это приблизительно:
>>> # Rough implementation of reduce(), taken from Python official documentation: >>> # https://docs.python.org/3/library/functools.html#functools.reduce
>>> def reduce(function, iterable, initializer=None): ... it = iter(iterable) ... if initializer is None: ... value = next(it) ... else: ... value = initializer ... for element in it: ... value = function(value, element) ... return value
reduce()
применяет предоставленный function
кумулятивно со всеми элементами в итерации, а затем возвращает окончательный результат. Другими словами, reduce()
формирует результат, изменяя его при просмотре каждого элемента итерации ввода.
Если указан initializer
, он будет использоваться как первый элемент перед переходом к любым элементам итерации. В следующих частях вы увидите примеры, когда инициализатор необходим.
Однако вы также должны знать, что предикат function
должен вести себя следующим образом:
- Он принимает 2 позиционных аргумента: первый известен как значение накопления, второй - значение обновления.
- Всегда возвращает накопленное значение
Это общий шаблон кода предиката:
В нашем предыдущем примере умножения списка целых чисел вы также заметили оператор
lambda
. Если вы не знакомы с этим ключевым словом, это, по сути, анонимная функция, которая неявно возвращает значение своего последнего выражения. Чтобы узнать больше о лямбда-функции, вы можете обратиться к этой статье.
Python Reduce в примерах
В этом разделе вы рассмотрите несколько вариантов использования reduce()
.
Пример 1: Расчет количества вхождений
В этом примере вы увидите, как можно использовать функцию сокращения Python для подсчета количества вхождений четных чисел в список.
Во-первых, давайте попробуем добиться с помощью for
цикла:
>>> values = [22, 4, 12, 43, 19, 71, 20] >>> # Using for loop, the imperative way >>> count = 0
>>> for number in values: ... if not number % 2: ... count += 1
>>> count 4
Теперь, когда вы подсчитали появление четного числа с помощью цикла for
, вы можете попробовать метод функционального программирования с reduce()
:
>>> from functools import reduce
>>> values = [22, 4, 12, 43, 19, 71, 20]
>>> count = reduce( ... lambda acc, num: acc if num % 2 else acc + 1, ... values ... )
>>> count 25
Однако если вы запустите приведенный выше код, вы получите 25
вместо 4
.
Теперь вы понимаете, как работает инициализатор, давайте исправим ошибочный код, добавив initializer
:
>>> # Using reduce() with initializer >>> from functools import reduce
>>> values = [22, 4, 12, 43, 19, 71, 20]
>>> count = reduce( ... lambda acc, num: acc if num % 2 else acc + 1, ... values, ... 0 # Initializer ... )
>>> count 4
Давайте рассмотрим предикат дальше:
Из лямбда-функции выше:
- Если число четное, увеличенное
acc
передается на следующую итерацию. - В противном случае вернуть текущее значение
acc
Не забывайте, что значение первого аргумента (acc
в этом примере) предиката всегда будет значением, возвращенным с предыдущей итерации!
Пример 2: Создание новой dict
структуры
В этом примере вам будет предложен сценарий, когда вам нужно извлечь ответы RSVP на приглашения на мероприятие. Вы получите список приглашенных, как показано ниже:
list_of_invitees = [
{"email": "[email protected]", "name": "Alex", "status": attending"},
{"email": "[email protected]", "name": "Brian", "status": "declined"},
{"email": "[email protected]", "name": "Carol", "status": pending"},
{"email": "[email protected]", "name": "Derek", "status": "attending"},
{"email": "[email protected]", "name": "Ellen", "status": "attending"}
]
Допустим, вы хотите визуализировать статус ответа на приглашения, создав словарь как таковой:
{
"[email protected]": "attending",
"[email protected]": "declined",
"[email protected]": "pending",
"[email protected]": "attending",
"[email protected]": "attending"
}
Приведенный выше фрагмент будет результатом преобразования ваших данных. Он был преобразован из списка словарей, каждый из которых содержит адрес электронной почты приглашенного, имя и статус ответа, в словарь статусов ответов, который доступен по электронной почте приглашенных.
Для этого вы можете воспользоваться функцией уменьшения Python. Сначала вы определяете свою предикатную функцию.
>>> def transform_data(acc, invitee):
... acc[invitee["email"]] = invitee["status"]
... return acc
Как видите, предикат не обязательно должен быть лямбда-функцией. Это может быть обычная функция, метод или любой вызываемый Python.
>>> results = reduce( ... transform_data, ... list_of_invitees, ... {} # Initializer ... )
>>> results {'[email protected]': 'attending', '[email protected]': 'declined', '[email protected]': 'pending', '[email protected]': 'attending', '[email protected]': 'attending'}
Как видно из вышеизложенного, вы использовали reduce()
для визуализации статуса RSVP приглашений с помощью словаря, ключи которого представляют собой электронные письма приглашенных, а значения - их соответствующие ответы.
Поздравляю! Вы только что создали новую структуру словаря с функцией reduce()
. Ключи вашего словаря - это электронные письма приглашенных, а значение каждого ключа - это соответствующий статус RSVP.
Это нормально, что вы сомневаетесь, использовать ли инициализатор, и если да, то что нужно передать в качестве инициализатора. Мыслительный процесс, которым вы можете руководствоваться, таков:
- Спросите себя: «Соответствуют ли структура данных ожидаемых результатов и элементов вашей итерации»? - ›Если нет, используйте инициализатор, иначе он может вам не понадобиться.
- Если требуется инициализатор, структура данных или тип данных обычно должны совпадать с ожидаемым результатом.
Конечно, этот мыслительный процесс не является надежным, поскольку нет двух одинаковых ситуаций.
Пример 3. Получение информации из списка участников мероприятия
В третьем примере вам будет предоставлен список участников (обратите внимание, что этот пример не имеет ничего общего с примером 2), и ваша задача - сообщить:
- Количество сопровождаемых гостей и общее количество гостей
- Сколько веганов и невеганов посетили
>>> # Your given list of attendees
>>> list_of_attendees = [
... {"name": "Zeke", "vegan": True, "brought_guests": True,
... "guests": [{"name": "Amanda", "vegan": False},
... {"name": "Wayne", "vegan": True}]},
... {"name": "Xavier", "vegan": True, "brought_guests": False},
... {"name": "Yohanna", "vegan": False,
... "brought_guests": True,
... "guests": [{"name": "Lily", "vegan": True},
... {"name": "Stefano", "vegan": True}]},
... {"name": "Kael", "vegan": False, "brought_guests": False},
... {"name": "Landon", "vegan": True, "brought_guests": False},
... ]
Задача 1. Подсчитайте количество посетителей, которые привели гостей
Ожидаемый результат должен быть словарём:
Не стесняйтесь открыть свой интерпретатор Python и пощекотать себе мозг, прежде чем переходить к решению, представленному ниже.
Пример решения задачи 1
>>> def derive_guest_count(acc, attendee): ... acc["total_guests"] += 1 ... ... if attendee["brought_guests"]: ... acc["guest_who_brought_guests"] += 1 ... acc["total_guests"] += len(attendee["guests"]) ... ... return acc
>>> results = reduce( ... derive_guest_count, ... list_of_attendees, ... { # Initializer ... "guest_who_brought_guests": 0, ... "total_guests": 0 ... } ... )
Давайте углубимся в предикат (derive_guest_count()
).
- В строке 1 вы увеличиваете счетчик до
total_guests
. - Впоследствии вы проверяете, привел ли участник гостей, используя инструкцию
if
. Если это так, вы увеличиваете счетчикguest_who_brought_guests
и добавляете общее количество пришедших гостей (len(attendee["guests"])
) к счетчикуtotal_guests
.
После определения предиката вы загружаете функцию reduce()
своим предикатом, списком участников (list_of_attendees
) и инициализатором словаря с ключами:
guest_who_brought_guests
: отслеживать количество сопровождающих гостейtotal_guests
: для подсчета общего количества пришедших гостей
Задача 2. Подсчитайте количество веганов и не веганов.
Во втором задании с тем же списком участников вас просят определить количество веганов и не веганов. Ожидаемый результат должен быть:
Опять же, попробуйте найти решение, прежде чем двигаться дальше.
Пример решения задачи 2
>>> def derive_vegan_info(acc, attendee): ... if attendee["vegan"]: ... acc["vegan"] += 1 ... else: ... acc["non_vegan"] += 1 ... ... if attendee.get("brought_guests"): ... for guest_brought in attendee["guests"]: ... # Check guests recursively ... acc = derive_vegan_info(acc, guest_brought) ... ... return acc
>>> results = reduce( ... derive_vegan_info, ... list_of_attendees, ... {"vegan": 0,"non_vegan": 0} ... )
>>> results {"vegan": 6, "non_vegan": 3}
Давайте посмотрим на предикат (derive_vegan_info
):
- Вы сначала увеличиваете счетчик
vegan
, если значениеattendee["vegan"]
равноTrue
. В противном случае увеличьте значениеnon_vegan
. - Затем вы проверяете, привел ли
attendee
с собой гостей. Если это так, вы просматриваете список дополнительных гостей, на которых присутствовали. Для каждого посещенного дополнительного гостя вы рекурсивно получаете веганскую информацию, вызываяderive_vegan_info
и передавая значение накопления (acc
) и информацию о дополнительном госте (guest_baught).
До сих пор, я надеюсь, вы понимаете, как работает функция сокращения Python. Напоминаем, что для того, чтобы к нему привыкнуть, требуется несколько тренировок.
Близкий родственник функции сокращения Python: accumulate()
В Python функция уменьшения иногда ассоциируется со своим близким родственником, объектом itertools.accumulate()
. С reduce()
все, что вы получаете, - это конечный результат после внутренних итераций. Что будет, если вам понадобятся промежуточные результаты каждой итерации? Вот где сияет accumulate()
.
Помните, что в самом начале этого урока вы видели умножение с использованием reduce()
. Давайте визуализируем промежуточные результаты с помощью accumulate()
:
>>> from itertools import accumulate >>> numbers = [2, 10, 4, 16]
>>> accumulation = accumulate( ... numbers, # Iterable ... lambda acc, number: acc * number # Predicate ... )
>>> list(accumulation) [2, 20, 80, 1280]
Читатели с проницательными глазами заметят, что сигнатура функции accumulate()
немного отличается. Первый аргумент является повторяемым, и теперь предикат является вторым позиционным аргументом.
Давайте посмотрим на сигнатуру функции itertools.accumulate()
:
itertools.accumulate(iterable[,func, *, initial=None])
Обратите внимание, что аргументы в квадратных скобках [ ]
необязательны, аргумент по умолчанию для второго позиционного аргумента func
равен operator.add
. Модуль operator
экспортирует набор функций, соответствующих внутренним операторам Python. Например, operator.add(x, y)
эквивалентно выражению x + y
.
При этом, если второй аргумент func
отсутствует, accumulate
по умолчанию суммирует все элементы в итерации.
Обратите внимание, что необязательный аргумент ключевого слова
initial
добавлен, начиная с Python 3.8. Если вы используете более раннюю версию, подпись функции будетitertools.accumulate(iterable[, func])
.
>>> import operator >>> from itertools import accumulate
>>> numbers = [2, 10, 4, 16]
>>> accumulation = accumulate( ... numbers, # Iterable ... operator.mul # This line has changed ... )
>>> list(accumulation) [2, 20, 80, 1280]
Еще одно различие между reduce()
и accumulate()
заключается в том, что последний возвращает итерируемый объект itertools.accumulate
. Чтобы перебрать результаты, передайте возвращаемый объект в цикл for
или в любые функции, которые принимают итерацию. С itertools.accumulate()
вы получаете все значения, полученные в процессе накопления, а с functools.reduce()
вы получаете только последнее.
Антипаттерны Python Reduce
Антишаблоны использования функции Python reduce во многом проистекают из принципов функционального программирования. При использовании Python уменьшить:
- Вы не должны изменять никаких аргументов, кроме значения накопления.
- Вы не должны создавать никаких побочных эффектов в своей функции предиката.
Вот демонстрация плохого предиката с использованием списка участников, который вы видели в примере 3:
def derive_guest_count (acc, attendee): """ A bad predicate """
# Anti-pattern 1: Mutating the input argument attendee["processed"] = True # Anti-pattern 2: Creating side-effect, printing to console print(f"Processing {attendee['name']}")
# The lines below remain unchangedacc["total_guests"] += 1
if attendee["brought_guests"]: acc["guest_who_brought_guests"] += 1 acc["total_guests"] += len(attendee["guests"]) return acc
Из приведенного выше кода предикат пытается изменить входной аргумент attendee
, изменив значение ключа processed
на True
.
Он также пытается вывести оператор на консоль с помощью функции print()
.
Обе эти ошибки являются распространенными ошибками даже для некоторых опытных питонистов.
Заключение
В этом руководстве:
- Вы узнали, как
reduce()
в Python работает и как его можно использовать для получения аналитических сведений и преобразования данных, а также о некоторых анти-шаблонах. - Вы познакомились с аналогичной функцией
accumulate()
для сбора промежуточных результатов и поняли, что она отличается отreduce()
- Вы познакомились с модулем
operator
, который содержит множество удобных функций, соответствующих операторам Python.
Первоначально опубликовано на https://melvinkoh.me.