Декоратор @lru_cache() модуля functools в Python.
Декоратор @lru_cache() модуля functools оборачивает функцию с переданными в нее аргументами и запоминает возвращаемый результат соответствующий этим аргументам. Такое поведение может сэкономить время и ресурсы, когда дорогая или связанная с вводом/выводом функция периодически вызывается с одинаковыми аргументами.
Позиционные и ключевые аргументы, переданные в функцию должны быть хешируемыми, т.к. для кэширования результатов декоратор lru_cache() использует словарь.
Различные аргументы можно рассматривать как отдельные вызовы с отдельными записями в кэше. Например f(a=1, b=2) и f(b=2, a=1) различаются по порядку ключевых аргументов и могут иметь две отдельные записи в кэше.
Если указана user_function , то она должна быть вызываемой. Это позволяет применять декоратор @lru_cache непосредственно к пользовательской функции, оставляя значение maxsize по умолчанию равным 128:
Если для параметра maxsize установлено значение None , функция LRU декоратора отключена и кэш может расти без ограничений. Функция LRU работает лучше всего, когда maxsize является степенью двойки.
Если для typed задано значение True , то аргументы функций разных типов будут кэшироваться отдельно. Например f(3) и f(3.0) будут рассматриваться как отдельные вызовы с разными результатами. Если введено typed=False , ТО реализация обычно будет рассматривать их как эквивалентные вызовы и кэшировать только один результат. (Некоторые типы, такие как str и int , могут кэшироваться отдельно, даже если передано False )
Обратите внимание: специфичность типа применяется только к непосредственным аргументам функции, а не к их содержимому. Скалярные аргументы Decimal(42) и Fraction(42) обрабатываются как отдельные вызовы с разными результатами. Напротив, аргументы кортежа ('answer', Decimal(42)) и ('answer', Fraction(42)) обрабатываются как эквивалентные.
Обернутая функция в этот декоратор будет иметь метод .cache_parameters() (Новое в версии 3.9), которая возвращает новый dict , показывающий значения maxsize и typed . Этот метод служит только для информационных целей. Изменение значений не имеет никакого эффекта.
Чтобы помочь измерить эффективность кэша и настроить параметр maxsize , декорированная функция получает метод .cache_info() , который возвращает именованный кортеж, показывающий hits , misses , maxsize и currsize . В многопоточной среде hits и misses являются приблизительными.
Декоратор также предоставляет метод .cache_clear() для очистки или аннулирования кэша.
Исходная базовая функция доступна через атрибут __wrapped__ , что полезно для самоанализа, для обхода кеша или для преобразования функции в другой кеш.
Кэш LRU работает лучше всего, т.к. алгоритм кэширования LRU, при наличии установленного параметра maxsize , в первую очередь вытесняет неиспользованные дольше всех кэшированные значения. Ограничение размера кеша гарантирует, что кеш не будет расти без привязки к длительным процессам, таким как веб-серверы.
В общем случае кэш LRU следует использовать только тогда, когда вы хотите повторно использовать ранее вычисленные значения. Нет смысла кэшировать функции, которые должны создавать различные изменяемые объекты при каждом вызове или нечистые функции, такие как time.time() или random.random() .
Примеры использования:
Пример кэша LRU для статического веб-контента:
Пример эффективного вычисления чисел Фибоначчи с использованием кэша для реализации техники динамического программирования:
functools — Higher-order functions and operations on callable objects¶
The functools module is for higher-order functions: functions that act on or return other functions. In general, any callable object can be treated as a function for the purposes of this module.
The functools module defines the following functions:
@ functools. cache ( user_function ) ¶
Simple lightweight unbounded function cache. Sometimes called “memoize”.
Returns the same as lru_cache(maxsize=None) , creating a thin wrapper around a dictionary lookup for the function arguments. Because it never needs to evict old values, this is smaller and faster than lru_cache() with a size limit.
New in version 3.9.
Transform a method of a class into a property whose value is computed once and then cached as a normal attribute for the life of the instance. Similar to property() , with the addition of caching. Useful for expensive computed properties of instances that are otherwise effectively immutable.
The mechanics of cached_property() are somewhat different from property() . A regular property blocks attribute writes unless a setter is defined. In contrast, a cached_property allows writes.
The cached_property decorator only runs on lookups and only when an attribute of the same name doesn’t exist. When it does run, the cached_property writes to the attribute with the same name. Subsequent attribute reads and writes take precedence over the cached_property method and it works like a normal attribute.
The cached value can be cleared by deleting the attribute. This allows the cached_property method to run again.
Note, this decorator interferes with the operation of PEP 412 key-sharing dictionaries. This means that instance dictionaries can take more space than usual.
Also, this decorator requires that the __dict__ attribute on each instance be a mutable mapping. This means it will not work with some types, such as metaclasses (since the __dict__ attributes on type instances are read-only proxies for the class namespace), and those that specify __slots__ without including __dict__ as one of the defined slots (as such classes don’t provide a __dict__ attribute at all).
If a mutable mapping is not available or if space-efficient key sharing is desired, an effect similar to cached_property() can be achieved by a stacking property() on top of cache() :
New in version 3.8.
Transform an old-style comparison function to a key function . Used with tools that accept key functions (such as sorted() , min() , max() , heapq.nlargest() , heapq.nsmallest() , itertools.groupby() ). This function is primarily used as a transition tool for programs being converted from Python 2 which supported the use of comparison functions.
A comparison function is any callable that accepts two arguments, compares them, and returns a negative number for less-than, zero for equality, or a positive number for greater-than. A key function is a callable that accepts one argument and returns another value to be used as the sort key.
For sorting examples and a brief sorting tutorial, see Sorting HOW TO .
New in version 3.2.
Decorator to wrap a function with a memoizing callable that saves up to the maxsize most recent calls. It can save time when an expensive or I/O bound function is periodically called with the same arguments.
Since a dictionary is used to cache results, the positional and keyword arguments to the function must be hashable.
Distinct argument patterns may be considered to be distinct calls with separate cache entries. For example, f(a=1, b=2) and f(b=2, a=1) differ in their keyword argument order and may have two separate cache entries.
If user_function is specified, it must be a callable. This allows the lru_cache decorator to be applied directly to a user function, leaving the maxsize at its default value of 128:
If maxsize is set to None , the LRU feature is disabled and the cache can grow without bound.
If typed is set to true, function arguments of different types will be cached separately. If typed is false, the implementation will usually regard them as equivalent calls and only cache a single result. (Some types such as str and int may be cached separately even when typed is false.)
Note, type specificity applies only to the function’s immediate arguments rather than their contents. The scalar arguments, Decimal(42) and Fraction(42) are be treated as distinct calls with distinct results. In contrast, the tuple arguments (‘answer’, Decimal(42)) and (‘answer’, Fraction(42)) are treated as equivalent.
The wrapped function is instrumented with a cache_parameters() function that returns a new dict showing the values for maxsize and typed. This is for information purposes only. Mutating the values has no effect.
To help measure the effectiveness of the cache and tune the maxsize parameter, the wrapped function is instrumented with a cache_info() function that returns a named tuple showing hits, misses, maxsize and currsize.
The decorator also provides a cache_clear() function for clearing or invalidating the cache.
The original underlying function is accessible through the __wrapped__ attribute. This is useful for introspection, for bypassing the cache, or for rewrapping the function with a different cache.
The cache keeps references to the arguments and return values until they age out of the cache or until the cache is cleared.
An LRU (least recently used) cache works best when the most recent calls are the best predictors of upcoming calls (for example, the most popular articles on a news server tend to change each day). The cache’s size limit assures that the cache does not grow without bound on long-running processes such as web servers.
In general, the LRU cache should only be used when you want to reuse previously computed values. Accordingly, it doesn’t make sense to cache functions with side-effects, functions that need to create distinct mutable objects on each call, or impure functions such as time() or random().
Example of an LRU cache for static web content:
Example of efficiently computing Fibonacci numbers using a cache to implement a dynamic programming technique:
Кэширование в Python: алгоритм LRU
Эта публикация – незначительно сокращенный перевод статьи Сантьяго Валдаррама Caching in Python Using the LRU Cache Strategy. Переведенный текст также доступен в виде блокнота Jupyter.
Кэширование – один из подходов, который при правильном использовании значительно ускоряет работу и снижает нагрузку на вычислительные ресурсы. В модуле стандартной библиотеки Python functools реализован декоратор @lru_cache , дающий возможность кэшировать вывод функций, используя стратегию Least Recently Used (LRU, «вытеснение давно неиспользуемых»). Это простой, но мощный метод, который позволяет использовать в коде возможности кэширования.
В этом руководстве мы рассмотрим:
- какие стратегии кэширования доступны и как их реализовать с помощью декораторов;
- что такое LRU и как работает этот подход;
- как повысить производительность программы с помощью декоратора @lru_cache ;
- как расширить функциональность декоратора @lru_cache и прекратить кэширование по истечении определенного интервала времени.
Кэширование в Python: в чем польза
Кэширование – это метод оптимизации хранения данных, при котором операции с данными производятся эффективнее, чем в их источнике.
Представим, что мы создаем приложение для чтения новостей, которое агрегирует новости из различных источников. Пользователь перемещается по списку, приложение загружает статьи и отображает их на экране.
Как поступит программа, если читатель решит сравнить пару статей и станет многократно между ними перемещаться? Без кэширования приложению придется каждый раз получать одно и то же содержимое. В этом случае неэффективно используется и система пользователя, и сервер со статьями, на котором создается дополнительная нагрузка.
Лучшим подходом после получения статьи было бы хранить контент локально. Когда пользователь в следующий раз откроет статью, приложение сможет открыть контент из сохраненной копии, вместо того, чтобы заново загружать материал из источника. В информатике этот метод называется кэшированием.
Реализация кэширования в Python посредством словаря
В Python можно реализовать кэширование, используя словарь. Вместо того, чтобы каждый раз обращаться к серверу, можно проверять, есть ли контент в кэше, и опрашивать сервер только если контента нет. В качестве ключа можно использовать URL статьи, а в качестве значения – ее содержимое:
Примечание. Для запуска этого примера у вас должна быть установлена библиотека requests :
Хотя вызов get_article() выполняется дважды, статья с сервера загружается лишь один раз. После первого доступа к статье мы помещаем ее URL и содержимое в словарь cache . Во второй раз код не требует повторного получения элемента с сервера.
Стратегии кэширования
В этой простой реализации кэширования закралась очевидная проблема: содержимое словаря будет неограниченно расти: чем больше статей открыл пользователь, тем больше было использовано места в памяти.
Чтобы обойти эту проблему, нам нужна стратегия, которая позволит программе решить, какие статьи пора удалить. Существует несколько различных стратегий, которые можно использовать для удаления элементов из кэша и предотвращения превышения его максимального размера. Пять самых популярных перечислены в таблице.
Стратегия | Какую запись удаляем | Эти записи чаще других используются повторно |
First-In/First-Out (FIFO) | Самая старая | Новые |
Last-In/First-Out (LIFO) | Самая недавняя | Старые |
Least Recently Used (LRU) | Использовалась наиболее давно | Недавно прочитанные |
Most Recently Used (MRU) | Использовалась последней | Прочитанные первыми |
Least Frequently Used (LFU) | Использовалась наиболее редко | Использовались часто |
Погружаемся в идею LRU-кэширования
Кэш, реализованный посредством стратегии LRU, упорядочивает элементы в порядке их использования. Каждый раз, когда мы обращаемся к записи, алгоритм LRU перемещает ее в верхнюю часть кэша. Таким образом, алгоритм может быстро определить запись, которая дольше всех не использовалась, проверив конец списка.
На следующем рисунке показано представление кэша после того, как пользователь запросил статью из сети.
Процесс заполнения LRU-кэша, шаг 1
Статья сохраняется в последнем слоте кэша перед тем, как будет передана пользователю. На следующем рисунке показано, что происходит, когда пользователь запрашивает следующую статью.
Процесс заполнения LRU-кэша, шаг 2
Вторая статья занимает последний слот, перемещая первую статью вниз по списку.
Стратегия LRU предполагает: чем позже использовался объект, тем больше вероятность, что он понадобится в будущем. Алгоритм сохраняет такой объект в кэше в течение максимально длительного времени.
Заглядываем за кулисы кэша LRU
Один из способов реализовать кэш LRU в Python – использовать комбинацию двусвязного списка и хеш-таблицы. Головной элемент двусвязного списка указывает на последнюю запрошенную запись, а хвостовой – на наиболее давно использовавшуюся.
На рисунке ниже показана возможная структура реализации кэша LRU.
Схема реализации кэширования
Используя хеш-таблицу, мы обеспечиваем доступ к каждому элементу в кэше, сопоставляя каждую запись с определенным местом в двусвязном списке. При этом доступ к недавно использовавшемуся элементу и обновление кэша – это операции, выполняемые за константное время (то есть с временной сложностью алгоритма ( 1 ) .
Начиная с версии 3.2, для реализации стратегии LRU Python включает декоратор @lru_cache .
Использование @lru_cache для реализации кэша LRU в Python
Декоратор @lru_cache за кулисами использует словарь. Результат выполнения функции кэшируется под ключом, соответствующим вызову функции и предоставленным аргументам. То есть чтобы декоратор работал, аргументы должны быть хешируемыми.
Наглядное представление алгоритма: перепрыгиваем ступеньки
Представим, что мы хотим определить число способов, которыми можем достичь определенной ступеньки на лестнице. Сколько есть способов, например, добраться до четвертой ступеньки, если мы можем переступить-перепрыгнуть 1, 2, 3 (но не более) ступеньки? На рисунке ниже представлены соответствующие комбинации.
Под каждым из рисунков приведен путь с указанием числа ступенек, преодоленных за один прыжок. При этом количество способов достижения четвертой ступеньки равно общему числу способов, которыми можно добраться до третьей, второй и первой ступенек:
Получается, что решение задачи можно разложить на более мелкие подзадачи. Чтобы определить различные пути к четвертой ступеньке, мы можем сложить четыре способа достижения третьей ступеньки, два способа достижения второй ступеньки и единственный способ для первой. То есть можно использовать рекурсивный подход.
Опишем программно рекурсивное решение в точности, как мы его сейчас видим:
Код работает для 4 ступенек. Давайте проверим, как он подсчитает число вариантов для лестницы из 30 ступенек.
Получилось свыше 53 млн. комбинаций. Однако когда мы искали решение для тридцатой ступеньки, сценарий мог длиться довольно долго.
Засекаем время выполнения программного кода
Измерим, как долго длится выполнение кода.
Для этого мы можем использовать модуль Python timeit или соответствующую команду в блокноте Jupyter.
Количество секунд зависит от характеристик используемого компьютера. В моей системе расчет занял 3 секунды, что довольно медленно для всего тридцати ступенек. Это решение можно значительно улучшить c помощью мемоизации.
Использование мемоизации для улучшения решения
Наша рекурсивная реализация решает проблему, разбивая ее на более мелкие шаги, которые дополняют друг друга. На следующем рисунке показано дерево для семи ступенек, в котором каждый узел представляет определенный вызов steps_to() :
Дерево вызовов функции step_to()
Можно заметить, что алгоритму приходится вызывать steps_to() с одним и тем же аргументом несколько раз. Например, steps_to(5) вычисляется два раза, steps_to(4) – четыре раза, steps_to(3) – семь раз и т. д. Вызов одной и той же функции несколько раз запускает вычисления, в которых нет необходимости – результат всегда один и тот же.
Чтобы решить эту проблему, мы можем использовать мемоизацию: мы сохраняем в памяти результат, полученный для одних и тех же входных значений и затем возвращаем при следующем аналогичном запросе. Прекрасная возможность применить декоратор @lru_cache !
Импортируем декоратор из модуля functools и применим к основной функции.
От единиц секунд к десяткам наносекунд – потрясающее улучшение, обязанное тем, что за кулисами декоратор @lru_cache сохраняет результаты вызова steps_to() для каждого уникального входного значения.
Другие возможности @lru_cache
Подключив декоратор @lru_cache , мы сохраняем каждый вызов и ответ в памяти для последующего доступа, если они потребуются снова. Но сколько таких комбинаций мы можем сохранить, пока не иссякнет память?
У декоратора @lru_cache есть атрибут maxsize , определяющий максимальное количество записей до того, как кэш начнет удалять старые элементы. По умолчанию maxsize равен 128. Если мы присвоим maxsize значение None , то кэш будет расти без всякого удаления записей. Это может стать проблемой, если мы храним в памяти слишком много различных вызовов.
Применим @lru_cache с использованием атрибута maxsize и добавим вызов метода cache_info() :
Мы можем использовать информацию, возвращаемую cache_info() , чтобы понять, как работает кэш, и настроить его, чтобы найти подходящий баланс между скоростью работы и объемом памяти:
- hits=52 – количество вызовов, которые @lru_cache вернул непосредственно из памяти, поскольку они присутствовали в кэше;
- misses=30 – количество вызовов, которые взяты не из памяти, а были вычислены (в случае нашей задачи это каждая новая ступень);
- maxsize=16 – это размер кэша, который мы определили, передав его декоратору;
- currsize=16 – текущий размер кэша, в этом случае кэш заполнен.
Добавление срока действия кэша
Перейдем от учебного примера к более реалистичному. Представьте, что мы хотим отслеживать появление на ресурсе Real Python новых статей, содержащих в заголовке слово python – выводить название, скачивать статью и отображать ее объем (число символов).
Real Python предоставляет протокол Atom, так что мы можем использовать библиотеку feedparser для анализа канала и библиотеку requests для загрузки содержимого статьи, как мы это делали раньше.
Скрипт будет работать непрерывно, пока мы не остановим его, нажав [Ctrl + C] в окне терминала (или не прервем выполнение в Jupyter-блокноте).
Код загружает и анализирует xml-файл из RealPython. Далее цикл перебирает первые пять записей в списке. Если слово python является частью заголовка, код печатает заголовок и длину статьи. Затем код «засыпает» на 5 секунд, после чего вновь запускается мониторинг.
Каждый раз, когда сценарий загружает статью, в консоль выводится сообщение «Получение статьи с сервера. ». Если мы позволим скрипту работать достаточно долго, мы увидим, что это сообщение появляется повторно даже при загрузке той же ссылки.
Мы можем использовать декоратор @lru_cache , однако содержание статьи со временем может измениться. При первой загрузке статьи декоратор сохранит ее содержимое и каждый раз будет возвращать одни и те же данные. Если сообщение обновлено, то сценарий мониторинга никогда об этом не узнает. Чтобы решить эту проблему, мы должны установить срок хранения записей в кэше.
Критерии исключения записей из кэша
Мы можем реализовать описанную идею в новом декораторе, который расширяет @lru_cache . Кэш должен возвращать результат на запрос только, если срок кэширования записи еще не истек – в обратном случае результат должен забираться с сервера. Вот возможная реализация нового декоратора:
Декоратор @timed_lru_cache реализует функциональность для оперирования временем жизни записей в кэше (в секундах) и максимальным размером кэша.
Код оборачивает функцию декоратором @lru_cache . Это позволяет нам использовать уже знакомую функциональность кэширования.
Перед доступом к записи в кэше декоратор проверяет, не наступила ли дата истечения срока действия. Если это так, декоратор очищает кэш и повторно вычисляет время жизни и срок действия. Время жизни распространяется на кэш в целом, а не на отдельные статьи.
Кэширование статей с помощью нового декоратора
Теперь мы можем использовать новый декоратор @timed_lru_cache с функцией monitor() , чтобы предотвратить скачивание с сервера содержимого статьи при каждом новом запросе. Собрав код в одном месте, получим следующий результат:
Обратите внимание, как код печатает сообщение «Получение статьи с сервера . » при первом доступе к соответствующим статьям. После этого, в зависимости от скорости cети, сценарий будет извлекать статьи из кэша несколько раз, прежде чем снова обратится к серверу.
В приведенном примере скрипт пытается получить доступ к статьям каждые 5 секунд, а срок действия кэша истекает раз в минуту.
Заключение
Кэширование – важный метод оптимизации, повышающий производительность любой программной системы. Понимание того, как работает кэширование, является фундаментальным шагом на пути к его эффективному включению в программный код.
В этом уроке мы кратко рассмотрели:
- какие бывают стратегии кэширования;
- как работает LRU-кэширование в Python;
- как использовать декоратор @lru_cache ;
- как рекурсивный подход в сочетании с кэшированием помогает достаточно быстро решить задачу.
Следующим шагом к реализации различных стратегий кэширования в ваших приложениях может стать библиотека cachetools, предоставляющая особые типы данных и декораторы, охватывающие самые популярные стратегии кэширования.
Functools – сила функций высшего порядка в Python
В стандартной библиотеке Python есть множество замечательных модулей, которые помогают делать ваш код чище и проще, и functools определенно является одним из них. В этом модуле есть множество полезных функций высшего порядка, которые можно использовать для кэширования, перегрузки, создания декораторов и в целом для того, чтобы делать код более функциональным, поэтому давайте отправимся на экскурсию по этому модулю и посмотрим, что он может нам предложить.
Кэширование
Давайте начнем с самых простых, но довольно мощных функций модуля functools . Начнем с функций кэширования (а также декораторов) — lru_cache , cache и cached_property . Первая из них — lru_cache предоставляет кэш последних результатов выполнения функций, или другими словами, запоминает результат их работы:
В этом примере мы делаем GET-запросы и кэшируем их результаты (до 32 результатов) с помощью декоратора @lru_cache . Чтобы увидеть, действительно ли кэширование работает, можно проверить информацию о кэше функции, с помощью метода cache_info , который показывает количество удачных и неудачных обращений в кэш. Декоратор также предоставляет методы clear_cache и cache_parameters для аннулирования кэшированных результатов и проверки параметров, соответственно.
Если вам нужно более детализированное кэширование, можете включить необязательный аргумент typed=true , что позволяет кэшировать аргументы разных типов по отдельности.
Еще один декоратор для кэширования в functools – это функция, которая называется просто cache . Она является простой оберткой lru_cache , которая опускает аргумент max_size , уменьшая его, и не удаляет старые значения.
Еще один декоратор, который вы можете использовать для кэширования – это cached_property . Как можно догадаться из названия, он используется для кэширования результатов атрибутов класса. Механика очень полезная, если у вас есть свойство, которое дорого вычислять, но оно при этом остается неизменным.
Этот простой пример показывает. Как можно использовать cached_property , например, для кэширования отрисованной HTML — страницы, которая должна снова и снова показываться пользователю. То же самое можно сделать для определенных запросов к базе данных или долгих математических вычислений.
Еще одна прелесть cached_property в том, что он запускается только при поиске, поэтому позволяет нам менять значение атрибута. После изменения атрибута закэшированное ранее значение меняться не будет, вместо этого будет вычислено и закэшировано новое значение. А еще кэш можно очистить, и все, что нужно для этого сделать – это удалить атрибут.
Я хочу закончить этот раздел предостережением относительно всех вышеперечисленных декораторов – не используйте их, если у вашей функции есть какие-то побочные эффекты или если она при каждом вызове создает изменяемые объекты, поскольку это явно не те функции, которые вы захотите кэшировать.
Сравнение и упорядочивание
Вероятно, вы уже знаете, что в Python можно реализовать операторы сравнения, такие как < , >= или == , с помощью lt , gt или eq . Однако, может быть довольно неприятно реализовывать каждый из eq , lt , le , gt или ge . К счастью, в functools есть декоратор @total_ordering , который может помочь нам в этом, ведь все, что нам нужно реализовать – это eq и один из оставшихся методов, а остальные декоратор сгенерирует автоматически.
Так мы можем реализовать все расширенные операции сравнения несмотря на то, что руками написали только eq и lt . Наиболее очевидным преимуществом является удобство, которое заключается в том, что не нужно писать все эти дополнительные волшебные метода, но, вероятно, важнее здесь уменьшение количества кода и его лучшая читаемость.
Перегрузка
Вероятно, всех нас учили, что перегрузки в Python нет, но на самом деле есть простой способ реализовать ее с помощью двух функций из functools , а именно singledispatch и/или singledispatchmethod . Эти функции помогают нам реализовать то, что мы бы назвали алгоритмом множественной диспетчеризации, который позволяет динамически типизированным языкам программирования, таким как Python, различать типы во время выполнения.
Учитывая, что перегрузка функций сама по себе является довольно обширной темой, я посвятил отдельную статью singledispatch и singledispatchmethod . Если вы хотите узнать о теме побольше, вы можете прочитать о ней здесь.
Partial
Мы все работаем с различными внешними библиотеками или фреймворками, многие из которых предоставляют функции и интерфейсы, требующие от нас передачи коллбэков, например, для асинхронных операций или прослушивания событий. В этом нет ничего нового, но что, если нам нужно еще и передать некоторые аргументы вместе с коллбэком. Именно здесь и пригодится functools.partial . Можно использовать partial для замораживания некоторых (или всех) аргументов функции, создавая новый объект с упрощенной сигнатурой функции. Запутались? Давайте рассмотрим несколько примеров из практики:
Код выше показывает, как можно использовать partial для передачи функции ( output_result ) вместе с аргументом ( log=logger ) в качестве коллбэка. В этом случае мы воспользуемся multiprocessing.apply_async , которая асинхронно вычисляет результат функции ( concat ) и возвращает результат коллбэка. Однако apply_async всегда будет передавать результат в качестве первого аргумента, и, если мы хотим включить какие-то дополнительные аргументы, как в случае с log=logger , нужно использовать partial .
Мы рассмотрели достаточно продвинутый вариант использования, а более простым примером может быть обычное создание функции, которая пишет в stderr вместо stdout :
С помощью этого простого трюка мы создали новую callable-функцию, которая всегда будет передавать file=sys.stderr в качестве именованного аргумента для вывода, что позволяет нам упростить код и не указывать значение именованного аргумента каждый раз.
И последний хороший пример. Мы можем использовать partial в связке с малоизвестной функцией iter , чтобы создать итератор, передав вызываемый объект и sentinel в iter , что можно применить следующим образом:
Обычно при чтении файла мы хотим итерироваться по строкам, но в случае бинарных данных нам может понадобиться итерироваться по записям фиксированного размера. Сделать это можно, создав вызываемый объект с помощью partial , который считывает указанный чанк данных и передает их в iter для создания итератора. Затем этот итератор вызывает функцию чтения до тех пор, пока не дойдет до конца файла, всегда беря только указанный объем чанка ( RECORD_SIZE ). Наконец, по достижении конца файла возвращается значение sentinel (b») и итерация прекращается.
Декораторы
Мы уже говорили о кое-каких декораторах в прошлых разделах, но не о декораторах для создания еще большего количества декораторов. Одним из таких декораторов является functools.wraps . Чтобы понять зачем он нужен, давайте просто посмотрим на пример:
Этот пример показывает, как можно реализовать простой декоратор. Мы оборачиваем функцию, выполняющую определенную задачу ( actual_func ), внешним декоратором, и она сама становится декоратором, который затем можно применить к другим функциям, например, как в случае с greet . При вызове функции greet вы увидите, что она выводит сообщения как от actual_func , так и свои собственные. Вроде выглядит нормально, не так ли? Но что будет, если мы сделаем так:
Когда мы вызываем имя и документацию декорированной функции, мы понимаем, что они были заменены значениями из функции декоратора. Это плохо, поскольку мы не можем перезаписывать все наши имена функций и документацию каждый раз при использовании какого-либо декоратора. Как же решить эту проблему? Конечно же, с помощью functools.wraps :
Единственная задача функции wraps – это копирование имени, документации, списка аргументов и т.д., для предотвращения перезаписи. Если учесть, что wraps – это тоже декоратор, можно просто добавить его в нашу actual_func , и проблема решена!
Reduce
Последнее, но не менее важное в модуле functools – это reduce . Возможно, из других языков, вы можете знать ее как fold (Haskell). Эта функция берет итерируемый объект и сворачивает (складывает) все его значения в одно. Применений этому множество, например:
Как видно из кода, reduce может упростить или сжать код в одну строку, которая в противном случае была бы намного длиннее. С учетом сказанного злоупотреблять этой функцией только ради сокращения кода, делать ее «умнее» — обычно плохая идея, поскольку она быстро становится страшной и нечитаемой. По этой причине, на мой взгляд, пользоваться ей стоит экономно.
А если помнить, что reduce частенько сокращает все до одной строки, ее отлично можно скомбинировать с partial :
И, наконец, если вам нужен не только итоговый «свернутый» результат, то вы можете использовать accumulate – из другого замечательного модуля itertools . Для вычисления максимума ее можно использовать следующим образом:
Заключение
Как видите, в functools есть множество полезных функций и декораторов, которые могут облегчить вам жизнь, но это лишь верхушка айсберга. Как я говорил вначале, в стандартной библиотеке Python есть множество функций, которые помогают писать код лучше, поэтому, помимо функций, которые мы здесь рассмотрели, вы можете обратить внимание на другие модули, такие как operator или itertools (мою статью об этом модуле вы можете прочитать здесь или просто отправляйтесь в Python Module Index и изучите все, на что обратите внимание, и я просто уверен, что вы найдете там что-то полезное.
Всех желающих приглашаем на онлайн-интенсив «Быстрая разработка JSON API приложений на Flask». На этом уроке мы:
— Познакомимся со спецификацией JSON API;
— Узнаем, что такое сериализация/десериализация данных;
— Узнаем, что такое marshmallow и marshmallow-jsonapi;
— Познакомимся со Swagger;
— Посмотрим на обработку и выдачу связей.