So implementieren Sie Ihre eigene Datenstruktur in Python

Python bietet umfassende Unterstützung für die Implementierung Ihrer eigenen Datenstruktur mithilfe von Klassen und benutzerdefinierten Operatoren. In diesem Lernprogramm implementieren Sie eine benutzerdefinierte Pipeline-Datenstruktur, mit der beliebige Datenvorgänge ausgeführt werden können. Wir werden Python 3 verwenden.

Die Pipeline-Datenstruktur

Die Pipeline-Datenstruktur ist interessant, da sie sehr flexibel ist. Es besteht aus einer Liste beliebiger Funktionen, die auf eine Sammlung von Objekten angewendet werden können und eine Ergebnisliste erzeugen. Ich werde die Erweiterbarkeit von Python nutzen und zum Erstellen der Pipeline das Pipe-Zeichen ("|") verwenden.

Live-Beispiel

Bevor wir in alle Details eintauchen, sehen wir eine sehr einfache Pipeline in Aktion:

x = Bereich (5) | Pipeline () | doppelt | Ω-Druck (x) [0, 2, 4, 6, 8] 

Was ist denn hier los? Lassen Sie uns es Schritt für Schritt aufschlüsseln. Das erste Element Bereich (5) erstellt eine Liste mit Ganzzahlen [0, 1, 2, 3, 4]. Die ganzen Zahlen werden in eine leere Pipeline eingespeist, die mit bezeichnet ist Pipeline(). Dann wird der Pipeline eine "Double" -Funktion hinzugefügt und schließlich die Cool-Funktion Ω Funktion beendet die Pipeline und veranlasst sie, sich selbst auszuwerten. 

Die Bewertung besteht darin, die Eingabe zu übernehmen und alle Funktionen in der Pipeline anzuwenden (in diesem Fall nur die Doppelfunktion). Zum Schluss speichern wir das Ergebnis in einer Variablen namens x und drucken es.

Python-Klassen

Python unterstützt Klassen und verfügt über ein sehr ausgefeiltes objektorientiertes Modell, einschließlich mehrfacher Vererbung, Mixins und dynamischer Überladung. Ein __drin__() Funktion dient als Konstruktor, der neue Instanzen erstellt. Python unterstützt auch ein erweitertes Meta-Programmiermodell, auf das wir in diesem Artikel nicht eingehen werden. 

Hier ist eine einfache Klasse mit einem __drin__() Konstruktor, der ein optionales Argument akzeptiert x (Standardeinstellung 5) und speichert es in a self.x Attribut. Es hat auch eine foo () Methode, die das zurückgibt self.x Attribut multipliziert mit 3:

Klasse A: def __init __ (self, x = 5): self.x = x def foo (self): return self.x * 3 

So instantiieren Sie es mit und ohne explizites x-Argument:

>>> a = A (2) >>> print (a.foo ()) 6 a = A () print (a.foo ()) 15 

Benutzerdefinierte Operatoren

Mit Python können Sie benutzerdefinierte Operatoren für Ihre Klassen verwenden, um eine schönere Syntax zu erhalten. Es gibt spezielle Methoden, die als "Dunder" -Methoden bekannt sind. Der "Dunder" bedeutet "doppelter Unterstrich". Mit diesen Methoden wie "__eq__", "__gt__" und "__or__" können Sie Operatoren wie "==", ">" und "|" verwenden. mit Ihren Klasseninstanzen (Objekten). Mal sehen, wie sie mit der A-Klasse arbeiten.

Wenn Sie versuchen, zwei verschiedene Instanzen von A miteinander zu vergleichen, ist das Ergebnis unabhängig vom Wert von x immer False:

>>> print (A () == A ()) False 

Dies liegt daran, dass Python standardmäßig die Speicheradressen von Objekten vergleicht. Nehmen wir an, wir wollen den Wert von x vergleichen. Wir können einen speziellen Operator "__eq__" hinzufügen, der die zwei Argumente "self" und "other" akzeptiert und deren x-Attribut vergleicht:

 def __eq __ (self, other): return self.x == other.x 

Lass uns überprüfen:

>>> print (A () == A ()) True >>> print (A (4) == A (6)) False 

Implementieren der Pipeline als Python-Klasse

Nun, da wir uns mit den Grundlagen von Klassen und benutzerdefinierten Operatoren in Python befasst haben, wollen wir sie verwenden, um unsere Pipeline zu implementieren. Das __drin__() Der Konstruktor akzeptiert drei Argumente: Funktionen, Eingabe und Terminals. Das Argument "Funktionen" besteht aus einer oder mehreren Funktionen. Diese Funktionen sind die Stufen in der Pipeline, die die Eingangsdaten bearbeiten. 

Das Argument "input" ist die Liste der Objekte, mit denen die Pipeline arbeiten soll. Jedes Element der Eingabe wird von allen Pipeline-Funktionen verarbeitet. Das Argument "Terminals" ist eine Liste von Funktionen. Wenn eine von ihnen gefunden wird, wertet die Pipeline sich selbst aus und gibt das Ergebnis zurück. Die Terminals sind standardmäßig nur die Druckfunktion (in Python 3 ist "Drucken" eine Funktion). 

Beachten Sie, dass innerhalb des Konstruktors den Klemmen ein mysteriöses "Ω" hinzugefügt wird. Ich erkläre das als nächstes. 

Der Pipeline-Konstruktor

Hier ist die Klassendefinition und die __drin__() Konstrukteur:

Klasse Pipeline: def __init __ (self, Funktionen = (), Eingabe = (), Terminals = (print,)): if hasattr (Funktionen, '__call__'): self.functions = [Funktionen] else: self.functions = list (Funktionen) self.input = Eingabe self.terminals = [Ω] + Liste (Klemmen) 

Python 3 unterstützt Unicode in Bezeichnernamen vollständig. Das heißt, wir können coole Symbole wie "Ω" für Variablen- und Funktionsnamen verwenden. Hier habe ich eine Identitätsfunktion namens "Ω" deklariert, die als Terminalfunktion dient: Ω = Lambda x: x

Ich hätte auch die traditionelle Syntax verwenden können:

def Ω (x): Rückgabe x 

Die Operatoren "__or__" und "__ror__"

Hier kommt der Kern der Pipeline-Klasse. Um das "|" zu verwenden (Pipe-Symbol) müssen wir ein paar Operatoren überschreiben. Das "|" Symbol wird von Python für bitweise oder für ganze Zahlen verwendet. In unserem Fall möchten wir es außer Kraft setzen, um die Verkettung von Funktionen zu implementieren und die Eingabe am Anfang der Pipeline einzugeben. Das sind zwei getrennte Operationen.

Der Operator "__ror__" wird aufgerufen, wenn der zweite Operand eine Pipeline-Instanz ist, solange es der erste Operand nicht ist. Es betrachtet den ersten Operanden als Eingabe und speichert ihn in der selbst.input Attribut und gibt die Pipeline-Instanz zurück (das Selbst). Dadurch können später weitere Funktionen verkettet werden.

def __ror __ (self, Eingabe): self.input = input return self 

Hier ist ein Beispiel wo __ror __ () Operator würde aufgerufen werden: 'Hallo dort' | Pipeline()

Der Operator "__or__" wird aufgerufen, wenn der erste Operand eine Pipeline ist (auch wenn der zweite Operand auch eine Pipeline ist). Es akzeptiert den Operanden als aufrufbare Funktion und gibt an, dass der Operand "func" tatsächlich aufrufbar ist. 

Dann hängt es die Funktion an Selbstfunktionen Attribut und prüft, ob die Funktion eine der Terminalfunktionen ist. Wenn es sich um ein Terminal handelt, wird die gesamte Pipeline ausgewertet und das Ergebnis zurückgegeben. Wenn es sich nicht um ein Terminal handelt, wird die Pipeline selbst zurückgegeben.

def __oder __ (self, func): assert (hasattr (func, '__call__')) self.functions.append (func) wenn func in self.terminals: return self.eval () return self 

Auswertung der Pipeline

Wenn Sie der Pipeline immer mehr Nicht-Terminal-Funktionen hinzufügen, geschieht nichts. Die eigentliche Auswertung wird bis zum verschoben eval () Methode wird aufgerufen. Dies kann entweder durch Hinzufügen einer Terminalfunktion zur Pipeline oder durch Aufrufen geschehen eval () direkt. 

Die Auswertung besteht darin, alle Funktionen in der Pipeline (einschließlich der Terminalfunktion, falls vorhanden) zu iterieren und sie der Reihe nach auf den Ausgang der vorherigen Funktion auszuführen. Die erste Funktion in der Pipeline empfängt ein Eingabeelement.

def eval (self): result = [] für x in self.input: für f in self.funktionen: x = f (x) result.append (x) gibt das Ergebnis zurück 

Pipeline effektiv nutzen

Eine der besten Möglichkeiten, eine Pipeline zu verwenden, besteht darin, sie auf mehrere Eingabesätze anzuwenden. Im folgenden Beispiel wird eine Pipeline ohne Eingänge und ohne Terminalfunktionen definiert. Es hat zwei Funktionen: die berüchtigte doppelt Funktion, die wir zuvor definiert haben, und der Standard Mathematikboden

Dann bieten wir drei verschiedene Eingänge an. In der inneren Schleife fügen wir die Ω Terminalfunktion, wenn wir sie aufrufen, um die Ergebnisse vor dem Drucken zu sammeln:

p = Pipeline () | doppelt | Fußboden für Eingabe in ((0,5, 1,2, 3,1), (11,5, 21,2, -6,7, 34,7), (5, 8, 10,9)): Ergebnis = Eingabe | p | Ω Druck (Ergebnis) [1, 2, 6] [23, 42, -14, 69] [10, 16, 21] 

Sie könnten das verwenden drucken Terminalfunktion direkt, aber dann wird jedes Element in einer anderen Zeile gedruckt:

keep_palindromes = lambda x: (p für p in x wenn p [:: - 1] == p) keep_longer_than_3 = lambda x: (p für p in x wenn len (p)> 3) p = Pipeline () | keep_palindromes | keep_longer_than_3 | liste (('aba', 'abba', 'abcdef'),) | p | drucken ['abba'] 

Zukünftige Verbesserungen

Es gibt einige Verbesserungen, die die Pipeline nützlicher machen können:

  • Streaming hinzufügen, um unendliche Objektströme zu bearbeiten (z. B. Lesen von Dateien oder Netzwerkereignissen).
  • Stellen Sie einen Bewertungsmodus bereit, bei dem die gesamte Eingabe als einzelnes Objekt bereitgestellt wird, um die umständliche Problemumgehung durch das Bereitstellen einer Sammlung eines Elements zu vermeiden.
  • Fügen Sie verschiedene nützliche Pipeline-Funktionen hinzu.

Fazit

Python ist eine sehr ausdrucksstarke Sprache und eignet sich gut zum Entwerfen Ihrer eigenen Datenstruktur und benutzerdefinierten Typen. Die Möglichkeit, Standardoperatoren zu überschreiben, ist sehr leistungsfähig, wenn sich die Semantik für eine solche Notation eignet. Beispielsweise ist das Pipe-Symbol ("|") für eine Pipeline sehr natürlich. 

Viele Python-Entwickler genießen die in Python integrierten Datenstrukturen wie Tupel, Listen und Wörterbücher. Wenn Sie jedoch Ihre eigene Datenstruktur entwerfen und implementieren, kann die Arbeit mit Ihrem System einfacher und einfacher werden, indem die Abstraktionsebene erhöht und interne Details für Benutzer ausgeblendet werden. Versuche es.