Python: зачем нужен functools.partial?

200

Частичное приложение - это круто. Какие функции functools.partialвы не можете получить с помощью лямбда-выражений?

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

Как- functoolsто более эффективно или читабельно?

Ник Хайнер
источник

Ответы:

270

Какие функции functools.partialвы не можете получить с помощью лямбда-выражений?

Немногое с точки зрения дополнительной функциональности (но, см. Ниже) - и читаемость в глазах смотрящего.
Большинству людей, знакомых с функциональными языками программирования (в частности, из семейств Lisp / Scheme), кажется, это нравится lambdaпросто - я говорю «большинство», определенно не все, потому что мы с Гвидо, несомненно, относимся к тем, кто «знаком с» (т. ), но считают, lambdaчто это неприятная аномалия в Python ...
Он раскаивался в том, что когда-либо принимал это в Python, тогда как планировал удалить его из Python 3, как один из «глюков Python».
Я полностью его в этом поддержал. (Мне нравится lambda Scheme ... в то время как его ограничения в Python и странный способ, которым он просто не с остальным языком заставляю кожу ползать).

Однако не так для орды lambdaвлюбленных, которые устроили один из самых близких к восстанию событий в истории Python, пока Гвидо не отступил и не решил уйти lambda.
Несколько возможных дополнений к functools(чтобы сделать функции, возвращающие константы, идентичность, и т.д.) не произошло (чтобы избежать явного дублирования дополнительных lambdaфункций), хотя partial, конечно, осталось (это не полное дублирование и не бельмо на глазу).

Помните, что lambdaтело ограничено выражением , поэтому у него есть ограничения. Например...:

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

functools.partialВозвращаемая функция украшена атрибутами, полезными для самоанализа - функцией, которую она обертывает, и позиционными и именованными аргументами, которые она там исправляет. Кроме того, названные аргументы могут быть переопределены сразу же («исправление», скорее, в некотором смысле, установка значений по умолчанию):

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

Так что, как видите, это определенно не так упрощенно, как lambda s: int(s, base=2)! -)

Да, вы можете изменить свою лямбду, чтобы дать вам кое-что из этого - например, для переопределения ключевых слов,

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

но очень надеюсь, что даже самый ярый lambdaлюбитель не сочтет этот ужас более читабельным, чем partialпризыв! -). Часть «установки атрибута» еще сложнее из-за ограничения Python «тело - одно выражение» lambda(плюс тот факт, что присваивание никогда не может быть частью выражения Python) ... вы в конечном итоге «подделываете присваивания внутри выражения» расширяя понимание списка далеко за пределы его дизайна ...:

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

Теперь объединить названные-аргументы overridability, а также установку трех атрибутов, в одно выражение, и скажите мне, насколько читаемым , что будет ...!

Алекс Мартелли
источник
2
Да, я бы сказал, что дополнительные функциональные возможности, о functools.partialкоторых вы упомянули, делают его лучше лямбда. Возможно, это тема другого поста, но что вас так сильно беспокоит на уровне дизайна lambda?
Ник Хайнер,
12
@Rosarch, как я уже говорил: во- первых, ограничения (Python резко отличает выражения и высказывания - есть много вы не можете сделать, или не может сделать благоразумно , в одном выражении, и это то, что тело лямбда в это ); во-вторых, это совершенно странный синтаксический сахар. Если бы я мог вернуться во времени и изменить одну вещь в Python, это были бы абсурдные, бессмысленные, раздражающие глаза defи lambdaключевые слова: сделайте их и то и другое function(выбор одного имени Javascript был действительно правильным), и по крайней мере 1/3 моих возражений исчезнет ! -). Как я уже сказал, я не возражаю против лямбда в Лиспе ...! -)
Alex Martelli
1
@Alex Martelli, почему Гвидо установил такое ограничение для лямбды: "тело - это одно выражение"? Тело лямбда C # может быть любым допустимым в теле функции. Почему бы Гвидо просто не снять ограничение для лямбда-выражения Python?
Peter Long
3
@PeterLong Надеюсь, Гвидо сможет ответить на ваш вопрос. Суть в том, что это было бы слишком сложно, и вы все defравно можете использовать . Наш доброжелательный лидер заговорил!
new123456
5
@AlexMartelli DropBox оказал интересное влияние на Гвидо - twitter.com/gvanrossum/status/391769557758521345
Дэвид
83

Что ж, вот пример, который показывает разницу:

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

Эти сообщения Ивана Мура расширяют «ограничения лямбда» и замыкания в Python:

арс
источник
1
Хороший пример. На самом деле, мне кажется, что это больше "ошибка" с лямбдой, но я понимаю, что другие могут не согласиться. (Нечто подобное происходит с замыканиями, определенными внутри цикла, как это реализовано в нескольких языках программирования.)
ShreevatsaR
28
Исправление этой «дилеммы раннего и позднего связывания» состоит в том, чтобы явно использовать раннее связывание, когда вы этого хотите, с помощью lambda y, n=n: .... Позднее связывание (имена , появляющихся только в теле функции, а не в его defили эквиваленте lambda) ничего , но ошибка, как я уже показал, наконец , в длинных С.О. ответах в прошлом: вы рано связывать явно , когда это то, что вы хотите, используйте значение по умолчанию с поздним связыванием, если это то, что вы хотите, и это как раз правильный выбор дизайна, учитывая контекст остальной части дизайна Python.
Alex Martelli
1
@Alex Martelli: Да, извините. Я просто не могу должным образом привыкнуть к позднему связыванию, возможно, потому, что при определении функций я думаю, что на самом деле что-то определяю навсегда, а неожиданные сюрпризы вызывают у меня только головную боль. (Больше , когда я пытаюсь сделать функциональные вещи в JavaScript , чем в Python, хотя.) Я понимаю , что многие люди являются комфортно поздним связыванием, и это согласуется с остальной частью дизайна Python. Я все же хотел бы прочитать другие ваши длинные SO-ответы - ссылки? :-)
ShreevatsaR
3
Алекс прав, это не ошибка. Но это "ловушка", которая уводит в ловушку многих любителей лямбда. Относительно «ошибочной» стороны аргумента от haskel / функционального типа см. Сообщение Андрея Бауэра: math.andrej.com/2009/04/09/pythons-lambda-is-broken
ars
@ars: Ах да, спасибо за ссылку на пост Андрея Бауэра. Да, эффекты позднего связывания, безусловно, являются чем-то, что мы, математики (хуже того, имея опыт работы на Haskell), постоянно находим совершенно неожиданным и шокирующим. :-) Я не уверен , что я бы пойти так далеко , как профессор Бауэр и назвать его дизайн ошибки, но это трудно для человека программистов , чтобы полностью переключаться между одним способом мышления и другого. (Или, возможно, это просто мой недостаточный опыт работы с Python.)
ShreevatsaR
26

В последних версиях Python (> = 2.7), вы можете , но не :picklepartiallambda

>>> 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
К сожалению, частичные функции не подходят для multiprocessing.Pool.map(). stackoverflow.com/a/3637905/195139
wting
3
@wting Этот пост partialнаписан в 2010 году. Его можно редактировать в Python 2.7.
Фред Фу
24

Functools как-то эффективнее ..?

В качестве частичного ответа на это я решил проверить производительность. Вот мой пример:

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

на Python 3.3 это дает:

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

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

Триларион
источник
3
Что еще более важно, partialон написан на C, а не на чистом Python, что означает, что он может создавать более эффективный вызываемый объект, чем просто создание функции, которая вызывает другую функцию.
chepner
12

Помимо дополнительных функций, упомянутых Алексом, еще одним преимуществом functools.partial является скорость. С partial вы можете избежать создания (и разрушения) другого кадра стека.

Ни функция, сгенерированная частичным, ни лямбда-выражения по умолчанию не имеют строк документации (хотя вы можете установить строку документа для любых объектов через __doc__ ).

Вы можете найти более подробную информацию в этом блоге: Приложение с частичной функцией в Python

Леонардо.З
источник
Если вы проверили преимущество в скорости, какое улучшение скорости частичного по сравнению с лямбда-выражением можно ожидать?
Триларион 09
1
Когда вы говорите, что строка документации унаследована, на какую версию Python вы ссылаетесь? В Python 2.7.15 и Python 3.7.2 они не наследуются. Это хорошо, потому что исходная строка документации не обязательно верна для функции с частично примененными аргументами.
январь
Для python 2.7 ( docs.python.org/2/library/functools.html#partial-objects ): « атрибуты name и doc не создаются автоматически». То же самое для 3. [5-7].
Ярослав Никитенко
В вашей ссылке есть ошибка: log_info = partial (log_template, level = "info") - это невозможно, потому что уровень не является аргументом ключевого слова в примере. Оба python 2 и 3 говорят: «TypeError: log_template () получил несколько значений для аргумента 'level'».
Ярослав Никитенко
Фактически, я создал частичное (f) вручную, и оно дает поле документа как 'partial (func, * args, ** keywords) - новая функция с частичным применением \ n данных аргументов и ключевых слов. \ N' (оба для Python 2 и 3).
Ярослав Никитенко
1

Я быстрее всего понимаю намерение в третьем примере.

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

Также вы заметите, что третий пример - единственный, который не зависит от полной подписи sum2; что делает его немного более слабым.

Джон-Эрик
источник
2
Хм, на самом деле я придерживаюсь противоположного мнения, мне потребовалось гораздо больше времени, чтобы разобрать functools.partialвызов, тогда как лямбды очевидны.
David Z