wake-up-neo.com

Funktionsverkettung in Python

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

Erstellen Sie eine Funktion add, die beim aufeinander folgenden Aufruf Zahlen addiert. add(1) sollte 1 zurückgeben, add(1)(2) sollte 1+2 zurückgeben, ...

Obwohl ich mit den Grundlagen von Python vertraut bin, ist mir noch nie eine Funktion begegnet, die in einer solchen Reihenfolge aufgerufen werden kann, d. H. Eine Funktion f(x), die als f(x)(y)(z)... aufgerufen werden kann. Ich bin mir noch nicht einmal sicher, wie ich diese Notation interpretieren soll. 

Als Mathematiker vermute ich, dass f(x)(y) eine Funktion ist, die jeder x eine Funktion g_{x} zuordnet und dann g_{x}(y) und ebenfalls für f(x)(y)(z) zurückgibt. 

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

Wie nennen Sie dieses Konzept und wo kann ich mehr darüber lesen?

79
Stefan Mesken

Ich weiß nicht, ob es sich hierbei um function - Verkettung handelt, so wie es callable - Verkettung ist, aber da Funktionen callable sind, schätze ich, dass es keinen Schaden gibt. So oder so gibt es zwei Möglichkeiten, dies zu tun:

Unterklassifizierung int und Definieren von __call__:

Der erste Weg wäre mit einer benutzerdefinierten int-Unterklasse, die __call__ definiert, die 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 add kann jetzt definiert werden, um eine CustomInt-Instanz zurückzugeben, die als aufrufbare Funktion, 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

Als int-Unterklasse behält der zurückgegebene Wert das __repr__- und __str__-Verhalten von ints bei. Für komplexere Vorgänge sollten Sie jedoch andere Dunder entsprechend definieren

Wie @Caridorc in einem Kommentar feststellt, könnte add auch einfach geschrieben werden als:

add = CustomInt 

Das Umbenennen der Klasse in add anstelle von CustomInt funktioniert ähnlich.


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

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 bin not und verwende nonlocal und wähle Attribute für die Funktionsobjekte, 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 

Diese gibt sich ständig selbst zurück (_inner_adder), die, wenn eine val angegeben wird, diese 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

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 funktioniert das? Der Code ist identisch mit der Antwort von @Jim, aber alles geschieht in einer einzigen Zeile.

  1. type kann zum Erstellen neuer Typen verwendet werden: type(name, bases, dict) -> a new type. Für name stellen wir eine leere Zeichenfolge bereit, da der Name in diesem Fall nicht wirklich benötigt wird. Für bases (Tuple) stellen wir einen (int,) bereit, der identisch ist mit dem Erben von int. dict sind die Klassenattribute, an die der __call__-Lambda angehängt wird.
  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.
23
Jordan Jambazov

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 ein eigenes Objekt erstellen, indem Sie ein __call__-Attribut definieren, damit es aufgerufen werden kann .

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

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 eine integer ist, die nicht aufgerufen werden kann. Sie könnten jedoch darüber nachdenken, ob Sie dies in Ihr erwartetes Objekt umwandeln möchten ;-).

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
16
Kasrâmvd

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 vielleicht auch, aber ich dachte, ich würde sie trotzdem geben, wenn sich jemand fragen würde, ob dies nicht aus Neugierde, sondern wegen der Arbeit geschehen würde. Sie sollten wahrscheinlich die richtige Antwort haben.

5
nichochar

Wenn Sie bereit sind, ein zusätzliches _()_ zu akzeptieren, um das Ergebnis abzurufen, können Sie functools.partial verwenden:

_from functools import partial

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

Zum Beispiel:

_>>> 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 möchten, dass add(x)(y)(z) das Ergebnis und ohne Weiteres zurückgibt, ist die Unterklasse int der richtige Weg.

1
a_guest

Einfach:

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