Функциональное программирование 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 равно True, если X найден в потоке, возвращаемом итератором. Вы столкнётесь с очевидными проблемами, если итератор бесконечен; max(), min() никогда не вернутся, и если элемент X никогда не появится в потоке, операторы "in" и "not in" тоже не вернут значение.
Обратите внимание, что вы можете перемещаться только вперёд в итераторе; нет способа получить предыдущий элемент, перезапустить итератор или сделать его копию. Объекты итераторов могут необязательно предоставлять эти дополнительные возможности, но протокол итератора определяет только метод __next__(). Функции могут, таким образом, потреблять весь вывод итератора, и если вам нужно сделать что-то другое с тем же потоком, вам придётся создать новый итератор.
Типы данных, поддерживающие итераторы
Мы уже видели, как списки и кортежи поддерживают итераторы. В действительности любой тип последовательности Python, такой как строки, автоматически поддерживает создание итератора.
Вызов iter() для словаря возвращает итератор, который будет перебирать ключи словаря:
>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
... 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for key in m:
... print(key, m[key])
Jan 1
Feb 2
Mar 3
Apr 4
May 5
Jun 6
Jul 7
Aug 8
Sep 9
Oct 10
Nov 11
Dec 12
Обратите внимание, что начиная с Python 3.7 порядок итерации словарей гарантированно соответствует порядку вставки. В более ранних версиях поведение было неопределённым и могло меняться между реализациями.
Применение iter() к словарю всегда перебирает ключи, но словари имеют методы, которые возвращают другие итераторы. Если вы хотите перебрать значения или пары «ключ-значение», вы можете явно вызвать методы values() или items(), чтобы получить соответствующий итератор.
Конструктор dict() может принимать итератор, который возвращает конечный поток (key, value) кортежей:
>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}
Файлы также поддерживают итерацию, вызывая метод readline(), пока не закончатся строки в файле. Это означает, что вы можете читать каждую строку файла так:
for line in file:
# do something for each line
...
Множества могут получать своё содержимое из итерируемого объекта и позволяют перебирать элементы множества:
>>> S = {2, 3, 5, 7, 11, 13}
>>> for i in S:
... print(i)
2
3
5
7
11
13
Выражения-генераторы и списковые включения
Две распространённые операции над выводом итератора: 1) выполнение некоторой операции для каждого элемента, 2) выбор подмножества элементов, удовлетворяющих какому-либо условию. Например, при наличии списка строк вы можете захотеть удалить хвостовые пробелы из каждой строки или извлечь все строки, содержащие заданную подстроку.
Списковые включения и выражения-генераторы (краткое обозначение: «списковые включения» и «выражения-генераторы») представляют собой лаконичную запись для таких операций, позаимствованную из функционального языка программирования Haskell (https://www.haskell.org/). Вы можете удалить все пробелы из потока строк с помощью следующего кода:
>>> line_list = [' line 1\n', 'line 2 \n', ' \n', ''] >>> # Generator expression -- returns iterator >>> stripped_iter = (line.strip() for line in line_list) >>> # List comprehension -- returns list >>> stripped_list = [line.strip() for line in line_list]
Вы можете выбрать только определённые элементы, добавив "if" условие:
>>> stripped_list = [line.strip() for line in line_list ... if line != ""]
С помощью спискового включения вы получаете список Python; stripped_list — это список, содержащий результирующие строки, а не итератор. Выражения-генераторы возвращают итератор, который вычисляет значения по мере необходимости, не требуя материализации всех значений сразу. Это означает, что списковые включения не подходят, если вы работаете с итераторами, возвращающими бесконечный поток или очень большое количество данных. В этих ситуациях предпочтительнее использовать выражения-генераторы.
Выражения-генераторы заключены в круглые скобки («()»), а списковые включения — в квадратные скобки («[]»). Выражения-генераторы имеют вид:
( expression for expr in sequence1
if condition1
for expr2 in sequence2
if condition2
for expr3 in sequence3
...
if condition3
for exprN in sequenceN
if conditionN )
Опять же, для спискового включения различаются только внешние скобки (квадратные вместо круглых).
Элементы генерируемого вывода будут последовательными значениями expression. if — это все необязательные пункты. Если они присутствуют, expression оценивается и добавляется в результат только тогда, когда condition истинно.
Выражения-генераторы всегда должны быть написаны в круглых скобках, но также учитываются круглые скобки для вызова функции. Если вы хотите создать итератор, который будет немедленно передан в функцию, вы можете написать:
obj_total = sum(obj.count for obj in list_all_objects())
for...in содержат последовательности, которые нужно перебрать. Последовательности не обязательно должны иметь одинаковую длину, так как они перебираются слева направо, а не параллельно. Для каждого элемента в sequence1, sequence2 перебирается с самого начала. Затем sequence3 перебирается для каждой полученной пары элементов из sequence1 и sequence2.
Другими словами, списковое включение или выражение-генератор эквивалентны следующему коду Python:
for expr1 in sequence1:
if not (condition1):
continue # Skip this element
for expr2 in sequence2:
if not (condition2):
continue # Skip this element
...
for exprN in sequenceN:
if not (conditionN):
continue # Skip this element
# Output the value of
# the expression.
Это означает, что когда присутствуют несколько for...in пунктов, но нет if пунктов, длина результирующего вывода будет равна произведению длин всех последовательностей. Если у вас есть два списка длиной 3, список вывода имеет длину 9 элементов:
>>> seq1 = 'abc'
>>> seq2 = (1, 2, 3)
>>> [(x, y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3),
('b', 1), ('b', 2), ('b', 3),
('c', 1), ('c', 2), ('c', 3)]
Чтобы избежать двусмысленности в грамматике Python, если expression создаёт кортеж, он должен быть заключён в круглые скобки. Первое списковое включение ниже является синтаксической ошибкой, а второе — правильным:
# Syntax error [x, y for x in seq1 for y in seq2] # Correct [(x, y) for x in seq1 for y in seq2]
Генераторы
Генераторы — это специальный класс функций, упрощающий задачу написания итераторов. Обычные функции вычисляют значение и возвращают его, но генераторы возвращают итератор, который возвращает поток значений.
Вы, несомненно, знакомы с тем, как работают вызовы обычных функций в Python или C. При вызове функции создаётся частное пространство имён, в котором создаются её локальные переменные. Когда функция достигает оператора return, локальные переменные уничтожаются, а значение возвращается вызывающей стороне. Поздний вызов той же функции создаёт новое частное пространство имён и новый набор локальных переменных. Но что, если локальные переменные не удалялись при выходе из функции? Что, если вы могли бы позже продолжить выполнение функции с того места, где она остановилась? Именно это обеспечивают генераторы; их можно рассматривать как возобновляемые функции.
Вот самый простой пример функции-генератора:
>>> def generate_ints(N): ... for i in range(N): ... yield i
Любая функция, содержащая ключевое слово yield, является функцией-генератором; это обнаруживается компилятором байт-кода Python, который компилирует функцию специальным образом в результате.
При вызове функции-генератора она не возвращает единственное значение; вместо этого она возвращает объект генератора, поддерживающий протокол итератора. При выполнении выражения yield, генератор выводит значение i, аналогично оператору return. Главное отличие между yield и оператором return заключается в том, что при достижении yield состояние выполнения генератора приостанавливается, и локальные переменные сохраняются. При следующем вызове метода генератора __next__() функция возобновит выполнение.
Вот пример использования генератора generate_ints():
>>> gen = generate_ints(3) >>> gen <generator object generate_ints at ...> >>> next(gen) 0 >>> next(gen) 1 >>> next(gen) 2 >>> next(gen) Traceback (most recent call last): File "stdin", line 1, in <module> File "stdin", line 2, in generate_ints StopIteration
Вы могли бы также записать for i in generate_ints(5), или a, b, c =
generate_ints(3).
Внутри функции-генератора, return value вызывает StopIteration(value) быть поднятым из метода __next__(). После этого или достижения конца функции процесс значения прекращается, и генератор не может сгенерировать больше значений.
Вы могли бы достичь эффекта генераторов вручную, написав свой собственный класс и сохранив все локальные переменные генератора в качестве переменных экземпляра. Например, возврат списка целых чисел можно сделать, установив self.count в 0 и имея метод __next__() увеличение self.count и возвращение его. Однако для относительно сложного генератора написание соответствующего класса может быть намного сложнее.
Набор тестов, включённый в библиотеку Python, Lib/test/test_generators.py, содержит ряд более интересных примеров. Вот один генератор, реализующий обход дерева в порядке обхода в глубину с использованием рекурсивных генераторов.
# A recursive generator that generates Tree leaves in in-order.
def inorder(t):
if t:
for x in inorder(t.left):
yield x
yield t.label
for x in inorder(t.right):
yield x
Два других примера в test_generators.py дают решения задачи N ферзей (размещение N ферзей на шахматной доске NxN так, чтобы ни один ферзь не угрожал другому) и задачи путешествия коня (поиск маршрута, который перемещает коня по всем клеткам шахматной доски NxN без посещения любой клетки дважды).
Передача значений в генератор
В Python 2.4 и более ранних версиях генераторы только производили вывод. После вызова кода генератора для создания итератора не было возможности передать какую-либо новую информацию в функцию при возобновлении её выполнения. Вы могли бы подправить эту возможность, заставив генератор смотреть на глобальную переменную или передав некоторый изменяемый объект, который вызывающие стороны затем модифицируют, но эти подходы неуклюжи.
В Python 2.5 есть простой способ передачи значений в генератор. yield стал выражением, возвращающим значение, которое можно присвоить переменной или с которым можно выполнить другие операции:
val = (yield i)
Я рекомендую всегда заключать выражение yield в скобки, когда вы работаете со значениями, возвращаемыми генератором, как показано в примере выше. Скобки не всегда необходимы, но легче всегда добавлять их, чем помнить, когда они необходимы.
(PEP 342 объясняет точные правила, которые заключаются в том, что выражение yield всегда должно быть заключено в скобки, за исключением случаев, когда оно встречается в качестве выражения верхнего уровня в правой части оператора присваивания. Это означает, что вы можете написать val = yield i, но должны использовать скобки, когда присутствует операция, как в val = (yield i)
+ 12.)
Значения передаются в генератор путём вызова его метода send(value). Этот метод возобновляет код генератора, и выражение yield возвращает указанное значение. Если вызывается обычный метод __next__(), yield возвращает None.
Вот простой счётчик, который увеличивается на 1 и позволяет изменять значение внутреннего счётчика.
def counter(maximum):
i = 0
while i < maximum:
val = (yield i)
# If value provided, change counter
if val is not None:
i = val
else:
i += 1
И вот пример изменения счётчика:
>>> it = counter(10)
>>> next(it)
0
>>> next(it)
1
>>> it.send(8)
8
>>> next(it)
9
>>> next(it)
Traceback (most recent call last):
File "t.py", line 15, in <module>
it.next()
StopIteration
Поскольку yield часто будет возвращать None, вы всегда должны проверять этот случай. Не используйте его значение в выражениях, если вы не уверены, что метод send() будет единственным методом, используемым для возобновления вашей функции-генератора.
Помимо send(), существуют два других метода для генераторов:
-
throw(value)используется для поднятия исключения внутри генератора; исключение поднимается выражениемyieldв месте, где приостановлено выполнение генератора. -
close()поднимает исключениеGeneratorExitвнутри генератора для завершения итерации. При получении этого исключения код генератора должен либо поднятьGeneratorExit, либоStopIteration; перехват исключения и выполнение чего-либо ещё является незаконным и вызоветRuntimeError.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) нумерует элементы в итерируемом объекте, возвращая кортежи из двух элементов, содержащие номер (с начала 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(), возвращая все элементы, для которых предикат возвращает 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, что означает, что все элементы переставляются.
Обратите внимание, что эти функции генерируют все возможные комбинации по позиции и не требуют, чтобы элементы 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 содержит некоторые высшего порядка функции. Функция высшего порядка принимает на вход одну или несколько функций и возвращает новую функцию. Самым полезным инструментом в этом модуле является функция 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, написанному в функциональном стиле.
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/Partial_application: Статья о концепции частичного применения функций.
https://en.wikipedia.org/wiki/Currying: Статья о концепции каррирования.
Специфичные для Python
https://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–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/howto/functional.html