Spec-Zone.ru › Python 3.10

Функциональное программирование 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)
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) нумерует элементы в итерируемом объекте, возвращая 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:

  1. Напишите функцию lambda.
  2. Напишите комментарий, объясняющий, что делает эта функция lambda.
  3. Посмотрите на комментарий некоторое время и придумайте имя, которое отражает суть комментария.
  4. Преобразуйте lambda в оператор def, используя это имя.
  5. Удалите комментарий.

Мне очень нравятся эти правила, но вы можете не согласиться с тем, что такой стиль без 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/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–2023 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.10/howto/functional.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API