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

181

Я не могу понять, как работает частичное в 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)ИЭ 1 + y.

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

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

user1865341
источник

Ответы:

218

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

будь реальным
источник
1
откуда вы extra_args
взяли
2
extra_argsэто то, что прошло частичный вызывающий, в примере с p = partial(func, 1); f(2, 3, 4)ним есть (2, 3, 4).
2013 года
1
но зачем нам это делать, в любом особом случае использования, когда что-то должно быть сделано только частично, а не с другой
user1865341
@ user1865341 Я добавил пример к ответу.
2013 года
с вашим примером, какова связь между callbackиmy_callback
user1865341
92

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

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

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

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

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

>>> import multiprocessing as MP

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

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

>>> ppool.map(pfnx, [4, 6, 7, 8])
Дуг
источник
1
есть ли практическое использование этой функции где-нибудь
user1865341
3
@ user1865341 добавил два примерных варианта использования в мой ответ
doug
ИМХО, это лучший ответ, так как он не учитывает несвязанные понятия, такие как объекты и классы, и сосредотачивается на функциях, о чем идет речь.
Ахан
35

краткий ответ, 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
это наполовину верно, потому что мы можем переопределить значения по умолчанию, мы можем даже переопределить переопределенные параметры последующими partialи так далее
Азат Ибраков
33

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

Чтобы увидеть реальное использование партиалов, обратитесь к этому действительно хорошему сообщению в блоге:
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()

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

sisanared
источник
1
Разве это не эквивалентно is_spaced_apart = re.compile('[a-zA-Z]\s\=').search? Если так, есть ли гарантия, что partialидиома скомпилирует регулярное выражение для более быстрого повторного использования?
Аристид
10

На мой взгляд, это способ реализации карри в 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.

Ханьчжоу Тан
источник
1

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

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

ММС
источник
А? Вы делаете самый левый параметр, как это:prt=partial(func, 7)
DylanYoung
0

Этот ответ является скорее примером кода. Все приведенные выше ответы дают хорошие объяснения относительно того, почему следует использовать частичное. Я приведу свои наблюдения и буду использовать случаи частичного.

from functools import partial
 def adder(a,b,c):
    print('a:{},b:{},c:{}'.format(a,b,c))
    ans = a+b+c
    print(ans)
partial_adder = partial(adder,1,2)
partial_adder(3)  ## now partial_adder is a callable that can take only one argument

Вывод приведенного выше кода должен быть:

a:1,b:2,c:3
6

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

args = [1,2]
partial_adder = partial(adder,*args)
partial_adder(3)

Вывод вышеуказанного кода также:

a:1,b:2,c:3
6

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

Другое наблюдение: Пример ниже демонстрирует, что partal возвращает вызываемый объект, который примет необъявленный параметр (a) в качестве аргумента.

def adder(a,b=1,c=2,d=3,e=4):
    print('a:{},b:{},c:{},d:{},e:{}'.format(a,b,c,d,e))
    ans = a+b+c+d+e
    print(ans)
partial_adder = partial(adder,b=10,c=2)
partial_adder(20)

Вывод приведенного выше кода должен быть:

a:20,b:10,c:2,d:3,e:4
39

Так же,

kwargs = {'b':10,'c':2}
partial_adder = partial(adder,**kwargs)
partial_adder(20)

Выше кода печатает

a:20,b:10,c:2,d:3,e:4
39

Я должен был использовать его, когда я использовал Pool.map_asyncметод из multiprocessingмодуля. Вы можете передать только один аргумент в рабочую функцию, поэтому мне пришлось использовать, partialчтобы моя рабочая функция выглядела как вызываемая только с одним входным аргументом, но на самом деле моя рабочая функция имела несколько входных аргументов.

Рутвик Вайла
источник