Шаг 124.
Python: сборник рецептов. Функции. Заставляем ... объект с N аргументами работать так же, как ... объект с меньшим количеством аргументов

    На этом шаге мы рассмотрим использование функции functools.partial().

Задача

    У вас есть вызываемый объект, который вы хотели бы использовать в какой-то программе Python - возможно, в качестве функции обратного вызова (callback function) или обработчика (handler), но он принимает слишком много аргументов и при вызове возбуждает исключение.

Решение

    Если вам нужно уменьшить количество аргументов функции, используйте functools.partial(). Функция partial() позволяет присваивать фиксированные значения одному или более аргументам, что уменьшает количество аргументов, которые должны быть переданы в последующих вызовах. Например, у вас есть вот такая функция:

>>> def spam(a, b, c, d):
	print(a, b, c, d)

	
>>> 

    А теперь попробуем partial(), чтобы зафиксировать значения некоторых аргументов:

>>> from functools import partial
>>> s1 = partial(spam, 1)	# a = 1
>>> s1(2, 3, 4)
1 2 3 4
>>> s1(4, 5, 6)
1 4 5 6
>>> s2 = partial(spam, d=42) # d = 42
>>> s2(1, 2, 3)
1 2 3 42
>>> s2(4, 5, 5)
4 5 5 42
>>> s3 = partial(spam, 1, 2, d=42) # a = 1, b = 2, d = 42
>>> s3(3)
1 2 3 42
>>> s3(4)
1 2 4 42
>>> s3(5)
1 2 5 42
>>> 

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

Обсуждение

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

    Первый пример. Предположим, что у вас есть список точек, представленных как кортежи координат (x, y). Вы можете использовать такую функцию для вычисления расстояния между двумя точками:

>>> points = [(1, 2), (3, 4), (5, 6), (7, 8)]
>>> import math
>>> def distance(p1, p2):
	x1, y1 = p1
	x2, y2 = p2
	return math.hypot(x2 - x1, y2 - y1)

>>> 

    А теперь предположим, что вы хотите отсортировать все точки по их расстоянию до какой-то другой точки. Метод списков sort() принимает аргумент key, который может быть использован для настройки поиска, но он работает только с функциями, которые принимают один аргумент (то есть distance() не подходит). Вот как вы можете использовать partial(), чтобы решить данную проблему:

>>> pt = (4, 3)
>>> points.sort(key=partial(distance, pt))
>>> points
[(3, 4), (1, 2), (5, 6), (7, 8)]
>>> 

    Развивая эту идею, заметим, что partial() часто может использоваться для настройки сигнатур аргументов функций обратного вызова, используемых в других библиотеках. Например, вот пример кода, который использует multiprocessing для асинхронного вычисления результата, который передается функции обратного вызова, принимающей результат и необязательный аргумент настройки логирования:

def output_result(result, log=None):
    if log is not None:
        log.debug('Got: %r', result)


# Функция-пример
def add(x, y):
    return x + y


if __name__ == '__main__':
    import logging
    from multiprocessing import Pool
    from functools import partial

    logging.basicConfig(level=logging.DEBUG)
    log = logging.getLogger('test')
    p = Pool()
    p.apply_async(add, (3, 4), callback=partial(output_result, log=log))
    p.close()
    p.join()
Архив с файлом можно взять здесь.

    При передаче функции обратного вызова с использованием apply_async() дополнительный аргумент настройки логирования передается с использованием partial(). А multiprocessing просто вызывает функцию обратного вызова с единственным значением.

    В качестве похожего примера рассмотрим задачу написания сетевых серверов. Модуль socketserver позволяет сделать это без особого труда. Например, вот простой эхо-сервер:

from socketserver import StreamRequestHandler, TCPServer


class EchoHandler(StreamRequestHandler): 
    def handle(self): 
        for line in self.rfile:
            self.wfile.write(b'GOT:' + line)

serv = TCPServer(('', 15000), EchoHandler) 
serv.serve_forever()

    Предположим, что вы хотите наделить класс EchoHandler методом __init__(), который принимает дополнительный конфигурирующий аргумент. Например:

class EchoHandler(StreamRequestHandler):
#  ack - это добавленный обязательный именованный аргумент.
#  *args, **kwargs - это любые обычные предоставленные параметры
#  (которые переданы)
    def __init__(self, *args, ack, **kwargs):
        self.ack = ack
        super().__init__(*args, **kwargs)

    def handle(self):
        for line in self.rfile:
            self.wfile.write(self.ack + line)

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

Exception happened during processing of request from ('127.0.0.1', 59834)
Traceback (most recent call last):
.   .   .   .
TypeError: __init__() missing 1 required keyword-only argument: 'ack'

    На первый взгляд кажется невозможным исправить этот код без попыток поправить исходник socketserver или еще какого-то странного обходного решения. Однако задача легко решается с помощью partial() - используйте ее, чтобы предоставить значение аргумента ack:

from functools import partial

serv = TCPServer(('', 15000), partial(EchoHandler, ack=b'RECEIVED:')) 
serv.serve_forever()

    В этом примере определение аргумента ack в методе __init__() может показаться немного странным, но он определяется как обязательный именованный аргумент.

    Иногда функциональность partial() заменяется lambda-выражением. Например, в предыдущем примере можно применить такие инструкции:

points.sort(key=lambda p: distance(pt, p))
p.apply_async(add, (3, 4), callback= lambda result: output_result(result,log))
serv = TCPServer(('', 15000),
    lambda *args, **kwargs: EchoHandler(*args, ack=b'RECEIVED:', **kwargs))

    Этот код работает, но он более многословен и может запутать того, кто его читает. Использование partial() более явно сообщает о вашем намерении (передать значения некоторым аргументам).

    На следующем шаге мы рассмотрим замену классов с одним методом функциями.




Предыдущий шаг Содержание Следующий шаг