Spec-Zone.ru › Python 3.13

Руководство по функциональному программированию

Автор:

A. M. Kuchling

Версия:

0.32

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

Введение

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

Языки программирования позволяют разбивать задачи на части несколькими способами:

  • Большинство языков программирования являются процедурными: программы представляют собой списки инструкций, указывающих компьютеру, что делать с входными данными программы. C, Pascal и даже оболочки Unix — процедурные языки.
  • В декларативных языках вы пишете спецификацию, описывающую задачу, которую нужно решить, а реализация языка определяет, как выполнить вычисления эффективно. SQL — декларативный язык, с которым вы, скорее всего, знакомы: запрос SQL описывает набор данных, который нужно получить, а движок SQL решает, сканировать ли таблицы или использовать индексы, какие подзапросы выполнять первыми и так далее.
  • Объектно-ориентированные программы оперируют наборами объектов. Объекты имеют внутреннее состояние и поддерживают методы, которые определённым образом запрашивают или изменяют это состояние. Smalltalk и Java — объектно-ориентированные языки. C++ и Python поддерживают объектно-ориентированное программирование, но не требуют использовать объектно-ориентированные возможности.
  • Функциональное программирование разбивает задачу на набор функций. В идеале функции только принимают входные данные и возвращают результаты, не имея внутреннего состояния, влияющего на результат для заданных входных данных. К известным функциональным языкам относятся языки семейства ML (Standard ML, OCaml и другие варианты) и Haskell.

Создатели некоторых языков программирования выбирают один конкретный подход к программированию в качестве основного. Это часто затрудняет написание программ с использованием другого подхода. Другие языки являются мультипарадигменными и поддерживают несколько разных подходов. Lisp, C++ и Python — мультипарадигменные языки; на каждом из них можно писать преимущественно процедурные, объектно-ориентированные или функциональные программы и библиотеки. В большой программе разные разделы могут быть написаны с использованием разных подходов: например, графический интерфейс может быть объектно-ориентированным, а логика обработки — процедурной или функциональной.

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

Некоторые языки очень строго следят за чистотой и даже не имеют операторов присваивания, таких как a=3 или c = a + b, но сложно избежать всех побочных эффектов, например вывода на экран или записи в файл на диске. Другой пример — вызов функции print() или time.sleep(), ни одна из которых не возвращает полезного значения. Обе вызываются только ради побочных эффектов: вывода текста на экран или приостановки выполнения на секунду.

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

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

Функциональный подход может показаться странным ограничением. Зачем избегать объектов и побочных эффектов? У функционального стиля есть теоретические и практические преимущества:

  • Формальная доказуемость.
  • Модульность.
  • Композиционность.
  • Простота отладки и тестирования.

Формальная доказуемость

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

Уже давно исследователи пытаются найти способы математически доказывать корректность программ. Это не то же самое, что проверить программу на многочисленных входных данных и заключить, что обычно она выдаёт правильный результат, или прочитать исходный код и решить, что он выглядит правильно; цель состоит в том, чтобы строго доказать, что программа выдаёт правильный результат для всех возможных входных данных.

Метод доказательства корректности программ заключается в записи инвариантов — свойств входных данных и переменных программы, которые всегда истинны. Для каждой строки кода нужно показать, что если инварианты X и Y истинны до выполнения строки, то немного отличающиеся инварианты X’ и Y’ истинны после её выполнения. Так продолжают до конца программы; на этом этапе инварианты должны соответствовать желаемым условиям для выходных данных программы.

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

К сожалению, доказывать корректность программ в основном непрактично и неактуально для программного обеспечения на Python. Даже для тривиальных программ нужны доказательства длиной в несколько страниц; доказательство корректности умеренно сложной программы было бы огромным, а корректность немногих или вовсе ни одной из программ, которыми вы пользуетесь каждый день (интерпретатора Python, анализатора XML, веб-браузера), нельзя было бы доказать. Даже если бы вы записали или сгенерировали доказательство, осталось бы проверить его корректность: возможно, в нём есть ошибка, и вы ошибочно считаете, что доказали корректность программы.

Модульность

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

Простота отладки и тестирования

Отлаживать и тестировать программу в функциональном стиле проще.

Отладка упрощается, поскольку функции, как правило, невелики и имеют чётко определённое назначение. Если программа работает неправильно, каждую функцию можно проверить как отдельную точку взаимодействия, чтобы убедиться в корректности данных. Можно просмотреть промежуточные входные и выходные данные и быстро определить, в какой функции находится ошибка.

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

Композиционность

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

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

Итераторы

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

Итератор — это объект, представляющий поток данных; он возвращает данные по одному элементу за раз. Итератор Python должен поддерживать метод __next__(), который не принимает аргументов и всегда возвращает следующий элемент потока. Если элементов в потоке больше нет, метод __next__() должен возбуждать исключение StopIteration. Однако итераторы не обязательно должны быть конечными: вполне допустимо написать итератор, создающий бесконечный поток данных.

Встроенная функция iter() принимает произвольный объект и пытается вернуть итератор, который будет возвращать содержимое или элементы объекта. Если объект не поддерживает итерацию, возбуждается исключение TypeError. Несколько встроенных типов данных Python поддерживают итерацию; чаще всего это списки и словари. Объект называется итерируемым, если для него можно получить итератор.

Можно вручную поэкспериментировать с интерфейсом итерации:

>>> L = [1, 2, 3]
>>> it = iter(L)
>>> it
<...iterator object at ...>
>>> it.__next__()  # same as next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

Python ожидает итерируемые объекты в нескольких различных контекстах, важнейший из которых — инструкция for. В инструкции for X in Y Y должен быть итератором или объектом, для которого iter() может создать итератор. Эти две инструкции эквивалентны:

for i in iter(obj):
    print(i)

for i in obj:
    print(i)

Итераторы можно преобразовать в списки или кортежи с помощью функций-конструкторов list() или tuple():

>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> t = tuple(iterator)
>>> t
(1, 2, 3)

Распаковка последовательностей также поддерживает итераторы: если известно, что итератор вернёт N элементов, их можно распаковать в кортеж из N элементов:

>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> a, b, c = iterator
>>> a, b, c
(1, 2, 3)

Встроенные функции, такие как max() и min(), могут принимать один аргумент-итератор и возвращать наибольший или наименьший элемент. Операторы "in" и "not in" также поддерживают итераторы: X in iterator возвращает истину, если X найден в потоке, возвращаемом итератором. При работе с бесконечным итератором могут возникнуть очевидные проблемы: max() и min() никогда не вернут результат, а если элемент X никогда не появится в потоке, операторы "in" и "not in" тоже не вернут результат.

Обратите внимание, что перемещаться по итератору можно только вперёд: нельзя получить предыдущий элемент, сбросить итератор или создать его копию. Объекты-итераторы могут дополнительно предоставлять такие возможности, но протокол итератора определяет только метод __next__(). Поэтому функции могут потребить весь вывод итератора; если нужно иначе обработать тот же поток, придётся создать новый итератор.

Типы данных, поддерживающие итераторы

Мы уже видели, что списки и кортежи поддерживают итераторы. На самом деле любой последовательностный тип Python, например строка, автоматически поддерживает создание итератора.

Вызов iter() для словаря возвращает итератор, который перебирает ключи словаря:

>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
...      'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for key in m:
...     print(key, m[key])
Jan 1
Feb 2
Mar 3
Apr 4
May 5
Jun 6
Jul 7
Aug 8
Sep 9
Oct 10
Nov 11
Dec 12

Обратите внимание, что начиная с Python 3.7 порядок перебора элементов словаря гарантированно совпадает с порядком их добавления. В более ранних версиях поведение не было определено и могло различаться в разных реализациях.

Применение iter() к словарю всегда перебирает ключи, но у словарей есть методы, возвращающие другие итераторы. Чтобы перебирать значения или пары «ключ-значение», можно явно вызвать методы values() или items() и получить подходящий итератор.

Конструктор dict() может принимать итератор, возвращающий конечный поток кортежей (key, value):

>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}

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

for line in file:
    # do something for each line
    ...

Множества могут получать содержимое из итерируемого объекта и позволяют перебирать элементы множества:

>>> S = {2, 3, 5, 7, 11, 13}
>>> for i in S:
...     print(i)
2
3
5
7
11
13

Генераторные выражения и генераторы списков

Две распространённые операции над результатами итератора — 1) выполнить некоторую операцию для каждого элемента и 2) выбрать подмножество элементов, удовлетворяющих условию. Например, имея список строк, вы можете захотеть удалить конечные пробелы в каждой строке или выбрать все строки, содержащие заданную подстроку.

Генераторы списков и генераторные выражения (сокращённо — «listcomps» и «genexps») представляют собой краткую запись таких операций, заимствованную из функционального языка программирования Haskell (https://www.haskell.org/). Следующий код удаляет все пробелы из потока строк:

>>> line_list = ['  line 1\n', 'line 2  \n', ' \n', '']

>>> # Generator expression -- returns iterator
>>> stripped_iter = (line.strip() for line in line_list)

>>> # List comprehension -- returns list
>>> stripped_list = [line.strip() for line in line_list]

Выбрать только некоторые элементы можно, добавив условие "if":

>>> stripped_list = [line.strip() for line in line_list
...                  if line != ""]

Генератор списка возвращает список Python; stripped_list — это список с полученными строками, а не итератор. Генераторные выражения возвращают итератор, который вычисляет значения по мере необходимости, не создавая их все сразу. Это означает, что генераторы списков бесполезны при работе с итераторами, возвращающими бесконечный поток или очень большой объём данных. В таких случаях предпочтительнее генераторные выражения.

Генераторные выражения заключаются в круглые скобки («()»), а генераторы списков — в квадратные («[]»). Генераторное выражение имеет следующий вид:

( expression for expr in sequence1
             if condition1
             for expr2 in sequence2
             if condition2
             for expr3 in sequence3
             ...
             if condition3
             for exprN in sequenceN
             if conditionN )

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

Элементами сгенерированного результата будут последовательные значения expression. Все выражения if необязательны; если они указаны, выражение expression вычисляется и добавляется в результат, только когда condition истинно.

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

obj_total = sum(obj.count for obj in list_all_objects())

Выражения for...in содержат последовательности, по которым нужно выполнить перебор. Последовательности не обязаны иметь одинаковую длину, поскольку перебор выполняется слева направо, а не параллельно. Для каждого элемента в sequence1 последовательность sequence2 перебирается с начала. Затем sequence3 перебирается для каждой полученной пары элементов из sequence1 и sequence2.

Иными словами, генератор списка или генераторное выражение эквивалентны следующему коду Python:

for expr1 in sequence1:
    if not (condition1):
        continue   # Skip this element
    for expr2 in sequence2:
        if not (condition2):
            continue   # Skip this element
        ...
        for exprN in sequenceN:
            if not (conditionN):
                continue   # Skip this element

            # Output the value of
            # the expression.

Это означает, что если выражений for...in несколько, а выражений if нет, длина полученного результата будет равна произведению длин всех последовательностей. Если есть два списка длиной 3, результирующий список будет содержать 9 элементов:

>>> seq1 = 'abc'
>>> seq2 = (1, 2, 3)
>>> [(x, y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3),
 ('b', 1), ('b', 2), ('b', 3),
 ('c', 1), ('c', 2), ('c', 3)]

Чтобы не создавать неоднозначность в грамматике Python, если expression создаёт кортеж, его нужно заключить в круглые скобки. Первый генератор списка ниже содержит синтаксическую ошибку, а второй записан правильно:

# Syntax error
[x, y for x in seq1 for y in seq2]
# Correct
[(x, y) for x in seq1 for y in seq2]

Генераторы

Генераторы — это особый класс функций, упрощающий создание итераторов. Обычные функции вычисляют значение и возвращают его, а генераторы возвращают итератор, который выдаёт поток значений.

Вы наверняка знакомы с тем, как работают вызовы обычных функций в Python или C. При вызове функции для неё создаётся собственное пространство имён, в котором создаются локальные переменные. Когда функция доходит до инструкции return, локальные переменные уничтожаются, а значение возвращается вызывающему коду. Последующий вызов той же функции создаёт новое пространство имён и новый набор локальных переменных. Но что, если бы локальные переменные не уничтожались при выходе из функции? Что, если бы позже можно было возобновить выполнение функции с места, на котором оно остановилось? Именно это и обеспечивают генераторы: их можно считать возобновляемыми функциями.

Вот простейший пример функции-генератора:

>>> def generate_ints(N):
...    for i in range(N):
...        yield i

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

Вызов функции-генератора не возвращает одно значение; вместо этого он возвращает объект-генератор, поддерживающий протокол итератора. При выполнении выражения yield генератор выдаёт значение i, подобно инструкции return. Основное различие между yield и инструкцией return заключается в том, что при достижении yield выполнение генератора приостанавливается, а локальные переменные сохраняются. При следующем вызове метода __next__() генератора выполнение функции продолжится.

Пример использования генератора generate_ints():

>>> gen = generate_ints(3)
>>> gen
<generator object generate_ints at ...>
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
  File "stdin", line 1, in <module>
  File "stdin", line 2, in generate_ints
StopIteration

Можно также написать for i in generate_ints(5) или a, b, c = generate_ints(3).

Внутри функции-генератора выражение return value приводит к возникновению исключения StopIteration(value) в методе __next__(). После этого или при достижении конца функции последовательность значений заканчивается, и генератор больше не может выдавать значения.

Эффект генераторов можно было бы реализовать вручную, написав собственный класс и сохранив все локальные переменные генератора в переменных экземпляра. Например, список целых чисел можно возвращать, установив self.count в 0 и заставив метод __next__() увеличивать self.count и возвращать его. Однако для достаточно сложного генератора соответствующий класс написать гораздо труднее.

Набор тестов, входящий в библиотеку Python, Lib/test/test_generators.py, содержит ещё несколько интересных примеров. Вот генератор, который с помощью рекурсивных генераторов выполняет симметричный обход дерева.

# A recursive generator that generates Tree leaves in in-order.
def inorder(t):
    if t:
        for x in inorder(t.left):
            yield x

        yield t.label

        for x in inorder(t.right):
            yield x

Ещё два примера в test_generators.py находят решения задачи о N ферзях (размещение N ферзей на шахматной доске NxN так, чтобы ни один ферзь не угрожал другому) и задачи о ходе коня (поиск маршрута, по которому конь посетит каждую клетку шахматной доски NxN, не посещая ни одну клетку дважды).

Передача значений генератору

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

В Python 2.5 появился простой способ передавать значения генератору. Выражение yield стало возвращать значение, которое можно присвоить переменной или иным образом использовать:

val = (yield i)

Рекомендую всегда заключать выражение yield в скобки, если вы что-либо делаете с возвращённым значением, как в примере выше. Скобки нужны не всегда, но проще добавлять их каждый раз, чем запоминать, когда они требуются.

(В PEP 342 описаны точные правила: выражение yield всегда нужно заключать в скобки, кроме случая, когда оно находится на верхнем уровне выражения в правой части присваивания. Это означает, что можно написать val = yield i, но если в выражении есть операция, нужны скобки, как в val = (yield i) + 12.)

Значения передаются генератору вызовом его метода send(value). Этот метод возобновляет выполнение кода генератора, а выражение yield возвращает указанное значение. Если вызвать обычный метод __next__(), выражение yield возвращает None.

Вот простой счётчик, который увеличивается на 1 и позволяет изменять внутреннее значение счётчика.

def counter(maximum):
    i = 0
    while i < maximum:
        val = (yield i)
        # If value provided, change counter
        if val is not None:
            i = val
        else:
            i += 1

А вот пример изменения счётчика:

>>> it = counter(10)
>>> next(it)
0
>>> next(it)
1
>>> it.send(8)
8
>>> next(it)
9
>>> next(it)
Traceback (most recent call last):
  File "t.py", line 15, in <module>
    it.next()
StopIteration

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

Помимо send(), у генераторов есть ещё два метода:

  • throw(value) используется для возбуждения исключения внутри генератора; исключение возникает в выражении yield, на котором приостановилось выполнение генератора.
  • close() посылает генератору исключение GeneratorExit, чтобы завершить итерацию. Получив это исключение, код генератора должен либо возбудить GeneratorExit, либо StopIteration; перехватывать исключение и делать что-либо ещё запрещено, это приведёт к RuntimeError. Python также вызовет close(), когда сборщик мусора удалит генератор.

    Если при возникновении GeneratorExit нужно выполнить код очистки, рекомендую использовать блок try: ... finally: вместо перехвата GeneratorExit.

В результате этих изменений генераторы превратились из однонаправленных источников данных в средства, которые могут как выдавать, так и принимать данные.

Генераторы также становятся сопрограммами — более обобщённой формой подпрограмм. Подпрограммы запускаются в одной точке и завершаются в другой (в начале функции и при инструкции return), а сопрограммы можно запускать, завершать и возобновлять во множестве разных точек (при инструкциях yield).

Встроенные функции

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

Две встроенные функции Python, map() и filter(), дублируют возможности генераторных выражений:

map(f, iterA, iterB, ...) returns an iterator over the sequence

f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ....

>>> def upper(s):
...     return s.upper()
>>> list(map(upper, ['sentence', 'fragment']))
['SENTENCE', 'FRAGMENT']
>>> [upper(s) for s in ['sentence', 'fragment']]
['SENTENCE', 'FRAGMENT']

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

filter(predicate, iter) возвращает итератор по всем элементам последовательности, удовлетворяющим определённому условию; списковые включения предоставляют аналогичную возможность. Предикат — это функция, возвращающая логическое значение некоторого условия; при использовании с filter() предикат должен принимать одно значение.

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

Это также можно записать как списковое включение:

>>> list(x for x in range(10) if is_even(x))
[0, 2, 4, 6, 8]

enumerate(iter, start=0) нумерует элементы и возвращает пары из двух элементов, содержащие номер (начиная с start) и каждый элемент.

>>> for item in enumerate(['subject', 'verb', 'object']):
...     print(item)
(0, 'subject')
(1, 'verb')
(2, 'object')

enumerate() часто используют при переборе списка, чтобы записывать индексы, на которых выполняются определённые условия:

f = open('data.txt', 'r')
for i, line in enumerate(f):
    if line.strip() == '':
        print('Blank line at line #%i' % i)

sorted(iterable, key=None, reverse=False) собирает все элементы итерируемого объекта в список, сортирует его и возвращает отсортированный результат. Аргументы key и reverse передаются методу sort() созданного списка.

>>> import random
>>> # Generate 8 random numbers between [0, 10000)
>>> rand_list = random.sample(range(10000), 8)
>>> rand_list
[769, 7953, 9828, 6431, 8442, 9878, 6213, 2207]
>>> sorted(rand_list)
[769, 2207, 6213, 6431, 7953, 8442, 9828, 9878]
>>> sorted(rand_list, reverse=True)
[9878, 9828, 8442, 7953, 6431, 6213, 2207, 769]

(Подробнее о сортировке см. в разделе Методы сортировки.)

Встроенные функции any(iter) и all(iter) проверяют логические значения содержимого итерируемого объекта. any() возвращает True, если хотя бы один элемент итерируемого объекта имеет истинное значение, а all() возвращает True, если истинные значения имеют все элементы:

>>> any([0, 1, 0])
True
>>> any([0, 0, 0])
False
>>> any([1, 1, 1])
True
>>> all([0, 1, 0])
False
>>> all([0, 0, 0])
False
>>> all([1, 1, 1])
True

zip(iterA, iterB, ...) берёт по одному элементу из каждого итерируемого объекта и возвращает их в виде кортежа:

zip(['a', 'b', 'c'], (1, 2, 3)) =>
  ('a', 1), ('b', 2), ('c', 3)

Эта функция не создаёт список в памяти и не исчерпывает все входные итераторы до возврата результата; вместо этого кортежи создаются и возвращаются только по запросу. (Технический термин для такого поведения — ленивое вычисление.)

Этот итератор предназначен для использования с итерируемыми объектами одинаковой длины. Если их длины различаются, результирующий поток будет иметь длину самого короткого из них.

zip(['a', 'b'], (1, 2, 3)) =>
  ('a', 1), ('b', 2)

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

Модуль itertools

Модуль itertools содержит ряд часто используемых итераторов, а также функции для объединения нескольких итераторов. В этом разделе мы познакомимся с содержимым модуля на небольших примерах.

Функции модуля можно разделить на несколько основных категорий:

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

Создание новых итераторов

itertools.count(start, step) возвращает бесконечный поток равномерно распределённых значений. При желании можно указать начальное число (по умолчанию 0) и интервал между числами (по умолчанию 1):

itertools.count() =>
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
itertools.count(10) =>
  10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.count(10, 5) =>
  10, 15, 20, 25, 30, 35, 40, 45, 50, 55, ...

itertools.cycle(iter) сохраняет копию содержимого переданного итерируемого объекта и возвращает новый итератор, выдающий его элементы от первого до последнего. Затем новый итератор будет бесконечно повторять эти элементы.

itertools.cycle([1, 2, 3, 4, 5]) =>
  1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...

itertools.repeat(elem, [n]) возвращает переданный элемент n раз или бесконечно, если значение n не указано.

itertools.repeat('abc') =>
  abc, abc, abc, abc, abc, abc, abc, abc, abc, abc, ...
itertools.repeat('abc', 5) =>
  abc, abc, abc, abc, abc

itertools.chain(iterA, iterB, ...) принимает произвольное количество итерируемых объектов и возвращает все элементы первого итератора, затем все элементы второго и так далее, пока не будут исчерпаны все итерируемые объекты.

itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
  a, b, c, 1, 2, 3

itertools.islice(iter, [start], stop, [step]) возвращает поток, представляющий собой срез итератора. Если передан только аргумент stop, будут возвращены первые stop элементов. Если указать начальный индекс, будут возвращены stop-start элементов; если указать значение step, соответствующие элементы будут пропускаться. В отличие от срезов строк и списков в Python, для start, stop и step нельзя использовать отрицательные значения.

itertools.islice(range(10), 8) =>
  0, 1, 2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8) =>
  2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8, 2) =>
  2, 4, 6

itertools.tee(iter, [n]) дублирует итератор: она возвращает n независимых итераторов, каждый из которых выдаёт содержимое исходного итератора. Если значение n не указано, по умолчанию оно равно 2. Для дублирования итераторов необходимо сохранять часть содержимого исходного итератора, поэтому при большом объёме данных это может потребовать значительного количества памяти, если один из новых итераторов обрабатывается быстрее остальных.

itertools.tee( itertools.count() ) =>
   iterA, iterB

where iterA ->
   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...

and   iterB ->
   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...

Вызов функций для элементов

Модуль operator содержит набор функций, соответствующих операторам Python. Например, operator.add(a, b) (складывает два значения), operator.ne(a, b) (эквивалентна a != b) и operator.attrgetter('id') (возвращает вызываемый объект, получающий атрибут .id).

itertools.starmap(func, iter) предполагает, что итерируемый объект будет выдавать поток кортежей, и вызывает func, используя эти кортежи в качестве аргументов:

itertools.starmap(os.path.join,
                  [('/bin', 'python'), ('/usr', 'bin', 'java'),
                   ('/usr', 'bin', 'perl'), ('/usr', 'bin', 'ruby')])
=>
  /bin/python, /usr/bin/java, /usr/bin/perl, /usr/bin/ruby

Выбор элементов

Ещё одна группа функций выбирает подмножество элементов итератора на основе предиката.

itertools.filterfalse(predicate, iter) противоположна функции filter(): она возвращает все элементы, для которых предикат возвращает ложное значение:

itertools.filterfalse(is_even, itertools.count()) =>
  1, 3, 5, 7, 9, 11, 13, 15, ...

itertools.takewhile(predicate, iter) возвращает элементы, пока предикат возвращает истинное значение. Как только предикат вернёт ложное значение, итератор сообщит об окончании результатов.

def less_than_10(x):
    return x < 10

itertools.takewhile(less_than_10, itertools.count()) =>
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9

itertools.takewhile(is_even, itertools.count()) =>
  0

itertools.dropwhile(predicate, iter) отбрасывает элементы, пока предикат возвращает истинное значение, а затем возвращает оставшиеся результаты итерируемого объекта.

itertools.dropwhile(less_than_10, itertools.count()) =>
  10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...

itertools.dropwhile(is_even, itertools.count()) =>
  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...

itertools.compress(data, selectors) принимает два итератора и возвращает только те элементы data, для которых соответствующий элемент selectors имеет истинное значение; выполнение прекращается, когда исчерпывается любой из итераторов:

itertools.compress([1, 2, 3, 4, 5], [True, True, False, False, True]) =>
   1, 2, 5

Комбинаторные функции

itertools.combinations(iterable, r) возвращает итератор, выдающий все возможные комбинации элементов iterable в виде кортежей длины r.

itertools.combinations([1, 2, 3, 4, 5], 2) =>
  (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 3), (2, 4), (2, 5),
  (3, 4), (3, 5),
  (4, 5)

itertools.combinations([1, 2, 3, 4, 5], 3) =>
  (1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 4), (1, 3, 5), (1, 4, 5),
  (2, 3, 4), (2, 3, 5), (2, 4, 5),
  (3, 4, 5)

Элементы в каждом кортеже сохраняют тот же порядок, в котором их выдал iterable. Например, в приведённых выше примерах число 1 всегда стоит перед числами 2, 3, 4 или 5. Похожая функция itertools.permutations(iterable, r=None) снимает это ограничение на порядок и возвращает все возможные перестановки длины r:

itertools.permutations([1, 2, 3, 4, 5], 2) =>
  (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 1), (2, 3), (2, 4), (2, 5),
  (3, 1), (3, 2), (3, 4), (3, 5),
  (4, 1), (4, 2), (4, 3), (4, 5),
  (5, 1), (5, 2), (5, 3), (5, 4)

itertools.permutations([1, 2, 3, 4, 5]) =>
  (1, 2, 3, 4, 5), (1, 2, 3, 5, 4), (1, 2, 4, 3, 5),
  ...
  (5, 4, 3, 2, 1)

Если не указать значение r, используется длина итерируемого объекта, то есть переставляются все его элементы.

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

itertools.permutations('aba', 3) =>
  ('a', 'b', 'a'), ('a', 'a', 'b'), ('b', 'a', 'a'),
  ('b', 'a', 'a'), ('a', 'a', 'b'), ('a', 'b', 'a')

Идентичный кортеж ('a', 'a', 'b') встречается дважды, но две строки «a» взяты из разных позиций.

Функция itertools.combinations_with_replacement(iterable, r) снимает другое ограничение: элементы могут повторяться в одном кортеже. Условно говоря, для первой позиции каждого кортежа выбирается элемент, затем он возвращается обратно, прежде чем будет выбран второй элемент.

itertools.combinations_with_replacement([1, 2, 3, 4, 5], 2) =>
  (1, 1), (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 2), (2, 3), (2, 4), (2, 5),
  (3, 3), (3, 4), (3, 5),
  (4, 4), (4, 5),
  (5, 5)

Группировка элементов

Последняя функция, о которой я расскажу, itertools.groupby(iter, key_func=None), — самая сложная. key_func(elem) — это функция, которая вычисляет ключ для каждого элемента, возвращаемого итерируемым объектом. Если функцию-ключ не указать, ключом будет сам элемент.

groupby() собирает все подряд идущие элементы исходного итерируемого объекта с одинаковым значением ключа и возвращает поток пар из двух элементов: значения ключа и итератора по элементам с этим ключом.

city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
             ('Anchorage', 'AK'), ('Nome', 'AK'),
             ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
             ...
            ]

def get_state(city_state):
    return city_state[1]

itertools.groupby(city_list, get_state) =>
  ('AL', iterator-1),
  ('AK', iterator-2),
  ('AZ', iterator-3), ...

where
iterator-1 =>
  ('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
iterator-2 =>
  ('Anchorage', 'AK'), ('Nome', 'AK')
iterator-3 =>
  ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')

groupby() предполагает, что содержимое исходного итерируемого объекта уже отсортировано по ключу. Обратите внимание: возвращаемые итераторы также используют исходный итерируемый объект, поэтому результаты iterator-1 необходимо обработать до того, как запрашивать iterator-2 и соответствующий ему ключ.

Модуль functools

Модуль functools содержит несколько функций высшего порядка. Функция высшего порядка принимает одну или несколько функций и возвращает новую функцию. Самый полезный инструмент этого модуля — функция functools.partial().

При написании программ в функциональном стиле иногда требуется создавать варианты существующих функций с уже заданными значениями некоторых параметров. Рассмотрим функцию Python f(a, b, c); может понадобиться создать новую функцию g(b, c), эквивалентную f(1, b, c), задав значение одного из параметров функции f(). Это называется «частичным применением функции».

Конструктор partial() принимает аргументы (function, arg1, arg2, ..., kwarg1=value1, kwarg2=value2). Полученный объект можно вызвать, чтобы выполнить function с заданными аргументами.

Вот небольшой, но реалистичный пример:

import functools

def log(message, subsystem):
    """Write the contents of 'message' to the specified subsystem."""
    print('%s: %s' % (subsystem, message))
    ...

server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')

functools.reduce(func, iter, [initial_value]) последовательно применяет операцию ко всем элементам итерируемого объекта, поэтому её нельзя применять к бесконечным итерируемым объектам. func должна быть функцией, которая принимает два элемента и возвращает одно значение. functools.reduce() берёт первые два элемента A и B, возвращённые итератором, и вычисляет func(A, B). Затем она запрашивает третий элемент C, вычисляет func(func(A, B), C), объединяет результат с четвёртым возвращённым элементом и продолжает работу, пока итерируемый объект не будет исчерпан. Если итерируемый объект не возвращает ни одного значения, возникает исключение TypeError. Если задано начальное значение, оно используется в качестве отправной точки, а func(initial_value, A) становится первым вычислением.

>>> import operator, functools
>>> functools.reduce(operator.concat, ['A', 'BB', 'C'])
'ABBC'
>>> functools.reduce(operator.concat, [])
Traceback (most recent call last):
  ...
TypeError: reduce() of empty sequence with no initial value
>>> functools.reduce(operator.mul, [1, 2, 3], 1)
6
>>> functools.reduce(operator.mul, [], 1)
1

Если использовать operator.add() с functools.reduce(), можно сложить все элементы итерируемого объекта. Этот случай настолько распространён, что для него существует специальная встроенная функция sum():

>>> import functools, operator
>>> functools.reduce(operator.add, [1, 2, 3, 4], 0)
10
>>> sum([1, 2, 3, 4])
10
>>> sum([])
0

Однако во многих случаях использования functools.reduce() понятнее будет написать очевидный цикл for:

import functools
# Instead of:
product = functools.reduce(operator.mul, [1, 2, 3], 1)

# You can write:
product = 1
for i in [1, 2, 3]:
    product *= i

Связанная с ней функция — itertools.accumulate(iterable, func=operator.add). Она выполняет те же вычисления, но вместо одного итогового результата accumulate() возвращает итератор, который также выдаёт каждый промежуточный результат:

itertools.accumulate([1, 2, 3, 4, 5]) =>
  1, 3, 6, 10, 15

itertools.accumulate([1, 2, 3, 4, 5], operator.mul) =>
  1, 2, 6, 24, 120

Модуль operator

Ранее упоминался модуль operator. Он содержит набор функций, соответствующих операторам Python. Эти функции часто полезны в коде, написанном в функциональном стиле, поскольку избавляют от необходимости создавать тривиальные функции, выполняющие одну операцию.

Вот некоторые функции этого модуля:

  • Математические операции: add(), sub(), mul(), floordiv(), abs(), …
  • Логические операции: not_(), truth().
  • Побитовые операции: and_(), or_(), invert().
  • Сравнения: eq(), ne(), lt(), le(), gt() и ge().
  • Идентичность объектов: is_(), is_not().

Небольшие функции и lambda-выражение

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

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

stripped_lines = [line.strip() for line in lines]
existing_files = filter(os.path.exists, file_list)

Если нужной функции нет, её придётся написать. Один из способов создавать небольшие функции — использовать выражение lambda. lambda принимает несколько параметров и выражение, объединяющее эти параметры, и создаёт анонимную функцию, возвращающую значение выражения:

adder = lambda x, y: x+y

print_assign = lambda name, value: name + '=' + str(value)

Вместо этого можно использовать инструкцию def и определить функцию обычным способом:

def adder(x, y):
    return x + y

def print_assign(name, value):
    return name + '=' + str(value)

Какой вариант предпочтительнее? Это вопрос стиля; обычно я стараюсь избегать lambda.

Одна из причин такого предпочтения — ограниченные возможности lambda при определении функций. Результат должен вычисляться как одно выражение, поэтому нельзя использовать многоступенчатые сравнения if... elif... else или инструкции try... except. Если попытаться сделать слишком многое в инструкции lambda, получится чрезмерно сложное и трудночитаемое выражение. Попробуйте быстро понять, что делает следующий код:

import functools
total = functools.reduce(lambda a, b: (0, a[1] + b[1]), items)[1]

Разобраться можно, но придётся потратить время, чтобы распутать выражение и понять, что происходит. Несколько вложенных инструкций def немного упрощают задачу:

import functools
def combine(a, b):
    return 0, a[1] + b[1]

total = functools.reduce(combine, items)[1]

Но лучше всего было бы просто использовать цикл for:

total = 0
for a, b in items:
    total += b

Или встроенную функцию sum() и генераторное выражение:

total = sum(b for a, b in items)

Во многих случаях вызовы functools.reduce() понятнее записать в виде циклов for.

Фредрик Лунд однажды предложил следующий набор правил для рефакторинга кода с lambda:

  1. Напишите lambda-функцию.
  2. Напишите комментарий, объясняющий, что же делает эта lambda-функция.
  3. Некоторое время поразмышляйте над комментарием и придумайте имя, отражающее его суть.
  4. Преобразуйте lambda-функцию в инструкцию def, используя это имя.
  5. Удалите комментарий.

Мне очень нравятся эти правила, но вы вправе не согласиться с тем, что стиль без lambda-функций лучше.

История изменений и благодарности

Автор хотел бы поблагодарить следующих людей за предложения, исправления и помощь с различными черновиками этой статьи: Ian Bicking, Nick Coghlan, Nick Efford, Raymond Hettinger, Jim Jewett, Mike Krell, Leandro Lameiro, Jussi Salmela, Collin Winter, Blake Winton.

Версия 0.1: опубликована 30 июня 2006 года.

Версия 0.11: опубликована 1 июля 2006 года. Исправлены опечатки.

Версия 0.2: опубликована 10 июля 2006 года. Разделы о генераторных выражениях и списковых включениях объединены. Исправлены опечатки.

Версия 0.21: добавлены ссылки, предложенные в рассылке tutor.

Версия 0.30: добавлен раздел о модуле functional, написанный Collin Winter; добавлен краткий раздел о модуле operator; внесено несколько других изменений.

Ссылки

Общие сведения

Структура и интерпретация компьютерных программ, авторы — Harold Abelson и Gerald Jay Sussman при участии Julie Sussman. Книгу можно найти по адресу https://mitpress.mit.edu/sicp. В этой классической книге по информатике в главах 2 и 3 обсуждается использование последовательностей и потоков для организации потоков данных внутри программы. В примерах используется Scheme, но многие описанные в этих главах подходы к проектированию применимы к коду Python, написанному в функциональном стиле.

https://defmacro.org/2006/06/19/fp.html: общее введение в функциональное программирование с примерами на Java и подробным историческим обзором.

https://en.wikipedia.org/wiki/Functional_programming: общая статья Википедии о функциональном программировании.

https://en.wikipedia.org/wiki/Coroutine: статья о сопрограммах.

https://en.wikipedia.org/wiki/Partial_application: статья о понятии частичного применения функции.

https://en.wikipedia.org/wiki/Currying: статья о каррировании.

Материалы по Python

https://gnosis.cx/TPiP/: в первой главе книги David Mertz Text Processing in Python рассматривается функциональное программирование при обработке текста, в разделе «Использование функций высшего порядка при обработке текста».

Mertz также написал серию из трёх статей о функциональном программировании для сайта IBM DeveloperWorks; см. часть 1, часть 2 и часть 3,

Документация Python

Документация модуля itertools.

Документация модуля functools.

Документация модуля operator.

PEP 289: «Генераторные выражения»

PEP 342: «Сопрограммы с помощью усовершенствованных генераторов» описывает новые возможности генераторов в Python 2.5.

© 2001–2025 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/howto/functional.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API