Документация Python неофициальный перевод
Содержание страницы

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

Автор:A. M. Kuchling
Версия:0.31

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

ВведениеIntroduction

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

МодульностьModularity

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

Лёгкость отладки и тестированияEase of debugging and testing

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

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

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

КомпозируемостьComposability

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

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

ИтераторыIterators

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

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

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

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

>>> L = [1,2,3]
>>> it = iter(L)
>>> it  
<...iterator object at ...>
>>> it.__next__()  # то же, что и next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
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 итератор истинно, если X находится в потоке, возвращаемом итератором. С бесконечным итератором возникнут очевидные проблемы: max(), min() никогда не вернут результат, и если элемент X никогда не появится в потоке, операторы "in" и "not in" тоже не вернут результат.

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

Типы данных, поддерживающие итераторыData Types That Support Iterators

Мы уже видели, как списки и кортежи поддерживают итераторы. На самом деле любой тип последовательности 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])
Mar 3
Feb 2
Aug 8
Sep 9
Apr 4
Jun 6
Jul 7
Jan 1
May 5
Nov 11
Dec 12
Oct 10

Порядок по существу случаен, так как он основан на хеш-упорядочении объектов в словаре.

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

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

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

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

for line in file:
    # сделать что-то для каждой строки
    ...

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

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

Генераторные выражения и списковые включенияGenerator expressions and list comprehensions

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

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

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

# Генераторное выражение – возвращает итератор
stripped_iter = (line.strip() for line in line_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 )

Опять же, для спискового включения отличаются только внешние скобки (квадратные вместо круглых).

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

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

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   # Пропустить этот элемент
    for expr2 in sequence2:
        if not (condition2):
            continue    # Пропустить этот элемент
        ...
        for exprN in sequenceN:
             if not (conditionN):
                 continue   # Пропустить этот элемент

             # Вывести значение
             # выражения.

Это означает, что при наличии нескольких предложений 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, если выражение создаёт кортеж, оно должно быть заключено в круглые скобки. Первый генератор списка ниже содержит синтаксическую ошибку, а второй – корректен:

# Синтаксическая ошибка
[x, y for x in seq1 for y in seq2]
# Верно
[(x, y) for x in seq1 for y in seq2]

ГенераторыGenerators

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

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

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

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

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

При вызове генераторной функции она возвращает не одно значение, а объект-генератор, поддерживающий протокол итератора. При выполнении выражения 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 ?
  File "stdin", line 2, in generate_ints
StopIteration

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

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

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

Тестовый набор, входящий в библиотеку Python, Lib/test/test_generators.py, содержит ряд более интересных примеров. Вот один генератор, который реализует обход дерева в порядке (in-order) с использованием генераторов рекурсивно.

# Рекурсивный генератор, порождающий листья дерева в порядке 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, не посещая ни одну клетку дважды).

Передача значений в генераторPassing values into a generator

В 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)
        # Если передано значение, изменить 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 ?
    it.next()
StopIteration

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

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

  • throw(type, value=None, traceback=None) используется для возбуждения исключения внутри генератора; исключение возбуждается выражением yield в том месте, где выполнение генератора приостановлено.

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

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

Совокупный эффект этих изменений – превращение генераторов из однонаправленных производителей информации одновременно и в производителей, и в потребителей.

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

Встроенные функцииBuilt-in functions

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

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

map(f, iterA, iterB, ...) возвращает итератор по последовательности

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) нумерует элементы итерируемого объекта, возвращая кортежи из двух элементов, содержащие номер и значение.

>>> 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
>>> # Сгенерировать 8 случайных чисел в диапазоне [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)

Тем не менее, так делать не стоит, поскольку из более длинных итераторов может быть взят и отброшен элемент. Это означает, что использовать эти итераторы дальше нельзя – есть риск пропустить отброшенный элемент.

Модуль itertoolsThe itertools module

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

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

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

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

itertools.count(n) возвращает бесконечный поток целых чисел, увеличивающихся на 1 на каждом шаге. Можно указать начальное число; по умолчанию оно равно 0:

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.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, ...

Применение функций к элементамCalling functions on elements

Модуль 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

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

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

itertools.filterfalse(predicate, iter) – это противоположность, возвращающая все элементы, для которых предикат возвращает false:

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, ...

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

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

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

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 и соответствующий ему ключ.

Модуль functoolsThe functools module

Модуль functools в Python 2.5 содержит несколько функций высшего порядка. Функция высшего порядка принимает на вход одну или несколько функций и возвращает новую функцию. Самый полезный инструмент в этом модуле – функция 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):
    """Записать содержимое 'message' в указанную подсистему."""
    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
>>> functools.reduce(operator.add, [1,2,3,4], 0)
10
>>> sum([1,2,3,4])
10
>>> sum([])
0

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

import functools
# Вместо:
product = functools.reduce(operator.mul, [1,2,3], 1)

# Можно написать:
product = 1
for i in [1,2,3]:
    product *= i

Модуль operatorThe operator module

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

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

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

Полный список – в документации модуля operator.

Небольшие функции и лямбда-выражениеSmall functions and the lambda expression

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

Если есть подходящая встроенная функция 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. Написать лямбда-функцию.
  2. Написать комментарий, объясняющий, что, черт возьми, делает эта лямбда.
  3. Некоторое время изучать комментарий и придумать имя, отражающее суть комментария.
  4. Преобразовать лямбду в инструкцию def, используя это имя.
  5. Удалить комментарий.

Мне очень нравятся эти правила, но вы вольны не соглашаться с тем, что этот стиль без лямбда лучше.

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

Автор хотел бы поблагодарить следующих людей за предложения, исправления и помощь с различными черновиками этой статьи: 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, написанный Коллином Уинтером; добавлен краткий раздел о модуле operator; несколько других правок.

СсылкиReferences

ОбщиеGeneral

Структура и интерпретация компьютерных программ, авторы Гарольд Абельсон и Джеральд Джей Сассман при участии Джули Сассман. Полный текст по адресу http://mitpress.mit.edu/sicp/. В этом классическом учебнике по информатике главы 2 и 3 рассматривают использование последовательностей и потоков для организации потока данных внутри программы. В книге для примеров используется Scheme, но многие подходы к проектированию, описанные в этих главах, применимы к коду на Python в функциональном стиле.

http://www.defmacro.org/ramblings/fp.html: Общее введение в функциональное программирование на примерах Java, содержащее обширное историческое введение.

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

http://en.wikipedia.org/wiki/Coroutine: статья о корутинах.

http://en.wikipedia.org/wiki/Currying: статья о концепции каррирования.

Специфическое для PythonPython-specific

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

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

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

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

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

PEP 289: “Генераторные выражения”

PEP 342: “Корутины через улучшенные генераторы” описывает новые возможности генераторов в Python 2.5.