Làm thế nào để funcools một phần làm những gì nó làm?


180

Tôi không thể hiểu được cách thức hoạt động của một phần trong funcools. Tôi có đoạn mã sau từ đây :

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

Bây giờ trong dòng

incr = lambda y : sum(1, y)

Tôi nhận được rằng bất cứ điều gì tranh luận tôi vượt qua để incrnó sẽ được thông qua như yđể lambdamà sẽ quay trở lại sum(1, y)ví dụ 1 + y.

Tôi hiểu điều đó. Nhưng tôi đã không hiểu điều này incr2(4).

Làm thế nào để 4được thông qua như xtrong chức năng một phần? Với tôi, 4nên thay thế sum2. Mối quan hệ giữa xvà là 4gì?

Câu trả lời:


218

Roughly, partiallàm một cái gì đó như thế này (ngoài hỗ trợ từ khóa args vv):

def partial(func, *part_args):
    def wrapper(*extra_args):
        args = list(part_args)
        args.extend(extra_args)
        return func(*args)

    return wrapper

Vì vậy, bằng cách gọi partial(sum2, 4)bạn tạo một hàm mới (chính xác, có thể gọi được), hoạt động như thế sum2, nhưng có ít hơn một đối số vị trí. Đối số còn thiếu đó luôn được thay thế bởi 4, do đópartial(sum2, 4)(2) == sum2(4, 2)

Về lý do tại sao nó cần thiết, có nhiều trường hợp. Chỉ với một, giả sử bạn phải vượt qua một hàm ở nơi mà nó dự kiến ​​sẽ có 2 đối số:

class EventNotifier(object):
    def __init__(self):
        self._listeners = []

    def add_listener(self, callback):
        ''' callback should accept two positional arguments, event and params '''
        self._listeners.append(callback)
        # ...

    def notify(self, event, *params):
        for f in self._listeners:
            f(event, params)

Nhưng một chức năng bạn đã có nhu cầu truy cập vào một số contextđối tượng thứ ba để thực hiện công việc của nó:

def log_event(context, event, params):
    context.log_event("Something happened %s, %s", event, params)

Vì vậy, có một số giải pháp:

Một đối tượng tùy chỉnh:

class Listener(object):
   def __init__(self, context):
       self._context = context

   def __call__(self, event, params):
       self._context.log_event("Something happened %s, %s", event, params)


 notifier.add_listener(Listener(context))

Lambda:

log_listener = lambda event, params: log_event(context, event, params)
notifier.add_listener(log_listener)

Với các hạt:

context = get_context()  # whatever
notifier.add_listener(partial(log_event, context))

Trong ba người đó, partiallà ngắn nhất và nhanh nhất. (Đối với trường hợp phức tạp hơn, bạn có thể muốn một đối tượng tùy chỉnh).


1
bạn lấy extra_argsbiến từ đâu
user1865341

2
extra_argslà một cái gì đó được thông qua bởi người gọi một phần, trong ví dụ với p = partial(func, 1); f(2, 3, 4)nó là (2, 3, 4).
mất

1
nhưng tại sao chúng ta sẽ làm điều đó, bất kỳ trường hợp sử dụng đặc biệt nào mà chỉ phải thực hiện một phần nào đó và không thể được thực hiện với điều khác
user1865341

@ user1865341 Tôi đã thêm một ví dụ cho câu trả lời.
mất

với ví dụ của bạn, mối quan hệ giữa callbackmy_callback
user1865341

92

partials là vô cùng hữu ích.

Chẳng hạn, trong một chuỗi các lệnh gọi hàm 'được xếp thành ống' (trong đó giá trị được trả về từ một hàm là đối số được truyền cho hàm tiếp theo).

Đôi khi một hàm trong một đường ống như vậy đòi hỏi một đối số duy nhất , nhưng hàm ngay lập tức ngược dòng từ nó trả về hai giá trị .

Trong trường hợp này, functools.partialcó thể cho phép bạn giữ nguyên đường ống chức năng này.

Đây là một ví dụ cụ thể, riêng biệt: giả sử bạn muốn sắp xếp một số dữ liệu theo khoảng cách của từng điểm dữ liệu từ một số mục tiêu:

# create some data
import random as RND
fnx = lambda: RND.randint(0, 10)
data = [ (fnx(), fnx()) for c in range(10) ]
target = (2, 4)

import math
def euclid_dist(v1, v2):
    x1, y1 = v1
    x2, y2 = v2
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

Để sắp xếp dữ liệu này theo khoảng cách từ mục tiêu, tất nhiên những gì bạn muốn làm là:

data.sort(key=euclid_dist)

nhưng bạn không thể - tham số khóa của phương thức sắp xếp chỉ chấp nhận các hàm có một đối số duy nhất .

nên viết lại euclid_distnhư một chức năng tham gia một đơn tham số:

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist bây giờ chấp nhận một đối số duy nhất,

>>> p_euclid_dist((3, 3))
  1.4142135623730951

vì vậy bây giờ bạn có thể sắp xếp dữ liệu của mình bằng cách chuyển vào hàm một phần cho đối số khóa của phương thức sắp xếp:

data.sort(key=p_euclid_dist)

# verify that it works:
for p in data:
    print(round(p_euclid_dist(p), 3))

    1.0
    2.236
    2.236
    3.606
    4.243
    5.0
    5.831
    6.325
    7.071
    8.602

Hoặc ví dụ, một trong các đối số của hàm thay đổi trong một vòng lặp bên ngoài nhưng được cố định trong quá trình lặp trong vòng lặp bên trong. Bằng cách sử dụng một phần, bạn không phải truyền tham số bổ sung trong quá trình lặp của vòng lặp bên trong, bởi vì hàm (một phần) đã sửa đổi không yêu cầu nó.

>>> from functools import partial

>>> def fnx(a, b, c):
      return a + b + c

>>> fnx(3, 4, 5)
      12

tạo một phần chức năng (sử dụng từ khóa arg)

>>> pfnx = partial(fnx, a=12)

>>> pfnx(b=4, c=5)
     21

bạn cũng có thể tạo một hàm một phần với một đối số vị trí

>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

nhưng điều này sẽ ném (ví dụ: tạo một phần với đối số từ khóa sau đó gọi bằng cách sử dụng đối số vị trí)

>>> pfnx = partial(fnx, a=12)

>>> pfnx(4, 5)
      Traceback (most recent call last):
      File "<pyshell#80>", line 1, in <module>
      pfnx(4, 5)
      TypeError: fnx() got multiple values for keyword argument 'a'

trường hợp sử dụng khác: viết mã phân tán bằng multiprocessingthư viện của python . Nhóm quy trình được tạo bằng phương thức Pool:

>>> import multiprocessing as MP

>>> # create a process pool:
>>> ppool = MP.Pool()

Pool có một phương thức bản đồ, nhưng nó chỉ mất một lần lặp duy nhất, vì vậy nếu bạn cần truyền vào một hàm có danh sách tham số dài hơn, hãy xác định lại hàm là một phần, để sửa tất cả trừ một:

>>> ppool.map(pfnx, [4, 6, 7, 8])

1
Có cách nào sử dụng thực tế chức năng này không?
user1865341

3
@ user1865341 đã thêm hai trường hợp sử dụng mẫu mực vào câu trả lời của tôi
doug

IMHO, đây là một câu trả lời tốt hơn vì nó loại bỏ các khái niệm không liên quan như các đối tượng và các lớp và tập trung vào các chức năng, đó là tất cả những gì về điều này.
akhan

35

câu trả lời ngắn, partialđưa ra các giá trị mặc định cho các tham số của hàm nếu không có giá trị mặc định.

from functools import partial

def foo(a,b):
    return a+b

bar = partial(foo, a=1) # equivalent to: foo(a=1, b)
bar(b=10)
#11 = 1+10
bar(a=101, b=10)
#111=101+10

5
điều này đúng một nửa vì chúng ta có thể ghi đè các giá trị mặc định, thậm chí chúng ta có thể ghi đè các tham số được ghi đè bằng cách tiếp theo partialvà cứ thế
Azat Ibrakov

33

Các phần có thể được sử dụng để tạo các hàm dẫn xuất mới có một số tham số đầu vào được gán trước

Để xem một số cách sử dụng partials trong thế giới thực, hãy tham khảo bài đăng trên blog thực sự tốt này:
http://chriskiehl.com/article/Cleaner-coding-ENC-partimate-applied-fifts/

Một đơn giản nhưng gọn gàng dụ người mới bắt đầu từ blog, bìa như thế nào người ta có thể sử dụng partialtrên re.searchđể làm cho mã dễ đọc hơn. re.searchchữ ký của phương thức là:

search(pattern, string, flags=0) 

Bằng cách áp dụng, partialchúng tôi có thể tạo nhiều phiên bản của biểu thức chính quy searchcho phù hợp với yêu cầu của chúng tôi, ví dụ:

is_spaced_apart = partial(re.search, '[a-zA-Z]\s\=')
is_grouped_together = partial(re.search, '[a-zA-Z]\=')

Bây giờ is_spaced_apartis_grouped_togetherlà hai hàm mới xuất phát từ re.searchđó có patternđối số được áp dụng (vì patternlà đối số đầu tiên trong re.searchchữ ký của phương thức).

Chữ ký của hai chức năng mới này (có thể gọi được) là:

is_spaced_apart(string, flags=0)     # pattern '[a-zA-Z]\s\=' applied
is_grouped_together(string, flags=0) # pattern '[a-zA-Z]\=' applied

Đây là cách bạn có thể sử dụng các chức năng một phần này trên một số văn bản:

for text in lines:
    if is_grouped_together(text):
        some_action(text)
    elif is_spaced_apart(text):
        some_other_action(text)
    else:
        some_default_action()

Bạn có thể tham khảo liên kết ở trên để hiểu sâu hơn về chủ đề này, vì nó bao gồm ví dụ cụ thể này và nhiều hơn nữa ..


1
Điều này không tương đương với is_spaced_apart = re.compile('[a-zA-Z]\s\=').search? Nếu vậy, có đảm bảo rằng partialthành ngữ biên dịch biểu thức chính quy để sử dụng lại nhanh hơn không?
Aristide

10

Theo tôi, đó là một cách để thực hiện cà ri trong trăn.

from functools import partial
def add(a,b):
    return a + b

def add2number(x,y,z):
    return x + y + z

if __name__ == "__main__":
    add2 = partial(add,2)
    print("result of add2 ",add2(1))
    add3 = partial(partial(add2number,1),2)
    print("result of add3",add3(1))

Kết quả là 3 và 4.


1

Cũng đáng đề cập, rằng khi một phần chức năng vượt qua một chức năng khác mà chúng tôi muốn "mã cứng" một số tham số, đó phải là tham số ngoài cùng bên phải

def func(a,b):
    return a*b
prt = partial(func, b=7)
    print(prt(4))
#return 28

nhưng nếu chúng ta làm như vậy, nhưng thay đổi một tham số thay thế

def func(a,b):
    return a*b
 prt = partial(func, a=7)
    print(prt(4))

nó sẽ đưa ra lỗi, "TypeError: func () có nhiều giá trị cho đối số 'a'"


Huh? Bạn thực hiện tham số ngoài cùng bên trái như thế này:prt=partial(func, 7)
DylanYoung

0

Câu trả lời này là nhiều hơn một mã ví dụ. Tất cả các câu trả lời ở trên cung cấp giải thích tốt về lý do tại sao người ta nên sử dụng một phần. Tôi sẽ đưa ra quan sát của tôi và sử dụng các trường hợp về một phần.

from functools import partial
 def adder(a,b,c):
    print('a:{},b:{},c:{}'.format(a,b,c))
    ans = a+b+c
    print(ans)
partial_adder = partial(adder,1,2)
partial_adder(3)  ## now partial_adder is a callable that can take only one argument

Đầu ra của đoạn mã trên phải là:

a:1,b:2,c:3
6

Lưu ý rằng trong ví dụ trên, một cuộc gọi mới đã được trả về sẽ lấy tham số (c) làm đối số. Lưu ý rằng đó cũng là đối số cuối cùng của hàm.

args = [1,2]
partial_adder = partial(adder,*args)
partial_adder(3)

Đầu ra của đoạn mã trên cũng là:

a:1,b:2,c:3
6

Lưu ý rằng * đã được sử dụng để giải nén các đối số không phải từ khóa và có thể gọi được trả về về mặt đối số mà nó có thể thực hiện giống như trên.

Một quan sát khác là: Ví dụ dưới đây chứng minh rằng một phần trả về một cuộc gọi sẽ lấy tham số không khai báo (a) làm đối số.

def adder(a,b=1,c=2,d=3,e=4):
    print('a:{},b:{},c:{},d:{},e:{}'.format(a,b,c,d,e))
    ans = a+b+c+d+e
    print(ans)
partial_adder = partial(adder,b=10,c=2)
partial_adder(20)

Đầu ra của đoạn mã trên phải là:

a:20,b:10,c:2,d:3,e:4
39

Tương tự

kwargs = {'b':10,'c':2}
partial_adder = partial(adder,**kwargs)
partial_adder(20)

Trên mã in

a:20,b:10,c:2,d:3,e:4
39

Tôi đã phải sử dụng nó khi tôi đang sử dụng Pool.map_asyncphương thức từ multiprocessingmô-đun. Bạn chỉ có thể truyền một đối số cho hàm worker, vì vậy tôi phải sử dụng partialđể làm cho hàm worker của mình trông giống như một cuộc gọi chỉ với một đối số đầu vào nhưng trong thực tế, hàm worker của tôi có nhiều đối số đầu vào.

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.