Python: Por que o functools.partial é necessário?

193

Aplicação parcial é legal. Que funcionalidade functools.partialoferece e que você não consegue passar pelas lambdas?

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

De functoolsalguma forma, é mais eficiente ou legível?

Nick Heiner
fonte

Respostas:

266

Que funcionalidade functools.partialoferece e que você não consegue passar pelas lambdas?

Não há muito em termos de funcionalidade extra (mas, veja mais adiante) - e a legibilidade está nos olhos de quem vê.
A maioria das pessoas que estão familiarizadas com linguagens de programação funcionais (aquelas da família Lisp / Scheme em particular) parece gostar lambdamuito bem - eu digo "a maioria", definitivamente não todas, porque Guido e eu certamente estamos entre os "familiarizados" (etc ) ainda pense lambdacomo uma anomalia desagradável em Python ...
Ele se arrependeu de ter aceitado o Python, enquanto planejava removê-lo do Python 3, como uma das "falhas do Python".
Eu o apoiei totalmente nisso. (Adoro lambda no Scheme ... enquanto suas limitações em Python e a maneira estranha como ele simplesmente não faz ' com o resto do idioma, faça minha pele arrepiar).

Não é assim, no entanto, para as hordas de lambdaamantes - que encenaram uma das coisas mais próximas de uma rebelião já vista na história de Python, até Guido voltar atrás e decidir sair lambda.
Várias adições possíveis a functools(para fazer funções retornarem constantes, identidade, etc) não aconteceu (para evitar duplicar explicitamente mais lambdafuncionalidades), embora partialtenha permanecido, é claro (não é uma duplicação total , nem é uma desagradável).

Lembre-se de que lambdao corpo é limitado para ser uma expressão , por isso tem limitações. Por exemplo...:

>>> import functools
>>> f = functools.partial(int, base=2)
>>> f.args
()
>>> f.func
<type 'int'>
>>> f.keywords
{'base': 2}
>>> 

functools.partialA função retornada é decorada com atributos úteis para introspecção - a função que está agrupando e quais argumentos posicionais e nomeados ela fixa nela. Além disso, os argumentos nomeados podem ser substituídos de volta (a "correção" é, em certo sentido, a configuração dos padrões):

>>> f('23', base=10)
23

Então, como você vê, definitivamente não é tão simplista quanto lambda s: int(s, base=2)! -)

Sim, você pode contorcer seu lambda para fornecer um pouco disso, por exemplo, para a substituição de palavras-chave,

>>> f = lambda s, **k: int(s, **dict({'base': 2}, **k))

mas espero sinceramente que mesmo o lambdaamante mais ardente não considere esse horror mais legível do que a partialligação! -). A parte "configuração do atributo" é ainda mais difícil, devido à limitação "do corpo como uma única expressão" do Python lambda(além do fato de que a atribuição nunca poder fazer parte de uma expressão do Python) ... você acaba "fingindo atribuições dentro de uma expressão" estendendo a compreensão da lista muito além dos limites de design ...:

>>> f = [f for f in (lambda f: int(s, base=2),)
           if setattr(f, 'keywords', {'base': 2}) is None][0]

Agora combine a substituibilidade dos argumentos nomeados, mais a configuração de três atributos, em uma única expressão e me diga o quão legível isso será ...!

Alex Martelli
fonte
2
Sim, eu diria que a funcionalidade extra functools.partialmencionada o torna superior ao lambda. Talvez este seja o tópico de outro post, mas o que é que em um nível de design o incomoda tanto lambda?
Nick Heiner
11
@Rosarch, como eu disse: em primeiro lugar, que as limitações (Python distingue nitidamente expressões e declarações - há muito que você não pode fazer, ou não fazer de forma sensata , dentro de uma única expressão, e é isso que o corpo de uma lambda é ); segundo, é absolutamente estranho o açúcar da sintaxe. Se eu pudesse voltar no tempo e mudar uma coisa no Python, seria absurdo, sem sentido, desagradável defe lambdapalavras-chave: crie os dois function(uma escolha de nome Javascript ficou realmente certa) e pelo menos 1/3 das minhas objeções desapareceriam ! -). Como eu disse, não tenho nenhuma objeção a lambda no Lisp ...!)
Alex Martelli
1
@ Alex Martelli, Por que Guido estabeleceu uma limitação para lambda: "o corpo é uma expressão única"? O corpo lambda do C # pode ser qualquer coisa válida no corpo de uma função. Por que o Guido não remove a limitação do python lambda?
Peter Long
3
@ PeterLong Espero que Guido possa responder sua pergunta. A essência disso é que seria muito complexo e você pode usar um de defqualquer maneira. Nosso líder benevolente falou!
New123456
5
@AlexMartelli DropBox teve uma influência interessante sobre Guido - twitter.com/gvanrossum/status/391769557758521345
David
82

Bem, aqui está um exemplo que mostra uma diferença:

In [132]: sum = lambda x, y: x + y

In [133]: n = 5

In [134]: incr = lambda y: sum(n, y)

In [135]: incr2 = partial(sum, n)

In [136]: print incr(3), incr2(3)
8 8

In [137]: n = 9

In [138]: print incr(3), incr2(3)
12 8

Essas postagens de Ivan Moore expandem as "limitações do lambda" e os fechamentos em python:

ars
fonte
1
Bom exemplo. Para mim, isso parece mais um "bug" com o lambda, na verdade, mas eu entendo que outros possam discordar. (Algo semelhante acontece com fechos definidos dentro de um loop, como implementado em várias linguagens de programação.)
ShreevatsaR
28
A correção para esse "dilema de vinculação antecipada e tardia" é usar explicitamente a ligação antecipada, quando você desejar lambda y, n=n: .... A ligação tardia (de nomes que aparecem apenas no corpo de uma função, não em seu defequivalente ou em outra lambda) é apenas um bug, como já mostrei detalhadamente em longas respostas de SO no passado: você vincula explicitamente quando é isso que deseja, use o padrão de ligação tardia quando é isso que você deseja, e essa é exatamente a escolha certa para o design, dado o contexto do restante do design do Python.
Alex-Martelli
1
@ Alex Martelli: Sim, desculpe. Eu simplesmente não consigo me acostumar com a ligação tardia corretamente, talvez porque acho que, ao definir funções, eu esteja realmente definindo algo para o bem, e as surpresas inesperadas só me causem dores de cabeça. (Mais quando tento fazer coisas funcionais em Javascript do que em Python.) Entendo que muitas pessoas se sentem confortáveis ​​com a ligação tardia e que isso é consistente com o restante do design do Python. Eu ainda gostaria de ler suas outras respostas longas para o SO - links? :-)
ShreevatsaR
3
Alex está certo, não é um bug. Mas é uma pegadinha que prende muitos entusiastas de lambda. Para o lado "bug" do argumento de um tipo haskel / funcional, consulte a publicação de Andrej Bauer: math.andrej.com/2009/04/09/pythons-lambda-is-broken
ars
@ars: Ah, sim, obrigado pelo link para a publicação de Andrej Bauer. Sim, os efeitos da ligação tardia são certamente algo que nós do tipo matemática (pior, com experiência em Haskell) continuamos achando grosseiramente inesperado e chocante. :-) Não tenho certeza se chegaria até o Prof. Bauer e chamaria de erro de design, mas é difícil para programadores humanos alternar completamente entre uma maneira de pensar e outra. (Ou talvez este é apenas o meu insuficiente experiência Python.)
ShreevatsaR
26

Nas versões mais recentes do Python (> = 2.7), você pode picklea partial, mas não a lambda:

>>> pickle.dumps(partial(int))
'cfunctools\npartial\np0\n(c__builtin__\nint\np1\ntp2\nRp3\n(g1\n(tNNtp4\nb.'
>>> pickle.dumps(lambda x: int(x))
Traceback (most recent call last):
  File "<ipython-input-11-e32d5a050739>", line 1, in <module>
    pickle.dumps(lambda x: int(x))
  File "/usr/lib/python2.7/pickle.py", line 1374, in dumps
    Pickler(file, protocol).dump(obj)
  File "/usr/lib/python2.7/pickle.py", line 224, in dump
    self.save(obj)
  File "/usr/lib/python2.7/pickle.py", line 286, in save
    f(self, obj) # Call unbound method with explicit self
  File "/usr/lib/python2.7/pickle.py", line 748, in save_global
    (obj, module, name))
PicklingError: Can't pickle <function <lambda> at 0x1729aa0>: it's not found as __main__.<lambda>
Fred Foo
fonte
1
Infelizmente, as funções parciais falham em buscar multiprocessing.Pool.map(). stackoverflow.com/a/3637905/195139
wting
3
@wting Essa publicação é de 2010. partialé selecionável no Python 2.7.
Fred Foo
22

O functools é de alguma forma mais eficiente ..?

Como resposta parcial a isso, decidi testar o desempenho. Aqui está o meu exemplo:

from functools import partial
import time, math

def make_lambda():
    x = 1.3
    return lambda: math.sin(x)

def make_partial():
    x = 1.3
    return partial(math.sin, x)

Iter = 10**7

start = time.clock()
for i in range(0, Iter):
    l = make_lambda()
stop = time.clock()
print('lambda creation time {}'.format(stop - start))

start = time.clock()
for i in range(0, Iter):
    l()
stop = time.clock()
print('lambda execution time {}'.format(stop - start))

start = time.clock()
for i in range(0, Iter):
    p = make_partial()
stop = time.clock()
print('partial creation time {}'.format(stop - start))

start = time.clock()
for i in range(0, Iter):
    p()
stop = time.clock()
print('partial execution time {}'.format(stop - start))

no Python 3.3, ele fornece:

lambda creation time 3.1743163756961392
lambda execution time 3.040552701787919
partial creation time 3.514482823352731
partial execution time 1.7113973411608114

O que significa que o parcial precisa de um pouco mais de tempo para criação, mas consideravelmente menos tempo para a execução. Isso pode muito bem ser o efeito da ligação antecipada e tardia, discutida na resposta de ars .

Trilarion
fonte
3
Mais importante, partialé escrito em C, em vez de puro Python, o que significa que pode produzir uma chamada mais eficiente do que simplesmente criar uma função que chama outra função.
chepner
12

Além da funcionalidade extra mencionada por Alex, outra vantagem do functools.partial é a velocidade. Com parcial, você pode evitar construir (e destruir) outro quadro de pilha.

Nem a função gerada por parciais nem por lambdas possui documentos por padrão (embora você possa definir o documento para qualquer objeto via __doc__).

Você pode encontrar mais detalhes neste blog: Aplicativo de Função Parcial em Python

Leonardo.Z
fonte
Se você testou a vantagem da velocidade, que melhoria de velocidade parcial sobre lambda pode ser esperada?
Trilarion
1
Quando você diz que a docstring é herdada, a qual versão do Python você se refere? No Python 2.7.15 e Python 3.7.2, eles não são herdados. O que é bom, porque a documentação original não está necessariamente correta para a função com argumentos parcialmente aplicados.
jan
Para python 2.7 ( docs.python.org/2/library/functools.html#partial-objects ): "o nome e os atributos do documento não são criados automaticamente". O mesmo para 3. [5-7].
Yaroslav Nikitenko
Há um erro no seu link: log_info = parcial (log_template, level = "info") - não é possível porque level não é um argumento de palavra-chave no exemplo. Tanto o python 2 quanto o 3 dizem: "TypeError: log_template () obteve vários valores para o argumento 'level'".
Yaroslav Nikitenko
De fato, criei um parcial (f) manualmente e ele fornece o campo doc como 'parcial (func, * args, ** palavras-chave) - nova função com aplicação parcial \ n dos argumentos e das palavras-chave fornecidas. \ N' (ambos para python 2 e 3).
Yaroslav Nikitenko
1

Entendo a intenção mais rapidamente no terceiro exemplo.

Quando analiso lambdas, espero mais complexidade / singularidade do que a oferecida pela biblioteca padrão diretamente.

Além disso, você notará que o terceiro exemplo é o único que não depende da assinatura completa de sum2; tornando-o assim um pouco mais frouxamente acoplado.

Jon-Eric
fonte
1
Hum, na verdade sou da persuasão oposta, demorei muito mais para analisar a functools.partialligação, enquanto as lambdas são evidentes.
David