Функциональное программирование на Python: Когда и как его использовать

Оглавление

Функциональное программирование - парадигма программирования, в которой основным методом вычислений является оценка функций. В этом уроке вы изучите функциональное программирование в Python.

Функциональное программирование обычно играет довольно небольшую роль в коде Python. Но быть знакомым с ним полезно. Как минимум, вы наверняка будете сталкиваться с ним время от времени при чтении кода, написанного другими людьми. Возможно, вы даже найдете ситуации, когда будет выгодно использовать возможности функционального программирования Python в своем собственном коде.

В этом уроке вы узнаете:

  • Что влечет за собой парадигма функционального программирования
  • Что значит сказать, что функции являются гражданами первого класса в Python
  • Как определять анонимные функции с помощью lambda ключевого слова
  • Как реализовать функциональный код с помощью map(), filter() и reduce()

Что такое функциональное программирование?

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

Функциональная парадигма популярна, поскольку обладает рядом преимуществ по сравнению с другими парадигмами программирования. Функциональный код - это:

  • Высокий уровень: Вы описываете результат, который хотите получить, а не указываете шаги, необходимые для его достижения. Одиночные утверждения, как правило, лаконичны, но обладают большой силой.
  • Прозрачность: Поведение чистой функции зависит только от ее входов и выходов, без промежуточных значений. Это исключает возможность возникновения побочных эффектов, что облегчает отладку.
  • Распараллеливаемость: Маршруты, не вызывающие побочных эффектов, легче запускать параллельно друг с другом.

Многие языки программирования поддерживают функциональное программирование в той или иной степени. В некоторых языках практически весь код следует функциональной парадигме. Одним из таких примеров является Haskell. Python, напротив, поддерживает функциональное программирование, но содержит черты и других моделей программирования.

Хотя глубокое описание функционального программирования является несколько сложным, цель данной статьи - не дать строгое определение, а показать, что можно сделать с помощью функционального программирования на Python.

Насколько хорошо Python поддерживает функциональное программирование?

Для поддержки функционального программирования полезно, чтобы функция в данном языке программирования обладала двумя способностями:

  1. Принимать другую функцию в качестве аргумента
  2. Возвращать другую функцию вызывающему ее пользователю

Python отлично справляется с обеими этими задачами. Как вы уже узнали из этой серии, все в программе на Python является объектом. Все объекты в Python имеют более или менее равный статус, и функции не являются исключением.

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

Например, вы можете присвоить функцию переменной. Затем вы можете использовать эту переменную так же, как и саму функцию:

 1 >>> def func():
 2 ...     print("I am function func()!")
 3 ...
 4
 5 >>> func()
 6 I am function func()!
 7
 8 >>> another_name = func
 9 >>> another_name()
10 I am function func()!

Присвоение another_name = func в строке 8 создает новую ссылку на func() с именем another_name. Затем вы можете вызвать функцию по любому имени, func или another_name, как показано в строках 5 и 9.

Вы можете вывести функцию на консоль с помощью print(), включить ее как элемент в составной объект данных, например list, или даже использовать ее как dictionary ключ:

>>> def func():
...     print("I am function func()!")
...

>>> print("cat", func, 42)
cat <function func at 0x7f81b4d29bf8> 42

>>> objects = ["cat", func, 42]
>>> objects[1]
<function func at 0x7f81b4d29bf8>
>>> objects[1]()
I am function func()!

>>> d = {"cat": 1, func: 2, 42: 3}
>>> d[func]
2

В этом примере func() появляется во всех тех же контекстах, что и значения "cat" и 42, и интерпретатор прекрасно с ним справляется.

Примечание: То, что можно или нельзя делать с объектом в Python, в некоторой степени зависит от контекста. Например, есть некоторые операции, которые работают с определенными типами объектов, но не с другими.

Вы можете сложить два целочисленных объекта или конкатенировать два строковых объекта с помощью оператора плюс (+). Но оператор plus не определен для объектов функций.

Для нынешних целей важно, чтобы функции в Python удовлетворяли двум критериям, полезным для функционального программирования, перечисленным выше. Вы можете передать функцию другой функции в качестве аргумента:

 1 >>> def inner():
 2 ...     print("I am function inner()!")
 3 ...
 4
 5 >>> def outer(function):
 6 ...     function()
 7 ...
 8
 9 >>> outer(inner)
10 I am function inner()!

Вот что происходит в приведенном выше примере:

  • Вызов в строке 9 передает inner() в качестве аргумента в outer().
  • Внутри outer(), Python связывает inner() с параметром функции function.
  • outer() может затем вызывать inner() непосредственно через function.

Это известно как композиция функций.

Техническое примечание: В Python предусмотрена нотация быстрого доступа, называемая декоратором, для облегчения обертывания одной функции внутри другой. Для получения дополнительной информации ознакомьтесь с Примером по декораторам Python.

Когда вы передаете функцию другой функции, переданную функцию иногда называют callback, потому что callback к inner function может изменить поведение внешней функции.

Хорошим примером этого является функция Python sorted(). Обычно, если вы передаете в sorted() список строковых значений, то она сортирует их в лексическом порядке:

>>> animals = ["ferret", "vole", "dog", "gecko"]
>>> sorted(animals)
['dog', 'ferret', 'gecko', 'vole']

Однако sorted() принимает необязательный аргумент key, задающий функцию обратного вызова, которая может служить ключом сортировки. Так, например, вы можете сортировать по длине строки:

>>> animals = ["ferret", "vole", "dog", "gecko"]
>>> sorted(animals, key=len)
['dog', 'vole', 'gecko', 'ferret']

sorted() может также принимать необязательный аргумент, задающий сортировку в обратном порядке. Но вы можете сделать то же самое, определив собственную функцию обратного вызова, которая изменит смысл len():

>>> animals = ["ferret", "vole", "dog", "gecko"]
>>> sorted(animals, key=len, reverse=True)
['ferret', 'gecko', 'vole', 'dog']

>>> def reverse_len(s):
...     return -len(s)
...
>>> sorted(animals, key=reverse_len)
['ferret', 'gecko', 'vole', 'dog']

Для получения дополнительной информации о сортировке данных в Python вы можете прочесть Как использовать sorted() и .sort() в Python.

Точно так же, как вы можете передать функцию другой функции в качестве аргумента, функция может указать другую функцию в качестве возвращаемого значения:

 1 >>> def outer():
 2 ...     def inner():
 3 ...             print("I am function inner()!")
 4 ...
 5 ...     # Function outer() returns function inner()
 6 ...     return inner
 7 ...
 8
 9 >>> function = outer()
10 >>> function
11 <function outer.<locals>.inner at 0x7f18bc85faf0>
12 >>> function()
13 I am function inner()!
14
15 >>> outer()()
16 I am function inner()!

Вот что происходит в этом примере:

  • Строки 2-3: outer() определяет локальную функцию inner().
  • Строка 6: outer() передает inner() обратно в качестве возвращаемого значения.
  • Строка 9: Возвращаемое значение из outer() присваивается переменной function.

После этого вы можете вызвать inner() косвенно через function, как показано в строке 12. Вы также можете вызвать его косвенно, используя возвращаемое значение из outer() без промежуточного присваивания, как в строке 15.

Как видите, в Python есть все необходимое для поддержки функционального программирования. Однако прежде чем перейти к функциональному коду, вам будет полезно изучить еще одно понятие: выражение lambda..

Определение анонимной функции с помощью lambda

Функциональное программирование - это вызов функций и их передача, поэтому оно, естественно, предполагает определение большого количества функций. Вы всегда можете определить функцию обычным способом, используя ключевое слово def, как вы уже видели в предыдущих уроках этой серии.

Иногда, однако, удобно иметь возможность определить анонимную функцию на лету, без необходимости давать ей имя. В Python это можно сделать с помощью выражения lambda.

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

Синтаксис выражения lambda выглядит следующим образом:

lambda <parameter_list>: <expression>

В следующей таблице представлены части выражения lambda:

Component Meaning
lambda The keyword that introduces a lambda expression
<parameter_list> An optional comma-separated list of parameter names
: Punctuation that separates <parameter_list> from <expression>
<expression> An expression usually involving the names in <parameter_list>

Значение выражения lambda - это вызываемая функция, как и функция, определенная с помощью ключевого слова def. Она принимает аргументы, как указано <parameter_list>, и возвращает значение, как указано <expression>.

Вот первый быстрый пример:

 1 >>> lambda s: s[::-1]
 2 <function <lambda> at 0x7fef8b452e18>
 3
 4 >>> callable(lambda s: s[::-1])
 5 True

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

Встроенная функция Python callable() возвращает True, если переданный ей аргумент оказывается вызываемым, и False в противном случае. Строки 4 и 5 показывают, что значение, возвращаемое выражением lambda, на самом деле является вызываемым, как и положено функции.

В этом случае список параметров состоит из единственного параметра s. Последующее выражение s[::-1] представляет собой синтаксис нарезки, который возвращает символы в s в обратном порядке . Таким образом, это выражение lambda определяет временную безымянную функцию, которая принимает строковый аргумент и возвращает строку аргумента с обращенными символами.

Объект, созданный выражением lambda, является гражданином первого класса, как и стандартная функция или любой другой объект в Python. Вы можете присвоить его переменной, а затем вызвать функцию, используя это имя:

>>> reverse = lambda s: s[::-1]
>>> reverse("I am a string")
'gnirts a ma I'

Это функционально - без каламбура - эквивалентно определению reverse() с помощью ключевого слова def:

 1 >>> def reverse(s):
 2 ...     return s[::-1]
 3 ...
 4 >>> reverse("I am a string")
 5 'gnirts a ma I'
 6
 7 >>> reverse = lambda s: s[::-1]
 8 >>> reverse("I am a string")
 9 'gnirts a ma I'

Вызовы в строках 4 и 8 выше ведут себя одинаково.

Однако не обязательно присваивать переменную выражению lambda перед его вызовом. Вы также можете вызвать функцию, определенную выражением lambda, напрямую:

>>> (lambda s: s[::-1])("I am a string")
'gnirts a ma I'

Вот еще один пример:

>>> (lambda x1, x2, x3: (x1 + x2 + x3) / 3)(9, 6, 6)
7.0
>>> (lambda x1, x2, x3: (x1 + x2 + x3) / 3)(1.4, 1.1, 0.5)
1.0

В этом случае параметрами являются x1, x2 и x3, а выражением - x1 + x2 + x3 / 3. Это анонимная функция lambda для вычисления среднего значения трех чисел.

В качестве другого примера вспомните, как выше вы определили reverse_len() для функции обратного вызова sorted():

>>> animals = ["ferret", "vole", "dog", "gecko"]

>>> def reverse_len(s):
...     return -len(s)
...
>>> sorted(animals, key=reverse_len)
['ferret', 'gecko', 'vole', 'dog']

Вы можете использовать функцию lambda и здесь:

>>> animals = ["ferret", "vole", "dog", "gecko"]
>>> sorted(animals, key=lambda s: -len(s))
['ferret', 'gecko', 'vole', 'dog']

В выражении lambda обычно есть список параметров, но он не обязателен. Вы можете определить функцию lambda без параметров. Тогда возвращаемое значение не зависит ни от каких входных параметров:

>>> forty_two_producer = lambda: 42
>>> forty_two_producer()
42

Обратите внимание, что с помощью lambda можно задавать только довольно простые функции. Возвращаемое значение выражения lambda может быть только одним единственным выражением. Выражение lambda не может содержать операторы присваивания или return, а также управляющие структуры, такие как for, while, if, else или def.

В предыдущем уроке по определению функции Python вы узнали, что функция, определенная с помощью def, может эффективно возвращать несколько значений. Если оператор return в функции содержит несколько значений, разделенных запятыми, то Python упаковывает их и возвращает в виде кортежа:

>>> def func(x):
...     return x, x ** 2, x ** 3
...
>>> func(3)
(3, 9, 27)

Эта неявная упаковка кортежей не работает с анонимной lambda функцией:

>>> (lambda x: x, x ** 2, x ** 3)(3)
<stdin>:1: SyntaxWarning: 'tuple' object is not callable; perhaps you missed a comma?
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined

Но вы можете вернуть кортеж из функции lambda. Для этого нужно просто явно обозначить кортеж круглыми скобками. Из функции lambda можно также вернуть список или словарь:

>>> (lambda x: (x, x ** 2, x ** 3))(3)
(3, 9, 27)
>>> (lambda x: [x, x ** 2, x ** 3])(3)
[3, 9, 27]
>>> (lambda x: {1: x, 2: x ** 2, 3: x ** 3})(3)
{1: 3, 2: 9, 3: 27}

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

Есть еще одна странность, о которой следует знать. Если вам понадобится включить выражение lambda в отформатированный строковый литерал (f-строку), то вам придется заключить его в явные круглые скобки:

>>> print(f"--- {lambda s: s[::-1]} ---")
  File "<stdin>", line 1
    (lambda s)
             ^
SyntaxError: f-string: invalid syntax

>>> print(f"--- {(lambda s: s[::-1])} ---")
--- <function <lambda> at 0x7f97b775fa60> ---
>>> print(f"--- {(lambda s: s[::-1])('I am a string')} ---")
--- gnirts a ma I ---

Теперь вы знаете, как определить анонимную функцию с помощью lambda. Для дальнейшего чтения о lambda функциях, ознакомьтесь с Как использовать лямбда-функции Python.

Далее пришло время углубиться в функциональное программирование на Python. Вы увидите, как функции lambda особенно удобны при написании функционального кода.

Python предлагает две встроенные функции, map() и filter(), которые соответствуют парадигме функционального программирования. Третья, reduce(), больше не является частью основного языка, но по-прежнему доступна в модуле functools. Каждая из этих трех функций принимает в качестве одного из аргументов другую функцию.

Применение функции к итерабельной таблице с помощью map()

Первой функцией в списке является map(), которая является встроенной функцией Python. С помощью map() вы можете применять функцию к каждому элементу iterable по очереди, а map() вернет iterator, в котором будут содержаться результаты. Это может позволить создать очень лаконичный код, поскольку оператор map() часто может занять место явного цикла.

Вызов map() с одной итерабельной переменной

Синтаксис вызова map() на одном итерабеле выглядит так:

map(<f>, <iterable>)

map(<f>, <iterable>) возвращает итератор, который дает результат применения функции <f> к каждому элементу <iterable>.

Вот пример. Предположим, вы определили reverse(), функцию, которая принимает строковый аргумент и возвращает его обратное значение, используя ваш старый друг механизм нарезки строк [::-1]:

>>> def reverse(s):
...     return s[::-1]
...
>>> reverse("I am a string")
'gnirts a ma I'

Если у вас есть список строк, то вы можете использовать map() для применения reverse() к каждому элементу списка:

>>> animals = ["cat", "dog", "hedgehog", "gecko"]
>>> iterator = map(reverse, animals)
>>> iterator
<map object at 0x7fd3558cbef0>

Но помните, что map() не возвращает список. Он возвращает итератор, называемый объектом карты. Чтобы получить значения из итератора, нужно либо итерировать по нему, либо использовать list():

>>> iterator = map(reverse, animals)
>>> for i in iterator:
...     print(i)
...
tac
god
gohegdeh
okceg

>>> iterator = map(reverse, animals)
>>> list(iterator)
['tac', 'god', 'gohegdeh', 'okceg']

Итерация над iterator дает элементы исходного списка animals, причем каждая строка инвертируется на reverse().

В этом примере reverse() - довольно короткая функция, которая может и не понадобиться за пределами этого использования с map(). Вместо того чтобы загромождать код бросовой функцией, вы можете использовать анонимную функцию lambda:

>>> animals = ["cat", "dog", "hedgehog", "gecko"]
>>> iterator = map(lambda s: s[::-1], animals)
>>> list(iterator)
['tac', 'god', 'gohegdeh', 'okceg']

>>> # Combining it all into one line:
>>> list(map(lambda s: s[::-1], ["cat", "dog", "hedgehog", "gecko"]))
['tac', 'god', 'gohegdeh', 'okceg']

Если итерируемая таблица содержит элементы, которые не подходят для указанной функции, то Python вызывает исключение:

>>> list(map(lambda s: s[::-1], ["cat", "dog", 3.14159, "gecko"]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <lambda>
TypeError: 'float' object is not subscriptable

В данном случае функция lambda ожидает строковый аргумент, который она пытается разрезать. Третий элемент списка, 3.14159, является объектом float, который не поддается нарезке. Поэтому возникает TypeError.

Вот несколько более реальный пример: В разделе учебника, посвященном встроенным строковым методам, вы встретили str.join(), который конкатенирует строки из итерируемого файла, разделенные указанной строкой:

>>> "+".join(["cat", "dog", "hedgehog", "gecko"])
'cat+dog+hedgehog+gecko'

Это отлично работает, если объекты в списке являются строками. Если это не так, то str.join() вызывает TypeError исключение:

>>> "+".join([1, 2, 3, 4, 5])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sequence item 0: expected str instance, int found

Один из способов исправить это - использовать цикл. Используя цикл for, вы можете создать новый список, содержащий строковые представления чисел в исходном списке. Затем вы можете передать новый список в циклы .join():

>>> strings = []
>>> for i in [1, 2, 3, 4, 5]:
...     strings.append(str(i))
...
>>> strings
['1', '2', '3', '4', '5']
>>> "+".join(strings)
'1+2+3+4+5'

Однако, поскольку map() применяет функцию к каждому объекту списка по очереди, она часто может устранить необходимость в явном цикле. В этом случае вы можете использовать map() для применения str() к объектам списка перед их объединением:

>>> "+".join(map(str, [1, 2, 3, 4, 5]))
'1+2+3+4+5'

map(str, [1, 2, 3, 4, 5]) возвращает итератор, который выдает список строковых объектов ["1", "2", "3", "4", "5"], и вы можете успешно передать этот список в .join().

Хотя в приведенном выше примере map() достигается желаемый эффект, было бы более питонично использовать понимание списка для замены явного цикла в подобном случае.

Вызов map() с несколькими итерациями

Существует другая форма map(), которая принимает более одного аргумента итерируемого:

map(<f>, <iterable₁>, <iterable₂>, ..., <iterableₙ>)

map(<f>, <iterable1>, <iterable2>, ..., <iterablen>) applies <f> to the elements in each <iterablei> in parallel and returns an iterator that yields the results.

Количество аргументов <iterablei>, указанных в map(), должно совпадать с количеством аргументов, которые ожидает <f>. <f> acts on the first item of each <iterablei>, and that result becomes the first item that the return iterator yields. Then <f> acts on the second item in each <iterablei>, and that becomes the second yielded item, and so on.

Пример должен помочь прояснить ситуацию:

>>> def f(a, b, c):
...     return a + b + c
...

>>> list(map(f, [1, 2, 3], [10, 20, 30], [100, 200, 300]))
[111, 222, 333]

В данном случае f() принимает три аргумента. Соответственно, у map() есть три итерируемых аргумента: списки [1, 2, 3], [10, 20, 30] и [100, 200, 300].

Первый возвращаемый элемент - это результат применения f() к первому элементу каждого списка: f(1, 10, 100). Второй возвращаемый элемент - f(2, 20, 200), а третий - f(3, 30, 300), как показано на следующей диаграмме:

Diagram of map() call with multiple iterables

Возвращаемое значение из map() - итератор, который выдает список [111, 222, 333].

Опять же, в этом случае, поскольку f() настолько короткий, вы можете легко заменить его функцией lambda:

>>> list(
...     map(
...         (lambda a, b, c: a + b + c),
...         [1, 2, 3],
...         [10, 20, 30],
...         [100, 200, 300]
...     )
... )

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

Выбор элементов из итерабельной таблицы с помощью filter()

filter() позволяет выбирать или фильтровать элементы из итерабельной таблицы на основе оценки заданной функции. Она вызывается следующим образом:

filter(<f>, <iterable>)

filter(<f>, <iterable>) применяет функцию <f> к каждому элементу <iterable> и возвращает итератор, содержащий все элементы, для которых <f> является правдивым. И наоборот, он отфильтровывает все элементы, для которых <f> является ложным.

В следующем примере greater_than_100(x) является истинным, если x > 100:

>>> def greater_than_100(x):
...     return x > 100
...

>>> list(filter(greater_than_100, [1, 111, 2, 222, 3, 333]))
[111, 222, 333]

В этом случае greater_than_100() истинно для элементов 111, 222 и 333, поэтому эти элементы остаются, а 1, 2 и 3 отбрасываются. Как и в предыдущих примерах, greater_than_100() - это короткая функция, и вы можете заменить ее выражением lambda:

>>> list(filter(lambda x: x > 100, [1, 111, 2, 222, 3, 333]))
[111, 222, 333]

В следующем примере используется range(). range(n) создает итератор, который выдает целые числа от 0 до n - 1. В следующем примере используется filter(), чтобы выбрать из списка только четные числа и отфильтровать нечетные:

>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> def is_even(x):
...     return x % 2 == 0
...
>>> list(filter(is_even, range(10)))
[0, 2, 4, 6, 8]

>>> list(filter(lambda x: x % 2 == 0, range(10)))
[0, 2, 4, 6, 8]

Вот пример, использующий встроенный строковый метод:

>>> animals = ["cat", "Cat", "CAT", "dog", "Dog", "DOG", "emu", "Emu", "EMU"]

>>> def all_caps(s):
...     return s.isupper()
...
>>> list(filter(all_caps, animals))
['CAT', 'DOG', 'EMU']

>>> list(filter(lambda s: s.isupper(), animals))
['CAT', 'DOG', 'EMU']

Помните из предыдущего урока по строковым методам, что s.isupper() возвращает True, если все алфавитные символы в s заглавные, и False в противном случае.

Преобразование итерабельной таблицы к одному значению с помощью reduce()

reduce() применяет функцию к элементам итерируемой переменной по два за раз, постепенно объединяя их для получения единого результата.

reduce() когда-то была встроенной функцией в Python. Гвидо ван Россум, по-видимому, очень не любил reduce() и выступал за ее полное удаление из языка. Вот что он сказал по этому поводу:

Теперь reduce(). Этот оператор я всегда ненавидел больше всего, потому что, за исключением нескольких примеров с + или *, почти каждый раз, когда я вижу вызов reduce() с нетривиальным аргументом функции, мне приходится брать ручку и бумагу, чтобы нарисовать схему того, что на самом деле подается в эту функцию, прежде чем я пойму, что должен делать reduce(). Поэтому, на мой взгляд, применимость reduce() практически ограничена ассоциативными операторами, а во всех остальных случаях лучше явно написать цикл накопления. (Source)

Гвидо на самом деле выступал за то, чтобы исключить из Python все три варианта reduce(), map() и filter(). Можно только догадываться о его обосновании. Как оказалось, упомянутое ранее понимание списка охватывает функциональность, предоставляемую всеми этими функциями, и многое другое. Вы можете узнать больше, прочитав статью Когда использовать списковое понимание в Python.

Как вы видели, map() и filter() остаются встроенными функциями в Python. reduce() больше не является встроенной функцией, но она доступна для импорта из модуля стандартной библиотеки, как вы увидите далее.

Чтобы использовать reduce(), вам нужно импортировать его из модуля под названием functools. Это возможно несколькими способами, но наиболее простым является следующий:

from functools import reduce

После этого интерпретатор помещает reduce() в глобальное пространство имен и делает его доступным для использования. В примерах, которые вы увидите ниже, предполагается, что так оно и есть.

Вызов reduce() с двумя аргументами

Наиболее простой вызов reduce() принимает одну функцию и одну итерируемую переменную, как показано ниже:

reduce(<f>, <iterable>)

reduce(<f>, <iterable>) использует <f>, которая должна быть функцией, принимающей ровно два аргумента, для постепенного объединения элементов в <iterable>. Для начала reduce() вызывает <f> на первых двух элементах <iterable>. Затем результат комбинируется с третьим элементом, затем с четвертым, и так далее, пока список не будет исчерпан. Затем reduce() возвращает окончательный результат.

Гвидо был прав, когда сказал, что наиболее простыми приложениями reduce() являются те, в которых используются ассоциативные операторы. Начнем с оператора плюс (+):

>>> def f(x, y):
...     return x + y
...

>>> from functools import reduce
>>> reduce(f, [1, 2, 3, 4, 5])
15

Этот вызов reduce() производит результат 15 из списка [1, 2, 3, 4, 5] следующим образом:

Reduce function illustration reduce(f, [1, 2, 3, 4, 5])

Это довольно крутой способ суммирования чисел в списке! Хотя он отлично работает, есть и более прямой способ. Встроенная в Python функция sum() возвращает сумму числовых значений в итерируемом списке:

>>> sum([1, 2, 3, 4, 5])
15

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

>>> reduce(f, ["cat", "dog", "hedgehog", "gecko"])
'catdoghedgehoggecko'

Опять же, есть способ добиться этого, который большинство сочтет более типично питоническим. Именно это и делает str.join():

>>> "".join(["cat", "dog", "hedgehog", "gecko"])
'catdoghedgehoggecko'

Теперь рассмотрим пример с использованием оператора двоичного умножения (*). Факториал <3>> положительного целого числа n определяется следующим образом:

Definition of factorial

Вы можете реализовать функцию факториала, используя reduce() и range(), как показано ниже:

>>> def multiply(x, y):
...     return x * y
...

>>> def factorial(n):
...     from functools import reduce
...     return reduce(multiply, range(1, n + 1))
...

>>> factorial(4)  # 1 * 2 * 3 * 4
24
>>> factorial(6)  # 1 * 2 * 3 * 4 * 5 * 6
720

Опять же, есть более простой способ сделать это. Вы можете использовать factorial(), предоставляемый стандартным math модулем :

>>> from math import factorial

>>> factorial(4)
24
>>> factorial(6)
720

В качестве последнего примера предположим, что вам нужно найти максимальное значение в списке. Python предоставляет для этого встроенную функцию max(), но вы можете использовать и reduce():

>>> max([23, 49, 6, 32])
49

>>> def greater(x, y):
...     return x if x > y else y
...

>>> from functools import reduce
>>> reduce(greater, [23, 49, 6, 32])
49

Обратите внимание, что в каждом из приведенных примеров функция, передаваемая в reduce(), является однострочной. В каждом случае вместо нее можно было бы использовать функцию lambda:

>>> reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])
15
>>> reduce(lambda x, y: x + y, ["foo", "bar", "baz", "quz"])
'foobarbazquz'

>>> def factorial(n):
...     from functools import reduce
...     return reduce(lambda x, y: x * y, range(1, n + 1))
...
>>> factorial(4)
24
>>> factorial(6)
720

>>> reduce((lambda x, y: x if x > y else y), [23, 49, 6, 32])
49

Это удобный способ избежать помещения в пространство имен ненужной в противном случае функции. С другой стороны, человеку, читающему код, может быть немного сложнее определить ваши намерения, когда вы используете lambda вместо определения отдельной функции. Как часто бывает, это баланс между читабельностью и удобством.

Вызов reduce() с начальным значением

Существует другой способ вызова reduce(), который задает начальное значение для последовательности уменьшения:

reduce(<f>, <iterable>, <init>)

Когда присутствует, <init> задает начальное значение для комбинации. При первом вызове <f> аргументами являются <init> и первый элемент <iterable>. Затем этот результат комбинируется со вторым элементом <iterable>, и так далее:

>>> def f(x, y):
...     return x + y
...

>>> from functools import reduce
>>> reduce(f, [1, 2, 3, 4, 5], 100)  # (100 + 1 + 2 + 3 + 4 + 5)
115

>>> # Using lambda:
>>> reduce(lambda x, y: x + y, [1, 2, 3, 4, 5], 100)
115

Теперь последовательность вызовов функций выглядит так:

Reduce function with <init> argument

reduce(f, [1, 2, 3, 4, 5], 100)

Вы могли бы легко достичь того же результата без reduce():

>>> 100 + sum([1, 2, 3, 4, 5])
115

Как вы видели из приведенных выше примеров, даже в тех случаях, когда вы можете решить задачу с помощью reduce(), часто можно найти более простой и питонический способ решить ту же задачу без него. Возможно, не так уж сложно понять, почему reduce() все-таки был удален из основного языка.

При этом reduce() - довольно примечательная функция. В описании в начале этого раздела говорится, что reduce() объединяет элементы, чтобы получить единый результат. Но этот результат может быть составным объектом, например списком или кортежем. По этой причине reduce() является очень обобщенной функцией высшего порядка, на основе которой можно реализовать множество других функций.

Например, вы можете реализовать map() в терминах reduce():

>>> numbers = [1, 2, 3, 4, 5]

>>> list(map(str, numbers))
['1', '2', '3', '4', '5']

>>> def custom_map(function, iterable):
...     from functools import reduce
...
...     return reduce(
...         lambda items, value: items + [function(value)],
...         iterable,
...         [],
...     )
...
>>> list(custom_map(str, numbers))
['1', '2', '3', '4', '5']

Вы можете реализовать filter(), используя reduce(), а также:

>>> numbers = list(range(10))
>>> numbers
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> def is_even(x):
...     return x % 2 == 0
...

>>> list(filter(is_even, numbers))
[0, 2, 4, 6, 8]

>>> def custom_filter(function, iterable):
...     from functools import reduce
...
...     return reduce(
...         lambda items, value: items + [value] if function(value) else items,
...         iterable,
...         []
...     )
...
>>> list(custom_filter(is_even, numbers))
[0, 2, 4, 6, 8]

Фактически, любая операция над последовательностью объектов может быть выражена в виде редукции.

Заключение

Функциональное программирование - парадигма программирования, в которой основным методом вычислений является оценка чистых функций. Хотя Python не является функциональным языком, полезно знать lambda, map(), filter() и reduce(), потому что они могут помочь вам писать краткий, высокоуровневый, распараллеливаемый код. Вы также увидите их в коде, написанном другими людьми.

В этом уроке вы узнали:

  • Что такое функциональное программирование
  • Как функции в Python являются гражданами первого сорта, и как это делает их пригодными для функционального программирования
  • Как определить простую анонимную функцию с помощью lambda
  • Как реализовать функциональный код с помощью map(), filter() и reduce()

На этом мы завершаем вводный курс по основам работы с Python. Поздравляем! Теперь у вас есть прочный фундамент для создания полезных программ в эффективном, питоновском стиле.

Если вы хотите поднять свои навыки работы с Python на новый уровень, то можете ознакомиться с другими учебниками intermediate и advanced. Вы также можете ознакомиться с некоторыми идеями проектов на Python, чтобы начать демонстрировать свои суперспособности в Python. Счастливого кодинга!

Вернуться на верх