Bagaimana functools parsial melakukan apa yang dilakukannya?

180

Saya tidak bisa mengerti bagaimana sebagian bekerja di functools. Saya memiliki kode berikut dari sini :

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

Sekarang dalam barisan

incr = lambda y : sum(1, y)

Saya mendapatkan bahwa argumen apa pun yang saya berikan incrakan diteruskan yke lambdayang akan kembali sum(1, y)yaitu 1 + y.

Aku mengerti itu. Tapi saya tidak mengerti ini incr2(4).

Bagaimana cara 4disahkan sebagai xfungsi parsial? Bagi saya, 4harus mengganti sum2. Apa hubungan antara xdan 4?

pengguna1865341
sumber

Jawaban:

218

Secara kasar, partiallakukan sesuatu seperti ini (terlepas dari dukungan kata kunci args, dll.):

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

    return wrapper

Jadi, dengan menelepon partial(sum2, 4)Anda membuat fungsi baru (yang bisa dipanggil, tepatnya) yang berperilaku seperti sum2, tetapi memiliki satu argumen posisi kurang. Argumen yang hilang selalu diganti oleh 4, sehinggapartial(sum2, 4)(2) == sum2(4, 2)

Adapun mengapa itu diperlukan, ada berbagai kasus. Hanya untuk satu, misalkan Anda harus melewati fungsi di suatu tempat di mana ia diharapkan memiliki 2 argumen:

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)

Tetapi fungsi yang sudah Anda miliki membutuhkan akses ke contextobjek ketiga untuk melakukan tugasnya:

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

Jadi, ada beberapa solusi:

Objek khusus:

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)

Dengan parsial:

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

Dari ketiganya, partialadalah yang terpendek dan tercepat. (Untuk kasus yang lebih kompleks, Anda mungkin menginginkan objek khusus).

menjadi nyata
sumber
1
dari mana Anda mendapatkan extra_argsvariabel
user1865341
2
extra_argsadalah sesuatu yang disahkan oleh penelepon parsial, dalam contoh dengan p = partial(func, 1); f(2, 3, 4)itu (2, 3, 4).
bereal
1
tetapi mengapa kita melakukan itu, setiap kasus penggunaan khusus di mana sesuatu harus dilakukan hanya sebagian dan tidak dapat dilakukan dengan hal lain
user1865341
@ user1865341 Saya menambahkan contoh pada jawabannya.
bereal
dengan contoh Anda, apa hubungan antara callbackdanmy_callback
user1865341
92

parsial sangat berguna.

Misalnya, dalam urutan pemanggilan fungsi 'pipa-baris' (di mana nilai yang dikembalikan dari satu fungsi adalah argumen yang diteruskan ke yang berikutnya).

Kadang-kadang fungsi dalam pipa seperti itu membutuhkan argumen tunggal , tetapi fungsi segera hulu dari itu mengembalikan dua nilai .

Dalam skenario ini, functools.partialmungkin memungkinkan Anda untuk menjaga fungsi pipeline fungsi ini.

Berikut adalah contoh khusus dan terisolasi: misalkan Anda ingin mengurutkan beberapa data berdasarkan jarak setiap titik data dari beberapa target:

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

Untuk mengurutkan data ini berdasarkan jarak dari target, apa yang ingin Anda lakukan tentu saja adalah ini:

data.sort(key=euclid_dist)

tetapi Anda tidak bisa - parameter kunci metode sortir hanya menerima fungsi yang mengambil satu argumen.

jadi tulis ulang euclid_distsebagai fungsi yang mengambil parameter tunggal :

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist sekarang menerima satu argumen,

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

jadi sekarang Anda dapat mengurutkan data Anda dengan mengirimkan fungsi parsial untuk argumen kunci metode sortir:

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

Atau misalnya, salah satu argumen fungsi berubah di loop luar tetapi diperbaiki selama iterasi di loop dalam. Dengan menggunakan parsial, Anda tidak harus meneruskan parameter tambahan selama iterasi loop dalam, karena fungsi yang dimodifikasi (parsial) tidak memerlukannya.

>>> from functools import partial

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

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

buat fungsi parsial (menggunakan kata kunci arg)

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

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

Anda juga dapat membuat fungsi parsial dengan argumen posisi

>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

tetapi ini akan melempar (mis., membuat sebagian dengan argumen kata kunci lalu memanggil menggunakan argumen posisional)

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

kasus penggunaan lain: menulis kode terdistribusi menggunakan multiprocessingpustaka python . Kumpulan proses dibuat menggunakan metode Pool:

>>> import multiprocessing as MP

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

Pool memiliki metode peta, tetapi hanya membutuhkan satu iterable, jadi jika Anda harus meneruskan fungsi dengan daftar parameter yang lebih panjang, tentukan kembali fungsi sebagai parsial, untuk memperbaiki semua kecuali satu:

>>> ppool.map(pfnx, [4, 6, 7, 8])
doug
sumber
1
apakah ada penggunaan praktis dari fungsi ini di suatu tempat
user1865341
3
@ user1865341 menambahkan dua contoh penggunaan contoh untuk jawaban saya
doug
IMHO, ini adalah jawaban yang lebih baik karena menjaga konsep yang tidak terkait seperti objek dan kelas dan berfokus pada fungsi yang merupakan inti dari semua ini.
akhan
35

jawaban singkat, partialmemberikan nilai default ke parameter fungsi yang seharusnya tidak memiliki nilai default.

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
Alex Fortin
sumber
5
ini setengah benar karena kita dapat mengesampingkan nilai default, kita bahkan dapat mengganti parameter yang partial
ditimpa
33

Partial dapat digunakan untuk membuat fungsi turunan baru yang memiliki beberapa parameter input yang ditugaskan sebelumnya

Untuk melihat beberapa penggunaan parsial di dunia nyata, lihat posting blog yang sangat bagus ini:
http://chriskiehl.com/article/Cleaner-coding-through-partially-applied-functions/

Sebuah sederhana tetapi contoh rapi pemula dari blog, selimut bagaimana seseorang dapat menggunakan partialpada re.searchuntuk membuat kode lebih mudah dibaca. re.searchtanda tangan metode adalah:

search(pattern, string, flags=0) 

Dengan menerapkan, partialkami dapat membuat beberapa versi ekspresi reguler yang searchsesuai dengan persyaratan kami, jadi misalnya:

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

Sekarang is_spaced_apartdan is_grouped_togetherdua fungsi baru yang berasal dari re.searchyang memiliki patternargumen diterapkan (karena patternmerupakan argumen pertama dalamre.search metode tanda tangan ini).

Tanda tangan dari dua fungsi baru ini (dapat dipanggil) adalah:

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

Ini adalah bagaimana Anda kemudian dapat menggunakan fungsi parsial ini pada beberapa teks:

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

Anda dapat merujuk tautan di atas untuk mendapatkan pemahaman yang lebih mendalam tentang subjek, karena mencakup contoh spesifik ini dan banyak lagi ..

sisanared
sumber
1
Bukankah ini setara dengan is_spaced_apart = re.compile('[a-zA-Z]\s\=').search? Jika demikian, apakah ada jaminan bahwa partialidiom mengkompilasi ekspresi reguler untuk digunakan kembali lebih cepat?
Aristide
10

Menurut pendapat saya, ini adalah cara untuk menerapkan currying dengan python.

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

Hasilnya adalah 3 dan 4.

Hanzhou Tang
sumber
1

Juga perlu disebutkan, bahwa ketika fungsi parsial melewati fungsi lain di mana kita ingin "kode keras" beberapa parameter, itu harus menjadi parameter paling kanan

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

tetapi jika kita melakukan hal yang sama, tetapi mengubah suatu parameter sebagai gantinya

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

itu akan melempar kesalahan, "TypeError: func () mendapat beberapa nilai untuk argumen 'a'"

MSK
sumber
Hah? Anda melakukan parameter paling kiri seperti ini:prt=partial(func, 7)
DylanYoung
0

Jawaban ini lebih merupakan contoh kode. Semua jawaban di atas memberikan penjelasan yang baik tentang mengapa seseorang harus menggunakan sebagian. Saya akan memberikan pengamatan saya dan menggunakan kasus tentang parsial.

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

Output dari kode di atas harus:

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

Perhatikan bahwa dalam contoh di atas dikembalikan callable baru yang akan mengambil parameter (c) sebagai argumennya. Perhatikan bahwa ini juga argumen terakhir untuk fungsi.

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

Output dari kode di atas juga:

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

Perhatikan bahwa * digunakan untuk membongkar argumen non-kata kunci dan callable yang dikembalikan dengan argumen yang dapat diambil sama dengan di atas.

Pengamatan lain adalah: Contoh di bawah ini menunjukkan bahwa sebagian mengembalikan callable yang akan mengambil parameter yang tidak dideklarasikan (a) sebagai argumen.

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)

Output dari kode di atas harus:

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

Demikian pula,

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

Cetak kode di atas

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

Saya harus menggunakannya ketika saya menggunakan Pool.map_asyncmetode dari multiprocessingmodul. Anda hanya dapat mengirimkan satu argumen ke fungsi pekerja sehingga saya harus menggunakan partialuntuk membuat fungsi pekerja saya tampak seperti dapat dipanggil dengan hanya satu argumen input tetapi pada kenyataannya fungsi pekerja saya memiliki beberapa argumen input.

Ruthvik Vaila
sumber