Funktionsverkettung in Python

88

Auf codewars.com bin ich auf folgende Aufgabe gestoßen:

Erstellen Sie eine Funktion add, die beim aufeinanderfolgenden Aufruf Zahlen addiert. Also add(1)sollte zurückkehren 1, add(1)(2)sollte zurückkehren 1+2, ...

Obwohl ich mit den Grundlagen von Python vertraut bin, bin ich noch nie auf eine Funktion gestoßen, die in einer solchen Folge f(x)aufgerufen werden kann , dh eine Funktion , die als aufgerufen werden kann f(x)(y)(z).... Bisher bin ich mir nicht einmal sicher, wie ich diese Notation interpretieren soll.

Als Mathematiker würde ich vermuten, dass dies f(x)(y)eine Funktion ist, die jeder xFunktion eine Funktion zuweist g_{x}und dann zurückkehrt g_{x}(y)und ebenfalls für f(x)(y)(z).

Sollte diese Interpretation korrekt sein, würde Python mir erlauben, dynamisch Funktionen zu erstellen, was mir sehr interessant erscheint. Ich habe in der letzten Stunde im Internet gesucht, konnte aber keinen Hinweis in die richtige Richtung finden. Da ich jedoch nicht weiß, wie dieses Programmierkonzept heißt, ist dies möglicherweise nicht allzu überraschend.

Wie nennt man dieses Konzept und wo kann ich mehr darüber lesen?

Stefan Mesken
quelle
7
Sieht so aus, als ob Sie nach Curry-Funktionen suchen
OneCricketeer
2
Hinweis: Eine verschachtelte Funktion wird dynamisch erstellt, hat Zugriff auf die lokalen Funktionen ihrer übergeordneten Funktion und kann als (aufrufbares) Objekt zurückgegeben werden.
Jonathon Reinhart
@ JonathonReinhart So habe ich über das Problem nachgedacht. Aber ich habe nicht wirklich gesehen, wie ich es umsetzen soll.
Stefan Mesken
3
Nebenbei: Mit Python können Sie auf jeden Fall dynamisch Funktionen erstellen. Wenn Sie interessiert sind, finden Sie hier einige verwandte Konzepte, die Sie nachlesen sollten: WP: Erstklassige Funktionen | Wie macht man eine Funktion höherer Ordnung in Python? | functools.partial()| WP: Closures
Lukas Graf
@ LukasGraf Ich werde es mir ansehen. Danke dir!
Stefan Mesken

Antworten:

98

Ich weiß nicht, ob dies eine Funktionsverkettung ist , sondern eine aufrufbare Verkettung, aber da Funktionen aufrufbar sind , wird wohl kein Schaden angerichtet. In beiden Fällen kann ich mir zwei Möglichkeiten vorstellen:

Unterklassifizierung intund Definition __call__:

Der erste Weg wäre mit einer benutzerdefinierten intUnterklasse, die definiert, __call__welche eine neue Instanz von sich selbst mit dem aktualisierten Wert zurückgibt:

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

Die Funktion addkann jetzt so definiert werden, dass eine CustomIntInstanz zurückgegeben wird, die als aufrufbare Instanz, die einen aktualisierten Wert von sich selbst zurückgibt, nacheinander aufgerufen werden kann:

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

Darüber hinaus als intUnterklasse, behält der zurückgegebene Wert das __repr__und __str__Verhalten ints. Für komplexere Vorgänge sollten Sie jedoch andere Dunder entsprechend definieren .

Wie @Caridorc in einem Kommentar feststellte, addkönnte man auch einfach schreiben als:

add = CustomInt 

Das Umbenennen der Klasse in addstatt CustomIntfunktioniert ebenfalls ähnlich.


Definieren Sie einen Abschluss, erfordert einen zusätzlichen Aufruf, um einen Wert zu erzielen:

Die einzige andere Möglichkeit, die ich mir vorstellen kann, ist eine verschachtelte Funktion, die einen zusätzlichen leeren Argumentaufruf erfordert, um das Ergebnis zurückzugeben. Ich verwende und entscheide mich nicht dafür, nonlocalAttribute an die Funktionsobjekte anzuhängen, um sie zwischen Pythons portierbar zu machen:

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 

Dies gibt kontinuierlich sich selbst zurück ( _inner_adder), was, wenn a angegeben valwird, es erhöht ( _inner_adder += val) und wenn nicht, den Wert so zurückgibt, wie er ist. Wie bereits erwähnt, ist ein zusätzlicher ()Aufruf erforderlich, um den inkrementierten Wert zurückzugeben:

>>> add(1)(2)()
3
>>> add(1)(2)(3)()  # and so on..
6
Dimitris Fasarakis Hilliard
quelle
6
Im interaktiven Code add = CostumIntsollte auch funktionieren und einfacher sein.
Caridorc
4
Das Problem bei integrierten Unterklassen besteht darin, dass (2*add(1)(2))(3)ein TypeErrorweil fehlschlägt, weil intes nicht aufrufbar ist. Grundsätzlich CustomIntwird das in normal konvertiert, intwenn es in einem beliebigen Kontext verwendet wird, außer beim Aufrufen. Für eine robustere Lösung müssen Sie grundsätzlich alle __*__Methoden einschließlich der __r*__Versionen neu implementieren ...
Bakuriu
@Caridorc Oder nenne es gar nicht, CustomIntsondern addbeim Definieren.
Minipif
26

Du kannst mich hassen, aber hier ist ein Einzeiler :)

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

Edit: Ok, wie geht das? Der Code ist identisch mit der Antwort von @Jim, aber alles geschieht in einer einzigen Zeile.

  1. typekann verwendet werden, um neue Typen zu erstellen : type(name, bases, dict) -> a new type. Denn namewir geben eine leere Zeichenfolge an, da der Name in diesem Fall nicht wirklich benötigt wird. Für bases(Tupel) stellen wir ein zur Verfügung (int,), das mit dem Erben identisch ist int. dictsind die Klassenattribute, an die wir das __call__Lambda anhängen .
  2. self.__class__(self + v) ist identisch mit return CustomInt(self + v)
  3. Der neue Typ wird innerhalb des äußeren Lambda konstruiert und zurückgegeben.
Jordan Jambazov
quelle
17
Oder noch kürzer:class add(int):__call__ = lambda self, v: add(self+v)
Bakuriu
3
Der Code in einer Klasse wird genau wie normaler Code ausgeführt, sodass Sie spezielle Methoden durch Zuweisungen definieren können. Der einzige Unterschied ist, dass der Klassenumfang etwas ... eigenartig ist.
Bakuriu
16

Wenn Sie eine Funktion definieren möchten, die mehrmals aufgerufen werden soll, müssen Sie zuerst jedes Mal ein aufrufbares Objekt zurückgeben (z. B. eine Funktion). Andernfalls müssen Sie Ihr eigenes Objekt erstellen, indem Sie ein __call__Attribut definieren , damit es aufrufbar ist.

Der nächste Punkt ist, dass Sie alle Argumente beibehalten müssen. In diesem Fall möchten Sie möglicherweise Coroutines oder eine rekursive Funktion verwenden. Beachten Sie jedoch, dass Coroutinen speziell für solche Aufgaben viel optimierter / flexibler sind als rekursive Funktionen .

Hier ist eine Beispielfunktion mit Coroutines, die den neuesten Status von sich selbst beibehält. Beachten Sie, dass es nicht mehrmals aufgerufen werden kann, da der Rückgabewert integernicht aufrufbar ist, aber Sie könnten darüber nachdenken, dies in Ihr erwartetes Objekt umzuwandeln ;-).

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
Kasravnd
quelle
6

Der pythonische Weg, dies zu tun, wäre die Verwendung dynamischer Argumente:

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

Dies ist nicht die Antwort, nach der Sie suchen, und Sie wissen das vielleicht, aber ich dachte, ich würde es trotzdem geben, denn wenn sich jemand wundert, dies nicht aus Neugier, sondern aus beruflichen Gründen zu tun. Sie sollten wahrscheinlich die "richtige Antwort" haben.

Nichochar
quelle
1
Ich habe deine PS -Notiz entfernt, Nichochar. Wir alle wissen, wie elegant Python ist :-) Ich glaube nicht, dass es in den Hauptteil der Antwort gehört.
Dimitris Fasarakis Hilliard
6
Ich denke, Sie hätten es einfach tun können, add = sumwenn Sie diesen Weg
gegangen wären
3

Einfach:

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

Wenn Sie bereit sind, eine zusätzliche zu akzeptieren (), um das Ergebnis abzurufen, können Sie Folgendes verwenden functools.partial:

from functools import partial

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

Beispielsweise:

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

Auf diese Weise können auch mehrere Nummern gleichzeitig angegeben werden:

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

Wenn Sie es auf eine einzelne Zahl beschränken möchten, können Sie Folgendes tun:

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

Wenn Sie add(x)(y)(z)das Ergebnis sofort zurückgeben und weiter aufrufbar sein möchten , intist die Unterklassifizierung der richtige Weg.

ein Gast
quelle