functools — Функции высшего порядка и операции над вызываемыми объектами

Источник: Lib/functools.py


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

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

@functools.cache(user_function)

Простой легкий неограниченный кэш функций. Иногда называется «memoize».

Возвращает то же, что и lru_cache(maxsize=None), создавая тонкую обертку вокруг поиска аргументов функции по словарю. Поскольку никогда не нужно изгонять старые значения, это меньше и быстрее, чем lru_cache() с ограничением на размер.

Например:

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

>>> factorial(10)      # no previously cached result, makes 11 recursive calls
3628800
>>> factorial(5)       # just looks up cached value result
120
>>> factorial(12)      # makes two new recursive calls, the other 10 are cached
479001600

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

Обернутая функция может быть вызвана более одного раза, если другой поток выполнит дополнительный вызов до того, как первый вызов будет завершен и закеширован.

Added in version 3.9.

@functools.cached_property(func)

Преобразует метод класса в свойство, значение которого вычисляется один раз, а затем кэшируется как обычный атрибут на протяжении всей жизни экземпляра. Аналогично property(), с добавлением кэширования. Полезно для дорогих вычисляемых свойств экземпляров, которые в остальном являются неизменяемыми.

Пример:

class DataSet:

    def __init__(self, sequence_of_numbers):
        self._data = tuple(sequence_of_numbers)

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

Механика cached_property() несколько отличается от property(). Обычное свойство блокирует запись атрибутов, если не определен сеттер. В отличие от него, cached_property разрешает запись.

Декоратор cached_property запускается только при поиске и только в том случае, если атрибут с таким же именем не существует. Когда он выполняется, cached_property записывает в атрибут с тем же именем. Последующие чтения и записи атрибутов имеют приоритет над методом cached_property, и он работает как обычный атрибут.

Кэшированное значение можно очистить, удалив атрибут. Это позволит снова запустить метод cached_property.

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

Обратите внимание, что этот декоратор вмешивается в работу словарей PEP 412 с общим доступом к ключу. Это означает, что словари экземпляров могут занимать больше места, чем обычно.

Кроме того, этот декоратор требует, чтобы атрибут __dict__ у каждого экземпляра был мутабельным отображением. Это означает, что он не будет работать с некоторыми типами, такими как метаклассы (поскольку атрибуты __dict__ у экземпляров типов являются прокси для пространства имен класса только для чтения), а также с теми, которые указывают __slots__, не включая __dict__ в качестве одного из определенных слотов (поскольку такие классы вообще не предоставляют атрибут __dict__).

Если мутабельное отображение недоступно или требуется экономия места при совместном использовании ключей, эффекта, подобного cached_property(), можно также добиться, укладывая property() поверх lru_cache(). Подробнее о том, чем это отличается от cached_property(), см. в разделе Как кэшировать вызовы методов?.

Added in version 3.8.

Изменено в версии 3.12: До Python 3.12 cached_property включал недокументированную блокировку, чтобы гарантировать, что при многопоточном использовании функция getter будет выполняться только один раз для каждого экземпляра. Однако блокировка была на свойство, а не на экземпляр, что могло привести к неприемлемо высокой конкуренции за блокировку. В Python 3.12+ эта блокировка снята.

functools.cmp_to_key(func)

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

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

Пример:

sorted(iterable, key=cmp_to_key(locale.strcoll))  # locale-aware sort order

Примеры сортировки и краткое руководство по сортировке смотрите в Техника сортировки.

Added in version 3.2.

@functools.lru_cache(user_function)
@functools.lru_cache(maxsize=128, typed=False)

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

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

Обернутая функция может быть вызвана более одного раза, если другой поток выполнит дополнительный вызов до того, как первый вызов будет завершен и закеширован.

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

Различные шаблоны аргументов могут рассматриваться как отдельные вызовы с отдельными записями в кэше. Например, f(a=1, b=2) и f(b=2, a=1) отличаются порядком аргументов ключевых слов и могут иметь две отдельные записи в кэше.

Если указана user_function, она должна быть вызываемой. Это позволяет применить декоратор lru_cache непосредственно к пользовательской функции, оставив значение maxsize по умолчанию равным 128:

@lru_cache
def count_vowels(sentence):
    return sum(sentence.count(vowel) for vowel in 'AEIOUaeiou')

Если maxsize установлен в None, функция LRU отключена и кэш может расти неограниченно.

Если typed имеет значение true, аргументы функции разных типов будут кэшироваться отдельно. Если typed имеет значение false, реализация обычно рассматривает их как эквивалентные вызовы и кэширует только один результат. (Некоторые типы, такие как str и int, могут кэшироваться отдельно даже при значении typed, равном false).

Обратите внимание, что спецификация типов относится только к непосредственным аргументам функции, а не к их содержимому. Скалярные аргументы Decimal(42) и Fraction(42) рассматриваются как разные вызовы с разными результатами. Напротив, кортежные аргументы ('answer', Decimal(42)) и ('answer', Fraction(42)) рассматриваются как эквивалентные.

Обернутая функция оснащена функцией cache_parameters(), которая возвращает новую dict, показывающую значения maxsize и typed. Это только для информационных целей. Мутирование значений не имеет никакого эффекта.

Чтобы измерить эффективность кэша и настроить параметр maxsize, обернутая функция оснащена функцией cache_info(), которая возвращает named tuple, показывая hits, misses, maxsize и currsize.

Декоратор также предоставляет функцию cache_clear() для очистки или аннулирования кэша.

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

Кэш сохраняет ссылки на аргументы и возвращаемые значения до тех пор, пока они не выйдут из кэша или пока кэш не будет очищен.

Если метод кэшируется, аргумент экземпляра self включается в кэш. См. Как кэшировать вызовы методов?

Значение LRU (least recently used) cache лучше всего работает, когда последние обращения лучше всего предсказывают предстоящие обращения (например, самые популярные статьи на сервере новостей меняются каждый день). Ограничение размера кэша гарантирует, что кэш не будет расти неограниченно на длительных процессах, таких как веб-серверы.

В общем случае кэш LRU следует использовать только тогда, когда вы хотите повторно использовать ранее вычисленные значения. Соответственно, не имеет смысла кэшировать функции с побочными эффектами, функции, которым при каждом вызове необходимо создавать отдельные мутабельные объекты (например, генераторы и async-функции), или нечистые функции, такие как time() или random().

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

@lru_cache(maxsize=32)
def get_pep(num):
    'Retrieve text of a Python Enhancement Proposal'
    resource = f'https://peps.python.org/pep-{num:04d}'
    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)

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

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

Added in version 3.2.

Изменено в версии 3.3: Добавлена опция typed.

Изменено в версии 3.8: Добавлена опция user_function.

Изменено в версии 3.9: Добавлена функция cache_parameters()

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

Примечание

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

Примечание

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

Added in version 3.2.

Изменено в версии 3.4: Теперь поддерживается возврат NotImplemented из базовой функции сравнения для нераспознанных типов.

functools.partial(func, /, *args, **keywords)

Возвращает новый partial object, который при вызове будет вести себя как func, вызванный с позиционными аргументами args и аргументами ключевых слов keywords. Если при вызове передаются дополнительные аргументы, они добавляются к args. Если предоставлены дополнительные аргументы-ключевые слова, они расширяют и переопределяют keywords. Примерно эквивалентно:

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

Если func является дескриптором (например, обычной функцией Python, classmethod(), staticmethod(), abstractmethod() или другим экземпляром partialmethod), вызовы __get__ передаются нижележащему дескриптору, а в качестве результата возвращается соответствующий partial object.

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

Пример:

>>> class Cell:
...     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

Added in version 3.4.

functools.reduce(function, iterable, [initial, ]/)

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

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

initial_missing = object()

def reduce(function, iterable, initial=initial_missing, /):
    it = iter(iterable)
    if initial is initial_missing:
        value = next(it)
    else:
        value = initial
    for element in it:
        value = function(value, element)
    return value

Смотрите itertools.accumulate() для итератора, который выдает все промежуточные значения.

@functools.singledispatch

Преобразование функции в single-dispatch generic function.

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

>>> from functools import singledispatch
>>> @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)

Также можно использовать types.UnionType и typing.Union:

>>> @fun.register
... def _(arg: int | float, verbose=False):
...     if verbose:
...         print("Strength in numbers, eh?", end=" ")
...     print(arg)
...
>>> from typing import Union
>>> @fun.register
... def _(arg: Union[list, set], verbose=False):
...     if verbose:
...         print("Enumerate this:")
...     for i, elem in enumerate(arg):
...         print(i, elem)
...

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

>>> @fun.register(complex)
... def _(arg, verbose=False):
...     if verbose:
...         print("Better than complicated.", end=" ")
...     print(arg.real, arg.imag)
...

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

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

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

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

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

Если реализация зарегистрирована в abstract base class, виртуальные подклассы базового класса будут отправляться в эту реализацию:

>>> from collections.abc import Mapping
>>> @fun.register
... def _(arg: Mapping, verbose=False):
...     if verbose:
...         print("Keys & Values")
...     for key, value in arg.items():
...         print(key, "=>", value)
...
>>> fun({"a": "b"})
a => b

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

>>> fun.dispatch(float)
<function fun_num at 0x1035a2840>
>>> fun.dispatch(dict)    # note: default implementation
<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>

Added in version 3.4.

Изменено в версии 3.7: Атрибут register() теперь поддерживает использование аннотаций типов.

Изменено в версии 3.11: Атрибут register() теперь поддерживает types.UnionType и typing.Union в качестве аннотаций типов.

class functools.singledispatchmethod(func)

Преобразование метода в single-dispatch generic function.

Чтобы определить общий метод, украсьте его декоратором @singledispatchmethod. При определении функции с помощью @singledispatchmethod обратите внимание, что диспетчеризация происходит по типу первого не*self* или не*cls* аргумента:

class Negator:
    @singledispatchmethod
    def neg(self, arg):
        raise NotImplementedError("Cannot negate a")

    @neg.register
    def _(self, arg: int):
        return -arg

    @neg.register
    def _(self, arg: bool):
        return not arg

@singledispatchmethod поддерживает вложенность с другими декораторами, такими как @classmethod. Обратите внимание, что для того, чтобы разрешить dispatcher.register, singledispatchmethod должен быть внешним декоратором. Вот класс Negator с методами neg, привязанными к классу, а не к экземпляру класса:

class Negator:
    @singledispatchmethod
    @classmethod
    def neg(cls, arg):
        raise NotImplementedError("Cannot negate a")

    @neg.register
    @classmethod
    def _(cls, arg: int):
        return -arg

    @neg.register
    @classmethod
    def _(cls, arg: bool):
        return not arg

Этот же шаблон можно использовать и для других подобных декораторов: @staticmethod, @abstractmethod и других.

Added in version 3.8.

functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

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

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

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

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

Изменено в версии 3.2: Атрибут __wrapped__ теперь добавляется автоматически. Атрибут __annotations__ теперь копируется по умолчанию. Отсутствующие атрибуты больше не приводят к появлению AttributeError.

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

Изменено в версии 3.12: Атрибут __type_params__ теперь копируется по умолчанию.

@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():
...     """Docstring"""
...     print('Called example function')
...
>>> example()
Calling decorated function
Called example function
>>> example.__name__
'example'
>>> example.__doc__
'Docstring'

Без использования этой фабрики-декоратора имя функции примера было бы 'wrapper', а docstring оригинального example() был бы потерян.

partial Объекты

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

partial.func

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

partial.args

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

partial.keywords

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

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