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

Автор:

А. М. Кухлинг

Выпускать:

0.32

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

Вступление

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

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

  • Большинство языков программирования являются процедурными: программы - это списки инструкций, которые сообщают компьютеру, что делать с вводимыми программой данными. C, Pascal и даже оболочки Unix являются процедурными языками.

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

  • Объектно-ориентированные программы управляют коллекциями объектов. Объекты имеют внутреннее состояние и поддерживают методы, которые запрашивают или изменяют это внутреннее состояние каким-либо образом. Smalltalk и Java являются объектно-ориентированными языками. C++ и Python - это языки, которые поддерживают объектно-ориентированное программирование, но не требуют обязательного использования объектно-ориентированных функций.

  • *Функциональное программирование разбивает задачу на набор функций. В идеале функции принимают только входные данные и выдают выходные данные и не имеют никакого внутреннего состояния, которое влияет на выходные данные, полученные для данного входного сигнала. Хорошо известные функциональные языки включают семейство ML (стандартный 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 поддерживают итерацию, наиболее распространенными из которых являются списки и словари. Объект называется iterable, если для него можно получить итератор.

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

>>> 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 имеет значение true, если X найден в потоке, возвращаемом итератором. Вы будете работать в очевидных проблем, если итератор является бесконечным; max(), min() никогда не вернется, и если элемент X не отображается в потоке "in" и "not in" операторы не возвращают.

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

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

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

Вызов 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
    ...

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

>>> 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 != ""]

При использовании list вы получаете обратно список 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 имеет значение true.

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

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 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 <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. close() также будет вызываться сборщиком мусора Python при сборке мусора генератором.

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

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

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

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

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

Две встроенные функции 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, start=0) отсчитывает элементы в iterable, возвращающем 2 кортежа, содержащих количество (от 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) собирает все элементы iterable в список, сортирует список и возвращает отсортированный результат. Аргументы 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, если какой-либо элемент в iterable является истинным значением, и 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)

Он не создает список в памяти и не исчерпывает все входные итераторы перед возвратом; вместо этого кортежи создаются и возвращаются только в том случае, если они запрошены. (Технический термин для такого поведения - lazy evaluation.)

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

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) предполагает, что iterable вернет поток кортежей, и вызывает 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(), возвращая все элементы, для которых предикат возвращает значение false:

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

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

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

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 имеет значение true, останавливаясь всякий раз, когда какой-либо из них исчерпан:

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

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

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

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() собирает все последовательные элементы из базовой iterable, которые имеют одинаковое значение ключа, и возвращает поток из 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() предполагается, что исходное содержимое iterable уже будет отсортировано на основе ключа. Обратите внимание, что возвращаемые итераторы также используют исходный iterable, поэтому вам необходимо использовать результаты 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]) совокупно выполняет операцию со всеми элементами iterable и, следовательно, не может быть применена к бесконечным iterable. 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 уже упоминался ранее. Он содержит набор функций, соответствующих операторам Python. Эти функции часто полезны в коде функционального стиля, поскольку они избавляют вас от написания тривиальных функций, выполняющих одну операцию.

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

  • Математические операции: add(), sub(), mul(), floordiv(), abs(), …

  • Логические операции: not_(), truth().

  • Побитовые операции: and_(), or_(), invert().

  • Сравнения: eq(), ne(), lt(), le(), gt(), и ge().

  • Идентичность объекта: is_(), is_not().

Полный список можно найти в документации к операторскому модулю.

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

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

Если есть встроенная функция 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. Удалите комментарий.

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

История изменений и подтверждения

Автор хотел бы поблагодарить следующих людей за предложения, исправления и помощь в работе над различными вариантами этой статьи: Иэна Бикинга, Ника Коглана, Ника Эффорда, Рэймонда Хеттингера, Джима Джуэтта, Майка Крелла, Леандро Ламейро, Юсси Салмелу, Коллина Винтера, Блейка Уинтона.

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

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

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

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

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

Рекомендации

Общие

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

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

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

https://en.wikipedia.org/wiki/Coroutine : Запись для сопрограмм.

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

Специфичный для Python

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

Мерц также написал серию статей о функциональном программировании, состоящую из 3 частей, для сайта IBM developerWorks; смотрите part 1, part 2, и part 3,

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

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

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

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

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

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

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