Python: Mengapa functools.partial diperlukan?

193

Aplikasi parsial keren. Fungsi apa yang functools.partialditawarkan yang tidak bisa Anda dapatkan melalui 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

Apakah functoolsentah bagaimana lebih efisien, atau dapat dibaca?

Nick Heiner
sumber

Jawaban:

266

Fungsi apa yang functools.partialditawarkan yang tidak bisa Anda dapatkan melalui lambdas?

Tidak banyak dalam hal fungsi tambahan (tapi, lihat nanti) - dan, keterbacaan ada di mata yang melihatnya.
Kebanyakan orang yang terbiasa dengan bahasa pemrograman fungsional (khususnya yang berada dalam keluarga Lisp / Skema) tampaknya sangat menyukai lambda- saya katakan "sebagian besar", jelas tidak semua, karena Guido dan saya pasti termasuk di antara mereka yang "akrab dengan" (dll. ) namun dianggap lambdasebagai anomali yang merusak pemandangan di Python ...
Dia menyesal telah menerimanya ke Python sementara berencana untuk menghapusnya dari Python 3, sebagai salah satu "gangguan Python".
Saya sepenuhnya mendukungnya dalam hal itu. (Saya suka lambda Skema ... sementara keterbatasannya dalam Python , dan cara anehnya hanya tidak dengan sisa bahasa, buat kulit saya merangkak).

Namun tidak demikian bagi gerombolan lambdakekasih - yang melakukan salah satu hal paling dekat dengan pemberontakan yang pernah terjadi dalam sejarah Python, sampai Guido mundur dan memutuskan untuk pergi lambda.
Beberapa kemungkinan penambahan functools(untuk membuat fungsi mengembalikan konstanta, identitas, dll) tidak terjadi (untuk menghindari duplikasi lambdafungsi yang lebih eksplisit ), meskipun partialtentu saja tetap (tidak ada duplikasi total , juga tidak merusak pemandangan).

Ingat lambdatubuh itu terbatas pada ekspresi , jadi punya keterbatasan. Sebagai contoh...:

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

functools.partialFungsi yang dikembalikan dihiasi dengan atribut yang berguna untuk introspeksi - fungsi yang dibungkus, dan argumen posisional dan bernama apa yang diperbaiki di dalamnya. Lebih jauh, argumen yang dinamai dapat ditimpa segera kembali ("memperbaiki" agaknya, dalam arti, pengaturan default):

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

Jadi, seperti yang Anda lihat, itu pasti tidak sesederhanalambda s: int(s, base=2) ! -)

Ya, Anda dapat memutarbalikkan lambda Anda untuk memberi Anda ini - misalnya, untuk kata kunci-override,

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

tapi saya sangat berharap bahwa bahkan yang paling bersemangat lambda-lover tidak menganggap kengerian ini lebih mudah dibaca daripada partialpanggilan! -). Bagian "pengaturan atribut" bahkan lebih sulit, karena batasan "tubuh adalah ekspresi tunggal" dari Python lambda(ditambah fakta bahwa penugasan tidak pernah dapat menjadi bagian dari ekspresi Python) ... Anda akhirnya "memalsukan penugasan dalam ekspresi" dengan memperluas daftar pemahaman jauh melampaui batas desainnya ...:

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

Sekarang gabungkan override-argumen-nama yang dinamai, ditambah pengaturan tiga atribut, ke dalam satu ekspresi, dan katakan betapa mudahnya itu dibaca ...!

Alex Martelli
sumber
2
Ya, saya akan mengatakan bahwa fungsi ekstra functools.partialyang Anda sebutkan membuatnya lebih unggul daripada lambda. Mungkin ini adalah topik dari posting lain, tetapi apa itu pada tingkat desain yang sangat mengganggu Anda lambda?
Nick Heiner
11
@Rosarch, seperti yang saya katakan: pertama, keterbatasan (Python tajam membedakan ekspresi dan pernyataan - ada banyak Anda tidak dapat melakukan, atau tidak dapat melakukan bijaksana , dalam ekspresi tunggal, dan bahwa itu apa yang tubuh lambda ini adalah ); kedua, gula sintaks yang benar-benar aneh. Jika saya bisa kembali ke masa lalu dan mengubah satu hal dalam Python, itu akan menjadi tidak masuk akal, tidak berarti, merusak pemandangan defdan lambdakata kunci: buat keduanya function(salah satu pilihan Javascript jadi benar ), dan setidaknya 1/3 keberatan saya akan hilang ! -). Seperti yang saya katakan, saya tidak keberatan dengan lambda di Lisp ...! -)
Alex Martelli
1
@Alex Martelli, Mengapa Guido menetapkan batasan untuk lambda: "tubuh adalah ekspresi tunggal"? Tubuh lambda C # dapat berupa apa saja yang valid dalam tubuh fungsi. Mengapa Guido tidak menghapus batasan untuk python lambda saja?
Peter Long
3
@PeterLong Semoga Guido bisa menjawab pertanyaan Anda. Inti dari itu adalah bahwa itu akan terlalu rumit, dan Anda dapat menggunakannya def. Pemimpin kita yang baik hati telah berbicara!
new123456
5
@AlexMartelli DropBox memiliki pengaruh yang menarik pada Guido - twitter.com/gvanrossum/status/391769557758521345
David
82

Nah, inilah contoh yang menunjukkan perbedaan:

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

Pos-pos ini oleh Ivan Moore memperluas "keterbatasan lambda" dan penutupan dengan python:

ars
sumber
1
Contoh yang baik. Bagi saya, ini tampaknya lebih "bug" dengan lambda, sebenarnya, tapi saya mengerti orang lain mungkin tidak setuju. (Hal serupa terjadi dengan penutupan yang didefinisikan dalam satu lingkaran, seperti yang diterapkan dalam beberapa bahasa pemrograman.)
ShreevatsaR
28
Perbaikan untuk "dilema mengikat awal dan terlambat" ini adalah dengan secara eksplisit menggunakan ikatan awal, saat Anda menginginkannya, oleh lambda y, n=n: .... Ikatan yang terlambat (nama-nama yang hanya muncul di badan fungsi, tidak dalam defatau fungsi yang setara lambda) sama sekali bukan bug, seperti yang telah saya tunjukkan panjang lebar dalam jawaban SO di masa lalu: Anda lebih awal mengikat secara eksplisit ketika itu yang Anda inginkan, gunakan default yang mengikat saat itulah yang Anda inginkan, dan itulah pilihan desain yang tepat mengingat konteks sisa desain Python.
Alex Martelli
1
@ Alex Martelli: Ya, maaf. Saya hanya gagal membiasakan diri untuk mengikat dengan benar, mungkin karena saya pikir ketika mendefinisikan fungsi bahwa saya sebenarnya mendefinisikan sesuatu untuk kebaikan, dan kejutan yang tidak terduga hanya membuat saya sakit kepala. (Lebih ketika saya mencoba melakukan hal-hal fungsional dalam Javascript daripada di Python.) Saya mengerti bahwa banyak orang merasa nyaman dengan pengikatan yang terlambat, dan itu konsisten dengan sisa desain Python. Saya masih ingin membaca jawaban SO panjang Anda yang lain, meskipun - tautan? :-)
ShreevatsaR
3
Alex benar, itu bukan bug. Tapi itu adalah "gotcha" yang menjebak banyak penggemar lambda. Untuk sisi "bug" argumen dari tipe haskel / fungsional, lihat posting Andrej Bauer: math.andrej.com/2009/04/09/pythons-lambda-is-broken
ars
@ars: Ah ya, terima kasih atas tautan ke pos Andrej Bauer. Ya, efek dari pengikatan yang terlambat tentu saja sesuatu yang kita tipe matematika (lebih buruk, dengan latar belakang Haskell) terus menemukan yang sangat tak terduga dan mengejutkan. :-) Saya tidak yakin aku akan pergi sejauh Prof. Bauer dan menyebutnya kesalahan desain, tetapi adalah sulit bagi programmer manusia untuk sepenuhnya beralih di antara salah satu cara berpikir dan lain. (Atau mungkin ini hanya pengalaman Python yang tidak mencukupi.)
ShreevatsaR
26

Dalam versi terbaru Python (> = 2.7), Anda bisa picklea partial, tetapi bukan 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
sumber
1
Sayangnya fungsi parsial gagal untuk acar multiprocessing.Pool.map(). stackoverflow.com/a/3637905/195139
mulai
3
@wting Posting itu dari 2010. partialdapat dipilih dalam Python 2.7.
Fred Foo
22

Apakah functools entah bagaimana lebih efisien ..?

Sebagai jawaban sebagian untuk ini saya memutuskan untuk menguji kinerja. Ini contoh saya:

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

pada Python 3.3 itu memberi:

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

Yang berarti bahwa sebagian membutuhkan sedikit lebih banyak waktu untuk pembuatan tetapi jauh lebih sedikit waktu untuk eksekusi. Ini bisa jadi efek dari ikatan awal dan akhir yang dibahas dalam jawaban dari ars .

Trilarion
sumber
3
Lebih penting lagi, partialditulis dalam C, daripada Python murni, artinya dapat menghasilkan callable yang lebih efisien daripada hanya membuat fungsi yang memanggil fungsi lain.
chepner
12

Selain fungsi ekstra yang disebutkan Alex, keunggulan lain dari functools.partial adalah kecepatan. Dengan parsial Anda dapat menghindari membangun (dan merusak) bingkai tumpukan lain.

Baik fungsi yang dihasilkan oleh parsial maupun lambdas tidak memiliki dokumen secara default (meskipun Anda dapat mengatur string doc untuk objek apa pun melalui __doc__).

Anda dapat menemukan rincian lebih lanjut di blog ini: Aplikasi Fungsi Sebagian dengan Python

Leonardo.Z
sumber
Jika Anda telah menguji keunggulan kecepatan, apa peningkatan kecepatan parsial lebih dari lambda dapat diharapkan?
Trilarion
1
Ketika Anda mengatakan bahwa docstring diwarisi, versi Python mana yang Anda rujuk? Dalam Python 2.7.15 dan Python 3.7.2 mereka tidak diwarisi. Yang merupakan hal yang baik, karena docstring asli belum tentu benar untuk fungsi dengan argumen yang diterapkan sebagian.
Januari
Untuk python 2.7 ( docs.python.org/2/library/functools.html#partial-objects ): " nama dan atribut doc tidak dibuat secara otomatis". Sama untuk 3. [5-7].
Yaroslav Nikitenko
Ada kesalahan di tautan Anda: log_info = partial (log_template, level = "info") - itu tidak mungkin karena level bukan argumen kata kunci dalam contoh. Kedua python 2 dan 3 mengatakan: "TypeError: log_template () mendapat beberapa nilai untuk argumen 'level'".
Yaroslav Nikitenko
Bahkan, saya membuat parsial (f) dengan tangan dan memberikan bidang dokumen sebagai 'parsial (func, * args, ** kata kunci) - fungsi baru dengan aplikasi parsial \ n dari argumen dan kata kunci yang diberikan. \ N' (keduanya untuk python 2 dan 3).
Yaroslav Nikitenko
1

Saya memahami maksud tercepat dalam contoh ketiga.

Ketika saya mengurai lambdas, saya mengharapkan lebih banyak kompleksitas / keanehan daripada yang ditawarkan oleh perpustakaan standar secara langsung.

Anda juga akan melihat bahwa contoh ketiga adalah satu-satunya yang tidak bergantung pada tanda tangan lengkap dari sum2; sehingga membuatnya sedikit lebih longgar digabungkan.

Jon-Eric
sumber
1
Hm, saya sebenarnya dari bujukan yang berlawanan, saya butuh waktu lebih lama untuk mengurai functools.partialpanggilan, sedangkan lambda jelas.
David Z