Руководство по функциональному программированию
- Автор
-
А. М. Кучлинг
- Версия
-
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, но сложно избежать всех побочных эффектов. Например, вывод на экран или запись в файл — это побочные эффекты. Например, в Python вызов функции 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)
Выражения-генераторы и списковые включения
Две распространённые операции с выходными данными итератора: 1) выполнение некоторой операции для каждого элемента, 2) выбор подмножества элементов, удовлетворяющих некоторому условию. Например, учитывая список строк, вы можете захотеть удалить хвостовые пробелы из каждой строки или извлечь все строки, содержащие заданную подстроку.
Списковые включения и выражения-генераторы (краткое обозначение: «список включений» и «генераторное выражение») представляют собой компактную запись для таких операций, позаимствованную у функционального языка программирования Haskell (https://www.haskell.org/). Вы можете удалить все пробелы из потока строк с помощью следующего кода:
line_list = [' line 1\n', 'line 2 \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(type, value=None, traceback=None)используется для повышения исключения внутри генератора; исключение повышается выражениемyieldв месте приостановки выполнения генератора.close()вызывает исключениеGeneratorExitвнутри генератора для завершения итерации. При получении этого исключения код генератора должен либо повыситьGeneratorExit, либоStopIteration; перехват исключения и выполнение чего-либо ещё является незаконным и вызоветRuntimeError.close()также будет вызван сборщиком мусора Python при удалении генератора из памяти.Если вам необходимо выполнить код очистки при возникновении
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) нумерует элементы в итерируемом объекте, возвращая кортежи из 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) собирает все элементы итерируемого объекта в список, сортирует список и возвращает отсортированный результат. Аргументы 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) возвращает итератор, предоставляющий все возможные комбинации 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() собирает все последовательные элементы из исходной итерируемой последовательности, которые имеют одинаковое значение ключа, и возвращает поток 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() предполагает, что содержимое исходной итерируемой последовательности уже отсортированы по ключу. Обратите внимание, что возвращаемые итераторы также используют исходную итерируемую последовательность, поэтому вы должны обработать результаты итератора-1 перед запросом итератора-2 и его соответствующего ключа.
Модуль functools
Модуль 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):
"""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().
Обратитесь к документации модуля operator для получения полного списка.
Малые функции и выражение 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:
- Напишите функцию lambda.
- Напишите комментарий, объясняющий, что делает эта lambda-функция.
- Посмотрите на комментарий некоторое время и придумайте имя, которое отражает суть комментария.
- Преобразуйте lambda-функцию в оператор def, используя это имя.
- Удалите комментарий.
Мне очень нравятся эти правила, но вы можете не согласиться с тем, что такой стиль без 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 года. Объединены разделы genexp и listcomp в один. Исправлены опечатки.
Версия 0.21: добавлены дополнительные ссылки, предложенные на списке рассылки tutor.
Версия 0.30: добавлен раздел о модуле functional , написанный Коллин Уинтер; добавлен короткий раздел о модуле operator; несколько других правок.
Ссылки
Общие
Структура и интерпретация компьютерных программ, авторы Harold Abelson и Gerald Jay Sussman с Julie Sussman. Полный текст по адресу https://mitpress.mit.edu/sicp/. В этой классической книге по информатике в главах 2 и 3 рассматривается использование последовательностей и потоков для организации потока данных внутри программы. В книге для примеров используется Scheme, но многие подходы к проектированию, описанные в этих главах, применимы к коду Python функционального стиля.
http://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
http://gnosis.cx/TPiP/: В первой главе книги Дэвида Мерца Text Processing in Python обсуждается функциональное программирование для обработки текста в разделе «Использование функций высшего порядка в обработке текста».
Мерц также написал серию статей из 3 частей о функциональном программировании для сайта IBM DeveloperWorks; см. часть 1, часть 2 и часть 3,
Документация Python
Документация для модуля itertools.
Документация для модуля functools.
Документация для модуля operator.
PEP 289: «Выражения-генераторы»
PEP 342: «Сопроцедуры с помощью расширенных генераторов» описывает новые возможности генераторов в Python 2.5.
© 2001–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.8/howto/functional.html