it-roy-ru.com

Как частично функция 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, будет передан как ylambda, который будет возвращать sum(1, y) i.e 1 + y.

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

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

140
user1865341

Грубо говоря, 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))

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

169
bereal

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

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

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

В этом случае 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)

но вы не можете - параметр sort ​​метода key принимает только те функции, которые принимают аргумент single.

поэтому переписать euclid_dist как функцию, принимающую параметр single:

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'

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

>>> import multiprocessing as MP

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

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

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

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

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

Простой, но аккуратный пример новичка из блога рассказывает, как можно использовать partial в 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).

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

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

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

26
sisanared

короткий ответ, 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
19
Alex-Antoine Fortin

На мой взгляд, это способ реализовать curry в python.

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

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

if __== "__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.

7
Hanzhou Tang