Документация Python неофициальный перевод
Содержание страницы

10.2. functools – Функции высшего порядка и операции с вызываемыми объектамиfunctools – Higher-order functions and operations on callable objects

Исходный код: Lib/functools.py


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

Модуль functools определяет следующие функции:

functools.cmp_to_key(func)

Преобразует функцию сравнения старого образца в ключевую функцию. Используется с инструментами, которые принимают ключевые функции (например, sorted(), min(), max(), heapq.nlargest(), heapq.nsmallest(), itertools.groupby()). Эта функция в первую очередь используется как переходный инструмент для программ, переносимых из Python 2, который поддерживал использование функций сравнения.

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

Пример:

sorted(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 (least recently used – вытеснение давно неиспользуемых) работает наилучшим образом, когда самые последние вызовы являются лучшими предикторами будущих вызовов (например, самые популярные статьи на новостном сервере имеют тенденцию меняться каждый день). Ограничение размера кэша гарантирует, что он не будет бесконечно расти в долго работающих процессах, таких как веб-серверы.

Пример LRU-кэша для статического веб-контента:

@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)

Пример эффективного вычисления чисел Фибоначчи с использованием кэша для реализации техники динамического программирования:

@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

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

Класс должен определить один из методов: __lt__(), __le__(), __gt__() или __ge__(). Кроме того, класс должен предоставлять метод __eq__().

Например:

@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. Примерно эквивалентно:

def 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 newfunc

partial() используется для частичного применения функции, которое «замораживает» некоторую часть аргументов и/или ключевых слов функции, создавая новый объект с упрощённой сигнатурой. Например, partial() можно использовать для создания вызываемого объекта, который ведёт себя как функция int(), где аргумент base по умолчанию равен двум:

>>> 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 должен быть дескриптором или вызываемым объектом (объекты, являющиеся и тем и другим, как обычные функции, обрабатываются как дескрипторы).

When func is a descriptor (such as a normal Python function, classmethod(), staticmethod(), abstractmethod() or another instance of partialmethod), calls to __get__ are delegated to the underlying descriptor, and an appropriate partial object returned as the result.

Когда func является вызываемым объектом, не являющимся дескриптором, динамически создается соответствующий связанный метод. Он ведёт себя как обычная функция Python при использовании в качестве метода: аргумент self будет вставлен в качестве первого позиционного аргумента, даже перед аргументами args и keywords, переданными конструктору partialmethod.

Пример:

>>> 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(function, iterable[, initializer])

Применяет function с двумя аргументами последовательно к элементам sequence слева направо, сводя последовательность к одному значению. Например, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) вычисляет ((((1+2)+3)+4)+5). Левый аргумент, x, является накопленным значением, а правый аргумент, y, – обновляемым значением из sequence. Если присутствует необязательный initializer, он помещается перед элементами последовательности в вычислении и служит значением по умолчанию, когда последовательность пуста. Если initializer не задан и sequence содержит только один элемент, возвращается первый элемент.

Примерно эквивалентно:

def 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(default)

Преобразует функцию в одиночно-диспетчеризируемую обобщённую функцию.

Чтобы определить обобщённую функцию, декорируйте её декоратором @singledispatch. Обратите внимание, что диспетчеризация происходит по типу первого аргумента, поэтому создавайте функцию соответствующим образом:

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

Чтобы добавить перегруженные реализации к функции, используйте атрибут register() обобщённой функции. Это декоратор, принимающий параметр типа и декорирующий функцию, реализующую операцию для этого типа:

>>> @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() можно использовать в функциональной форме:

>>> def nothing(arg, verbose=False):
...     print("Nothing.")
...
>>> fun.register(type(None), nothing)

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

>>> @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

При вызове обобщённая функция выполняет диспетчеризацию по типу первого аргумента:

>>> 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

Если для конкретного типа нет зарегистрированной реализации, для поиска более общей реализации используется порядок разрешения методов (MRO). Исходная функция, декорированная @singledispatch, зарегистрирована для базового типа object, что означает, что она используется, если не найдено более подходящей реализации.

Чтобы проверить, какую реализацию выберет обобщённая функция для заданного типа, используйте атрибут dispatch():

>>> fun.dispatch(float)
<function fun_num at 0x1035a2840>
>>> fun.dispatch(dict)    # примечание: реализация по умолчанию
<function fun at 0x103fe0000>

Чтобы получить доступ ко всем зарегистрированным реализациям, используйте атрибут registry, доступный только для чтения:

>>> 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)

Обновляет функцию-обёртку так, чтобы она выглядела как обёрнутая функция. Необязательные аргументы – кортежи, определяющие, какие атрибуты исходной функции напрямую присваиваются соответствующим атрибутам функции-обёртки и какие атрибуты функции-обёртки обновляются соответствующими атрибутами исходной функции. Значения по умолчанию для этих аргументов – константы уровня модуля WRAPPER_ASSIGNMENTS (которая присваивает функции-обёртке атрибуты __name__, __module__, __annotations__ и __doc__, т.е. строку документации) и WRAPPER_UPDATES (которая обновляет у функции-обёртки атрибут __dict__, т.е. словарь экземпляра).

Чтобы разрешить доступ к исходной функции для интроспекции и других целей (например, обхода кеширующего декоратора вроде lru_cache()), данная функция автоматически добавляет атрибут __wrapped__ к обёртке, который ссылается на обёртываемую функцию.

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

update_wrapper() может использоваться с вызываемыми объектами, не являющимися функциями. Любые атрибуты, указанные в assigned или updated, отсутствующие у обёртываемого объекта, игнорируются (т.е. эта функция не будет пытаться установить их у функции-обёртки). AttributeError по-прежнему возбуждается, если сама функция-обёртка не имеет каких-либо атрибутов, указанных в updated.

Новое в версии 3.2: Автоматическое добавление атрибута __wrapped__.

Новое в версии 3.2: Копирование атрибута __annotations__ по умолчанию.

Изменено в версии 3.2: Отсутствующие атрибуты больше не вызывают AttributeError.

Изменено в версии 3.4: Атрибут __wrapped__ теперь всегда ссылается на обёрнутую функцию, даже если эта функция определяла атрибут __wrapped__. (см. issue 17482)

@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

Это удобная функция для вызова update_wrapper() в качестве декоратора функции при определении функции-обёртки. Она эквивалентна partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated). Например:

>>> 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 Objects

partial объекты – это вызываемые объекты, создаваемые с помощью partial(). Они имеют три атрибута только для чтения:

partial.func

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

partial.args

Крайние слева позиционные аргументы, которые будут добавлены в начало позиционных аргументов, передаваемых при вызове объекта partial.

partial.keywords

Ключевые аргументы, которые будут предоставлены, когда объект partial будет вызван.

partial объекты подобны объектам function в том, что они вызываемы, на них можно создавать слабые ссылки и они могут иметь атрибуты. Есть несколько важных отличий. Например, атрибуты __name__ и __doc__ не создаются автоматически. Кроме того, объекты partial, определённые в классах, ведут себя как статические методы и не преобразуются в связанные методы при поиске атрибута экземпляра.