Как частично функция functools делает то, что делает?

Я не могу понять, как частично работает в functools. У меня есть следующий код здесь:

>>> sum = lambda x, y : x + y
>>> sum(1, 2)
3
>>> incr = lambda y : sum(1, y)
>>> incr(2)
3
>>> def sum2(x, y):
    return x + y

>>> incr2 = functools.partial(sum2, 1)
>>> incr2(4)
5

Теперь в строке

incr = lambda y : sum(1, y)

Я получаю, что любой аргумент, передаваемый в incr, будет передан как y в lambda, который вернет sum(1, y) i.e 1 + y.

Я это понимаю. Но я не понял этого incr2(4).

Как передается 4 как x в частичной функции? Для меня 4 должен заменить sum2. Какова связь между x и 4?

Ответ 1

Грубо говоря, partial делает что-то вроде этого (кроме поддержки ключевых слов и т.д.):

def partial(func, *part_args):
    def wrapper(*extra_args):
        args = list(part_args)
        args.extend(extra_args)
        return func(*args)

    return wrapper

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

В связи с тем, почему это необходимо, существует множество случаев. Только для одного предположим, что вам нужно передать функцию где-нибудь, где ожидается, что у нее есть 2 аргумента:

class EventNotifier(object):
    def __init__(self):
        self._listeners = []

    def add_listener(self, callback):
        ''' callback should accept two positional arguments, event and params '''
        self._listeners.append(callback)
        # ...

    def notify(self, event, *params):
        for f in self._listeners:
            f(event, params)

Но у вас уже есть доступ к некоторому третьему объекту context, чтобы выполнить свою работу:

def log_event(context, event, params):
    context.log_event("Something happened %s, %s", event, params)

Итак, существует несколько решений:

Пользовательский объект:

class Listener(object):
   def __init__(self, context):
       self._context = context

   def __call__(self, event, params):
       self._context.log_event("Something happened %s, %s", event, params)


 notifier.add_listener(Listener(context))

Lambda:

log_listener = lambda event, params: log_event(context, event, params)
notifier.add_listener(log_listener)

С частичными:

context = get_context()  # whatever
notifier.add_listener(partial(log_event, context))

Из этих трех <<20 > является самым коротким и быстрым. (Для более сложного случая вам может понадобиться пользовательский объект).

Ответ 2

частичные невероятно полезны.

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

Иногда функция в таком конвейере требует одного аргумента, но функция, непосредственно выходящая из нее, возвращает два значения.

В этом случае functools.partial может позволить вам сохранить целостность конвейера этой функции.

Вот конкретный изолированный пример: предположим, что вы хотите отсортировать некоторые данные по каждому удалению точки данных от какой-либо цели:

# create some data
import random as RND
fnx = lambda: RND.randint(0, 10)
data = [ (fnx(), fnx()) for c in range(10) ]
target = (2, 4)

import math
def euclid_dist(v1, v2):
    x1, y1 = v1
    x2, y2 = v2
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

Чтобы отсортировать данные по расстоянию от цели, конечно, вы должны сделать следующее:

data.sort(key=euclid_dist)

но вы не можете - параметр ключа метода сортировки принимает только функции, принимающие один аргумент.

поэтому перезапишите euclid_dist как функцию, берущую один параметр:

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist теперь принимает единственный аргумент,

>>> p_euclid_dist((3, 3))
  1.4142135623730951

теперь вы можете сортировать свои данные, передав частичную функцию для аргумента ключа метода сортировки:

data.sort(key=p_euclid_dist)

# verify that it works:
for p in data:
    print(round(p_euclid_dist(p), 3))

    1.0
    2.236
    2.236
    3.606
    4.243
    5.0
    5.831
    6.325
    7.071
    8.602

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

>>> from functools import partial

>>> def fnx(a, b, c):
      return a + b + c

>>> fnx(3, 4, 5)
      12

создать частичную функцию (используя ключевое слово arg)

>>> pfnx = partial(fnx, a=12)

>>> pfnx(b=4, c=5)
     21

вы также можете создать частичную функцию с позиционным аргументом

>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

но это вызовет (например, создание частичного с аргументом ключевого слова, а затем вызов с использованием позиционных аргументов)

>>> pfnx = partial(fnx, a=12)

>>> pfnx(4, 5)
      Traceback (most recent call last):
      File "<pyshell#80>", line 1, in <module>
      pfnx(4, 5)
      TypeError: fnx() got multiple values for keyword argument 'a'

другой вариант использования: запись распределенного кода с использованием библиотеки python multiprocessing. Пул процессов создается с помощью метода пула:

>>> import multiprocessing as MP

>>> # create a process pool:
>>> ppool = MP.Pool()

Pool имеет метод отображения, но он принимает только один итеративный, поэтому, если вам нужно передать функцию с более длинным списком параметров, переопределите функцию как частичную, чтобы исправить все, кроме одного:/p >

>>> ppool.map(pfnx, [4, 6, 7, 8])

Ответ 3

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

Чтобы увидеть реальное использование партиалов, обратитесь к этому действительно хорошему сообщению в блоге:
http://chriskiehl.com/article/Cleaner-coding-through-partially-applied-functions/

Простой, но аккуратный пример для начинающих из блога, рассказывает, как можно использовать partial в re.search чтобы сделать код более читабельным. re.search метода re.search:

search(pattern, string, flags=0) 

Применяя partial мы можем создать несколько версий search по регулярному выражению в соответствии с нашими требованиями, например:

is_spaced_apart = partial(re.search, '[a-zA-Z]\s\=')
is_grouped_together = partial(re.search, '[a-zA-Z]\=')

Теперь is_spaced_apart и is_grouped_together - это две новые функции, производные от re.search, к которым применен аргумент pattern (так как pattern является первым аргументом в re.search метода re.search).

Сигнатура этих двух новых функций (вызываемых):

is_spaced_apart(string, flags=0)     # pattern '[a-zA-Z]\s\=' applied
is_grouped_together(string, flags=0) # pattern '[a-zA-Z]\=' applied

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

for text in lines:
    if is_grouped_together(text):
        some_action(text)
    elif is_spaced_apart(text):
        some_other_action(text)
    else:
        some_default_action()

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

Ответ 4

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

from functools import partial

def foo(a,b):
    return a+b

bar = partial(foo, a=1) # equivalent to: foo(a=1, b)
bar(b=10)
#11 = 1+10
bar(a=101, b=10)
#111=101+10

Ответ 5

На мой взгляд, это способ реализации карри в Python.

from functools import partial
def add(a,b):
    return a + b

def add2number(x,y,z):
    return x + y + z

if __name__ == "__main__":
    add2 = partial(add,2)
    print("result of add2 ",add2(1))
    add3 = partial(partial(add2number,1),2)
    print("result of add3",add3(1))

Результат 3 и 4.

Ответ 6

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

def func(a,b):
    return a*b
prt = partial(func, b=7)
    print(prt(4))
#return 28

но если мы сделаем то же самое, но изменим параметр вместо

def func(a,b):
    return a*b
 prt = partial(func, a=7)
    print(prt(4))

будет выдано сообщение об ошибке: "TypeError: func() получил несколько значений для аргумента" a ""