Рубрики
Без рубрики

functools – Инструменты для управления функциями

Автор оригинала: Doug Hellmann.

Цель:

Функции, которые работают с другими функциями.

Модуль functools предоставляет инструменты для адаптации или расширения функций и других вызываемых объектов без их полного переписывания.

Декораторы

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

Частичные объекты

В этом примере показаны два простых объекта partial для функции myfunc () . Вывод show_details () включает атрибуты func , args и keywords частичного объекта.

functools_partial.py

import functools


def myfunc(a, b2):
    "Docstring for myfunc()."
    print('  called myfunc with:', (a, b))


def show_details(name, f, is_partialFalse):
    "Show details of a callable object."
    print('{}:'.format(name))
    print('  object:', f)
    if not is_partial:
        print('  __name__:', f.__name__)
    if is_partial:
        print('  func:', f.func)
        print('  args:', f.args)
        print('  keywords:', f.keywords)
    return


show_details('myfunc', myfunc)
myfunc('a', 3)
print()

# Set a different default value for 'b', but require
# the caller to provide 'a'.
p1  functools.partial(myfunc, b4)
show_details('partial with named default', p1, True)
p1('passing a')
p1('override b', b5)
print()

# Set default values for both 'a' and 'b'.
p2  functools.partial(myfunc, 'default a', b99)
show_details('partial with defaults', p2, True)
p2()
p2(b'override b')
print()

print('Insufficient arguments:')
p1()

В конце примера первый созданный частичный вызывается без передачи значения для a , вызывая исключение.

$ python3 functools_partial.py

myfunc:
  object: 
  __name__: myfunc
  called myfunc with: ('a', 3)

partial with named default:
  object: functools.partial(,
  func: 
  args: ()
  keywords: {'b': 4}
  called myfunc with: ('passing a', 4)
  called myfunc with: ('override b', 5)

partial with defaults:
  object: functools.partial(,
'default a',
  func: 
  args: ('default a',)
  keywords: {'b': 99}
  called myfunc with: ('default a', 99)
  called myfunc with: ('default a', 'override b')

Insufficient arguments:
Traceback (most recent call last):
  File "functools_partial.py", line 51, in 
    p1()
TypeError: myfunc() missing 1 required positional argument: 'a'

Получение свойств функции

Объект partial по умолчанию не имеет атрибутов __name__ или __doc__ , и без этих атрибутов декорированные функции труднее отлаживать. Используя update_wrapper () , копирует или добавляет атрибуты из исходной функции в объект partial .

functools_update_wrapper.py

import functools


def myfunc(a, b2):
    "Docstring for myfunc()."
    print('  called myfunc with:', (a, b))


def show_details(name, f):
    "Show details of a callable object."
    print('{}:'.format(name))
    print('  object:', f)
    print('  __name__:', end' ')
    try:
        print(f.__name__)
    except AttributeError:
        print('(no __name__)')
    print('  __doc__', repr(f.__doc__))
    print()


show_details('myfunc', myfunc)

p1  functools.partial(myfunc, b4)
show_details('raw wrapper', p1)

print('Updating wrapper:')
print('  assign:', functools.WRAPPER_ASSIGNMENTS)
print('  update:', functools.WRAPPER_UPDATES)
print()

functools.update_wrapper(p1, myfunc)
show_details('updated wrapper', p1)

Атрибуты, добавленные в оболочку, определены в WRAPPER_ASSIGNMENTS , а в WRAPPER_UPDATES перечислены значения, которые необходимо изменить.

$ python3 functools_update_wrapper.py

myfunc:
  object: 
  __name__: myfunc
  __doc__ 'Docstring for myfunc().'

raw wrapper:
  object: functools.partial(,
  __name__: (no __name__)
  __doc__ 'partial(func, *args, **keywords) - new function with
partial application\n    of the given arguments and keywords.\n'

Updating wrapper:
  assign: ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')
  update: ('__dict__',)

updated wrapper:
  object: functools.partial(,
  __name__: myfunc
  __doc__ 'Docstring for myfunc().'

Прочие звонки

Частичные функции работают с любым вызываемым объектом, а не только с автономными функциями.

functools_callable.py

import functools


class MyClass:
    "Demonstration class for functools"

    def __call__(self, e, f6):
        "Docstring for MyClass.__call__"
        print('  called object with:', (self, e, f))


def show_details(name, f):
    "Show details of a callable object."
    print('{}:'.format(name))
    print('  object:', f)
    print('  __name__:', end' ')
    try:
        print(f.__name__)
    except AttributeError:
        print('(no __name__)')
    print('  __doc__', repr(f.__doc__))
    return


o  MyClass()

show_details('instance', o)
o('e goes here')
print()

p  functools.partial(o, e'default for e', f8)
functools.update_wrapper(p, o)
show_details('instance wrapper', p)
p()

В этом примере партиалы создаются из экземпляра класса с помощью метода __call __ () .

$ python3 functools_callable.py

instance:
  object: <__main__.MyClass object at 0x1011b1cf8>
  __name__: (no __name__)
  __doc__ 'Demonstration class for functools'
  called object with: (<__main__.MyClass object at 0x1011b1cf8>,
'e goes here', 6)

instance wrapper:
  object: functools.partial(<__main__.MyClass object at
0x1011b1cf8>,)
  __name__: (no __name__)
  __doc__ 'Demonstration class for functools'
  called object with: (<__main__.MyClass object at 0x1011b1cf8>,
'default for e', 8)

Методы и функции

В то время как partial () возвращает вызываемый объект, готовый к непосредственному использованию, partialmethod () возвращает вызываемый объект, готовый к использованию в качестве несвязанного метода объекта. В следующем примере одна и та же автономная функция добавляется как атрибут MyClass дважды, один раз с использованием partialmethod () как method1 () и снова используя partial () как method2 () .

functools_partialmethod.py

import functools


def standalone(self, a1, b2):
    "Standalone function"
    print('  called standalone with:', (self, a, b))
    if self is not None:
        print('  self.attr =', self.attr)


class MyClass:
    "Demonstration class for functools"

    def __init__(self):
        self.attr  'instance attribute'

    method1  functools.partialmethod(standalone)
    method2  functools.partial(standalone)


o  MyClass()

print('standalone')
standalone(None)
print()

print('method1 as partialmethod')
o.method1()
print()

print('method2 as partial')
try:
    o.method2()
except TypeError as err:
    print('ERROR: {}'.format(err))

method1 () может быть вызван из экземпляра MyClass , и этот экземпляр передается в качестве первого аргумента, как и в случае с обычными методами. method2 () не настроен как связанный метод, поэтому аргумент self должен быть передан явно, иначе вызов приведет к ошибке TypeError .

$ python3 functools_partialmethod.py

standalone
  called standalone with: (None, 1, 2)

method1 as partialmethod
  called standalone with: (<__main__.MyClass object at
0x1007b1d30>, 1, 2)
  self.attr = instance attribute

method2 as partial
ERROR: standalone() missing 1 required positional argument:
'self'

Получение свойств функции для декораторов

Обновление свойств обернутого вызываемого объекта особенно полезно при использовании в декораторе, поскольку преобразованная функция в конечном итоге имеет свойства исходной «голой» функции.

functools_wraps.py

import functools


def show_details(name, f):
    "Show details of a callable object."
    print('{}:'.format(name))
    print('  object:', f)
    print('  __name__:', end' ')
    try:
        print(f.__name__)
    except AttributeError:
        print('(no __name__)')
    print('  __doc__', repr(f.__doc__))
    print()


def simple_decorator(f):
    @functools.wraps(f)
    def decorated(a'decorated defaults', b1):
        print('  decorated:', (a, b))
        print('  ', end' ')
        return f(a, bb)
    return decorated


def myfunc(a, b2):
    "myfunc() is not complicated"
    print('  myfunc:', (a, b))
    return


# The raw function
show_details('myfunc', myfunc)
myfunc('unwrapped, default b')
myfunc('unwrapped, passing b', 3)
print()

# Wrap explicitly
wrapped_myfunc  simple_decorator(myfunc)
show_details('wrapped_myfunc', wrapped_myfunc)
wrapped_myfunc()
wrapped_myfunc('args to wrapped', 4)
print()


# Wrap with decorator syntax
@simple_decorator
def decorated_myfunc(a, b):
    myfunc(a, b)
    return


show_details('decorated_myfunc', decorated_myfunc)
decorated_myfunc()
decorated_myfunc('args to decorated', 4)

functools предоставляет декоратор, wraps () , который применяет update_wrapper () к украшенной функции.

$ python3 functools_wraps.py

myfunc:
  object: 
  __name__: myfunc
  __doc__ 'myfunc() is not complicated'

  myfunc: ('unwrapped, default b', 2)
  myfunc: ('unwrapped, passing b', 3)

wrapped_myfunc:
  object: 
  __name__: myfunc
  __doc__ 'myfunc() is not complicated'

  decorated: ('decorated defaults', 1)
     myfunc: ('decorated defaults', 1)
  decorated: ('args to wrapped', 4)
     myfunc: ('args to wrapped', 4)

decorated_myfunc:
  object: 
  __name__: decorated_myfunc
  __doc__ None

  decorated: ('decorated defaults', 1)
     myfunc: ('decorated defaults', 1)
  decorated: ('args to decorated', 4)
     myfunc: ('args to decorated', 4)

Сравнение

В Python 2 классы могут определять метод __cmp __ () , который возвращает -1 , 0 или 1 на основе от того, является ли объект меньше, равен или больше сравниваемого элемента. Python 2.1 представил API методов богатого сравнения ( __lt __ () , __le __ () , __eq __ () , < code> __ ne __ () , __gt __ () и __ge __ () ), которые выполняют одну операцию сравнения и возвращают логическое значение. Python 3 устарел __cmp __ () в пользу этих новых методов, а functools предоставляет инструменты, упрощающие написание классов, соответствующих новым требованиям сравнения в Python 3.

Богатое сравнение

Богатый API сравнения разработан, чтобы позволить классам со сложными сравнениями реализовать каждый тест наиболее эффективным способом. Однако для классов, где сравнение относительно просто, нет смысла вручную создавать каждый из богатых методов сравнения. Декоратор класса total_ordering () принимает класс, который предоставляет некоторые методы, и добавляет остальные из них.

functools_total_ordering.py

import functools
import inspect
from pprint import pprint


@functools.total_ordering
class MyObject:

    def __init__(self, val):
        self.val  val

    def __eq__(self, other):
        print('  testing __eq__({}, {})'.format(
            self.val, other.val))
        return self.val  other.val

    def __gt__(self, other):
        print('  testing __gt__({}, {})'.format(
            self.val, other.val))
        return self.val > other.val


print('Methods:\n')
pprint(inspect.getmembers(MyObject, inspect.isfunction))

a  MyObject(1)
b  MyObject(2)

print('\nComparisons:')
for expr in ['a < b', 'a, 'a, 'a, 'a > b']:
    print('\n{:<6}:'.format(expr))
    result  eval(expr)
    print('  result of {}: {}'.format(expr, result))

Класс должен обеспечивать реализацию __eq __ () и еще одного метода расширенного сравнения. Декоратор добавляет реализации остальных методов, которые работают с использованием предоставленных сравнений. Если сравнение не может быть выполнено, метод должен вернуть NotImplemented , чтобы сравнение можно было попробовать с помощью операторов обратного сравнения для другого объекта, прежде чем полностью потерпеть неудачу.

$ python3 functools_total_ordering.py

Methods:

[('__eq__', ),
 ('__ge__', ),
 ('__gt__', ),
 ('__init__', ),
 ('__le__', ),
 ('__lt__', )]

Comparisons:

a < b :
  testing __gt__(1, 2)
  testing __eq__(1, 2)
  result of a < b: True

a
  testing __gt__(1, 2)
  result of a

a
  testing __eq__(1, 2)
  result of a

a
  testing __gt__(1, 2)
  testing __eq__(1, 2)
  result of a

a > b :
  testing __gt__(1, 2)
  result of a > b: False

Порядок сопоставления

Поскольку функции сравнения старого стиля в Python 3 устарели, аргумент cmp для таких функций, как sort () , также больше не поддерживается. Старые программы, использующие функции сравнения, могут использовать cmp_to_key () для преобразования их в функцию, возвращающую ключ сопоставления , который используется для определения позиции в конечной последовательности.

functools_cmp_to_key.py

import functools


class MyObject:

    def __init__(self, val):
        self.val  val

    def __str__(self):
        return 'MyObject({})'.format(self.val)


def compare_obj(a, b):
    """Old-style comparison function.
    """
    print('comparing {} and {}'.format(a, b))
    if a.val < b.val:
        return -1
    elif a.val > b.val:
        return 1
    return 0


# Make a key function using cmp_to_key()
get_key  functools.cmp_to_key(compare_obj)

def get_key_wrapper(o):
    "Wrapper function for get_key to allow for print statements."
    new_key  get_key(o)
    print('key_wrapper({}) -> {!r}'.format(o, new_key))
    return new_key


objs  [MyObject(x) for x in range(5, 0, -1)]

for o in sorted(objs, keyget_key_wrapper):
    print(o)

Обычно cmp_to_key () используется напрямую, но в этом примере вводится дополнительная функция-оболочка для вывода дополнительной информации при вызове ключевой функции.

Вывод показывает, что sorted () начинается с вызова get_key_wrapper () для каждого элемента в последовательности для создания ключа. Ключи, возвращаемые функцией cmp_to_key () , являются экземплярами класса, определенного в functools , который реализует богатый API сравнения с использованием переданной функции сравнения старого стиля. После всех ключей создаются, последовательность сортируется путем сравнения ключей.

$ python3 functools_cmp_to_key.py

key_wrapper(MyObject(5)) -> 
key_wrapper(MyObject(4)) -> 
key_wrapper(MyObject(3)) -> 
key_wrapper(MyObject(2)) -> 
key_wrapper(MyObject(1)) -> 
comparing MyObject(4) and MyObject(5)
comparing MyObject(3) and MyObject(4)
comparing MyObject(2) and MyObject(3)
comparing MyObject(1) and MyObject(2)
MyObject(1)
MyObject(2)
MyObject(3)
MyObject(4)
MyObject(5)

Кеширование

Декоратор lru_cache () помещает функцию в кэш, который использовался не так давно. Аргументы функции используются для построения хеш-ключа, который затем сопоставляется с результатом. Последующие вызовы с теми же аргументами будут извлекать значение из кеша вместо вызова функции. Декоратор также добавляет к функции методы для проверки состояния кеша ( cache_info () ) и очистки кеша ( cache_clear () ).

functools_lru_cache.py

import functools


@functools.lru_cache()
def expensive(a, b):
    print('expensive({}, {})'.format(a, b))
    return a * b


MAX  2

print('First set of calls:')
for i in range(MAX):
    for j in range(MAX):
        expensive(i, j)
print(expensive.cache_info())

print('\nSecond set of calls:')
for i in range(MAX + 1):
    for j in range(MAX + 1):
        expensive(i, j)
print(expensive.cache_info())

print('\nClearing cache:')
expensive.cache_clear()
print(expensive.cache_info())

print('\nThird set of calls:')
for i in range(MAX):
    for j in range(MAX):
        expensive(i, j)
print(expensive.cache_info())

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

$ python3 functools_lru_cache.py

First set of calls:
expensive(0, 0)
expensive(0, 1)
expensive(1, 0)
expensive(1, 1)

Second set of calls:
expensive(0, 2)
expensive(1, 2)
expensive(2, 0)
expensive(2, 1)
expensive(2, 2)

Clearing cache:

Third set of calls:
expensive(0, 0)
expensive(0, 1)
expensive(1, 0)
expensive(1, 1)

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

functools_lru_cache_expire.py

import functools


@functools.lru_cache(maxsize2)
def expensive(a, b):
    print('called expensive({}, {})'.format(a, b))
    return a * b


def make_call(a, b):
    print('({}, {})'.format(a, b), end' ')
    pre_hits  expensive.cache_info().hits
    expensive(a, b)
    post_hits  expensive.cache_info().hits
    if post_hits > pre_hits:
        print('cache hit')


print('Establish the cache')
make_call(1, 2)
make_call(2, 3)

print('\nUse cached items')
make_call(1, 2)
make_call(2, 3)

print('\nCompute a new value, triggering cache expiration')
make_call(3, 4)

print('\nCache still contains one old item')
make_call(2, 3)

print('\nOldest item needs to be recomputed')
make_call(1, 2)

В этом примере размер кеша установлен на 2 записи. При использовании третьего набора уникальных аргументов ( 3, 4 ) самый старый элемент в кэше удаляется и заменяется новым результатом.

$ python3 functools_lru_cache_expire.py

Establish the cache
(1, 2) called expensive(1, 2)
(2, 3) called expensive(2, 3)

Use cached items
(1, 2) cache hit
(2, 3) cache hit

Compute a new value, triggering cache expiration
(3, 4) called expensive(3, 4)

Cache still contains one old item
(2, 3) cache hit

Oldest item needs to be recomputed
(1, 2) called expensive(1, 2)

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

functools_lru_cache_arguments.py

import functools


@functools.lru_cache(maxsize2)
def expensive(a, b):
    print('called expensive({}, {})'.format(a, b))
    return a * b


def make_call(a, b):
    print('({}, {})'.format(a, b), end' ')
    pre_hits  expensive.cache_info().hits
    expensive(a, b)
    post_hits  expensive.cache_info().hits
    if post_hits > pre_hits:
        print('cache hit')


make_call(1, 2)

try:
    make_call([1], 2)
except TypeError as err:
    print('ERROR: {}'.format(err))

try:
    make_call(1, {'2': 'two'})
except TypeError as err:
    print('ERROR: {}'.format(err))

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

$ python3 functools_lru_cache_arguments.py

(1, 2) called expensive(1, 2)
([1], 2) ERROR: unhashable type: 'list'
(1, {'2': 'two'}) ERROR: unhashable type: 'dict'

Сокращение набора данных

Функция reduce () принимает вызываемый объект и последовательность данных в качестве входных данных и создает одно значение в качестве выходных данных на основе вызова вызываемого объекта со значениями из последовательности и накопления результирующего вывода.

functools_reduce.py

import functools


def do_reduce(a, b):
    print('do_reduce({}, {})'.format(a, b))
    return a + b


data  range(1, 5)
print(data)
result  functools.reduce(do_reduce, data)
print('result: {}'.format(result))

В этом примере складываются числа во входной последовательности.

$ python3 functools_reduce.py

range(1, 5)
do_reduce(1, 2)
do_reduce(3, 3)
do_reduce(6, 4)
result: 10

Необязательный аргумент initializer помещается в начало последовательности и обрабатывается вместе с другими элементами. Это можно использовать для обновления ранее вычисленного значения новыми входными данными.

functools_reduce_initializer.py

import functools


def do_reduce(a, b):
    print('do_reduce({}, {})'.format(a, b))
    return a + b


data  range(1, 5)
print(data)
result  functools.reduce(do_reduce, data, 99)
print('result: {}'.format(result))

В этом примере предыдущая сумма 99 используется для инициализации значения, вычисленного с помощью reduce () .

$ python3 functools_reduce_initializer.py

range(1, 5)
do_reduce(99, 1)
do_reduce(100, 2)
do_reduce(102, 3)
do_reduce(105, 4)
result: 109

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

functools_reduce_short_sequences.py

import functools


def do_reduce(a, b):
    print('do_reduce({}, {})'.format(a, b))
    return a + b


print('Single item in sequence:',
      functools.reduce(do_reduce, [1]))

print('Single item in sequence with initializer:',
      functools.reduce(do_reduce, [1], 99))

print('Empty sequence with initializer:',
      functools.reduce(do_reduce, [], 99))

try:
    print('Empty sequence:', functools.reduce(do_reduce, []))
except TypeError as err:
    print('ERROR: {}'.format(err))

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

$ python3 functools_reduce_short_sequences.py

Single item in sequence: 1
do_reduce(99, 1)
Single item in sequence with initializer: 100
Empty sequence with initializer: 99
ERROR: reduce() of empty sequence with no initial value

Общие функции

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

functools_singledispatch.py

import functools


@functools.singledispatch
def myfunc(arg):
    print('default myfunc({!r})'.format(arg))


@myfunc.register(int)
def myfunc_int(arg):
    print('myfunc_int({})'.format(arg))


@myfunc.register(list)
def myfunc_list(arg):
    print('myfunc_list()')
    for item in arg:
        print('  {}'.format(item))


myfunc('string argument')
myfunc(1)
myfunc(2.3)
myfunc(['a', 'b', 'c'])

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

$ python3 functools_singledispatch.py

default myfunc('string argument')
myfunc_int(1)
default myfunc(2.3)
myfunc_list()
  a
  b
  c

Если для типа не найдено точное соответствие, оценивается порядок наследования и используется наиболее близкий соответствующий тип.

functools_singledispatch_mro.py

import functools


class A:
    pass


class B(A):
    pass


class C(A):
    pass


class D(B):
    pass


class E(C, D):
    pass


@functools.singledispatch
def myfunc(arg):
    print('default myfunc({})'.format(arg.__class__.__name__))


@myfunc.register(A)
def myfunc_A(arg):
    print('myfunc_A({})'.format(arg.__class__.__name__))


@myfunc.register(B)
def myfunc_B(arg):
    print('myfunc_B({})'.format(arg.__class__.__name__))


@myfunc.register(C)
def myfunc_C(arg):
    print('myfunc_C({})'.format(arg.__class__.__name__))


myfunc(A())
myfunc(B())
myfunc(C())
myfunc(D())
myfunc(E())

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

$ python3 functools_singledispatch_mro.py

myfunc_A(A)
myfunc_B(B)
myfunc_C(C)
myfunc_B(D)
myfunc_C(E)

Смотрите также