Функциональное программирование HOWTO
- Автор
-
А. М. Кучлинг
- Релиз
-
0.32
В данном документе мы рассмотрим возможности Python, подходящие для реализации программ в функциональном стиле. После введения в концепции функционального программирования мы рассмотрим такие языковые особенности, как итераторы и генераторы, а также соответствующие модули библиотек, такие как itertools и functools.
Введение
В этом разделе объясняется основная концепция функционального программирования; если вас интересуют только языковые особенности Python, переходите к следующему разделу о Итераторах.
Языки программирования поддерживают декомпозицию задач несколькими способами:
- Большинство языков программирования являются процедурными: программы представляют собой списки инструкций, которые сообщают компьютеру, что делать с входными данными программы. C, Pascal и даже оболочки Unix являются процедурными языками.
- В декларативных языках вы записываете спецификацию, описывающую задачу, которую нужно решить, а реализация языка определяет, как выполнить вычисление эффективно. SQL — это декларативный язык, с которым вы, скорее всего, знакомы; запрос SQL описывает набор данных, который вы хотите получить, а движок SQL решает, сканировать ли таблицы или использовать индексы, какие подзапросы должны выполняться первыми и т. д.
- Программы на объектно-ориентированных языках манипулируют коллекциями объектов. Объекты имеют внутреннее состояние и поддерживают методы, которые запросом или изменяют это внутреннее состояние каким-либо образом. Smalltalk и Java являются объектно-ориентированными языками. C++ и Python поддерживают объектно-ориентированное программирование, но не навязывают использование объектно-ориентированных функций.
- В функциональном программировании задача разбивается на набор функций. В идеале функции принимают только входные данные и производят выходные данные, и не имеют внутреннего состояния, которое влияет на выходные данные для данного входного значения. Известные функциональные языки включают семейство ML (Standard ML, OCaml и другие варианты) и Haskell.
Разработчики некоторых языков программирования выбирают для акцента определенный подход к программированию. Это часто затрудняет написание программ, использующих другой подход. Другие языки являются языками многопарадигмы, которые поддерживают несколько различных подходов. Lisp, C++ и Python являются языками многопарадигмы; вы можете писать программы или библиотеки, которые в значительной степени являются процедурными, объектно-ориентированными или функциональными во всех этих языках. В большой программе разные разделы могут быть написаны с использованием разных подходов; например, графический интерфейс может быть объектно-ориентированным, в то время как логика обработки — процедурной или функциональной.
В функциональной программе входные данные проходят через набор функций. Каждая функция обрабатывает входные данные и производит выходные данные. Функциональный стиль не рекомендует функции с побочными эффектами, которые изменяют внутреннее состояние или вносят другие изменения, которые не видны в возвращаемом значении функции.
Функции, которые вообще не имеют побочных эффектов, называются чисто функциональными. Избежание побочных эффектов означает, что не нужно использовать структуры данных, которые обновляются по мере выполнения программы; выходные данные каждой функции должны зависеть только от ее входных данных.
Некоторые языки очень строго относятся к чистоте и даже не имеют операторов присваивания, таких как a=3 или c = a + b, но трудно избежать всех побочных эффектов, таких как вывод на экран или запись в файл диска. Другой пример — вызов функции print() или time.sleep(), ни одна из которых не возвращает полезного значения. Оба вызываются только для их побочных эффектов — отправки текста на экран или приостановки выполнения на секунду.
Программы Python, написанные в функциональном стиле, обычно не будут доходить до крайности, избегая всего ввода-вывода или всех присваиваний; вместо этого они предоставят функциональный интерфейс, но будут использовать внутренне не функциональные функции. Например, реализация функции по-прежнему будет использовать присваивания локальным переменным, но не будет изменять глобальные переменные или иметь другие побочные эффекты.
Функциональное программирование можно рассматривать как противоположность объектно-ориентированному программированию. Объекты — это небольшие капсулы, содержащие некоторое внутреннее состояние вместе с набором вызовов методов, которые позволяют вам изменять это состояние, и программы состоят из создания правильного набора изменений состояния. Функциональное программирование стремится максимально избежать изменений состояния и работает с данными, передаваемыми между функциями. В Python вы можете комбинировать оба подхода, написав функции, которые принимают и возвращают экземпляры, представляющие объекты в вашем приложении (сообщения электронной почты, транзакции и т. д.).
Функциональная разработка может показаться странным ограничением. Почему следует избегать объектов и побочных эффектов? Существуют теоретические и практические преимущества функционального стиля:
- Формальная проверяемость.
- Модульность.
- Композиционность.
- Простота отладки и тестирования.
Формальная проверяемость
Теоретическое преимущество состоит в том, что легче построить математическое доказательство правильности функциональной программы.
В течение долгого времени исследователи интересовались способами математического доказательства правильности программ. Это отличается от тестирования программы на множестве входных данных и заключения о том, что ее выходные данные обычно верны, или чтения исходного кода программы и заключения о том, что код выглядит правильно; вместо этого целью является строгий вывод о том, что программа дает правильный результат для всех возможных входных данных.
Метод доказательства правильности программ заключается в написании инвариантов, свойств входных данных и переменных программы, которые всегда истинны. Затем для каждой строки кода необходимо показать, что если инварианты X и Y истинны до выполнения строки, то немного измененные инварианты X' и Y' истинны после выполнения строки. Это продолжается до тех пор, пока не будет достигнут конец программы, в котором инварианты должны соответствовать желаемым условиям выходных данных программы.
Избегание присваиваний в функциональном программировании возникло из-за того, что присваивания трудно обрабатывать с помощью этого метода; присваивания могут нарушать инварианты, которые были верны до присваивания, не создавая новых инвариантов, которые могут быть переданы дальше.
К сожалению, доказательство правильности программ в значительной степени непрактично и не имеет отношения к программному обеспечению Python. Даже тривиальные программы требуют доказательств, которые занимают несколько страниц; доказательство правильности умеренно сложной программы было бы огромным, и мало или ни одна из программ, которые вы используете ежедневно (интерпретатор Python, ваш парсер XML, ваш веб-браузер), не может быть доказана. Даже если вы написали или сгенерировали доказательство, тогда встает вопрос о проверке доказательства; возможно, в нем есть ошибка, и вы ошибочно считаете, что доказали правильность программы.
Модульность
Более практическое преимущество функционального программирования заключается в том, что оно заставляет вас разбить проблему на мелкие части. В результате программы становятся более модульными. Легче указать и написать небольшую функцию, которая выполняет одну задачу, чем большую функцию, которая выполняет сложное преобразование. Маленькие функции также легче читать и проверять на ошибки.
Простота отладки и тестирования
Тестирование и отладка программы в функциональном стиле проще.
Отладка упрощается, потому что функции обычно небольшие и четко определены. Когда программа не работает, каждая функция является точкой интерфейса, где вы можете проверить, что данные корректны. Вы можете посмотреть на промежуточные входные и выходные данные, чтобы быстро изолировать функцию, ответственного за ошибку.
Тестирование проще, потому что каждая функция является потенциальным объектом для модульного тестирования. Функции не зависят от состояния системы, которое необходимо воспроизвести перед запуском теста; вместо этого вам нужно только синтезировать правильный ввод, а затем проверить, соответствуют ли выходные данные ожиданиям.
Композиционность
Работая над программой в функциональном стиле, вы напишете ряд функций с различными входными и выходными данными. Некоторые из этих функций неизбежно будут специализированы для конкретного приложения, но другие будут полезны в широком диапазоне программ. Например, функция, которая принимает путь к каталогу и возвращает все XML-файлы в каталоге, или функция, которая принимает имя файла и возвращает его содержимое, может быть применена к множеству различных ситуаций.
Со временем вы создадите личную библиотеку утилит. Часто вы будете собирать новые программы, комбинируя существующие функции в новой конфигурации и написав несколько функций, специализированных для текущей задачи.
Итераторы
Начну с рассмотрения важной для написания функциональных программ особенности языка Python: итераторов.
Итератор — это объект, представляющий поток данных; этот объект возвращает данные по одному элементу за раз. Итератор Python должен поддерживать метод с именем __next__(), который не принимает аргументов и всегда возвращает следующий элемент потока. Если в потоке больше нет элементов, __next__() должен возбудить исключение StopIteration. Итераторы не обязательно должны быть конечными; вполне разумно написать итератор, генерирующий бесконечный поток данных.
Встроенная функция iter() принимает произвольный объект и пытается вернуть итератор, который вернёт содержимое или элементы объекта, возбуждая TypeError, если объект не поддерживает итерацию. Несколько встроенных типов данных Python поддерживают итерацию, наиболее распространёнными из которых являются списки и словари. Объект называется итерируемым, если для него можно получить итератор.
Вы можете поэкспериментировать с интерфейсом итерации вручную:
>>> L = [1, 2, 3] >>> it = iter(L) >>> it <...iterator object at ...> >>> it.__next__() # same as next(it) 1 >>> next(it) 2 >>> next(it) 3 >>> next(it) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>>
Python ожидает итерируемые объекты в нескольких контекстах, наиболее важным из которых является оператор for. В операторе for X in Y, Y должен быть итератором или объектом, для которого iter() может создать итератор. Эти два оператора эквивалентны:
for i in iter(obj):
print(i)
for i in obj:
print(i)
Итераторы могут быть материализованы в списки или кортежи с помощью конструкторских функций list() или tuple():
>>> L = [1, 2, 3] >>> iterator = iter(L) >>> t = tuple(iterator) >>> t (1, 2, 3)
Распаковка последовательностей также поддерживает итераторы: если вы знаете, что итератор вернёт N элементов, вы можете распаковать их в N-кортеж:
>>> L = [1, 2, 3] >>> iterator = iter(L) >>> a, b, c = iterator >>> a, b, c (1, 2, 3)
Встроенные функции, такие как max() и min(), могут принимать один аргумент-итератор и возвращать наибольший или наименьший элемент. Операторы "in" и "not in" также поддерживают итераторы: X in iterator истинно, если X найден в потоке, возвращаемом итератором. Возникнут очевидные проблемы, если итератор бесконечен; max(), min() никогда не вернут результат, и если элемент X никогда не появляется в потоке, операторы "in" и "not in" тоже не вернут результат.
Обратите внимание, что вы можете перемещаться только вперёд в итераторе; нет способа получить предыдущий элемент, перезапустить итератор или сделать его копию. Объекты-итераторы могут по желанию предоставлять эти дополнительные возможности, но протокол итератора определяет только метод __next__(). Поэтому функции могут потреблять весь выходной поток итератора, и если вам нужно сделать что-то другое с тем же потоком, вам придётся создать новый итератор.
Типы данных, которые поддерживают итераторы
Мы уже видели, как списки и кортежи поддерживают итераторы. В действительности любой тип последовательности Python, такой как строки, автоматически поддерживает создание итератора.
Вызов iter() для словаря возвращает итератор, который перебирает ключи словаря:
>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
... 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for key in m:
... print(key, m[key])
Jan 1
Feb 2
Mar 3
Apr 4
May 5
Jun 6
Jul 7
Aug 8
Sep 9
Oct 10
Nov 11
Dec 12
Обратите внимание, что начиная с Python 3.7 порядок итерации словарей гарантируется как порядок вставки. В более ранних версиях поведение было неопределённым и могло различаться между реализациями.
Применение iter() к словарю всегда перебирает ключи, но словари имеют методы, которые возвращают другие итераторы. Если вы хотите перебрать значения или пары «ключ-значение», вы можете явно вызвать методы values() или items(), чтобы получить соответствующий итератор.
Конструктор dict() может принимать итератор, который возвращает конечный поток (key, value) кортежей:
>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}
Файлы также поддерживают итерацию, вызывая метод readline(), пока в файле нет больше строк. Это означает, что вы можете читать каждую строку файла следующим образом:
for line in file:
# do something for each line
...
Множества могут принимать своё содержимое из итерируемого объекта и позволяют перебирать элементы множества:
S = {2, 3, 5, 7, 11, 13}
for i in S:
print(i)
Выражения-генераторы и списковые включения
Две распространённые операции над выходом итератора: 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(value)используется для повышения исключения внутри генератора; исключение возбуждается выражением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 элементов. Если вы укажете стартовую позицию start, вы получите 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(), возвращающая все элементы, для которых предикат возвращает 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() собирает все последовательные элементы из основного итерируемого объекта, имеющие одинаковое значение ключа, и возвращает поток 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, написанном Collin Winter; добавление короткого раздела о модуле operator; несколько других правок.
Ссылки
Общие
Структура и интерпретация компьютерных программ, написанная Гарольдом Абельсоном и Джеральдом Джеем Суссманом с Джули Суссман. Полный текст по адресу 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.9/howto/functional.html