10.2. functools – Функции высшего порядка и операции над вызываемыми объектами
Исходный код: Lib/functools.py
Модуль functools предназначен для функций высшего порядка: функций, которые принимают другие функции в качестве аргументов или возвращают их. В общем случае любой вызываемый объект может рассматриваться как функция для целей этого модуля.
Модуль functools определяет следующие функции:
-
functools.cmp_to_key(func)¶ Преобразует функцию сравнения старого образца в функцию ключа. Используется с инструментами, которые принимают функции ключа (такими как
sorted(),min(),max(),heapq.nlargest(),heapq.nsmallest(),itertools.groupby()). Эта функция в первую очередь используется как переходный инструмент для программ, переносимых с Python 2, который поддерживал использование функций сравнения.Функция сравнения – это любой вызываемый объект, который принимает два аргумента, сравнивает их и возвращает отрицательное число, если первый меньше второго, ноль, если они равны, или положительное число, если первый больше второго. Функция ключа – это вызываемый объект, который принимает один аргумент и возвращает другое значение, используемое в качестве ключа сортировки.
Пример:
pythonsorted(iterable, key=cmp_to_key(locale.strcoll)) # порядок сортировки с учётом локалиДля примеров сортировки и краткого руководства по сортировке см. Sorting HOW TO.
Новое в версии 3.2.
-
@functools.lru_cache(maxsize=128, typed=False)¶ Декоратор для обёртки функции в запоминающий (мемоизирующий) вызываемый объект, который сохраняет до maxsize последних вызовов. Это может сэкономить время, когда дорогая или привязанная к вводу-выводу функция периодически вызывается с теми же аргументами.
Поскольку для кэширования результатов используется словарь, позиционные и именованные аргументы функции должны быть хешируемыми.
Если maxsize установлено в
None, механизм LRU отключается, и кэш может расти без ограничений. Механизм LRU работает наиболее эффективно, когда maxsize является степенью двойки.Если параметр typed установлен в true, аргументы функции разных типов будут кэшироваться отдельно. Например,
f(3)иf(3.0)будут рассматриваться как разные вызовы с разными результатами.Чтобы оценить эффективность кэша и настроить параметр maxsize, обёрнутая функция оснащается функцией
cache_info(), которая возвращает именованный кортеж, показывающий попаданий, промахов, maxsize и currsize. В многопоточной среде значения попаданий и промахов являются приблизительными.Декоратор также предоставляет функцию
cache_clear()для очистки или инвалидации кеша.Исходная нижележащая функция доступна через атрибут
__wrapped__. Это полезно для интроспекции, для обхода кеша или для повторной обёртки функции другим кешем.Кэш LRU (наименее недавно использованный) работает лучше всего, когда самые последние вызовы являются лучшими предсказателями будущих вызовов (например, самые популярные статьи на новостном сервере имеют тенденцию меняться каждый день). Ограничение размера кэша гарантирует, что кэш не будет бесконечно расти в долго работающих процессах, таких как веб-серверы.
В общем случае LRU-кеш следует использовать только тогда, когда требуется повторно использовать ранее вычисленные значения. Соответственно, нет смысла кешировать функции с побочными эффектами, функции, которые при каждом вызове создают разные изменяемые объекты, или нечистые функции, такие как time() или random().
Пример LRU-кеша для статического веб-контента:
python@lru_cache(maxsize=32) def get_pep(num): 'Retrieve text of a Python Enhancement Proposal' resource = 'http://www.python.org/dev/peps/pep-%04d/' % num try: with urllib.request.urlopen(resource) as s: return s.read() except urllib.error.HTTPError: return 'Not Found' >>> for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991: ... pep = get_pep(n) ... print(n, len(pep)) >>> get_pep.cache_info() CacheInfo(hits=3, misses=8, maxsize=32, currsize=8)Пример эффективного вычисления чисел Фибоначчи с использованием кеша для реализации техники динамического программирования:
python@lru_cache(maxsize=None) def fib(n): if n < 2: return n return fib(n-1) + fib(n-2) >>> [fib(n) for n in range(16)] [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610] >>> fib.cache_info() CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)Новое в версии 3.2.
Изменено в версии 3.3: Добавлена опция typed.
-
@functools.total_ordering¶ Если класс определяет один или несколько методов упорядочения с помощью расширенных сравнений (rich comparison), этот декоратор класса предоставляет остальные. Это упрощает усилия по заданию всех возможных операций расширенного сравнения:
Класс должен определять один из методов
__lt__(),__le__(),__gt__()или__ge__(). Кроме того, класс должен предоставлять метод__eq__().Например:
python@total_ordering class Student: def _is_valid_operand(self, other): return (hasattr(other, "lastname") and hasattr(other, "firstname")) def __eq__(self, other): if not self._is_valid_operand(other): return NotImplemented return ((self.lastname.lower(), self.firstname.lower()) == (other.lastname.lower(), other.firstname.lower())) def __lt__(self, other): if not self._is_valid_operand(other): return NotImplemented return ((self.lastname.lower(), self.firstname.lower()) < (other.lastname.lower(), other.firstname.lower()))Примечание
Хотя этот декоратор упрощает создание хорошо ведущих себя типов с полным порядком, он действительно имеет ценой более медленное выполнение и более сложные трассировки стека для производных методов сравнения. Если бенчмарки производительности показывают, что это является узким местом для данного приложения, то реализация всех шести методов расширенного сравнения вместо этого, вероятно, даст лёгкое ускорение.
Новое в версии 3.2.
Изменено в версии 3.4: Теперь поддерживается возврат NotImplemented из базовой функции сравнения для нераспознанных типов.
-
functools.partial(func, *args, **keywords)¶ Возвращает новый объект partial, который при вызове ведёт себя как func, вызванная с позиционными аргументами args и ключевыми аргументами keywords. Если при вызове передаются дополнительные аргументы, они добавляются к args. Если передаются дополнительные ключевые аргументы, они расширяют и переопределяют keywords. Приблизительно эквивалентно:
pythondef partial(func, *args, **keywords): def newfunc(*fargs, **fkeywords): newkeywords = keywords.copy() newkeywords.update(fkeywords) return func(*args, *fargs, **newkeywords) newfunc.func = func newfunc.args = args newfunc.keywords = keywords return newfuncpartial()используется для частичного применения функции, которое «замораживает» некоторую часть аргументов и/или ключевых слов функции, создавая новый объект с упрощённой сигнатурой. Например,partial()можно использовать для создания вызываемого объекта, который ведёт себя как функцияint(), где аргумент base по умолчанию равен двум:text>>> from functools import partial >>> basetwo = partial(int, base=2) >>> basetwo.__doc__ = 'Convert base 2 string to an int.' >>> basetwo('10010') 18
-
class
functools.partialmethod(func, *args, **keywords)¶ Возвращает новый дескриптор
partialmethod, который ведёт себя какpartial, за исключением того, что он предназначен для использования в качестве определения метода, а не для непосредственного вызова.func должен быть дескриптором или вызываемым объектом (объекты, являющиеся и тем и другим, как обычные функции, обрабатываются как дескрипторы).
Когда func является дескриптором (например, обычная функция Python,
classmethod(),staticmethod(),abstractmethod()или другой экземплярpartialmethod), вызовы__get__делегируются базовому дескриптору, а в качестве результата возвращается соответствующий объект partial.Когда func является вызываемым объектом, не являющимся дескриптором, динамически создаётся соответствующий связанный метод. Он ведёт себя как обычная функция Python при использовании в качестве метода: аргумент self будет вставлен первым позиционным аргументом, даже до args и keywords, переданных конструктору
partialmethod.Пример:
python>>> class Cell(object): ... def __init__(self): ... self._alive = False ... @property ... def alive(self): ... return self._alive ... def set_state(self, state): ... self._alive = bool(state) ... set_alive = partialmethod(set_state, True) ... set_dead = partialmethod(set_state, False) ... >>> c = Cell() >>> c.alive False >>> c.set_alive() >>> c.alive TrueНовое в версии 3.4.
-
functools.reduce(функция, итерируемый объект[, инициализатор])¶ Кумулятивно применяет функцию от двух аргументов к элементам последовательности слева направо, чтобы свести последовательность к одному значению. Например,
reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])вычисляет((((1+2)+3)+4)+5). Левый аргумент x – это накопленное значение, а правый аргумент y – это обновляемое значение из последовательности. Если указан необязательный initializer, он помещается перед элементами последовательности в вычислениях и служит значением по умолчанию для пустой последовательности. Если initializer не задан и sequence содержит только один элемент, возвращается первый элемент.Приблизительно эквивалентно:
pythondef reduce(function, iterable, initializer=None): it = iter(iterable) if initializer is None: value = next(it) else: value = initializer for element in it: value = function(value, element) return value
-
@functools.singledispatch¶ Преобразует функцию в single-dispatch обобщённую функцию.
Для определения обобщённой функции декорируйте её декоратором
@singledispatch. Обратите внимание, что диспетчеризация происходит по типу первого аргумента; создавайте функцию соответственно:python>>> from functools import singledispatch >>> @singledispatch ... def fun(arg, verbose=False): ... if verbose: ... print("Let me just say,", end=" ") ... print(arg)Чтобы добавить перегруженные реализации в функцию, используйте атрибут
register()обобщённой функции. Это декоратор, принимающий параметр типа и декорирующий функцию, реализующую операцию для этого типа:python>>> @fun.register(int) ... def _(arg, verbose=False): ... if verbose: ... print("Strength in numbers, eh?", end=" ") ... print(arg) ... >>> @fun.register(list) ... def _(arg, verbose=False): ... if verbose: ... print("Enumerate this:") ... for i, elem in enumerate(arg): ... print(i, elem)Чтобы разрешить регистрацию лямбд и уже существующих функций, атрибут
register()может использоваться в функциональной форме:python>>> def nothing(arg, verbose=False): ... print("Nothing.") ... >>> fun.register(type(None), nothing)Атрибут
register()возвращает недекорированную функцию, что позволяет накладывать декораторы, выполнять сериализацию, а также создавать модульные тесты для каждого варианта независимо:python>>> @fun.register(float) ... @fun.register(Decimal) ... def fun_num(arg, verbose=False): ... if verbose: ... print("Half of your number:", end=" ") ... print(arg / 2) ... >>> fun_num is fun FalseПри вызове обобщённая функция выполняет диспетчеризацию по типу первого аргумента:
python>>> fun("Hello, world.") Hello, world. >>> fun("test.", verbose=True) Let me just say, test. >>> fun(42, verbose=True) Strength in numbers, eh? 42 >>> fun(['spam', 'spam', 'eggs', 'spam'], verbose=True) Enumerate this: 0 spam 1 spam 2 eggs 3 spam >>> fun(None) Nothing. >>> fun(1.23) 0.615Если для конкретного типа нет зарегистрированной реализации, для поиска более общей реализации используется порядок разрешения методов. Исходная функция, декорированная с помощью
@singledispatch, регистрируется для базового типаobject, что означает, что она используется, если не найдена лучшая реализация.Чтобы проверить, какую реализацию выберет обобщённая функция для заданного типа, используйте атрибут
dispatch():python>>> fun.dispatch(float) <function fun_num at 0x1035a2840> >>> fun.dispatch(dict) # примечание: реализация по умолчанию <function fun at 0x103fe0000>Для доступа ко всем зарегистрированным реализациям используйте атрибут
registryтолько для чтения:python>>> fun.registry.keys() dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>, <class 'decimal.Decimal'>, <class 'list'>, <class 'float'>]) >>> fun.registry[float] <function fun_num at 0x1035a2840> >>> fun.registry[object] <function fun at 0x103fe0000>Новое в версии 3.4.
-
functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)¶ Обновляет функцию-обёртку так, чтобы она выглядела как функция wrapped. Необязательные аргументы – это кортежи, которые определяют, какие атрибуты исходной функции напрямую присваиваются соответствующим атрибутам функции-обёртки, а какие атрибуты функции-обёртки обновляются соответствующими атрибутами исходной функции. Значения по умолчанию для этих аргументов – константы уровня модуля
WRAPPER_ASSIGNMENTS(которая присваивает функции-обёртке атрибуты__module__,__name__,__qualname__,__annotations__и__doc__– строку документации) иWRAPPER_UPDATES(которая обновляет__dict__функции-обёртки, т.е. словарь экземпляра).Чтобы обеспечить доступ к исходной функции для интроспекции и других целей (например, обхода декоратора кэширования, такого как
lru_cache()), эта функция автоматически добавляет обёртке атрибут__wrapped__, ссылающийся на обёрнутую функцию.Основное назначение этой функции – функции-декораторы, которые оборачивают декорированную функцию и возвращают обёртку. Если функция-обёртка не обновлена, метаданные возвращаемой функции будут отражать определение обёртки, а не определение исходной функции, что обычно мало помогает.
update_wrapper()может использоваться с вызываемыми объектами, отличными от функций. Любые атрибуты, указанные в assigned или updated и отсутствующие у обёртываемого объекта, игнорируются (т.е. эта функция не будет пытаться установить их у функции-обёртки).AttributeErrorпо-прежнему возбуждается, если сама функция-обёртка не имеет каких-либо атрибутов, указанных в updated.Новое в версии 3.2: Автоматическое добавление атрибута
__wrapped__.Новое в версии 3.2: Копирование атрибута
__annotations__по умолчанию.Изменено в версии 3.2: Отсутствующие атрибуты больше не вызывают
AttributeError.Изменено в версии 3.4: Атрибут
__wrapped__теперь всегда ссылается на обёрнутую функцию, даже если эта функция определила атрибут__wrapped__. (см. bpo-17482)
-
@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)¶ Это удобная функция для вызова
update_wrapper()в качестве декоратора функции при определении функции-обёртки. Она эквивалентнаpartial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated). Например:python>>> from functools import wraps >>> def my_decorator(f): ... @wraps(f) ... def wrapper(*args, **kwds): ... print('Calling decorated function') ... return f(*args, **kwds) ... return wrapper ... >>> @my_decorator ... def example(): ... """Докстрока""" ... print('Called example function') ... >>> example() Calling decorated function Called example function >>> example.__name__ 'example' >>> example.__doc__ 'Docstring'Без использования этой фабрики декораторов имя примера функции было бы
'wrapper', а строка документации исходнойexample()была бы потеряна.
10.2.1. partial Объекты
Объекты partial – это вызываемые объекты, создаваемые с помощью partial(). Они имеют три атрибута только для чтения:
-
partial.func¶ Вызываемый объект или функция. Вызовы объекта
partialбудут перенаправлены кfuncс новыми аргументами и ключевыми словами.
-
partial.args¶ Самые левые позиционные аргументы, которые будут добавлены перед позиционными аргументами, передаваемыми при вызове объекта
partial.
Объекты partial подобны объектам function в том, что они вызываемы, могут иметь слабые ссылки и атрибуты. Есть несколько важных отличий. Например, атрибуты __name__ и __doc__ не создаются автоматически. Кроме того, объекты partial, определённые в классах, ведут себя как статические методы и не превращаются в связанные методы при поиске атрибутов экземпляра.