Pythonischer Weg zur Implementierung von Datentypen (Python 2.7)

8

Der Großteil meiner Programmiererfahrung war mit C ++. Inspiriert von Bjarne Stroustrups Vortrag hier , einer meiner Lieblingsprogramme Techniken ist "Typ-reiche" Programmierung; die Entwicklung neuer robuster Datentypen, die nicht nur die Menge an Code reduzieren, den ich schreiben muss, indem ich die Funktionalität in den Typ umgebe (z. B. Vektoraddition anstelle von newVec.x = vec1.x + vec2.x; newVec.y = ... etc, wir können einfach newVec = vec1 + vec2) verwenden, aber auch Probleme in Ihrem Code zur Kompilierzeit durch das starke Typsystem aufdecken.

Ein aktuelles Projekt, das ich in Python 2.7 durchgeführt habe, benötigt ganzzahlige Werte, die obere und untere Grenzen haben. Mein erster Instinkt besteht darin, einen neuen Datentyp (Klasse) zu erstellen, der das gleiche Verhalten wie eine normale Zahl in Python hat, aber immer innerhalb seiner (dynamischen) Randwerte liegt.

%Vor%

Dies ist ein guter Anfang, aber es erfordert den Zugriff auf das Fleisch dieser BoundInt-Typen wie folgt

%Vor%

Wir können der Klasse eine große Anzahl von Pythons "magic method" -Definitionen hinzufügen, um weitere Funktionen hinzuzufügen:

%Vor%

Jetzt explodiert der Code schnell, und es gibt eine Menge Wiederholung zu dem, was geschrieben wird, für sehr wenig Rückkehr erscheint das überhaupt nicht sehr pythonisch.

Die Dinge werden noch komplizierter, wenn ich anfangen will, BoundInt-Objekte aus normalen Python-Zahlen (Ganzzahlen?) und anderen BoundInt-Objekten

zu konstruieren %Vor%

Was, soweit ich weiß, erfordert die Verwendung von ziemlich großen / hässlichen if / else Typ-Check-Anweisungen innerhalb des BoundInt () - Konstruktors, da Python das Überladen nicht unterstützt (c-Stil).

All das fühlt sich schrecklich an, als würde ich versuchen, C ++ - Code in Python zu schreiben, eine Kardinalsünde, wenn eines meiner Lieblingsbücher, Code Complete 2 , ernst genommen wird. Ich fühle mich, als ob ich gegen den dynamischen Schreibstrom schwimme, anstatt ihn vorwärtstragen zu lassen.

Ich möchte sehr gerne lernen, python 'pythonal-ally' zu codieren, was ist der beste Weg, um diese Art von Problemdomäne zu erreichen? Was sind gute Ressourcen, um richtigen pythischen Stil zu lernen?

    
Sam Coulter 06.11.2012, 00:11
quelle

4 Antworten

4

Es gibt viel Code in der Standardbibliothek, in populären PyPI-Modulen und in ActiveState-Rezepten, die so etwas tun, also sind Sie wahrscheinlich besser dran, Beispiele zu lesen, als zu versuchen, es aus den ersten Prinzipien herauszufinden. Beachten Sie auch, dass dies der Erstellung einer list -like- oder dict -like-Klasse ziemlich ähnlich ist, für die es noch mehr Beispiele gibt.

Allerdings gibt es einige Antworten auf das, was Sie tun möchten. Ich fange mit dem Ernst an und arbeite dann rückwärts.

  

Die Dinge werden noch komplizierter, wenn ich anfangen will, BoundInt-Objekte aus normalen Python-Zahlen (Ganzzahlen?) und anderen BoundInt-Objekten zu konstruieren   ...   Was, soweit mir bekannt ist, erfordert die Verwendung von ziemlich großen / hässlichen if / else Typprüfungen innerhalb des BoundInt () - Konstruktors, da Python das Überladen nicht unterstützt (c-Stil).

Ah, aber denk darüber nach, was du tust: Du konstruierst ein BoundInt von allem, was sich wie eine ganze Zahl verhalten kann, einschließlich, sagen wir mal, ein tatsächliches int oder ein BoundInt , richtig? Also, warum nicht:

%Vor%

Ich gehe davon aus, dass Sie% __int__ natürlich bereits eine BoundInt -Methode hinzugefügt haben (das Äquivalent von C ++ explicit operator int() const ).

Bedenken Sie auch, dass das Überladen nicht so gravierend ist, wie Sie es von C ++ erwarten, da es keinen "Kopierkonstruktor" zum Erstellen von Kopien gibt; Du passierst das Objekt einfach herum, und alles wird unter der Decke aufgehoben.

Stellen Sie sich zum Beispiel diesen C ++ Code vor:

%Vor%

Dies kopiert bar in param , param in local , local in eine unbenannte "Rückgabewert" -Variable und das in baz . Einige davon werden optimiert, und andere (in C ++ 11) verwenden move anstelle von copy, aber trotzdem haben Sie 4 konzeptionelle Aufrufe der Konstruktoren / Zuweisungsoperatoren copy / move.

Sehen Sie sich nun das Python-Äquivalent an:

%Vor%

Hier haben wir nur eine BoundInt Instanz - die explizit erstellte - und wir binden nur neue Namen an sie. Auch wenn baz als Mitglied eines neuen Objekts zugewiesen wird, das den Bereich von bar und baz überlebt, wird keine Kopie erstellt. Das einzige, was eine Kopie erstellt, ist explizit den Aufruf von BoundInt(baz) . (Das stimmt nicht ganz zu 100%, denn jemand kann Ihr Objekt immer überprüfen und versuchen, es von außen zu klonen, und pickle , deepcopy usw. können das tatsächlich tun ... aber in diesem Fall sind sie es rufen Sie immer noch keinen "Kopierkonstruktor" auf, den Sie oder der Compiler geschrieben haben.)

Nun, was ist mit der Weiterleitung all dieser Operatoren an den Wert?

Nun, eine Möglichkeit besteht darin, es dynamisch zu machen. Die Details hängen davon ab, ob Sie in Python 3 oder 2 sind (und für 2, wie weit zurück Sie Unterstützung benötigen). Aber die Idee ist, dass Sie nur eine Liste von Namen haben und für jede eine Methode mit diesem Namen definieren, die die Methode mit dem gleichen Namen für das Wertobjekt aufruft. Wenn Sie eine Skizze davon möchten, geben Sie die zusätzlichen Informationen an und fragen Sie, aber Sie suchen wahrscheinlich nach Beispielen für die dynamische Methodenerstellung.

Also, ist das Pythonic? Nun, es kommt darauf an.

Wenn Sie Dutzende von "Integer-ähnlichen" Klassen erstellen, dann ist es sicherlich besser als Kopieren-Einfügen-Code oder Hinzufügen eines "Kompilierzeit" -Erzeugungsschritts, und es ist wahrscheinlich besser als das Hinzufügen einer ansonsten unnötigen Basis Klasse.

Und wenn Sie versuchen, über viele Versionen von Python hinweg zu arbeiten und sich nicht daran erinnern wollen, "welche Version soll ich nicht mehr __cmp__ liefern, um wieder wie int zu handeln?" Tippe Fragen, ich könnte sogar noch weiter gehen und die Liste der Methoden aus int selbst herausholen (nimm dir(int()) und schreibe ein paar Namen heraus).

Aber wenn Sie nur diese eine Klasse machen, sagen wir nur Python 2.6-2.7 oder nur 3.3+, dann ist das ein Toss-Up.

Eine gute zu lesende Klasse ist die Klasse fractions.Fraction in der Standardbibliothek. Es ist klar geschriebener reiner Python-Code. Und es demonstriert teilweise sowohl die dynamischen als auch die expliziten Mechanismen (weil es jede spezielle Nachricht explizit in Bezug auf generische dynamische Weiterleitungsfunktionen definiert), und wenn Sie sowohl 2.x als auch 3.x haben, können Sie beide vergleichen und kontrastieren.

Inzwischen scheint Ihre Klasse unterspezifiziert zu sein. Wenn x eine BoundInt und y eine int ist, sollte x+y wirklich eine int (wie in Ihrem Code) zurückgeben? Wenn nicht, müssen Sie es binden? Was ist mit y+x ? Was sollte x+=y tun? Und so weiter.

Schließlich lohnt es sich in Python oft, "Value-Klassen" wie diese unveränderlich zu machen, selbst wenn das intuitive C ++ - Äquivalent veränderbar wäre. Betrachten Sie zum Beispiel Folgendes:

%Vor%

Ich denke nicht, dass Sie das erwarten würden. Dies würde in C ++ (für eine typische Wertklasse) nicht passieren, weil j = i eine neue Kopie erstellen würde, aber in Python wird nur ein neuer Name an dieselbe Kopie gebunden. (Dies entspricht BoundInt &j = i , nicht BoundInt j = i .)

Wenn Sie möchten, dass BoundInt unveränderbar ist, müssen Sie neben den offensichtlichen Dingen wie set auch sicherstellen, dass __iadd__ und Freunde nicht implementiert werden.Wenn Sie __iadd__ auslassen, wird i += 2 in i = i.__add__(2) umgewandelt: Mit anderen Worten, es wird eine neue Instanz erstellt und anschließend i an diese neue Instanz gebunden, wobei die alte Instanz allein gelassen wird.

    
abarnert 06.11.2012, 01:05
quelle
2

Es gibt wahrscheinlich viele Meinungen dazu. Aber im Hinblick auf die Verbreitung von speziellen Methoden, müssen Sie nur tun, um es zu vervollständigen. Aber zumindest tust du das nur einmal, an einem Ort. Auch die eingebauten Nummerntypen können unterklassifiziert werden. Das habe ich für eine ähnliche Implementierung getan, die Sie so aussehen lassen können .

    
Keith 06.11.2012 01:10
quelle
1

Ihre set Methode ist ein Greuel. Sie erstellen nicht eine Zahl mit einem Standardwert von Null und ändern dann die Zahl in eine andere Zahl. Das versucht sehr viel C ++ in Python zu programmieren und wird Ihnen endlose Mengen an Kopfschmerzen bereiten, wenn Sie diese genauso behandeln wollen, wie Sie Zahlen machen, denn jedes Mal, wenn Sie sie an Funktionen übergeben, werden sie durch Referenz <übergeben / em> (wie alles in Python). Sie werden also große Mengen von Aliasing in Dingen finden, von denen Sie denken, dass Sie sie wie Zahlen behandeln können, und Sie werden mit Sicherheit auf Fehler stoßen, wenn Sie den Wert von Zahlen mutieren, die Sie nicht erkennen oder erwarten ein Wert, der in einem Verzeichnis mit einem BoundInt als Schlüssel gespeichert wird, indem ein weiterer BoundInt mit demselben Wert bereitgestellt wird.

Für mich sind high und low keine Datenwerte, die einem bestimmten BoundInt -Wert zugeordnet sind, sondern -Typ-Parameter . Ich möchte eine Zahl 7 im Typ BoundInt(1, 10) , nicht eine Zahl 7 , die auf 1 bis 10 beschränkt ist, was alles ein Wert vom Typ BoundInt ist.

Wenn ich wirklich so etwas machen wollte, würde ich die Unterklasse int verwenden, um BoundInt als Klassenfabrik zu behandeln; du gibst ihm einen Bereich, und es gibt dir die Art von ganzen Zahlen, die auf diesen Bereich beschränkt sind. Sie können diesen Typ auf jedes "int-like" -Objekt anwenden und erhalten einen Wert, der an diesen Bereich gebunden ist. Etwas wie:

%Vor%

(Der Cache dient lediglich dazu, sicherzustellen, dass zwei verschiedene Versuche, den BoundInt -Typ für die gleichen niedrigen / hohen Werte zu erhalten, genau die selbe Klasse ergeben, nicht zwei verschiedene Klassen, die sich auf die gleiche Weise verhalten in der Praxis die meiste Zeit, aber es scheint schöner.)

Sie würden das verwenden wie:

%Vor%

Der Ansatz der "Klassenfabrik" bedeutet, dass Sie die Klassen für diese Bereiche global (mit aussagekräftigen Namen) erstellen und dann genau so verwenden können, wenn Sie eine kleine Anzahl sinnvoller Bereiche haben, an die Sie Ihre Ganzzahlen binden möchten regelmäßige Klassen.

Die Unterklasse int macht diese Objekte unveränderlich (weshalb die Initialisierung in __new__ durchgeführt werden musste), was Sie von Aliasing-Bugs befreit (von denen die Leute sich beim Programmieren keine Sorgen machen müssen) mit einfachen Werttypen wie Zahlen und aus gutem Grund). Es gibt Ihnen auch all die Integer-Methoden kostenlos, und so verhalten sich diese BoundInt -Typen genau als int , außer dass beim Erstellen einer der Wert durch geklammert wird der Typ. Leider bedeutet dies, dass alle Operationen für diese Typen int -Objekte und nicht BoundInt -Objekte zurückgeben.

Wenn Sie einen Weg finden könnten, die niedrigen / hohen Werte für die zwei verschiedenen Werte, die z. x + y , dann können Sie die speziellen Methoden überschreiben, damit sie BoundInt -Werte zurückgeben. Die Ansätze, die mir in den Sinn kommen, sind:

  1. Nimm die Grenzen des linken Operanden und ignoriere den rechten Operanden (wirkt chaotisch und asymmetrisch; verletzt die Annahme, dass x + y = y + x )
  2. Nimm den maximalen low Wert und den minimalen high Wert. Es ist schön symmetrisch, und Sie können numerische Werte behandeln, die keine low und high Werte haben, als wären sie sys.minint und sys.maxint (d. H. Verwenden Sie einfach die Grenzen vom anderen Wert). Es macht keinen großen Sinn, wenn sich die Bereiche überhaupt nicht überschneiden, weil Sie eine leere Reihe haben werden, aber zusammen mit solchen Zahlen zu arbeiten macht wahrscheinlich sowieso keinen Sinn. li>
  3. Nimm den minimalen low Wert und den maximalen high Wert. Auch symmetrisch, aber hier möchten Sie wahrscheinlich normale Zahlen explizit ignorieren, anstatt vorzugeben, dass sie BoundInt -Werte sind, die über den ganzen Integerbereich reichen können.

Irgendeines der oben genannten könnte funktionieren, und irgendetwas von dem obigen wird Sie wahrscheinlich an einem gewissen Punkt überraschen (z. B. das Negieren einer Zahl, die auf einen positiven Bereich beschränkt ist, wird Ihnen immer die kleinste positive Zahl in dem Bereich geben, was merkwürdig erscheint ich).

Wenn Sie diesen Ansatz verwenden, möchten Sie wahrscheinlich nicht die Unterklasse int ableiten. Wenn Sie normalInt + boundedInt haben, würde normalInt die Addition übernehmen, ohne Ihren Code zu beachten. Stattdessen möchten Sie, dass boundedInt nicht als int -Wert erkannt wird, so dass int s __add__ nicht funktioniert und Ihrer Klasse eine Chance gibt, __radd__ auszuprobieren. Aber ich würde immer noch deine Klasse als "unveränderlich" behandeln und jede Operation, die mit einer neuen Nummer kommt, dazu bringen, ein neues Objekt zu konstruieren; Das mutieren von Zahlen ist praktisch garantiert, um irgendwann Fehler zu verursachen.

Also würde ich diesen Ansatz so behandeln:

%Vor%

Sieht immer noch nach mehr Code aus, als es sein sollte, aber das, was Sie versuchen, ist komplizierter als Sie denken.

    
Ben 06.11.2012 02:54
quelle
0

Der Typ, der sich in allen Situationen genau wie Zahlen verhält viele spezielle Methoden wegen der reichen Syntaxunterstützung in Python (es scheint nein andere Arten erfordern so viele Methoden, z. B. ist es viel einfacher zu definieren Typen, die sich wie eine Liste verhalten, dict in Python: ein paar Methoden und Sie haben eine Sequenz ). Es gibt einige Möglichkeiten, den Code weniger wiederholend zu machen.

ABC-Klassen wie numbers.Integral Bereitstellen von Standardimplementierungen für einige Methoden, z. B. wenn __add__ , __radd__ werden in einer Unterklasse implementiert, dann __sub__ , __rsub__ sind automatisch verfügbar.

fractions.Fraction Verwendet _operator_fallbacks Definieren von __r*__ und Bereitstellen von Fallback-Operatoren für mit anderen numerischen Typen umgehen:

%Vor%

Python erlaubt es, eine Klasse dynamisch in einer Factory zu erzeugen / ändern Funktion / Metaklasse z.B. Kann jemand diesen Python-Code zusammenfassen? . Sogar exec könnte in (sehr) seltenen Fällen verwendet werden, z. namedtuple() .

Zahlen sind in Python unveränderlich, also sollten Sie __new__ anstelle von __init__ verwenden.

Seltene Fälle, die nicht von __new__ abgedeckt sind, könnten in definiert werden from_sometype(cls, d: sometype) -> your_type Klassenmethoden. Und in Umgekehrt könnten Fälle verwendet werden, die nicht durch spezielle Methoden abgedeckt sind as_sometype(self) -> sometype Methoden.

Eine einfachere Lösung in Ihrem Fall könnte die Definition eines übergeordneten Typs sein spezifisch für Ihre Anwendungsdomäne. Zahlen Abstraktion könnte auch sein niedrige Ebene z.B. decimal.Decimal ist mehr als 6 KLOC.

    
jfs 06.11.2012 10:54
quelle

Tags und Links