Python: ทำไม functools.partial จึงมีความจำเป็น


193

แอพลิเคชันบางส่วนจะเย็น มีฟังก์ชั่นอะไรบ้างfunctools.partialที่คุณไม่สามารถผ่าน 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

คือfunctoolsอย่างใดมีประสิทธิภาพมากขึ้นหรืออ่าน?

คำตอบ:


266

มีฟังก์ชั่นอะไรบ้างfunctools.partialที่คุณไม่สามารถผ่าน lambdas ได้?

ไม่มากในแง่ของฟังก์ชั่นพิเศษ(แต่ดูในภายหลัง) - และการอ่านอยู่ในสายตาของคนดู
คนส่วนใหญ่ที่คุ้นเคยกับภาษาการเขียนโปรแกรมที่ใช้งานได้ (ในครอบครัว Lisp / Scheme โดยเฉพาะ) ดูเหมือนจะlambdaสบายดี - ฉันพูดว่า "ส่วนใหญ่" ไม่แน่นอนทั้งหมดเพราะ Guido และฉันมั่นใจว่าเป็นคนที่คุ้นเคย "(ฯลฯ ) ) แต่คิดว่าlambdaเป็นความผิดปกติของนัยน์ตาในงูหลาม ...
เขาสำนึกผิดว่าเคยยอมรับมันเป็นงูหลามในขณะที่วางแผนที่จะลบมันออกจากงูหลาม 3 เป็นหนึ่งใน "ข้อบกพร่องของงูใหญ่"
ฉันสนับสนุนเขาอย่างเต็มที่ในเรื่องนั้น (ฉันชอบlambda ใน Scheme ... ในขณะที่ข้อ จำกัดใน 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

ดังนั้นที่คุณดูจะdefinelyไม่เป็นที่เรียบง่ายเป็น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?
Nick Heiner

11
@Rarcharch ตามที่ฉันพูด: ก่อนอื่นมันมีข้อ จำกัด (Python แยกความแตกต่างของสำนวนและข้อความ - มีอะไรที่คุณทำไม่ได้หรือทำไม่ได้อย่างสมเหตุสมผลภายในการแสดงออกเพียงครั้งเดียวและนั่นคือร่างกายของแลมบ์ดา) ประการที่สองมันเป็นไวยากรณ์น้ำตาลแปลก ๆ ถ้าฉันย้อนเวลากลับไปและเปลี่ยนแปลงสิ่งหนึ่งใน Python มันจะไร้สาระไร้ความหมายนัยน์ตาdefและlambdaคำหลัก: ทำให้ทั้งคู่function(ตัวเลือกชื่อเดียว Javascript ถูกต้องจริงๆ ) และอย่างน้อย 1/3 ของการคัดค้านของฉันจะหายไป -) อย่างที่ฉันบอกว่าฉันไม่คัดค้านแลมบ์ดาใน Lisp ... ! -)
Alex Martelli

1
@Alex Martelli, ทำไม Guido ตั้งข้อ จำกัด ดังกล่าวสำหรับแลมบ์ดา: "body เป็นนิพจน์เดียว"? แลมบ์ดาร่างกายของ C # อาจเป็นสิ่งที่ถูกต้องในร่างกายของฟังก์ชัน ทำไมกุยโดไม่เพียงแค่ลบข้อ จำกัด สำหรับ python lambda?
Peter Long

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

ข้อความเหล่านี้ของ Ivan Moore ได้เพิ่มเติมเกี่ยวกับ "ข้อ จำกัด ของ lambda" และการปิดใน python:


1
ตัวอย่างที่ดี สำหรับฉันดูเหมือนว่านี่เป็น "แมลง" กับแลมบ์ดาจริงๆ แต่ฉันเข้าใจว่าคนอื่นอาจไม่เห็นด้วย (มีบางสิ่งคล้ายกันเกิดขึ้นกับการปิดที่กำหนดไว้ภายในลูปตามที่นำมาใช้ในภาษาการเขียนโปรแกรมหลายภาษา)
ShreevatsaR

28
การแก้ไขนี้ "ต้นเทียบกับช่วงปลายขึ้นเขียงผูกพัน" lambda y, n=n: ...คือการใช้อย่างชัดเจนในช่วงต้นที่มีผลผูกพันเมื่อคุณต้องการว่าโดย การรวมปลาย (ชื่อปรากฏเฉพาะในเนื้อหาของฟังก์ชันไม่ใช่ในdefหรือเทียบเท่าlambda) เป็นอะไรก็ได้แต่เป็นข้อผิดพลาดเนื่องจากฉันได้แสดงคำตอบยาว ๆ ดังนั้นในอดีต: คุณผูกต้นอย่างชัดเจนเมื่อคุณต้องการ ใช้ค่าเริ่มต้นปลายผูกพันเมื่อนั้นคือสิ่งที่คุณต้องการและที่ว่าเป็นทางเลือกที่ได้รับการออกแบบที่เหมาะสมกับบริบทของส่วนที่เหลือของการออกแบบของงูใหญ่ที่
Alex Martelli

1
@Alex Martelli: ใช่ขอโทษ ฉันไม่เคยชินกับการผูกปลายอย่างถูกต้องบางทีอาจเป็นเพราะฉันคิดว่าเมื่อกำหนดฟังก์ชั่นที่ฉันกำหนดสิ่งที่ดีและความประหลาดใจที่ไม่คาดคิดทำให้ฉันปวดหัวเท่านั้น (เพิ่มเติมเมื่อฉันพยายามที่จะทำสิ่งที่ทำงานใน Javascript กว่าในหลามแม้ว่า.) ฉันเข้าใจว่าหลาย ๆ คนที่มีความสะดวกสบายด้วยการผูกปลายและว่ามันเป็นที่สอดคล้องกับส่วนที่เหลือของการออกแบบของงูหลาม ฉันยังต้องการอ่านคำตอบที่ยาวมาก ๆ ของคุณ - ลิงก์? :-)
ShreevatsaR

3
อเล็กซ์พูดถูกมันไม่ใช่บั๊ก แต่มันเป็น "gotcha" ที่วางกับดักผู้ที่ชื่นชอบแลมบ์ดาหลายคน สำหรับทางด้าน "ข้อผิดพลาด" ด้านของการโต้แย้งจาก Haskel / ประเภทการทำงานที่เห็นโพสต์ Andrej ของ Bauer: math.andrej.com/2009/04/09/pythons-lambda-is-broken
ARS

@ars: อาใช่ขอบคุณสำหรับลิงก์ไปยังโพสต์ของ Andrej Bauer ใช่ผลของการผูกปลายนั้นเป็นสิ่งที่เราคิดว่าเป็นคณิตศาสตร์ (แย่กว่านั้นด้วยภูมิหลังของ Haskell) ทำให้เราค้นพบสิ่งที่ไม่คาดคิดและน่าตกใจอย่างยิ่ง :-) ผมไม่แน่ใจว่าผมจะไปให้ไกลที่สุดเท่าศ. บาวเออร์และเรียกว่าข้อผิดพลาดในการออกแบบ แต่มันเป็นเรื่องยากสำหรับโปรแกรมเมอร์มนุษย์จะสมบูรณ์สลับระหว่างวิธีหนึ่งในการคิดและการอื่น (หรือบางทีนี่อาจเป็นเพียงประสบการณ์ Python ที่ไม่เพียงพอของฉัน)
ShreevatsaR

26

ใน Python เวอร์ชันล่าสุด (> = 2.7) คุณสามารถpicklea 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>

1
multiprocessing.Pool.map()แต่น่าเสียดายที่ฟังก์ชั่นบางส่วนล้มเหลวในการดองสำหรับ stackoverflow.com/a/3637905/195139
wting

3
@wting โพสต์นั้นมาจากปี 2010 partialสามารถเลือกได้ใน Python 2.7
Fred Foo

23

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

ซึ่งหมายความว่าบางส่วนต้องการเวลาเพิ่มเล็กน้อยในการสร้าง แต่ใช้เวลาในการดำเนินการน้อยกว่ามาก นี้รวมทั้งสามารถเป็นผลของต้นและปลายที่มีผลผูกพันซึ่งจะกล่าวถึงในคำตอบจากอาร์


3
ที่สำคัญกว่าpartialนั้นเขียนด้วยภาษา C แทนที่จะเป็น Python แท้ซึ่งหมายความว่ามันสามารถสร้าง callable ที่มีประสิทธิภาพมากกว่าการสร้างฟังก์ชั่นที่เรียกใช้ฟังก์ชันอื่น
chepner

12

นอกเหนือจากฟังก์ชั่นพิเศษที่ Alex พูดถึงข้อดีอีกอย่างของ functools.partial ก็คือความเร็ว ด้วยบางส่วนคุณสามารถหลีกเลี่ยงการสร้าง (และทำลาย) สแต็กเฟรมอื่น

ทั้งฟังก์ชั่นที่สร้างขึ้นโดยบางส่วนหรือ lambdas ไม่มี docstrings ตามค่าเริ่มต้น (แม้ว่าคุณจะสามารถตั้งค่าสตริง doc สำหรับวัตถุใด ๆ ผ่าน__doc__)

คุณสามารถค้นหารายละเอียดเพิ่มเติมได้ในบล็อกนี้: แอปพลิเคชันฟังก์ชันบางส่วนใน Python


หากคุณได้ทดสอบความได้เปรียบด้านความเร็วคุณสามารถคาดหวังว่าจะมีการปรับปรุงความเร็วส่วนใดของแลมบ์ดาได้บ้าง
Trilarion

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]
Yaroslav Nikitenko

ลิงก์ของคุณมีข้อผิดพลาด: log_info = บางส่วน (log_template, level = "info") - เป็นไปไม่ได้เพราะระดับไม่ใช่อาร์กิวเมนต์ของคำหลักในตัวอย่าง python 2 และ 3 ทั้งสองพูดว่า: "TypeError: log_template () มีค่าหลายค่าสำหรับอาร์กิวเมนต์ 'level'"
Yaroslav Nikitenko

ในความเป็นจริงฉันสร้างบางส่วน (f) ด้วยมือและให้ฟิลด์docเป็น 'บางส่วน (func, * args, ** คำหลัก) - ฟังก์ชันใหม่ที่มีแอปพลิเคชันบางส่วน \ n ของอาร์กิวเมนต์และคำหลักที่กำหนด \ n' (ทั้ง สำหรับ python 2 และ 3)
Yaroslav Nikitenko

1

ฉันเข้าใจเจตนาที่เร็วที่สุดในตัวอย่างที่สาม

เมื่อฉันแยกวิเคราะห์ lambdas ฉันคาดหวังว่าจะมีความซับซ้อน / แปลกมากกว่าที่ห้องสมุดมาตรฐานเสนอโดยตรง

นอกจากนี้คุณจะสังเกตเห็นว่าตัวอย่างที่สามเป็นเพียงคนเดียวที่ไม่ได้ขึ้นอยู่กับลายเซ็นเต็มรูปแบบของsum2; จึงทำให้มันมีความแน่นมากขึ้นเล็กน้อย


1
หืมฉันจริง ๆ แล้วการชักชวนที่ตรงกันข้ามฉันใช้เวลานานกว่าในการแยกการfunctools.partialโทรออกในขณะที่ลูกแกะมีความชัดเจนในตัวเอง
David Z
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.