Прочтите его бесплатно на 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 должен вести себя следующим образом:

  1. Он принимает 2 позиционных аргумента: первый известен как значение накопления, второй - значение обновления.
  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.

Это нормально, что вы сомневаетесь, использовать ли инициализатор, и если да, то что нужно передать в качестве инициализатора. Мыслительный процесс, которым вы можете руководствоваться, таков:

  1. Спросите себя: «Соответствуют ли структура данных ожидаемых результатов и элементов вашей итерации»? - ›Если нет, используйте инициализатор, иначе он может вам не понадобиться.
  2. Если требуется инициализатор, структура данных или тип данных обычно должны совпадать с ожидаемым результатом.

Конечно, этот мыслительный процесс не является надежным, поскольку нет двух одинаковых ситуаций.

Пример 3. Получение информации из списка участников мероприятия

В третьем примере вам будет предоставлен список участников (обратите внимание, что этот пример не имеет ничего общего с примером 2), и ваша задача - сообщить:

  1. Количество сопровождаемых гостей и общее количество гостей
  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 уменьшить:

  1. Вы не должны изменять никаких аргументов, кроме значения накопления.
  2. Вы не должны создавать никаких побочных эффектов в своей функции предиката.

Вот демонстрация плохого предиката с использованием списка участников, который вы видели в примере 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 unchanged
    acc["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.