In che modo il functools parziale fa quello che fa?


181

Non riesco a capire come funziona il parziale in funzioni. Ho il seguente codice da qui :

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

Ora in linea

incr = lambda y : sum(1, y)

Capisco che qualsiasi argomento che passo ad incresso verrà passato ysu lambdaquale ritornerà, ad sum(1, y)es1 + y .

Lo capisco. Ma non l'ho capitoincr2(4) .

In che modo 4viene superato come xin funzione parziale? Per me, 4dovrebbe sostituire il sum2. Qual è la relazione tra xe 4?

Risposte:


218

In partialparole povere , fa qualcosa del genere (a parte la parola chiave args support etc):

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

    return wrapper

Quindi, chiamando partial(sum2, 4)si crea una nuova funzione (una chiamata , per essere precisi) che si comporta come sum2, ma ha un argomento posizionale in meno. L'argomento mancante viene sempre sostituito da 4, quindipartial(sum2, 4)(2) == sum2(4, 2)

Per quanto riguarda il motivo per cui è necessario, esiste una varietà di casi. Solo per uno, supponiamo che tu debba passare una funzione da qualche parte dove dovrebbe avere 2 argomenti:

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)

Ma una funzione che hai già ha bisogno di accedere ad un terzo contextoggetto per fare il suo lavoro:

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

Quindi, ci sono diverse soluzioni:

Un oggetto personalizzato:

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)

Con parziali:

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

Di questi tre, partialè il più corto e il più veloce. (Per un caso più complesso potresti volere un oggetto personalizzato).


1
da dove hai preso la extra_argsvariabile
user1865341

2
extra_argsè qualcosa che è passato dal chiamante parziale, nell'esempio con p = partial(func, 1); f(2, 3, 4)esso è (2, 3, 4).
Bereal

1
ma perché dovremmo farlo, qualsiasi caso d'uso speciale in cui qualcosa deve essere fatto solo in parte e non può essere fatto con altre cose
user1865341

@ user1865341 Ho aggiunto un esempio alla risposta.
Bereal

con il tuo esempio, qual è la relazione tra callbackemy_callback
user1865341

92

parziali sono incredibilmente utili.

Ad esempio, in una sequenza di chiamate di funzioni "allineate" (in cui il valore restituito da una funzione è l'argomento passato alla successiva).

A volte una funzione in tale pipeline richiede un singolo argomento , ma la funzione immediatamente a monte da essa restituisce due valori .

In questo scenario, functools.partialpotrebbe consentire di mantenere intatta questa pipeline di funzioni.

Ecco un esempio specifico e isolato: supponiamo di voler ordinare alcuni dati in base alla distanza di ciascun punto dati da un obiettivo:

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

Per ordinare questi dati in base alla distanza dal bersaglio, quello che vorresti fare ovviamente è questo:

data.sort(key=euclid_dist)

ma non puoi: il parametro chiave del metodo di ordinamento accetta solo funzioni che accettano un singolo argomento.

quindi riscrivi euclid_distcome una funzione prendendo un singolo parametro:

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist ora accetta un singolo argomento,

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

quindi ora puoi ordinare i tuoi dati passando la funzione parziale per l'argomento chiave del metodo di ordinamento:

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

O ad esempio, uno degli argomenti della funzione cambia in un ciclo esterno ma viene corretto durante l'iterazione nel ciclo interno. Usando un parziale, non è necessario passare il parametro aggiuntivo durante l'iterazione del ciclo interno, poiché la funzione modificata (parziale) non lo richiede.

>>> from functools import partial

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

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

creare una funzione parziale (usando la parola chiave arg)

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

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

puoi anche creare una funzione parziale con un argomento posizionale

>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

ma questo genererà (ad esempio, la creazione di un argomento parziale con l'argomento parola chiave, quindi la chiamata mediante argomenti posizionali)

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

un altro caso d'uso: scrivere codice distribuito usando la multiprocessinglibreria di Python . Un pool di processi viene creato utilizzando il metodo Pool:

>>> import multiprocessing as MP

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

Pool ha un metodo map, ma richiede solo un singolo iterabile, quindi se è necessario passare una funzione con un elenco di parametri più lungo, ridefinire la funzione come parziale, per correggere tutti tranne uno:

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

1
c'è qualche uso pratico di questa funzione in qualche modo
user1865341

3
@utente1865341 ha aggiunto due casi esemplari alla mia risposta
doug l'

IMHO, questa è una risposta migliore in quanto tiene fuori concetti non correlati come oggetti e classi e si concentra su funzioni di cui si tratta.
Akhan

35

risposta breve, partialfornisce valori predefiniti ai parametri di una funzione che altrimenti non avrebbero valori predefiniti.

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
questo è mezzo vero perché possiamo sovrascrivere i valori predefiniti, possiamo anche sovrascrivere i parametri scavalcati dai successivi partiale così via
Azat Ibrakov

33

I parziali possono essere usati per creare nuove funzioni derivate che hanno alcuni parametri di input preassegnati

Per vedere un po 'di utilizzo reale dei parziali, fare riferimento a questo post di blog davvero valido:
http://chriskiehl.com/article/Cleaner-coding-through-partially-applied-functions/

Un semplice ma ordinata esempio principiante di dal blog, spiega come si potrebbe usare partialsu re.searchper rendere il codice più leggibile. re.searchla firma del metodo è:

search(pattern, string, flags=0) 

Applicando partialpossiamo creare più versioni dell'espressione regolare searchper soddisfare i nostri requisiti, quindi ad esempio:

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

Adesso is_spaced_apart e is_grouped_togethersono due nuove funzioni derivate da re.searchcui è patternapplicato l' argomento (poiché patternè il primo argomento inre.search firma del metodo).

La firma di queste due nuove funzioni (richiamabile) è:

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

Ecco come è possibile utilizzare queste funzioni parziali su alcuni testi:

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

Puoi fare riferimento al link sopra per ottenere una comprensione più approfondita dell'argomento, in quanto copre questo esempio specifico e molto altro ancora.


1
Non è equivalente a is_spaced_apart = re.compile('[a-zA-Z]\s\=').search? In tal caso, esiste una garanzia che il partiallinguaggio compili l'espressione regolare per un riutilizzo più rapido?
Aristide,

10

Secondo me, è un modo per implementare il curry in 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))

Il risultato è 3 e 4.


1

Vale anche la pena ricordare che quando una funzione parziale ha superato un'altra funzione in cui vogliamo "codificare" alcuni parametri, questo dovrebbe essere il parametro più a destra

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

ma se facciamo lo stesso, cambiando invece un parametro

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

genererà un errore, "TypeError: func () ha ottenuto più valori per l'argomento 'a'"


Eh? Fai il parametro più a sinistra in questo modo:prt=partial(func, 7)
DylanYoung

0

Questa risposta è più di un codice di esempio. Tutte le risposte di cui sopra danno buone spiegazioni sul perché si dovrebbe usare parziale. Darò le mie osservazioni e userò casi parziali.

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

L'output del codice sopra dovrebbe essere:

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

Si noti che nell'esempio sopra è stato restituito un nuovo callable che prenderà il parametro (c) come argomento. Si noti che è anche l'ultimo argomento della funzione.

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

L'output del codice sopra è anche:

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

Si noti che * è stato utilizzato per decomprimere gli argomenti non relativi a parole chiave e il callable restituito in termini di quale argomento può essere utilizzato come sopra.

Un'altra osservazione è: L' esempio seguente dimostra che il parziale restituisce un callable che prenderà il parametro non dichiarato (a) come argomento.

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)

L'output del codice sopra dovrebbe essere:

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

Allo stesso modo,

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

Sopra le stampe di codice

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

Ho dovuto usarlo quando stavo usando il Pool.map_asyncmetodo dal multiprocessingmodulo. È possibile passare solo un argomento alla funzione worker, quindi ho dovuto usarlo partialper rendere la mia funzione worker simile a un callable con un solo argomento di input, ma in realtà la mia funzione worker aveva più argomenti di input.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.