Functools: улучшаем функции Python

База языка программирования Python на самом деле довольно обширна и включает в себя множество отличных инструментов для решения различных задач программирования. И многие из этих инструментов заслуживают отдельную статью для их рассмотрения и изучения. Модуль functools — как раз такой инструмент.

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

Кэширование вычислений

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

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

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

Что хорошо в кэшировании, которое обеспечивает модуль functools, так это простота в использовании и при этом лучший контроль за работой интерпретатора.

Воспользоваться этой замечательной функцией очень просто. Достаточно просто поместить ее над своей функцией в виде декоратора.

[python_ad_block]

Кэширование при вычислении факториала

Этот пример, как нам кажется, в полной мере демонстрирует преимущества кэширования.

У нас есть функция:

def factorial(n):
    return n * factorial(n-1) if n else 1

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

from functools import lru_cache
@lru_cache
def factorial(n):
    return n * factorial(n-1) if n else 1

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

Теперь перезагрузим ядро блокнота Jupyter и запустим нашу функцию факториала вместе с lru_cache:

Уже на данном примере мы видим большую выгоду от применения данной техники кэширования.

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

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

«key»-функции

Случалась ли у вас ситуация, когда вы хотели бы использовать функцию из какого-нибудь старого кода, но она была скомпилирована как функция сравнения (функция с «cmp»-параметрами)? Этот тип функций уже особо не поддерживается и в современном Python 3 более не используется. Но при помощи модуля functools это проблему можно решить одним простым методом:

newfn = cmp_to_key(myfunction)

Функция partial

Функция partial() используется для частичного использования определенной функции. Часть аргументов этой функции «замораживается», в результате чего создается новый объект с упрощенной сигнатурой.

Код функции выглядит примерно так:

def partial(func, /, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = {**keywords, **fkeywords}
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

Создадим объект функции partial, используя при этом ту же функцию factorial, что и ранее.

from functools import partial
fact = partial(factorial)
fact(10)

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

Функция reduce

Функция reduce кумулятивно применяет функцию двух аргументов к итерируемой последовательности. Обратите внимание: последовательность непременно должна быть итерируемой.

Для данного примера мы будем использовать генератор списков range. Это позволит нам создать список практически любой длины.

from functools import reduce
reduce(lambda x, y: x+y, range(1, 6))

Что этот код будет делать? Он сведет итерируемую последовательность к единственному значению в соответствии с указанной математической функцией.

В данном случае код вычислит сумму всех элементов последовательности. Это экономит время выполнения операций и просто удобно при написании кода.

Диспетчеризация

С помощью модуля functools в языке программирования Python может быть эффективно реализован алгоритм множественной диспетчеризации (multiple dispatch). А реализуется это простым добавлением декоратора.

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

@singledispatch
def fun(arg, verbose=False):
    if verbose:
        print("Let me just say,", end=" ")
    print(arg

А далее воспользуемся декоратором наша_функция.register, чтобы организовать диспетчеризацию для различных типов данных:

@fun.register
def _(arg: int, verbose=False):
    if verbose:
        print("Strength in numbers, eh?", end=" ")
    print(arg)
@fun.register
def _(arg: list, verbose=False):
    if verbose:
        print("Enumerate this:")
    for i, elem in enumerate(arg):
        print(i, elem)

Заключение

Functools — действительно отличный модуль! Более того, мы считаем, что этот модуль наверняка пригодится практически любому программисту на Python.

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

Практически все интересные вещи, которые вы можете захотеть сделать с вашими функциями, можно сделать при помощи модуля Functools, а это просто очень здорово!

Перевод статьи «FuncTools: An Underrated Python Package».