ฟังก์ชั่นบางส่วนทำหน้าที่อย่างไร


181

ฉันไม่สามารถเข้าใจวิธีการทำงานของฟังก์ชั่นบางส่วนได้ ฉันมีรหัสต่อไปนี้จากที่นี่ :

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

ตอนนี้อยู่ในสาย

incr = lambda y : sum(1, y)

ฉันจะได้รับว่าสิ่งที่โต้แย้งผมผ่านไปincrมันจะถูกส่งผ่านเป็นyเพื่อlambdaที่จะกลับมาคือsum(1, y)1 + y

ฉันเข้าใจ. incr2(4)แต่ผมไม่เข้าใจในเรื่องนี้

อย่างไร4จะได้รับการส่งผ่านเป็นxฟังก์ชั่นบางส่วน? ให้ฉันควรเปลี่ยน4 sum2ความสัมพันธ์ระหว่างxและ4คืออะไร?

คำตอบ:


218

ประมาณpartialทำบางสิ่งเช่นนี้ (นอกเหนือจากคำหลักสนับสนุน ฯลฯ ):

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

    return wrapper

ดังนั้นโดยการโทรpartial(sum2, 4)คุณสร้างฟังก์ชั่นใหม่ (callable, เพื่อความแม่นยำ) ที่มีลักษณะเหมือนsum2แต่มีอาร์กิวเมนต์ตำแหน่งน้อยกว่า อาร์กิวเมนต์ที่หายไปนั้นถูกแทนที่ด้วยเสมอ4ดังนั้นpartial(sum2, 4)(2) == sum2(4, 2)

สำหรับเหตุผลที่ต้องการมีหลายกรณี เพียงแค่หนึ่งสมมติว่าคุณต้องผ่านฟังก์ชั่นบางแห่งที่คาดว่าจะมี 2 ข้อโต้แย้ง:

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)

แต่ฟังก์ชั่นที่คุณมีอยู่แล้วต้องการเข้าถึงcontextวัตถุที่สามเพื่อทำงาน:

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

ดังนั้นจึงมีวิธีแก้ปัญหาหลายประการ:

วัตถุที่กำหนดเอง:

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

แลมบ์ดา:

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

ด้วย partials:

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

ในสามคนนั้นpartialสั้นที่สุดและเร็วที่สุด (สำหรับกรณีที่ซับซ้อนมากขึ้นคุณอาจต้องการวัตถุที่กำหนดเอง)


1
จากที่คุณได้รับextra_argsตัวแปร
user1865341

2
extra_argsเป็นสิ่งที่ผ่านไปได้โดยโทรบางส่วนในตัวอย่างที่มีมันเป็นp = partial(func, 1); f(2, 3, 4) (2, 3, 4)
ธ .

1
แต่ทำไมเราถึงทำอย่างนั้นกรณีการใช้งานพิเศษที่บางสิ่งบางอย่างต้องทำโดยบางส่วนเท่านั้นและไม่สามารถทำกับสิ่งอื่นได้
1865341

@ user1865341 ฉันเพิ่มตัวอย่างในคำตอบ
ธ.ค.

จากตัวอย่างของคุณความสัมพันธ์ระหว่างcallbackและmy_callback
user1865341

92

งาน Partialsมีประโยชน์อย่างเหลือเชื่อ

ตัวอย่างเช่นในลำดับ 'ไปป์ไลน์' ของการเรียกฟังก์ชัน (ซึ่งค่าที่ส่งคืนจากฟังก์ชันหนึ่งคืออาร์กิวเมนต์ที่ส่งผ่านไปยังถัดไป)

บางครั้งฟังก์ชั่นในท่อดังกล่าวต้องใช้อาร์กิวเมนต์เดียวแต่ฟังก์ชั่นได้ทันทีจากต้นน้ำก็จะส่งกลับสองค่า

ในสถานการณ์สมมตินี้functools.partialอาจอนุญาตให้คุณรักษาฟังก์ชันการทำงานของไพพ์ไลน์นี้ไว้

นี่คือตัวอย่างที่แยกได้เฉพาะ: สมมติว่าคุณต้องการเรียงลำดับข้อมูลตามระยะทางของจุดข้อมูลแต่ละจุดจากเป้าหมาย:

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

ในการจัดเรียงข้อมูลตามระยะทางจากเป้าหมายสิ่งที่คุณต้องการทำคือ:

data.sort(key=euclid_dist)

แต่คุณทำไม่ได้ - พารามิเตอร์หลักของวิธีการเรียงรับฟังก์ชั่นที่รับอาร์กิวเมนต์เดียวเท่านั้น

ดังนั้นเขียนใหม่euclid_distเป็นฟังก์ชันโดยใช้พารามิเตอร์เดียว :

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist ตอนนี้ยอมรับอาร์กิวเมนต์เดียว

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

ดังนั้นตอนนี้คุณสามารถจัดเรียงข้อมูลของคุณโดยส่งผ่านฟังก์ชั่นบางส่วนสำหรับอาร์กิวเมนต์ที่สำคัญของวิธีการจัดเรียง:

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

หรือตัวอย่างเช่นหนึ่งในข้อโต้แย้งของฟังก์ชั่นการเปลี่ยนแปลงในวงด้านนอก แต่ได้รับการแก้ไขในระหว่างการทำซ้ำในวงด้านใน ด้วยการใช้บางส่วนคุณไม่จำเป็นต้องผ่านพารามิเตอร์เพิ่มเติมในระหว่างการวนซ้ำของวงในเนื่องจากฟังก์ชั่นการแก้ไข (บางส่วน) ไม่จำเป็นต้องใช้

>>> from functools import partial

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

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

สร้างฟังก์ชั่นบางส่วน (ใช้ ARG คำหลัก)

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

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

คุณสามารถสร้างฟังก์ชั่นบางส่วนด้วยอาร์กิวเมนต์ตำแหน่ง

>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

แต่สิ่งนี้จะแสดง (เช่นการสร้างบางส่วนด้วยอาร์กิวเมนต์คำหลักจากนั้นเรียกใช้อาร์กิวเมนต์ตำแหน่ง)

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

กรณีการใช้งานอื่น: การเขียนรหัสการกระจายโดยใช้multiprocessingห้องสมุดของงูใหญ่ กลุ่มของกระบวนการถูกสร้างขึ้นโดยใช้วิธีกลุ่ม:

>>> import multiprocessing as MP

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

Pool มีวิธีการแมป แต่ใช้เพียง iterable เดียวเท่านั้นดังนั้นหากคุณต้องการส่งผ่านฟังก์ชันที่มีรายการพารามิเตอร์ที่ยาวขึ้นให้กำหนดฟังก์ชันเป็นบางส่วนอีกครั้งเพื่อแก้ไขทั้งหมดยกเว้นวิธีเดียว:

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

1
มีการใช้งานฟังก์ชั่นนี้ในทางปฏิบัติใด ๆ กับผู้ใช้
1865341

3
@ user1865341 เพิ่มอีกสองกรณีการใช้งาน exemplarly เพื่อคำตอบของฉัน
ดั๊ก

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

35

คำตอบสั้น ๆpartialให้ค่าเริ่มต้นกับพารามิเตอร์ของฟังก์ชั่นที่จะไม่มีค่าเริ่มต้น

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
นี่คือครึ่งจริงเพราะเราสามารถแทนที่ค่าเริ่มต้นเราสามารถแทนที่พารามิเตอร์ overriden ตามมาpartialเป็นต้น
Azat Ibrakov

33

สามารถใช้ Partials เพื่อสร้างฟังก์ชันที่ได้รับใหม่ซึ่งมีพารามิเตอร์อินพุตบางตัวที่กำหนดล่วงหน้า

หากต้องการดูการใช้งานพาร์ทิชันในโลกแห่งความเป็นจริงให้อ้างอิงโพสต์บล็อกที่ยอดเยี่ยม:
http://chriskiehl.com/article/Cleaner-coding-through-partially-applied-functions/

ตัวอย่างของผู้เริ่มต้นที่เรียบง่ายและเรียบร้อยจากบล็อกครอบคลุมถึงวิธีที่อาจใช้partialในre.searchการทำให้โค้ดอ่านง่ายขึ้น re.searchลายเซ็นของเมธอดคือ:

search(pattern, string, flags=0) 

ด้วยการใช้partialเราสามารถสร้างนิพจน์ทั่วไปได้หลายเวอร์ชันsearchเพื่อให้เหมาะกับความต้องการของเราตัวอย่างเช่น:

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

ตอนนี้is_spaced_apartและis_grouped_togetherมีสองฟังก์ชั่นใหม่ที่ได้มาจากการre.searchที่มีpatternการโต้แย้งใช้ (ตั้งแต่patternเป็นอาร์กิวเมนต์แรกในre.searchลายเซ็นของวิธีการ)

ลายเซ็นของทั้งสองฟังก์ชั่นใหม่ (callable) คือ:

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

นี่คือวิธีที่คุณสามารถใช้ฟังก์ชันบางส่วนเหล่านี้กับข้อความบางส่วน:

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

คุณสามารถอ้างอิงลิงค์ด้านบนเพื่อทำความเข้าใจเชิงลึกของหัวเรื่องได้มากขึ้นเนื่องจากครอบคลุมตัวอย่างเฉพาะนี้และอีกมากมาย ..


1
สิ่งนี้ไม่เทียบเท่าis_spaced_apart = re.compile('[a-zA-Z]\s\=').searchหรือ ถ้าเป็นเช่นนั้นมีการรับประกันว่าpartialสำนวนจะรวบรวมการแสดงออกปกติเพื่อนำมาใช้ใหม่ได้เร็วขึ้นหรือไม่?
Aristide

10

ในความคิดของฉันมันเป็นวิธีที่จะใช้การแกงในงูหลาม

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

ผลลัพธ์คือ 3 และ 4


1

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

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

แต่ถ้าเราทำเช่นเดียวกัน แต่เปลี่ยนพารามิเตอร์แทน

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

มันจะโยนข้อผิดพลาด "TypeError: func () ได้หลายค่าสำหรับการโต้เถียง 'a'"


ฮะ? คุณทำพารามิเตอร์ซ้ายสุดดังนี้:prt=partial(func, 7)
DylanYoung

0

คำตอบนี้เป็นรหัสตัวอย่างมากกว่า คำตอบข้างต้นทั้งหมดให้คำอธิบายที่ดีเกี่ยวกับสาเหตุที่ควรใช้บางส่วน ฉันจะให้ข้อสังเกตของฉันและใช้กรณีเกี่ยวกับบางส่วน

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

ผลลัพธ์ของรหัสข้างต้นควรเป็น:

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

โปรดสังเกตว่าในตัวอย่างด้านบนมีการส่งคืน callable ใหม่ที่จะรับพารามิเตอร์ (c) เนื่องจากเป็นอาร์กิวเมนต์ โปรดทราบว่ามันเป็นอาร์กิวเมนต์สุดท้ายของฟังก์ชั่น

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

ผลลัพธ์ของรหัสข้างต้นยังเป็น:

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

โปรดสังเกตว่า * ใช้เพื่อคลายข้อโต้แย้งที่ไม่ใช่คำหลักและสามารถเรียกคืนได้ในรูปของอาร์กิวเมนต์ที่สามารถใช้เป็นเหมือนข้างต้น

การสังเกตอีกอย่างคือ: ตัวอย่างด้านล่างแสดงให้เห็นว่าบางส่วนส่งกลับ callable ซึ่งจะใช้พารามิเตอร์ที่ไม่ได้ประกาศ (a) เป็นอาร์กิวเมนต์

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)

ผลลัพธ์ของรหัสข้างต้นควรเป็น:

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

ในทำนองเดียวกัน

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

พิมพ์รหัสข้างต้น

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

ฉันต้องใช้มันเมื่อฉันใช้Pool.map_asyncวิธีการจากmultiprocessingโมดูล คุณสามารถส่งผ่านอาร์กิวเมนต์เดียวไปยังฟังก์ชันผู้ปฏิบัติงานดังนั้นฉันจึงต้องใช้partialเพื่อทำให้ฟังก์ชันผู้ปฏิบัติงานของฉันดูเหมือน callable ที่มีอาร์กิวเมนต์อินพุตเดียวเท่านั้น แต่ในความเป็นจริงแล้วฟังก์ชันผู้ปฏิบัติงานของฉันมีอาร์กิวเมนต์หลายอินพุต

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