tl; dr
github.com/QratorLabs/fastenumpip install fast-enum
Warum Aufzählung benötigt wird
(Wenn Sie alles wissen, lesen Sie den Abschnitt "Aufzählungen in der Standardbibliothek".)
Stellen Sie sich vor, Sie müssen eine Menge aller möglichen Entitätszustände in Ihrem eigenen Datenbankmodell beschreiben. Höchstwahrscheinlich werden Sie eine Reihe von Konstanten verwenden, die direkt im Modulnamensraum definiert sind:
... oder als statische Klassenattribute:
class MyModelStates: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4
Dieser Ansatz hilft dabei, auf diese Zustände durch mnemonische Namen zu verweisen, während es sich in Ihrem Repository um gewöhnliche Ganzzahlen handelt. Auf diese Weise werden Sie gleichzeitig die magischen Zahlen los, die in verschiedenen Teilen des Codes verstreut sind, und machen ihn gleichzeitig lesbarer und informativer.
Sowohl die Modulkonstante als auch die Klasse mit statischen Attributen leiden unter der internen Natur von Python-Objekten: Sie sind alle veränderlich (veränderlich). Sie können Ihrer Konstante zur Laufzeit versehentlich einen Wert zuweisen, und das Debuggen und Zurücksetzen defekter Objekte ist ein separates Abenteuer. Möglicherweise möchten Sie das Konstantenbündel in dem Sinne ändern, dass sich die Anzahl der deklarierten Konstanten und ihre Werte, denen sie zugeordnet sind, während der Programmausführung nicht ändern.
Zu diesem
namedtuple()
können Sie versuchen, sie mit
namedtuple()
in benannte Tupel zu
namedtuple()
, wie im Beispiel:
MyModelStates = namedtuple('MyModelStates', ('INITIAL', 'PROCESSING', 'PROCESSED', 'DECLINED', 'RETURNED')) EntityStates = MyModelStates(0, 1, 2, 3, 4)
Dies sieht jedoch nicht sehr ordentlich und lesbar aus, und
namedtuple
Objekte sind wiederum nicht sehr erweiterbar. Angenommen, Sie haben eine Benutzeroberfläche, die alle diese Zustände anzeigt. Sie können Ihre Konstanten in Modulen, einer Klasse mit Attributen oder benannten Tupeln verwenden, um sie zu rendern (die letzten beiden sind einfacher zu rendern, da wir darüber sprechen). Mit einem solchen Code kann der Benutzer jedoch nicht für jeden von Ihnen definierten Zustand eine angemessene Beschreibung erhalten. Wenn Sie vorhaben, Mehrsprachigkeit und i18n-Unterstützung in Ihrer Benutzeroberfläche zu implementieren, werden Sie feststellen, wie schnell das Ausfüllen aller Übersetzungen für diese Beschreibungen zu einer unglaublich mühsamen Aufgabe wird. Übereinstimmende
INITIAL
bedeuten nicht unbedingt, dass die Beschreibung übereinstimmt. Sie können also nicht alle
INITIAL
auf dieselbe Beschreibung in
gettext
INITIAL
. Stattdessen hat Ihre Konstante die folgende Form:
INITIAL = (0, 'My_MODEL_INITIAL_STATE')
Oder deine Klasse wird so:
class MyModelStates: INITIAL = (0, 'MY_MODEL_INITIAL_STATE')
Schließlich verwandelt sich das genannte Tupel in:
EntityStates = MyModelStates((0, 'MY_MODEL_INITIAL_STATE'), ...)
Schon gar nicht schlecht - jetzt wird garantiert, dass sowohl der Statuswert als auch der Übersetzungsstub in den von der Benutzeroberfläche unterstützten Sprachen angezeigt werden. Möglicherweise stellen Sie jedoch fest, dass der Code, der diese Zuordnungen verwendet, zu einem Durcheinander geworden ist. Bei jedem Versuch, einen Entitätswert zuzuweisen, müssen Sie den Wert mit dem Index 0 aus der von Ihnen verwendeten Anzeige extrahieren:
my_entity.state = INITIAL[0]
oder
my_entity.state = MyModelStates.INITIAL[0]
oder
my_entity.state = EntityStates.INITIAL[0]
Usw. Denken Sie daran, dass die ersten beiden Ansätze, bei denen Konstanten bzw. Klassenattribute verwendet werden, an der Wandlungsfähigkeit leiden.
Und die Transfers kommen uns zu Hilfe
class MyEntityStates(Enum): def __init__(self, val, description): self.val = val self.description = description INITIAL = (0, 'MY_MODEL_INITIAL_STATE') PROCESSING = (1, 'MY_MODEL_BEING_PROCESSED_STATE') PROCESSED = (2, 'MY_MODEL_PROCESSED_STATE') DECLINED = (3, 'MY_MODEL_DECLINED_STATE') RETURNED = (4, 'MY_MODEL_RETURNED_STATE')
Das ist alles. Jetzt können Sie die Auflistung in Ihrem Render einfach durchlaufen (Jinja2-Syntax):
{% for state in MyEntityState %} <option value=”{{ state.val }}”>{{ _(state.description) }}</option> {% endfor %}
Eine Aufzählung ist sowohl für eine Reihe von Elementen unveränderlich - Sie können zur Laufzeit kein neues Mitglied einer Aufzählung definieren und kein bereits definiertes Mitglied löschen, als auch für die Werte von Elementen, die darin gespeichert sind, können Sie keine Attributwerte [neu] zuweisen oder ein Attribut löschen.
In Ihrem Code weisen Sie Ihren Entitäten einfach Werte zu:
my_entity.state = MyEntityStates.INITIAL.val
Alles ist klar genug, informativ und erweiterbar. Dafür verwenden wir Aufzählungen.
Wie könnten wir es schneller machen?
Die Aufzählung aus der Standardbibliothek ist ziemlich langsam, daher haben wir uns gefragt: Können wir sie beschleunigen? Wie sich herausstellte, können wir nämlich die Umsetzung unserer Aufzählung:
- Dreimal schneller beim Zugriff auf die Mitgliederzählung;
- ~ 8.5 schneller beim Zugriff auf das Attribut (
name
, value
) eines Mitglieds; - 3-mal schneller beim Zugriff auf ein Member nach Wert (Konstruktor der Enumeration
MyEnum(value))
; - 1,5-mal schneller beim Zugriff auf ein Mitglied über den Namen (wie im Wörterbuch
MyEnum[name]
).
Typen und Objekte in Python sind dynamisch. Es gibt jedoch Werkzeuge, um die Dynamik von Objekten zu begrenzen. Mit
__slots__
können Sie eine deutliche Leistungssteigerung
__slots__
. Es besteht auch die Möglichkeit von Geschwindigkeitsgewinnen, wenn Sie die Verwendung von Datendeskriptoren nach Möglichkeit vermeiden. Sie müssen jedoch die Möglichkeit einer signifikanten Erhöhung der Anwendungskomplexität in Betracht ziehen.
Spielautomaten
Beispielsweise können Sie eine Klassendeklaration mit
__slots__
In diesem Fall verfügen alle Instanzen von Klassen nur über einen begrenzten Satz von Eigenschaften, die in
__slots__
und allen
__slots__
übergeordneten Klassen
__slots__
sind.
Deskriptoren
Standardmäßig gibt der Python-Interpreter den Wert des Attributs des Objekts direkt zurück (gleichzeitig legen wir fest, dass der Wert in diesem Fall auch ein Python-Objekt ist und beispielsweise in Bezug auf die C-Sprache kein langes Vorzeichen hat):
value = my_obj.attribute # , .
Wenn der Attributwert nach dem Python-Datenmodell ein Objekt ist, das das Deskriptorprotokoll implementiert, findet der Interpreter beim Versuch, den Wert dieses Attributs
__get__
zunächst einen Verweis auf das Objekt, auf das sich die Eigenschaft bezieht, und ruft dann die spezielle Methode
__get__
auf, die an unser ursprüngliches Objekt als übergeben wird Argument:
obj_attribute = my_obj.attribute obj_attribute_value = obj_attribute.__get__(my_obj)
Aufzählungen in der Standardbibliothek
Mindestens die
name
und Werteigenschaften der Mitglieder der Standard-Enumerationsimplementierung werden als
types.DynamicClassAttribute
. Dies bedeutet, dass beim Versuch, die Werte für
name
und
value
, Folgendes passiert:
one_value = StdEnum.ONE.value
Somit kann die gesamte Abfolge von Aufrufen durch den folgenden Pseudocode dargestellt werden:
def get_func(enum_member, attrname):
Wir haben ein einfaches Skript geschrieben, das die oben beschriebene Ausgabe demonstriert:
from enum import Enum class StdEnum(Enum): def __init__(self, value, description): self.v = value self.description = description A = 1, 'One' B = 2, 'Two' def get_name(): return StdEnum.A.name from pycallgraph import PyCallGraph from pycallgraph.output import GraphvizOutput graphviz = GraphvizOutput(output_file='stdenum.png') with PyCallGraph(output=graphviz): v = get_name()
Und nach der Ausführung hat uns das Skript folgendes Bild gegeben:

Dies zeigt, dass jedes Mal, wenn Sie auf die
name
und Wertattribute von Enumerationsmitgliedern aus der Standardbibliothek zugreifen, ein Handle aufgerufen wird. Dieser Deskriptor endet wiederum mit einem Aufruf der Klasse
Enum
aus der Standardbibliothek der Methode
def name(self)
, die mit dem Deskriptor verziert ist.
Vergleichen Sie mit unserem FastEnum:
from fast_enum import FastEnum class MyNewEnum(metaclass=FastEnum): A = 1 B = 2 def get_name(): return MyNewEnum.A.name from pycallgraph import PyCallGraph from pycallgraph.output import GraphvizOutput graphviz = GraphvizOutput(output_file='fastenum.png') with PyCallGraph(output=graphviz): v = get_name()
Was ist auf dem folgenden Bild zu sehen:

All dies geschieht tatsächlich in der Standard-Enumerationsimplementierung, wenn Sie auf die
name
und Werteigenschaften ihrer Mitglieder zugreifen. Dies ist auch der Grund, warum unsere Implementierung schneller ist.
Das Implementieren von Aufzählungen in der Python-Standardbibliothek erfordert viele Aufrufe von Objekten, die das Datendeskriptorprotokoll implementieren. Als wir versuchten, die Standard-Enumerationsimplementierung in unseren Projekten zu verwenden, stellten wir sofort fest, wie viele Datendeskriptoren für
name
und
value
aufgerufen wurden.
Und da die Aufzählungen im gesamten Code ziemlich häufig verwendet wurden, war die resultierende Leistung gering.
Darüber hinaus enthält die Standardklasse "Enum" mehrere zusätzliche "geschützte" Attribute:
_member_names_
- eine Liste mit allen Namen der Mitglieder der Aufzählung;_member_map_
- OrderedDict
, das den Namen eines Aufzählungsmitglieds seinem Wert OrderedDict
;_value2member_map_
- ein Wörterbuch, in dem der Abgleich in die entgegengesetzte Richtung erfolgt: die Werte der Mitglieder der Aufzählung zu den entsprechenden Mitgliedern der Aufzählung.
Die Wörterbuchsuche ist langsam, da jeder Aufruf zur Berechnung der Hash-Funktion führt (es sei denn, das Ergebnis wird separat zwischengespeichert, was für nicht verwalteten Code nicht immer möglich ist) und die Suche in der Hash-Tabelle, wodurch diese Wörterbücher keine optimale Grundlage für Aufzählungen darstellen. Auch die Suche nach Enumerationsmitgliedern (wie in
StdEnum.MEMBER
) selbst ist eine Wörterbuchsuche.
Unser Ansatz
Wir haben unsere Implementierung von Aufzählungen mit Blick auf elegante Aufzählungen in C und schöne erweiterbare Aufzählungen in Java erstellt. Die Hauptfunktionen, die wir zu Hause implementieren wollten, waren wie folgt:
- Aufzählung sollte so statisch wie möglich sein; "Statisch" bedeutet hier Folgendes - wenn etwas nur einmal und während der Ansage berechnet werden kann, sollte es zu diesem (und nur zu diesem) Zeitpunkt berechnet werden;
- Es ist unmöglich, von einer Aufzählung zu erben (es muss eine "letzte" Klasse sein), wenn die erbende Klasse neue Mitglieder der Aufzählung definiert. Dies gilt für die Implementierung in der Standardbibliothek, mit der Ausnahme, dass die Vererbung dort verboten ist, selbst wenn die erbende Klasse keine neuen Mitglieder definiert.
- Die Aufzählung sollte ausreichend Erweiterungsmöglichkeiten bieten (zusätzliche Attribute, Methoden usw.).
In diesem Fall wird nur die Wörterbuchsuche verwendet. Dies ist die umgekehrte Zuordnung des
value
zu einem Aufzählungselement. Alle anderen Berechnungen werden nur einmal während der Klassendeklaration ausgeführt (wobei Metaklassen zum Konfigurieren der Typerstellung verwendet werden).
Im Gegensatz zur Standardbibliothek verarbeiten wir in der Klassendeklaration nur den ersten Wert nach dem Zeichen
=
als Member-Wert:
A = 1, 'One'
in der Standardbibliothek, das gesamte Tupel
1, "One"
als
value
;
A: 'MyEnum' = 1, 'One'
in unserer Implementierung wird nur
1
als
A: 'MyEnum' = 1, 'One'
betrachtet.
Eine weitere Beschleunigung wird durch die Verwendung von
__slots__
erreicht,
__slots__
möglich ist. In Python-Klassen, die mit
__slots__
deklariert
__slots__
, wird das Attribut
__dict__
nicht für die
__dict__
, die die Zuordnung von Attributnamen zu ihren Werten enthalten (daher können Sie keine Eigenschaft der Instanz deklarieren, die nicht in
__slots__
). Außerdem wird mit einem konstanten Offset im Objektinstanzzeiger auf die Werte der in
__slots__
definierten
__slots__
zugegriffen. Dies ist ein Hochgeschwindigkeitszugriff auf Eigenschaften, da Hash-Berechnungen und Hash-Tabellenscans vermieden werden.
Was sind die zusätzlichen Chips?
FastEnum ist mit keiner Version von Python vor 3.6 kompatibel, da es universell Typanmerkungen verwendet, die in Python 3.6 implementiert sind. Es kann davon ausgegangen werden, dass die Installation des
typing
von PyPi hilfreich ist. Die kurze Antwort lautet nein. Die Implementierung verwendet PEP-484 für die Argumente einiger Funktionen, Methoden und Zeiger auf den Rückgabetyp, sodass eine Version vor Python 3.5 aufgrund von Syntaxinkompatibilität nicht unterstützt wird. Die erste Codezeile in der
__new__
Metaklasse verwendet jedoch die PEP-526-Syntax, um den Variablentyp anzugeben. Python 3.5 wird also auch nicht funktionieren. Sie können die Implementierung auf ältere Versionen portieren, obwohl wir von Qrator Labs, wenn immer möglich, Typanmerkungen verwenden, da dies bei der Entwicklung komplexer Projekte sehr hilfreich ist. Na ja, am Ende! Sie möchten vor Version 3.6 nicht mehr in Python stecken bleiben, da in neueren Versionen keine Inkompatibilität mit Ihrem vorhandenen Code besteht (vorausgesetzt, Sie verwenden nicht Python 2) und bei der Implementierung von
asyncio
Vergleich zu
asyncio
3.5 viel Arbeit geleistet wurde Unserer Ansicht nach ein sofortiges Update wert.
Dies macht im Gegensatz zur Standardbibliothek den speziellen Import von
auto
überflüssig. Sie geben einfach an, dass das Mitglied der Aufzählung eine Instanz dieser Aufzählung ist, ohne überhaupt einen Wert anzugeben - und der Wert wird automatisch für Sie generiert. Obwohl Python 3.6 für die Arbeit mit FastEnum ausreicht, wurde die Beibehaltung der Schlüsselreihenfolge in Wörterbüchern nur in Python 3.7 eingeführt (und wir haben
OrderedDict
Fall 3.6 nicht separat verwendet). Wir kennen keine Beispiele, bei denen die automatisch generierte Reihenfolge der Werte wichtig ist, da wir davon ausgehen, dass der Wert selbst für die Umgebung nicht so wichtig ist, wenn der Entwickler die Umgebung mit der Aufgabe versehen hat, einen Wert zu generieren und einem Aufzählungselement zuzuweisen. Wenn Sie jedoch noch nicht zu Python 3.7 gewechselt sind, haben wir Sie gewarnt.
Diejenigen, deren Aufzählungen bei 0 (Null) anstelle des Standardwerts (1) beginnen müssen, können dies mit einem speziellen Attribut tun, wenn sie die Aufzählung
_ZERO_VALUED
deklarieren, die nicht in der resultierenden Klasse gespeichert wird.
Es gibt jedoch einige Einschränkungen: Alle Namen der Mitglieder der Aufzählung müssen in GROSSBUCHSTABEN geschrieben sein, da sie sonst von der Metaklasse nicht verarbeitet werden.
Schließlich können Sie eine Basisklasse für Ihre Aufzählungen deklarieren (bedenken Sie, dass die Basisklasse die Metaklasse selbst verwenden kann, sodass Sie die Metaklasse nicht für alle Unterklassen bereitstellen müssen). Definieren Sie lediglich die allgemeine Logik (Attribute und Methoden) in dieser Klasse und nicht die Mitglieder der Aufzählung (damit die Klasse nicht "finalisiert" wird). Nachdem Sie so viele erbende Klassen dieser Klasse deklarieren können, wie Sie möchten, haben die Erben selbst dieselbe Logik.
Aliase und wie sie helfen können
Angenommen, Sie haben Code mit:
package_a.some_lib_enum.MyEnum
Und dass die MyEnum-Klasse wie folgt deklariert ist:
class MyEnum(metaclass=FastEnum): ONE: 'MyEnum' TWO: 'MyEnum'
Jetzt haben Sie sich entschieden, ein Refactoring durchzuführen und die Auflistung auf ein anderes Paket zu übertragen. Sie erstellen so etwas:
package_b.some_lib_enum.MyMovedEnum
Wo MyMovedEnum wie folgt deklariert ist:
class MyMovedEnum(MyEnum): pass
Jetzt sind Sie bereit für die Phase, in der die Übertragung an der alten Adresse als veraltet betrachtet wird. Sie schreiben die Importe und Aufrufe dieser Enumeration neu, sodass jetzt der neue Name dieser Enumeration (ihr Alias) verwendet wird. Sie können sicher sein, dass alle Mitglieder dieser Alias-Enumeration tatsächlich in der Klasse mit dem alten Namen deklariert werden. In Ihrer Projektdokumentation erklären Sie, dass
MyEnum
veraltet ist und zukünftig aus dem Code entfernt wird. Zum Beispiel in der nächsten Version. Angenommen, Ihr Code speichert Ihre Objekte mit Attributen, die Aufzählungselemente enthalten, mithilfe von
pickle
. Zu diesem Zeitpunkt verwenden Sie
MyMovedEnum
in Ihrem Code, aber intern sind alle Enumerationsmitglieder weiterhin Instanzen von
MyEnum
. Im nächsten Schritt tauschen Sie die Deklarationen von
MyEnum
und
MyMovedEnum
sodass
MyMovedEnum
keine Unterklasse von
MyEnum
und alle ihre Mitglieder selbst
MyEnum
. Andererseits deklariert
MyEnum
jetzt keine Mitglieder mehr, sondern wird nur noch ein Alias (eine Unterklasse) von
MyMovedEnum
.
Das ist alles. Wenn Sie Ihre Anwendungen im
unpickle
Stadium neu starten
unpickle
alle Mitglieder der Enumeration als Instanzen von
MyMovedEnum
und dieser neuen Klasse zugeordnet. In dem Moment, in dem Sie sicher sind, dass alle Ihre Objekte, die beispielsweise in der Datenbank gespeichert sind, erneut deserialisiert (und möglicherweise erneut serialisiert und im Repository gespeichert) wurden, können Sie eine neue Version freigeben, in der sie zuvor als veraltete Klasse markiert war
MyEnum
möglicherweise für unnötiger erklärt und aus der Codebasis entfernt.
Probieren Sie es aus:
github.com/QratorLabs/fastenum ,
pypi.org/project/fastenum .
Karma-Profis gehen an den Autor FastEnum -
santjagocorkez .
UPD: In Version 1.3.0 wurde es möglich, von vorhandenen Klassen zu erben, z. B.
int
,
float
,
str
. Mitglieder solcher Aufzählungen bestehen den Test auf Gleichheit erfolgreich mit einem sauberen Objekt mit demselben Wert (
IntEnum.MEMBER == int(value_given_to_member)
) und natürlich mit Instanzen dieser geerbten Klassen. Dies wiederum ermöglicht es dem von
int
geerbten Mitglied der
sys.exit()
, ein direktes Argument für
sys.exit()
als Rückgabecode des Python-Interpreters zu sein.