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?
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:
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.
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 .
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:
(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:
x + y
= y + x
) 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>
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.
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:
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.