Python: Tại sao funcools.partial cần thiết?


193

Ứng dụng một phần là mát mẻ. Những chức năng nào functools.partialcung cấp mà bạn không thể có được thông qua 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

functoolsbằng cách nào đó hiệu quả hơn, hoặc có thể đọc được?

Câu trả lời:


265

Những chức năng nào functools.partialcung cấp mà bạn không thể có được thông qua lambdas?

Không nhiều về chức năng bổ sung (nhưng, xem sau) - và, khả năng đọc là trong mắt của kẻ si tình.
Hầu hết những người quen thuộc với các ngôn ngữ lập trình chức năng (đặc biệt là những người trong gia đình Lisp / Scheme) dường như rất thích lambda- tôi nói "nhất", chắc chắn không phải tất cả, bởi vì Guido và tôi chắc chắn nằm trong số những người "quen thuộc" (v.v. ) chưa nghĩ đến lambdasự bất thường trong Python ...
Anh ta đã hối hận vì đã từng chấp nhận nó vào Python trong khi dự định xóa nó khỏi Python 3, như một trong những "trục trặc của Python".
Tôi hoàn toàn ủng hộ anh ấy trong đó. (Tôi thích lambda Scheme ... trong khi những hạn chế của nó trong Python và cách kỳ lạ mà nó không làm được ' với phần còn lại của ngôn ngữ, làm cho da tôi bò lên).

Không phải như vậy, tuy nhiên, đối với đám lambdangười yêu - người dàn dựng một trong những điều gần gũi nhất với một cuộc nổi loạn từng thấy trong lịch sử của Python, cho đến khi Guido rút lui và quyết định rời khỏi lambda. Trong
số bổ sung có thể functools(để thực hiện chức năng trở về hằng số, nhận dạng, v.v.) đã không xảy ra (để tránh sao chép rõ ràng nhiều lambdachức năng hơn), mặc dù partialtất nhiên vẫn còn (nó không hoàn toàn trùng lặp, cũng không phải là chướng mắt).

Hãy nhớ rằng lambdacơ thể của chúng bị giới hạn là một biểu thức , vì vậy nó có những hạn chế. Ví dụ...:

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

functools.partialHàm trả về được trang trí với các thuộc tính hữu ích cho việc xem xét nội bộ - chức năng mà nó bao bọc, và các đối số vị trí và được đặt tên mà nó sửa trong đó. Hơn nữa, các đối số được đặt tên có thể được ghi đè lại ngay ("sửa lỗi", theo một nghĩa nào đó, cài đặt mặc định):

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

Vì vậy, như bạn thấy, nó definely không phải là đơn giản như lambda s: int(s, base=2)-!)

Có, bạn có thể buộc lambda của bạn cung cấp cho bạn một số điều này - ví dụ: đối với từ khóa ghi đè,

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

nhưng tôi hy vọng rằng ngay cả những người hăng hái nhất lambdacũng không coi điều kinh dị này dễ đọc hơn partialcuộc gọi! -). Phần "cài đặt thuộc tính" thậm chí còn khó hơn, vì giới hạn "cơ thể là một biểu thức" của Python lambda(cộng với thực tế là phép gán không bao giờ là một phần của biểu thức Python) ... bạn kết thúc "giả mạo bài tập trong một biểu thức" bằng cách kéo dài sự hiểu biết danh sách vượt quá giới hạn thiết kế của nó ...:

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

Bây giờ, hãy kết hợp tính quá mức của các đối số được đặt tên, cộng với việc thiết lập ba thuộc tính, thành một biểu thức duy nhất và cho tôi biết mức độ dễ đọc của !


2
Vâng, tôi muốn nói rằng chức năng bổ sung functools.partialmà bạn đề cập làm cho nó vượt trội hơn lambda. Có lẽ đây là chủ đề của một bài viết khác, nhưng nó ở cấp độ thiết kế nào làm phiền bạn rất nhiều lambda?
Nick Heiner

11
@Rosarch, như tôi đã nói: thứ nhất, nó hạn chế (Python mạnh phân biệt biểu và báo cáo - có nhiều bạn không thể làm, hoặc không thể làm một cách hợp lý , trong một biểu thức duy nhất, và đó là những gì cơ thể của một lambda ); Thứ hai, cú pháp đường hoàn toàn kỳ lạ của nó. Nếu tôi có thể quay ngược thời gian và thay đổi một điều trong Python, thì đó sẽ là điều vô lý, vô nghĩa, chướng mắt deflambdatừ khóa: làm cho cả hai function(một lựa chọn tên Javascript đã thực sự đúng) và ít nhất 1/3 sự phản đối của tôi sẽ biến mất ! -). Như tôi đã nói, tôi không phản đối lambda ở Lisp ...! -)
Alex Martelli

1
@Alex Martelli, Tại sao Guido lại đặt ra giới hạn như vậy cho lambda: "cơ thể là một biểu hiện duy nhất"? Cơ thể lambda của C # có thể là bất cứ điều gì hợp lệ trong cơ thể của một chức năng. Tại sao Guido không loại bỏ giới hạn cho python lambda?
Peter Long

3
@PeterLong Hy vọng Guido có thể trả lời câu hỏi của bạn. Ý chính của nó là nó quá phức tạp và defdù sao bạn cũng có thể sử dụng . Nhà lãnh đạo nhân từ của chúng tôi đã lên tiếng!
new123456

5
@AlexMartelli DropBox đã có một ảnh hưởng thú vị trên Guido - twitter.com/gvanrossum/status/391769557758521345
David

82

Chà, đây là một ví dụ cho thấy sự khác biệt:

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

Những bài đăng này của Ivan Moore mở rộng về "những hạn chế của lambda" và đóng cửa trong python:


1
Ví dụ tốt. Đối với tôi, đây thực sự là một "lỗi" với lambda, nhưng tôi hiểu những người khác có thể không đồng ý. (Một cái gì đó tương tự xảy ra với các bao đóng được xác định trong một vòng lặp, như được thực hiện trong một số ngôn ngữ lập trình.)
ShreevatsaR

28
Cách khắc phục "tình trạng khó xử ràng buộc sớm và muộn" này là sử dụng rõ ràng ràng buộc sớm, khi bạn muốn điều đó, bởi lambda y, n=n: .... Liên kết muộn (tên chỉ xuất hiện trong cơ thể của hàm, không phải trong hàm defhoặc tương đương lambda) là bất cứ điều gì ngoại trừ lỗi, như tôi đã thể hiện trong các câu trả lời SO dài trong quá khứ: bạn liên kết sớm một cách rõ ràng khi đó là điều bạn muốn, sử dụng mặc định liên kết muộn khi đó là những gì bạn muốn và đó chính xác là lựa chọn thiết kế phù hợp với bối cảnh của phần còn lại của thiết kế Python.
Alex Martelli

1
@Alex Martelli: Vâng, xin lỗi. Tôi chỉ không quen với việc ràng buộc muộn đúng cách, có lẽ bởi vì tôi nghĩ khi xác định các chức năng mà tôi thực sự xác định một cái gì đó là tốt, và những bất ngờ bất ngờ chỉ khiến tôi đau đầu. (Xem thêm khi tôi cố gắng làm những việc chức năng trong Javascript so với Python, mặc dù.) Tôi hiểu rằng nhiều người đang cảm thấy thoải mái với cuối ràng buộc, và rằng nó phù hợp với phần còn lại của thiết kế của Python. Tôi vẫn muốn đọc các câu trả lời SO dài khác của bạn, mặc dù - liên kết? :-)
ShreevatsaR

3
Alex nói đúng, đó không phải là một lỗi. Nhưng đó là một "gotcha" bẫy nhiều người đam mê lambda. Đối với khía cạnh "lỗi" của đối số từ loại haskel / chức năng, hãy xem bài đăng của Andrej Bauer: math.andrej.com/2009/04/09/pythons-lambda-is-broken
ars

@ars: À vâng, cảm ơn vì đường dẫn đến bài viết của Andrej Bauer. Vâng, những ảnh hưởng của ràng buộc muộn chắc chắn là thứ mà chúng ta thuộc loại toán học (tệ hơn, với nền tảng Haskell) tiếp tục tìm thấy sự bất ngờ và gây sốc. :-) Tôi không chắc chắn tôi muốn đi xa như GS Bauer và gọi nó là một lỗi thiết kế, nhưng nó khó khăn cho các lập trình viên con người để hoàn toàn chuyển đổi giữa một cách suy nghĩ và khác. (Hoặc có lẽ đây chỉ là trải nghiệm Python chưa đủ của tôi.)
ShreevatsaR

26

Trong các phiên bản mới nhất của Python (> = 2.7), bạn có thể picklea partial, nhưng không phải là 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
Thật không may, một phần chức năng không thể tìm kiếm multiprocessing.Pool.map(). stackoverflow.com/a/3637905/195139
wting

3
@wting Bài đăng đó là từ năm 2010 partialcó thể chọn được trong Python 2.7.
Fred Foo

22

Là funcools bằng cách nào đó hiệu quả hơn ..?

Như một phần trả lời cho điều này, tôi quyết định kiểm tra hiệu suất. Đây là ví dụ của tôi:

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

trên Python 3.3 nó cung cấp:

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

Điều đó có nghĩa là một phần cần thêm một chút thời gian để tạo nhưng thời gian thực hiện ít hơn đáng kể. Đây cũng có thể là hiệu ứng của ràng buộc sớm và muộn được thảo luận trong câu trả lời từ ars .


3
Quan trọng hơn, partialđược viết bằng C, thay vì Python thuần túy, có nghĩa là nó có thể tạo ra một cuộc gọi hiệu quả hơn là chỉ đơn giản là tạo một hàm gọi một hàm khác.
chepner

12

Bên cạnh chức năng bổ sung mà Alex đã đề cập, một ưu điểm khác của funcools.partial là tốc độ. Với một phần, bạn có thể tránh xây dựng (và phá hủy) một khung ngăn xếp khác.

Cả hàm được tạo bởi một phần hay lambdas đều không có tài liệu theo mặc định (mặc dù bạn có thể đặt chuỗi doc cho bất kỳ đối tượng nào thông qua __doc__).

Bạn có thể tìm thêm chi tiết trong blog này: Ứng dụng chức năng một phần trong Python


Nếu bạn đã thử nghiệm lợi thế tốc độ, có thể mong đợi cải thiện tốc độ nào của lambda một phần?
Trilarion

1
Khi bạn nói rằng chuỗi doc được kế thừa, bạn muốn nói đến phiên bản Python nào? Trong Python 2.7.15 và Python 3.7.2 chúng không được kế thừa. Đó là một điều tốt, bởi vì chuỗi gốc không nhất thiết đúng cho hàm với các đối số được áp dụng một phần.
Tháng Một

Đối với python 2.7 ( docs.python.org/2/l Library / funcools.html#partial-objects ): " thuộc tính têntài liệu không được tạo tự động". Tương tự cho 3. [5-7].
Yaroslav Nikitenko

Có một lỗi trong liên kết của bạn: log_info = một phần (log_template, level = "thông tin") - không thể vì cấp độ không phải là đối số từ khóa trong ví dụ. Cả python 2 và 3 đều nói: "TypeError: log_template () có nhiều giá trị cho đối số 'level'".
Yaroslav Nikitenko

Trên thực tế, tôi đã tạo một phần (f) bằng tay và nó cung cấp trường doc dưới dạng 'một phần (func, * args, ** từ khóa) - hàm mới với ứng dụng một phần \ n của các đối số và từ khóa đã cho. \ N' (cả hai cho trăn 2 và 3).
Yaroslav Nikitenko

1

Tôi hiểu ý định nhanh nhất trong ví dụ thứ ba.

Khi tôi phân tích lambdas, tôi mong đợi sự phức tạp / kỳ quặc hơn so với thư viện chuẩn được cung cấp trực tiếp.

Ngoài ra, bạn sẽ nhận thấy rằng ví dụ thứ ba là ví dụ duy nhất không phụ thuộc vào chữ ký đầy đủ của sum2; do đó làm cho nó hơi lỏng lẻo hơn kết hợp.


1
Hừm, tôi thực sự là một sự thuyết phục ngược lại, tôi mất nhiều thời gian hơn để phân tích functools.partialcuộc gọi, trong khi lambdas là hiển nhiên.
David Z
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.