Одна интересная вещь в Python — это поддержка функционального программирования, в котором говорится, что этапы обработки выполняются с помощью функций.

К счастью, Python поставляется с полноценными пакетами, тем самым усиливая его мультипарадигмальную выигрышную карту, некоторые из которых (если не полностью) изначально реализованы на C. Фактически вы можете прочитать реализации в папках Lib / Modules в репозиторий CPython проекта Github Python.

В этой статье я расскажу о двух модулях функционального программирования, которые Python предлагает нам для выполнения различных функциональных вычислений: itertools and functools.

Я не буду воспроизводить шаблон, который вы найдете в официальной документации. Я просто хотел упомянуть некоторые функции, которые вы не очень часто встречаете в Интернете, но каким-то образом ухитрились быть там, чтобы спасти меня, когда я больше всего в них нуждался.

При всей благодарности, которую я испытываю к ним, давайте начнем.

1. Итертулс

Проще говоря, itertools позволяет эффективно зацикливаться.

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

1.1. цикл

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

>> from itertools import cycle
>> for el in cycle(range(10)): print(el, end="")
>> 0123456789012345 ...
>> ..

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

1.2. накапливать

Если вы хотите разработать какой-то аккумулятор с минимальным количеством строк кода:

>> from itertools import accumulate
>> list(accumulate(range(10)))
>> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]
>> ...

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

Лично мне потребовалось бы не менее 6 строк для реализации с чистыми встроенными функциями. Однако, поскольку потребность в правильном накопителе присутствует почти в каждой агрегации фреймов данных, вам может понравиться, как это работает.

1.3. группа по

На самом деле мне было очень весело использовать его как-то в нескольких моих проектах. Вот пример:

>>> from itertools import groupby
>>> data = 'AAAABBBCCDAABBB'
>>> for k, v in groupby(data):
...   print(k, len(list(v)))
... 
A 4
B 3
C 2
D 1
A 2
B 3

Она работает примерно так же, как функция numpy.unique, но имеет больше возможностей.
Каждый раз, когда значение ключевой функции изменяется, создается разрыв или новая группа. Это отличается от GROUP BY SQL, который группирует похожие данные независимо от порядка. Поэтому важно сначала отсортировать данные, прежде чем передавать их в itertools.groupby.

1.4. попарно

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

>>> from itertools import pairwise
>>> list(pairwise('ABCDEFG'))
... ['AB','BC','CD','DE','EF','FG']

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

1.5. звездная карта

Если вы когда-либо использовали карту для работы с итерируемым объектом, вы можете думать о звездной карте как об операторе карты, который разветвляется на небольшие итерируемые объекты. Давайте поработаем над небольшим примером:

>>> from itertools import starmap
>>> v = starmap(sum, [[range(5)], [range(5, 10)], [range(10, 15)]])
>>> list(v)
[10, 35, 60]
>>> ...

В этом примере sum применяется к каждому набору аргументов и создает список результатов с тем же номером. Важно видеть каждую итерацию как аргумент, а не как простой ввод, поэтому каждая итерация была помещена в скобки, иначе возникнет ошибка.

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

1.6. zip_длиннейший

zip также используется в Haskell. Это весьма полезно для параллельных итераций.
Однако он ожидает одинаковую длину от каждого из своих итерируемых аргументов.
Если вы попытались заархивировать две итерации разной длины, zip отрезает лишнюю часть более длинной.
zip_longest заполнит более короткую итерацию до той же длины, что и более длинная:

>>> from itertools import zip_longest
>>> list(zip_longest('ABCD', 'xy', fillvalue='-')) 
... [('A', 'x'), ('B', 'y'), ('C', '-'), ('D', '-')]

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

1.7. продукт

Скажем, у вас есть матрица, ожидающая обновления, и вы должны сначала просмотреть строки, а затем столбцы (или наоборот).
Вы бы сделали:

for i in ncolumns: 
    for j in nrows :
        ...

Есть более крутой способ соединить обе петли с функцией itertools под названием product:

>>> from itertools import product
>>> for i,j in product([0,1], [10,11]):
...   print(i,j)
... 
0 10
0 11
1 10
1 11

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

>>> for i, j in product(ncolumns, nrows):
    ...

Небольшой недостаток, это стирает любую возможность инициализировать промежуточные или временные переменные между двумя циклами. В конце концов, это зависит от того, чего вы хотите достичь.

Это, наверное, почти все itertools, которые я могу придумать, чтобы сделать мой день легче и круче. Я не буду подробно останавливаться на этом, поскольку в библиотеке никогда не заканчиваются сюрпризы, особенно рецепты itertools: они были приготовлены авторами, чтобы максимально использовать ранее рассмотренные функции, которые действуют как векторизованные и высокооптимизированные строительные блоки. В рецептах вы на самом деле видите, в какой степени эти функции имеют потенциал для создания более мощных инструментов с такой же высокой производительностью, как и базовый набор инструментов. Там творится что-то очень интересное, лучше сразу проверить...

2.функции

Проще говоря, functools — это модуль для функций более высокого порядка: они используют функции в качестве аргументов и выходных данных. Декораторы, свойства, кэши — это примеры функций высшего порядка, которые размещаются в виде аннотаций над определениями функций.
Давайте посмотрим на другие примеры.

2.1. частичный

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

Рассмотрим следующий набор массивов и весов:

>>> import numpy as np
>>> x1 = np.array([1,2,1])
>>> w1 = np.array([.2, .3, .2])
>>> n1 = 3
>>> 
>>> x2 = np.array([2,1,2])
>>> w2 = np.array([.1, .1, .05])
>>> n2 = 3
>>>

Давайте затем рассмотрим эту функцию, которая вычисляет средневзвешенные значения этих массивов:

>>> def weighted_means(x1, w1, n1, x2, w2, n2): 
...   return np.dot(x1,w1)/n1 , np.dot(x2,w2)/n2

Приводим функцию в действие:

>>> weighted_means(x1, w1, n1, x2, w2, n2)
... (0.3333333333333333, 0.13333333333333333)

Предположим, вы хотите уменьшить количество переменных аргументов, заморозив x2, w2 and n2 следующим образом:

>>> from functools import partial
>>> reduced_weighted_means = partial(weighted_means, x2 = x2 , w2 = w2 , n2 = n2)

Затем вы используете новую сокращенную функцию с уменьшенным количеством аргументов:

>>> reduced_weighted_means(x1, w1, n1)
... (0.3333333333333333, 0.13333333333333333)

Обратите внимание, что это тот же результат, что и раньше, что указывает на то, что функция работает так, как если бы фиксированные аргументы были введены статически. Это кажется идеальным обходным путем, когда функция используется с определенными ограничениями, связанными с множественностью аргументов.

2.2. частичный метод

partialmethod является экстраполяцией частичного, за исключением того, что он используется как метод класса. Давайте проиллюстрируем это на примере (очень глупом, я признаю):

Давайте проверим это:

>>> mn = ModelName('clustering')
>>> mn.set_cls()
>>> print(mn.algorithm)
>>> 'classification'
>>>
>>> mn.set_regr()
>>> print(mn.algorithm)
>>> 'regression'

Класс служит для хранения строки и делегирует роль установщика двум разделяемым методам. set_cls и set_regr работают так, как если бы они были должным образом определенными методами, и устанавливают для каждого algorithm разные значения. Небольшое предупреждение: после определения свойства algorithm не должно быть algorithm.setter.

2.3. разовая отправка

Предположим, вы определили простую функцию, которая выполняет некоторые инструкции, а затем решили сделать ее более универсальной:

Затем вы перегружаете функцию дополнительными реализациями, используя атрибут register() для zen_of_python, который теперь используется в качестве декоратора.

Вы определяете различные реализации в зависимости от типа аргумента:

Обратите внимание, как ведёт себя универсальная функция при обработке различных типов аргументов:

>>> zen_of_python('hello')
... Beautiful is better than ugly.
>>> zen_of_python(1)
... There should be one-- and preferably only one --obvious way to do it.
>>> zen_of_python(1.0)
... Readability counts.
>>> zen_of_python([1, 2, "a"])
... Namespaces are one honking great idea -- let's do more of those!

Результаты соответствуют нашим реализациям. Однако, поскольку универсальная функция еще не знает адекватной реализации для типа dict, она перейдет к реализации по умолчанию:

>>> zen_of_python(dict())
... Beautiful is better than ugly.

Это круто. Я знаю.

2.4. единый метод отправки

Наш последний гость: singledispatchmethod, который можно использовать внутри определения класса.
Может быть, пример?
Предположим, у вас есть класс, который выводит строку на основе типа введенного вами аргумента:

Прочь с небольшой демонстрацией:

>>> gen = Generic()
>>> gen.generic_method(1)
...'First case'
>>> gen.generic_method(1.0)
>>> 'Second case'
>>> gen.generic_method([1,2,3])
>>> 'Third case'

Давайте попробуем тип dict:

>>> gen.generic_method(dict(a=1, b=2))
Traceback (most recent call last):
    ...
  NotImplementedError: Never heard of this type ..

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

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

Официальная документация — itertools и functools