Concatenamento di funzioni in Python


90

Su Codewars.com ho riscontrato la seguente attività:

Crea una funzione addche somma i numeri quando viene chiamata in successione. Quindi add(1)dovrebbe tornare 1, add(1)(2)dovrebbe tornare 1+2, ...

Pur conoscendo le basi di Python, non ho mai incontrato una funzione che possa essere chiamata in tale successione, ovvero una funzione f(x)che possa essere chiamata come f(x)(y)(z).... Finora, non sono nemmeno sicuro di come interpretare questa notazione.

Come matematico, sospetto che f(x)(y)sia una funzione che assegna a ogni xuna funzione g_{x}e quindi restituisce g_{x}(y)e allo stesso modo per f(x)(y)(z).

Se questa interpretazione fosse corretta, Python mi permetterebbe di creare dinamicamente funzioni che mi sembrano molto interessanti. Ho cercato sul Web nell'ultima ora, ma non sono riuscito a trovare una pista nella giusta direzione. Dal momento che non so come viene chiamato questo concetto di programmazione, tuttavia, questo potrebbe non essere troppo sorprendente.

Come si chiama questo concetto e dove posso leggere di più al riguardo?


7
Sembra che tu stia cercando funzioni di curry
OneCricketeer

2
Suggerimento: una funzione nidificata viene creata dinamicamente, ha accesso alle variabili locali della sua funzione padre ed è in grado di essere restituita come oggetto (richiamabile).
Jonathon Reinhart

@JonathonReinhart Questo è il modo in cui stavo pensando al problema. Ma non ho davvero visto come implementarlo.
Stefan Mesken

3
Per inciso: Python ti consentirà sicuramente di creare funzioni dinamicamente. Se sei interessato, ecco un paio di concetti correlati su cui leggere: WP: Funzioni di prima classe | Come si crea una funzione di ordine superiore in Python? | functools.partial()| WP: Closures
Lukas Graf

@LukasGraf lo darò un'occhiata. Grazie!
Stefan Mesken

Risposte:


100

Non so se si tratta di concatenamento di funzioni tanto quanto di concatenamento richiamabile , ma, poiché le funzioni sono richiamabili, immagino che non ci sia alcun danno. Ad ogni modo, ci sono due modi in cui posso pensare di farlo:

Sottoclassificazione inte definizione __call__:

Il primo modo sarebbe con una intsottoclasse personalizzata che definisce __call__quale restituisce una nuova istanza di se stesso con il valore aggiornato:

class CustomInt(int):
    def __call__(self, v):
        return CustomInt(self + v)

Funzione addpuò essere definito per restituire un CustomIntesempio, che, come richiamabile che restituisca un valore aggiornato di per sé, possono essere chiamati in successione:

>>> def add(v):
...    return CustomInt(v)
>>> add(1)
1
>>> add(1)(2)
3
>>> add(1)(2)(3)(44)  # and so on..
50

Inoltre, come intsottoclasse, il valore restituito mantiene il comportamento __repr__e __str__di ints. Per operazioni più complesse, tuttavia, dovresti definire altri dunder in modo appropriato .

Come ha notato @Caridorc in un commento, addpotrebbe anche essere scritto semplicemente come:

add = CustomInt 

Anche rinominare la classe in addinvece di CustomIntfunziona in modo simile.


Definire una chiusura, richiede una chiamata extra per produrre valore:

L'unico altro modo in cui posso pensare implica una funzione annidata che richiede una chiamata a un argomento vuoto extra per restituire il risultato. Sto Non utilizzare nonlocale optare per il fissaggio attributi per gli oggetti funzione per renderlo portabile tra Pythons:

def add(v):
    def _inner_adder(val=None):  
        """ 
        if val is None we return _inner_adder.v 
        else we increment and return ourselves
        """
        if val is None:    
            return _inner_adder.v
        _inner_adder.v += val
        return _inner_adder
    _inner_adder.v = v  # save value
    return _inner_adder 

Questo restituisce continuamente se stesso ( _inner_adder) che, se valviene fornito a, lo incrementa ( _inner_adder += val) e in caso contrario restituisce il valore così com'è. Come ho già detto, richiede una ()chiamata in più per restituire il valore incrementato:

>>> add(1)(2)()
3
>>> add(1)(2)(3)()  # and so on..
6

6
Anche nel codice interattivo add = CostumIntdovrebbe funzionare ed essere più semplice.
Caridorc

4
Il problema con la creazione di sottoclassi incorporati è che (2*add(1)(2))(3)non riesce con un TypeErrorperché intnon è richiamabile. Fondamentalmente CustomIntviene convertito in normale intquando viene utilizzato in qualsiasi contesto tranne quando si chiama. Per una soluzione più robusta devi sostanzialmente reimplementare tutti i __*__metodi comprese le __r*__versioni ...
Bakuriu

@Caridorc Oppure non chiamatelo CustomIntaffatto ma addquando lo definite.
minipif

27

Puoi odiarmi, ma ecco una battuta :)

add = lambda v: type("", (int,), {"__call__": lambda self, v: self.__class__(self + v)})(v)

Modifica: ok, come funziona? Il codice è identico alla risposta di @Jim, ma tutto avviene su una sola riga.

  1. typeè possibile utilizzare per la costruzione di nuovi tipi: type(name, bases, dict) -> a new type. Poiché nameforniamo una stringa vuota, poiché in questo caso il nome non è realmente necessario. Per bases(tuple) forniamo un (int,), che è identico all'ereditarietà int. dictsono gli attributi della classe, dove alleghiamo __call__lambda.
  2. self.__class__(self + v) è identico a return CustomInt(self + v)
  3. Il nuovo tipo viene costruito e restituito all'interno del lambda esterno.

17
O anche più breve:class add(int):__call__ = lambda self, v: add(self+v)
Bakuriu

3
Il codice all'interno di una classe viene eseguito esattamente come il codice normale, quindi puoi definire metodi speciali tramite assegnazioni. L'unica differenza è che l'ambito della classe è un po '... peculiare.
Bakuriu

16

Se vuoi definire una funzione da chiamare più volte, prima devi restituire ogni volta un oggetto richiamabile (ad esempio una funzione) altrimenti devi creare il tuo oggetto definendo un __call__attributo, in modo che sia richiamabile.

Il punto successivo è che devi preservare tutti gli argomenti, il che in questo caso significa che potresti voler usare Coroutines o una funzione ricorsiva. Ma nota che le coroutine sono molto più ottimizzate / flessibili delle funzioni ricorsive , specialmente per tali attività.

Ecco una funzione di esempio che utilizza Coroutines, che conserva lo stato più recente di se stesso. Nota che non può essere chiamato più volte poiché il valore restituito è an integerche non è richiamabile, ma potresti pensare di trasformarlo nell'oggetto previsto ;-).

def add():
    current = yield
    while True:
        value = yield current
        current = value + current


it = add()
next(it)
print(it.send(10))
print(it.send(2))
print(it.send(4))

10
12
16

6

Il modo pitonico per farlo sarebbe usare argomenti dinamici:

def add(*args):
    return sum(args)

Questa non è la risposta che stai cercando, e potresti saperlo, ma ho pensato di darla comunque perché se qualcuno si chiedesse di farlo non per curiosità ma per lavoro. Probabilmente dovrebbero avere la risposta "cosa giusta da fare".


1
Ho rimosso la tua nota " PS ", Nichochar. Siamo tutti consapevoli di quanto sia elegante Python :-) Non credo che appartenga al corpo della risposta.
Dimitris Fasarakis Hilliard

6
Penso che avresti potuto farlo add = sumse
avessi seguito

3

Semplicemente:

class add(int):
   def __call__(self, n):
      return add(self + n)

3

Se sei disposto ad accettare un ulteriore ()per recuperare il risultato puoi utilizzare functools.partial:

from functools import partial

def add(*args, result=0):
    return partial(add, result=sum(args)+result) if args else result

Per esempio:

>>> add(1)
functools.partial(<function add at 0x7ffbcf3ff430>, result=1)
>>> add(1)(2)
functools.partial(<function add at 0x7ffbcf3ff430>, result=3)
>>> add(1)(2)()
3

Ciò consente anche di specificare più numeri contemporaneamente:

>>> add(1, 2, 3)(4, 5)(6)()
21

Se vuoi restringerlo a un solo numero puoi fare quanto segue:

def add(x=None, *, result=0):
    return partial(add, result=x+result) if x is not None else result

Se vuoi add(x)(y)(z)restituire prontamente il risultato ed essere ulteriormente richiamabile, la sottoclasse intè la strada da percorrere.

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.