Python: Для чого потрібні functools.partial?


193

Часткове застосування класно. Яку функціональність 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якось ефективніше чи читабельніше?

Відповіді:


266

Яку функціональність 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]

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


2
Так, я б сказав, що додаткова функціональність того, functools.partialщо ви згадали, робить його вищим від лямбда. Можливо, це тема іншої публікації, але що саме на рівні дизайну вас так сильно турбує lambda?
Нік Хайнер

11
@Rosarch, як я вже говорив: по- перше, обмеження (Python різко відрізняє вираження і висловлювання - є багато ви не можете зробити, або не може зробити розсудливо , в одному вираженні, і це те, що тіло лямбда в це ); по-друге, його абсолютно дивний синтаксичний цукор. Якби я міг повернутись у часі і змінити одне в межах Python, це були б абсурдні, безглузді очі defі lambdaключові слова: зробіть їх обома function(один вибір імені Javascript отримав дійсно правильно), і принаймні 1/3 моїх заперечень зникне ! -). Як я вже сказав, я не заперечую проти лямбда в Ліспі ...! -)
Alex Martelli

1
@ Алекс Мартеллі, Чому Гвідо встановив таке обмеження для лямбда: "тіло - це єдиний вираз"? Тіло лямбда C # 'може бути будь-яким дійсним у тілі функції. Чому Guido просто не зніме обмеження на лямбда пітона?
Пітер Лонг

3
@PeterLong Сподіваємось, Guido може відповісти на ваше запитання. Суть її в тому, що це було б занадто складно, і ви можете використовувати defбудь-який спосіб. Наш доброзичливий лідер говорив!
new123456

5
@AlexMartelli DropBox мав цікавий вплив на Guido - twitter.com/gvanrossum/status/391769557758521345
David

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) - це не що інше, як помилка, як я в минулому показав довгі відповіді на SO: ви рано пов'язуєте явно, коли це те, що ви хочете, використовувати пізніше зв'язування по замовчуванням , коли що то , що ви хочете, і це саме те вибір правильного дизайну , з огляду на контекст іншої частини дизайну Python.
Алекс Мартеллі

1
@ Алекс Мартеллі: Так, вибачте. Я просто не призвичаююсь до запізнення належним чином, можливо, тому що, коли я визначаю функції, я насправді визначаю щось на добро, і несподівані сюрпризи викликають у мене тільки головні болі. (Більше , коли я намагаюся зробити функціональні речі в JavaScript , ніж в Python, хоча.) Я розумію , що багато людей є комфортно пізнім зв'язуванням, і це узгоджується з іншою частиною дизайну Python. Я б хотів прочитати ваші інші довгі відповіді, хоча - посилання? :-)
ShreevatsaR

3
Алекс правий, це не помилка. Але це "готча", яка захоплює багатьох любителів лямбда. Для «помилки» сторони аргументу з Хаскеля / функціонального типу, див пост Андрія Бауера: math.andrej.com/2009/04/09/pythons-lambda-is-broken
АРС

@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 Ця посада починається з 2010 р. partialВибір на Python 2.7.
Фред Фоо

23

Чи є функціональні функції якось ефективнішими ..?

Як часткова відповідь на це я вирішив перевірити продуктивність. Ось мій приклад:

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

Що означає, що частковому потрібно трохи більше часу на створення, але значно менше часу на виконання. Це може бути наслідком раннього та пізнього зв’язування, про які йдеться у відповіді арс .


3
Що ще важливіше, partialвін написаний на C, а не чистому Python, це означає, що він може створити ефективніший дзвінок, ніж просто створення функції, яка викликає іншу функцію.
чепнер

12

Окрім додаткової функціональності, про яку згадував Алекс, ще однією перевагою functools.partial є швидкість. За допомогою часткового ви можете уникнути побудови (і руйнування) іншого кадру стека.

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

Більш детальну інформацію ви можете знайти в цьому блозі: Додаток з частковими функціями в Python


Якщо ви перевірили перевагу швидкості, чого можна очікувати часткового покращення швидкості в порівнянні з лямбда?
Триларіон

1
Коли ви говорите, що docstring успадковується, до якої версії Python ви посилаєтесь? У Python 2.7.15 та Python 3.7.2 вони не передаються у спадок. Що добре, адже оригінальний docstring не обов'язково правильний для функції з частково застосованими аргументами.
січень

Для python 2.7 ( docs.python.org/2/library/functools.html#partial-objects ): " ім'я та атрибути doc не створюються автоматично". Те саме для 3. [5-7].
Ярослав Нікітенко

У вашому посиланні є помилка: log_info = часткова (log_template, level = "інформація") - це неможливо, оскільки рівень не є аргументом ключового слова у прикладі. Обидва python 2 і 3 кажуть: "TypeError: log_template () отримав кілька значень для аргументу" level "".
Ярослав Нікітенко

Насправді я створив частковий (f) вручну, і він дає поле doc як "частковий (func, * args, ** ключові слова) - нова функція з частковим застосуванням \ n заданих аргументів та ключових слів. \ N" (обидва для пітона 2 і 3).
Ярослав Нікітенко

1

Я розумію намір швидше в третьому прикладі.

Коли я розбираю лямбдаси, я очікую більше складності / дивацтва, ніж пропонується безпосередньо стандартною бібліотекою.

Також ви помітите, що третій приклад - єдиний, який не залежить від повного підпису sum2; тим самим зробивши його трохи більш вільно з'єднаним.


1
Гм, насправді я протилежного переконання, мені було потрібно набагато більше часу, щоб розібратися у functools.partialвиклику, тоді як лямбда - це само собою зрозуміло.
David Z
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.