Python: Почему необходим functools.partial?
Частичное применение это круто. Какая функциональность делает 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
как-то эффективнее или читабельнее?
8 ответов
Какой функционал предлагает functools.partial, который вы не можете получить через лямбды?
Не так много с точки зрения дополнительной функциональности (но, см. Позже) - и читаемость в глазах смотрящего.
Большинству людей, знакомых с функциональными языками программирования (особенно в семьях Lisp/Scheme), нравится lambda
просто отлично - я говорю "большинство", определенно не все, потому что мы с Гвидо, несомненно, являемся одними из тех, кто "знаком с" (и т. д.), но думают о lambda
как аномалия глазного яблока в Python...
Он раскаялся в том, что когда-либо принимал его в Python, тогда как планировал удалить его из Python 3, как один из "глюков Python".
Я полностью поддержал его в этом. (Я люблю lambda
в Схеме... хотя его ограничения в Python и странный способ, которым он просто не вписывается в остальную часть языка, заставляют мою оболочку ползти).
Однако не так для полчищ lambda
любовники - которые устроили одну из самых близких вещей к восстанию, когда-либо виденному в истории Питона, пока Гвидо не отступил и не решил уйти 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]
Теперь объедините переопределяемость именованных аргументов, плюс настройку трех атрибутов, в одно выражение, и скажите мне, насколько читабельным это будет...!-)
Ну, вот пример, который показывает разницу:
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:
В последних версиях Python (>=2.7) вы можете pickle
partial
, но не 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>
Является ли 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.
Помимо дополнительной функциональности, о которой говорил Алекс, еще одно преимущество functools.partial - скорость. С частичным вы можете избежать создания (и разрушения) другого стекового фрейма.
Функция, созданная частичным, наследует строку документации от исходной функции, в то время как лямбды не имеют строк документации по умолчанию (хотя вы можете установить строку документа для любых объектов через __doc__
)
Вы можете найти более подробную информацию в этом блоге: Приложение с частичной функцией в Python
Очень старый вопрос, но я решил оставить его здесь, вдруг он кому-нибудь пригодится.
Одно из преимуществ партиалов перед лямбда-выражениями заключается в том, как они ведут себя в циклах. Из-за того, как лямбда-функции оценивают параметры, простой способ запутаться при их использовании — это объявление лямбда-функций как части цикла (например, методы обратного вызова для пунктов меню).
Например, это дает вам ответ, который может удивить разработчиков, незнакомых с таким поведением в лямбда-выражениях:
funcs = []
for i in range(10):
f = lambda j: i + j
funcs.append(f)
print(f(1) for f in funcs)
# [10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
Однако если вы используете частичные элементы, у вас не возникнет этой проблемы;
parts = []
for i in range(10):
p = partial(sum, (i, ))
parts.append(p)
print([p(1) for p in parts])
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Конечно, есть способы обойти проблему с лямбда-выражениями, но с частичными синтаксис (IMO) гораздо более простой и ведет себя так, как и следовало ожидать.
Я понимаю намерение быстрее всего в третьем примере.
Когда я разбираю лямбды, я ожидаю большей сложности / странности, чем предлагается стандартной библиотекой напрямую.
Кроме того, вы заметите, что третий пример является единственным, который не зависит от полной подписи sum2
; таким образом делая это немного более слабо связанным.
Функционалы служат полезной цели, когда оцениваются определенные переменные.
Если исходить от постороннего, вот ряд более дружелюбных примеров:
from functools import partial
sum = lambda x, y: x + y # sum(x, y) == x + y
n = 2
normalSum = lambda x: sum(x, n) # normalSum(x) == sum(x, y=n)
partialSum = partial(sum, y = n) # partialSum(sum(y=n)) == sum(x, 2)
print(normalSum(2), partialSum(2)) # 4 4
n = 6
print(normalSum(2), partialSum(2)) # 8 4
Обратите внимание, как партиал содержит значение того, что было в то время.
...
n = 2
partialSumOrig = partial(sum, y = n) # partialSumOrig(sum(y=n)) == sum(x, 2)
n = 6
partialSumNew = partial(sum, y = n) # partialSumNew(sum(y=n)) == sum(x, 6)
print(partialSumOrig(2), partialSumNew(2)) # 4 8
Дополнительный пример, показывающий, как аргументы передаются во вложенные лямбды:
...
n = 8
partialSumOrig = partial(sum, y = n) # partialSumOrig(sum(y=n)) == sum(x, 8)
partialSumNew = partial(sum, n) # partialSumNew(sum(n)) == sum(8, y)
print(partialSumOrig(2)) # 10 # partialSumOrig(sum(2, 8)) == sum(2, 8)
print(partialSumNew(2)) # 10 # partialSumNew(sum(8, 2)) == sum(8, 2)
Последний пример, показывающий, как аргументы передаются в партиалах:
...
n = 2
m = 2
partialSumSilly = partial(sum, n, m) # partialSumSilly(sum(n, m)) == sum(2, 2)
print(partialSumSilly()) # 4
Большой вывод заключается в том, что:
-
normalSum()
ведет себя как позднее связывание, где оценивается при запуске. -
partialSum()
ведет себя как раннее связывание, гдеn
оценивается при определении.
Примечание. На самом деле почти все является поздним связыванием в cpython из-за его интерпретируемого характера.