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.