Como functools parcial faz o que faz?

180

Não consigo entender como a parcial funciona nas funções. Eu tenho o seguinte código daqui :

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

Agora na fila

incr = lambda y : sum(1, y)

Eu entendo que qualquer argumento que eu passar a increle será passado ysobre o lambdaque retornará sum(1, y)ie 1 + y.

Eu entendi aquilo. Mas eu não entendi isso incr2(4).

Como as 4passadas são passadas como xna função parcial? Para mim, 4deve substituir o sum2. Qual é a relação entre xe 4?

user1865341
fonte

Respostas:

218

Aproximadamente, partialfaz algo assim (além do suporte à palavra-chave args, etc.):

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

    return wrapper

Assim, chamando partial(sum2, 4)você cria uma nova função (uma chamada, para ser preciso) que se comporta como sum2, mas tem um argumento posicional a menos. Esse argumento ausente é sempre substituído por 4, para quepartial(sum2, 4)(2) == sum2(4, 2)

Quanto ao motivo, é necessário vários casos. Por um lado, suponha que você precise passar uma função em algum lugar onde se espera que tenha 2 argumentos:

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)

Mas uma função que você já tem precisa acessar algum terceiro contextobjeto para realizar seu trabalho:

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

Portanto, existem várias soluções:

Um objeto personalizado:

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)

Com parciais:

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

Desses três, partialé o mais curto e o mais rápido. (Para um caso mais complexo, você pode querer um objeto personalizado).

Sê real
fonte
1
de onde você conseguiu a extra_argsvariável
user1865341 11/11/2013
2
extra_argsé algo que foi passado pelo chamador parcial, no exemplo com p = partial(func, 1); f(2, 3, 4)ele (2, 3, 4).
bereal
1
mas por que faria isso, qualquer caso de uso especial, onde algo tem que ser feito por apenas parcial e não pode ser feito com outra coisa
user1865341
@ user1865341 Adicionei um exemplo à resposta.
11125 bereal
com o seu exemplo, qual é a relação entre callbackemy_callback
user1865341 11/11/2013
92

parciais são incrivelmente úteis.

Por exemplo, em uma sequência de chamadas de função "alinhadas por canal" (na qual o valor retornado de uma função é o argumento passado para a próxima).

Às vezes, uma função nesse pipeline requer um único argumento , mas a função imediatamente a montante dele retorna dois valores .

Nesse cenário, functools.partialpode permitir que você mantenha esse pipeline de funções intacto.

Aqui está um exemplo específico e isolado: suponha que você queira classificar alguns dados pela distância de cada ponto de dados de algum destino:

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

Para classificar esses dados por distância do alvo, o que você gostaria de fazer é:

data.sort(key=euclid_dist)

mas você não pode - o parâmetro- chave do método de classificação aceita apenas funções que usam um único argumento.

reescreva euclid_distcomo uma função usando um único parâmetro:

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist agora aceita um único argumento,

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

então agora você pode classificar seus dados passando a função parcial para o argumento-chave do método de classificação:

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

Ou, por exemplo, um dos argumentos da função muda em um loop externo, mas é corrigido durante a iteração no loop interno. Ao usar um parcial, você não precisa passar o parâmetro adicional durante a iteração do loop interno, porque a função modificada (parcial) não exige isso.

>>> from functools import partial

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

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

crie uma função parcial (usando a palavra-chave arg)

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

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

você também pode criar uma função parcial com um argumento posicional

>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

mas isso será lançado (por exemplo, criando parcial com argumento de palavra-chave e depois chamando usando argumentos posicionais)

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

outro caso de uso: escrever código distribuído usando a multiprocessingbiblioteca do python . Um conjunto de processos é criado usando o método Pool:

>>> import multiprocessing as MP

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

Pool possui um método de mapa, mas são necessárias apenas uma iterável; portanto, se você precisar transmitir uma função com uma lista de parâmetros mais longa, redefina a função como parcial, para corrigir todos, exceto um:

>>> ppool.map(pfnx, [4, 6, 7, 8])
doug
fonte
1
existe algum uso prático desta função somewher
user1865341
3
@ user1865341 acrescentou dois casos de uso exemplarmente para a minha resposta
Doug
IMHO, essa é uma resposta melhor, pois mantém conceitos não relacionados, como objetos e classes, e se concentra em funções, e é isso que trata.
akhan 4/04
35

resposta curta, partialfornece valores padrão aos parâmetros de uma função que, de outra forma, não teria valores padrão.

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
Alex Fortin
fonte
5
esta é uma meia verdade, porque podemos substituir os valores padrão, podemos até mesmo substituir anulado parâmetros por subseqüente partiale assim por diante
Azat Ibrakov
33

Parciais podem ser usados ​​para criar novas funções derivadas que têm alguns parâmetros de entrada pré-atribuídos

Para ver o uso real de parciais no mundo real, consulte esta publicação realmente boa no blog:
http://chriskiehl.com/article/Cleaner-coding-through-partially-applied-functions/

Um simples, mas o exemplo de iniciante puro do blog, covers como alguém pode usar partialno re.searchpara tornar o código mais legível. re.searchA assinatura do método é:

search(pattern, string, flags=0) 

Ao aplicar partial, podemos criar várias versões da expressão regular searchpara atender aos nossos requisitos, por exemplo:

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

Agora is_spaced_aparte is_grouped_togethersão duas novas funções derivadas re.searchque têm o patternargumento aplicado (já que patterné o primeiro argumento na re.searchassinatura do método).

A assinatura dessas duas novas funções (solicitadas) é:

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

É assim que você pode usar essas funções parciais em algum texto:

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

Você pode consultar o link acima para obter uma compreensão mais aprofundada do assunto, pois abrange este exemplo específico e muito mais.

sisanared
fonte
1
Isso não é equivalente a is_spaced_apart = re.compile('[a-zA-Z]\s\=').search? Em caso afirmativo, existe uma garantia de que o partialidioma compila a expressão regular para uma reutilização mais rápida?
Aristide
10

Na minha opinião, é uma maneira de implementar o curry em 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))

O resultado é 3 e 4.

Hanzhou Tang
fonte
1

Também vale mencionar que quando a função parcial passou em outra função em que queremos "codificar" alguns parâmetros, esse deve ser o parâmetro mais à direita

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

mas se fizermos o mesmo, mas mudar um parâmetro

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

ele lançará o erro "TypeError: func () obteve vários valores para o argumento 'a'"

MSK
fonte
Hã? Você faz o parâmetro mais à esquerda assim:prt=partial(func, 7)
DylanYoung
0

Esta resposta é mais um exemplo de código. Todas as respostas acima dão boas explicações sobre por que se deve usar parcial. Vou dar minhas observações e casos de uso sobre parcial.

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 saída do código acima deve ser:

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

Observe que no exemplo acima foi retornada uma nova chamada que aceitará o parâmetro (c) como argumento. Observe que também é o último argumento para a função.

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

A saída do código acima também é:

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

Observe que * foi usado para descompactar os argumentos que não são de palavra-chave e a chamada retornada em termos de qual argumento ele pode usar é o mesmo que acima.

Outra observação é: O exemplo abaixo demonstra que retornos parciais podem ser chamados e que tomarão o parâmetro não declarado (a) como argumento.

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 saída do código acima deve ser:

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

Similarmente,

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

Impressões acima do código

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

Eu tive que usá-lo quando eu estava usando o Pool.map_asyncmétodo do multiprocessingmódulo. Você pode passar apenas um argumento para a função worker, então eu tive que usar partialpara fazer com que minha função worker parecesse passível de chamada com apenas um argumento de entrada, mas, na realidade, minha função worker tinha vários argumentos de entrada.

Ruthvik Vaila
fonte