Модуль functools на примерах

автор

Python содержит удобный модуль под названием functools. Функции внутри functools можно определить как функции высокого порядка, которые взаимодействуют и возвращают другие функции. В этой статье мы рассмотрим содержимое пакета functools:

  • lru_cache
  • partials
  • singledispatch
  • wraps

Давайте начнем с изучения создания простого кэша в Python.

Кэширование с functools.lru_cache

Модуль functools содержит весьма полезный декоратор под названием lru_cache. Обратите внимание на то, что он был добавлен в версии Python 3.2. Соответственно документации, этот декоратор «оборачивает функцию вызываемым запоминанием, которое сохраняет максимальное количество всех последних вызовов«. Другими словами, это декоратор, который добавляет кэширование к декорируемой функции. Давайте напишем быструю функцию, которая основана на примере из документации functools и охватывает кое-какие веб страницы. В нашем случае, мы охватим страницы из сайта документации Python.

В данном коде мы декорируем нашу функцию get_webpage при помощи lru_cache и указываем максимальный размер в 24 вызова. Далее мы устанавливаем переменную строки веб странице и передаем тот модуль, который нужно получить. На практике я обнаружил, что это работает лучше всего, если выполнить запуск в интерпретаторе Python, в таком как IDLE. Это позволит вам запустить цикл несколько раз над функцией Python. Когда вы запустите код, первое что вы заметите, что выдача выводится сравнительно медленно. Но если вы запустите его еще раз в той же сессии, вы увидите, что выдача появится мгновенно, что оговорит о том, что lru_cache кешировал вызовы корректно. Попробуйте сделать это лично в своем интерпретаторе, чтобы увидеть результат. Также существует типизированный параметр, который мы можем передать декоратору. Это Boolean, указывающий декоратору кешировать аргументы разных типов раздельно, если для типизации задано значение True.

functool.partial

Один из классов functools называется partial. Вы можете использовать для создания новой функции с частичным приложением аргументов и ключевых слов, которые вы передаете. Вы также можете использовать partial для «заморозки» части аргументов вашей функции и\или ключей, которые отображаются в новом объекте. Еще один способ применения partial это создание функции с разными настройками. Давайте взглянем на пример:

Здесь мы создали простую функцию добавления, которая возвращает результат добавленных ею аргументов x и y. Далее мы создаем новую вызываемую, создав экземпляр partial и передав его нашей функции, а также аргумент для этой функции. Другими словами, мы в целом присваиваем параметру х в нашей функции значение 2. Наконец, мы вызываем p_add с аргументом числа 4, что в результате дает 6, так как 2+4=6.
Еще одним удобным вариантом использования для partials является передача аргументов коллбекам. Девайте взглянем на пример, используя wx:

Здесь мы используем partial для вызова onButton, обработчика событий с дополнительным аргументом, который представлен в виде ярлыка кнопки. Это может выглядеть не слишком полезным, но если вы связанны с программированием графического интерфейса, вы заметите, как часто люди задаются вопросом «а как это сделать?». Конечно, вы также можете использовать lambda функцию вместо передачи аргументов коллбекам.

Один из способов применения используется в работе для автоматического теста Фреймворка. Мы тестируем пользовательский интерфейс при помощи Python и нам нужно быть в состоянии передать функцию, чтобы отменить определенные диалоги. Обычно вы можете передать функцию вместе с названием диалога для отмены, но для корректной работы его потребуется вызвать в конкретной точке в процессе. Так как я не могу показать тот код, вот очень простой пример передачи функции partial:

Здесь мы создаем несколько функций partial в нашей главной функции. Далее мы передаем их нашей функции run, вызываем её и затем выводим результат вызванной функции.

Перегрузка функции с functools.singledispatch

Совсем недавно в Python была добавлена поддержка partial для перегрузки функции в версии 3.4. Этот инструмент является аккуратным небольшим декоратором для модуля functools под названием singledispatch. Этот декоратор превращает вашу обычную функцию в функцию родовой рассылки. Однако обратите внимание на то, что singledispatch появляется только на основании типа первого аргумента. Давайте взглянем на пример, чтобы увидеть, как это работает.

Здесь мы импортировали singledispatch из functools и применили его в простой функцию, которую мы назвали add. Эта функция является всеохватывающей и может быть вызвана только в том случае, если никакие другие декорированные функции не обрабатывают переданный тип. Вы заметите, что мы обрабатываем целые числа, строки и списки как первый аргумент. Если мы вызовем нашу функцию add с чем-то еще, например, со словарем, то это приведет к ошибке NotImplementedError. Попробуйте запустить этот код самостоятельно. Вы увидите выдачу, которая выглядит примерно следующим образом:

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

Это выведет что-то на подобии этого:

Это говорит нам о том, что мы можем обрабатывать строки, целые числа, списки и объекты (по умолчанию). Декоратор singledispatch также поддерживает укладку. Это позволяет нам создавать перегруженную функцию, которая обрабатывается несколько раз. Давайте взглянем на пример:

Это, как правило, говорит Python о том, что одна из перегруженных функции add может обрабатывать типы float и decimal.Decimal в качестве первого аргумента. Если вы запустите этот код, вы увидите следующее:

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

functools.wraps

Мы рассмотрим не самый известный инструмент, под названием called, который также является частью модуля functools. Вы можете использовать его как декоратор для исправления docstrings и наименований декорированных функций.

Имеет ли это значение? Как минимум, звучит немного странно, но если вы пишете API, или любой другой код, который будет использоваться кем-нибудь другим, а не только вами, эта часть может быть весьма важной. Причина в том, что когда вы используете интроспекцию Python, что бы разобраться в чужом коде, декорированная функция выдаст неправильную информацию. Давайте взглянем на простой пример, который я продублировал из decorum.py:

В этом коде мы декорируем функцию под названием a_function с another_function. Вы можете проверить название функции и docstring, выведя их, используя свойства функции __name__ и __doc__ . Если вы запустите данный пример, вы получите следующую выдачу:

Это не правильно! Если вы запустите эту программу IDLE или в интерпретаторе, станет понятно, насколько это может запутать.

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

Спасение во wraps!

Как исправить этот бардак? Разработчики Python предоставили нам отличное решение в лице functools.wraps! Давайте посмотрим:

Здесь мы импортируем wraps из модуля functools и используем его в качестве декоратора для вложенной функции-обертки внутри another_function. Если вы запустите его сейчас, выдача изменится:

Теперь все названия прописаны правильно. Если вы перейдете в интерпретатор Python, функция help также будет работать корректно. Я пропущу копирование выдачи здесь, и хочу, чтобы вы попробовали лично сделать это.

Подведем итоги

Давайте подумаем. В этой статье вы научились базовому кэшированию с использованием lru_cache. После этого мы изучили partial, который позволяет нам «замораживать» часть аргументов и\или ключей в вашей функции, позволяя вам создавать новый объект, который вам нужно вызвать. Далее, мы использовали singledispatch для перегрузки функций в Python. Так как это только позволяет функции перегрузиться на основании первого аргумента, этот инструмент может оказаться весьма кстати в будущем! Наконец, мы рассмотрели wraps, который обладает весьма узкой спецификой: исправление docstring и названий функций, которые были декорированы таким образом, что у них нет docstring декоратора, или названия.

Вам может быть интересно