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?

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á ...!


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:


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>

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 .


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


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.


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.