tl; dr
github.com/QratorLabs/fastenumpip install fast-enum
Was sind Aufzählungen?
(Wenn Sie glauben, dass Sie das wissen - scrollen Sie nach unten zum Abschnitt „Aufzählungen in der Standardbibliothek“).
Stellen Sie sich vor, Sie müssen einen Satz aller möglichen Zustände für die Entitäten in Ihrem Datenbankmodell beschreiben. Sie werden wahrscheinlich eine Reihe von Konstanten verwenden, die als Attribute auf Modulebene definiert sind:
... oder als Attribute auf Klassenebene, die in ihrer eigenen Klasse definiert sind:
class MyModelStates: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4
Auf diese Weise können Sie auf diese Zustände mit ihren mnemonischen Namen verweisen, während sie als einfache Ganzzahlen in Ihrem Speicher verbleiben. Auf diese Weise werden Sie die in Ihrem Code verstreuten magischen Zahlen los und machen ihn lesbarer und selbsterklärender.
Sowohl die Konstante auf Modulebene als auch die Klasse mit den statischen Attributen leiden unter der inhärenten Natur von Python-Objekten: Sie sind alle veränderlich. Möglicherweise weisen Sie Ihrer Konstante zur Laufzeit versehentlich einen Wert zu, und das ist ein Chaos beim Debuggen und Rollback Ihrer defekten Entitäten. Vielleicht möchten Sie Ihre Konstantensätze unveränderlich machen, dh, sowohl die Anzahl der deklarierten Konstanten als auch die Werte, denen sie zugeordnet sind, dürfen zur Laufzeit nicht geändert werden.
Zu diesem Zweck können Sie versuchen, sie mit
namedtuple()
in benannte Tupel zu organisieren. Beispiel:
MyModelStates = namedtuple('MyModelStates', ('INITIAL', 'PROCESSING', 'PROCESSED', 'DECLINED', 'RETURNED')) EntityStates = MyModelStates(0, 1, 2, 3, 4)
Dies sieht jedoch immer noch nicht allzu verständlich aus: Außerdem sind
namedtuple
Objekte nicht wirklich erweiterbar. Angenommen, Sie haben eine Benutzeroberfläche, die alle diese Zustände anzeigt. Sie können dann Ihre modulbasierten Konstanten, Ihre Klasse mit den Attributen oder benannte Tupel verwenden, um sie zu rendern (die letzten beiden sind einfacher zu rendern, wenn wir gerade dabei sind). Ihr Code bietet jedoch keine Möglichkeit, dem Benutzer eine angemessene Beschreibung für jeden von Ihnen definierten Status zu geben. Wenn Sie mehrsprachige Unterstützung und i18n in Ihrer Benutzeroberfläche implementieren möchten, werden Sie feststellen, dass das Ausfüllen aller Übersetzungen für diese Beschreibungen zu einer unglaublich mühsamen Aufgabe wird. Die übereinstimmenden
INITIAL
müssen nicht unbedingt übereinstimmende Beschreibungen haben, was bedeutet, dass Sie nicht alle
INITIAL
auf dieselbe Beschreibung in
gettext
INITIAL
. Stattdessen wird Ihre Konstante wie folgt:
INITIAL = (0, 'My_MODEL_INITIAL_STATE')
Ihre Klasse wird dann zu:
class MyModelStates: INITIAL = (0, 'MY_MODEL_INITIAL_STATE')
Und schließlich wird Ihr
namedtuple
:
EntityStates = MyModelStates((0, 'MY_MODEL_INITIAL_STATE'), ...)
Gut genug, jetzt wird sichergestellt, dass sowohl der Statuswert als auch der Übersetzungsstub den von Ihrer Benutzeroberfläche unterstützten Sprachen zugeordnet sind. Aber jetzt werden Sie vielleicht bemerken, dass der Code, der diese Zuordnungen verwendet, sich in ein Chaos verwandelt hat. Wenn Sie versuchen, Ihrer Entität einen Wert zuzuweisen, müssen Sie auch nicht vergessen, den Wert am Index 0 aus der verwendeten Zuordnung zu extrahieren:
my_entity.state = INITIAL[0]
oder
my_entity.state = MyModelStates.INITIAL[0]
oder
my_entity.state = EntityStates.INITIAL[0]
Und so weiter. Beachten Sie, dass die ersten beiden Ansätze, die Konstanten bzw. Klassenattribute verwenden, immer noch veränderlich sind.
Und dann kommen Enums auf die Bühne
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 war's Jetzt können Sie die Aufzählung in Ihrem Renderer leicht wiederholen (Jinja2-Syntax):
{% for state in MyEntityState %} <option value=”{{ state.val }}”>{{ _(state.description) }}</option> {% endfor %}
Enum ist sowohl für die Elementmenge (Sie können zur Laufzeit kein neues Element definieren, noch ein bereits definiertes Element löschen) als auch für die von ihnen beibehaltenen Elementwerte (Sie können keine Attributwerte neu zuweisen oder ein Attribut löschen) unveränderlich.
In Ihrem Code weisen Sie Ihren Entitäten Werte wie folgt zu:
my_entity.state = MyEntityStates.INITIAL.val
Klar genug. Selbstbeschreibend. Ziemlich erweiterbar. Dafür verwenden wir Enums.
Warum ist es schneller?
Da die Standardeinstellung von ENUM jedoch ziemlich langsam ist, haben wir uns gefragt, ob wir sie schneller machen können.
Wie sich herausstellt, können wir. Es ist nämlich möglich, es zu machen:
- 3-mal schneller auf Mitgliederzugang
- ~ 8,5-mal schneller beim Zugriff auf Attribute (
name
, value
) - 3-mal schnellerer Zugriff auf Enum nach Wert (Aufruf der Klasse
MyEnum(value)
von Enum) - 1,5-mal schneller bei Enum-Zugriff nach Namen (dict-like
MyEnum[name]
)
Typen und Objekte sind in Python dynamisch. Aber Python hat die Werkzeuge, um die Dynamik der Objekte einzuschränken. Mit ihrer Hilfe kann man durch die Verwendung von
__slots__
einen erheblichen Leistungszuwachs
__slots__
und die Verwendung von Data Descriptors vermeiden, wenn dies ohne erhebliches Komplexitätswachstum möglich ist oder wenn Sie die Geschwindigkeit verbessern können.
Spielautomaten
Beispielsweise könnte man eine Klassendeklaration mit
__slots__
In diesem Fall hätten Klasseninstanzen nur einen eingeschränkten Satz von Attributen: Attribute, die in
__slots__
deklariert
__slots__
und alle
__slots__
der übergeordneten Klassen.
Deskriptoren
Standardmäßig gibt der Python-Interpreter einen Attributwert eines Objekts direkt zurück:
value = my_obj.attribute
Wenn der Attributwert eines Objekts nach dem Python-Datenmodell selbst ein Objekt ist, das das Data Descriptor Protocol implementiert, bedeutet dies, dass Sie beim Versuch, diesen Wert
__get__
zuerst das Attribut als Objekt
__get__
und dann eine spezielle Methode
__get__
Aufgerufen auf das Attribut-Objekt, wobei das Keeper-Objekt selbst als Argument übergeben wird:
obj_attribute = my_obj.attribute obj_attribute_value = obj_attribute.__get__(my_obj)
Aufzählungen in der Standardbibliothek
Mindestens die Attribute
name
und
value
der Standardimplementierung von Enum werden als
types.DynamicClassAttribute
. Das bedeutet, dass beim Versuch, den
name
(oder den
value
) eines Mitglieds
value
der Ablauf wie folgt aussieht:
one_value = StdEnum.ONE.value
Der gesamte Ablauf könnte also als folgender Pseudocode dargestellt werden:
def get_func(enum_member, attrname):
Wir haben ein einfaches Skript erstellt, das die obige Schlussfolgerung 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 nachdem wir das Skript ausgeführt haben, hat es dieses Bild für uns erstellt:

Dies zeigt, dass bei jedem Zugriff auf die Attribute
name
und
value
stdlib enum ein Deskriptor aufgerufen wird. Dieser Deskriptor endet wiederum mit einem Aufruf der mit dem Deskriptor dekorierten Eigenschaft
def name(self)
von stdlib enum.
Nun, Sie können dies mit unserem FastEnum vergleichen:
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 gibt dieses Bild aus:

Dies geschieht in der Standard-Enum-Implementierung jedes Mal, wenn Sie auf die
name
und Wertattribute Ihrer Enum-Mitglieder zugreifen. Deshalb ist unsere Implementierung schneller.
Die Implementierung der Enum-Klasse in der Python Standard Library verwendet Tonnen von Deskriptorprotokollaufrufen. Als wir versuchten, Standardenzahlen in unseren Projekten zu verwenden, haben wir festgestellt, wie viele Deskriptorprotokollaufrufe für
name
und
Enum
der
Enum
Mitglieder aufgerufen wurden. Und weil Aufzählungen im gesamten Code übermäßig verwendet wurden, war die resultierende Leistung schlecht.
Darüber hinaus enthält die Standard-Enum-Klasse einige helfergeschützte Attribute:
_member_names_
- eine Liste, die alle Namen von Enum-Mitgliedern enthält;_member_map_
- ein OrderedDict, das dem Mitglied selbst einen Namen eines Aufzählungsmitglieds zuordnet;_value2member_map_
- Ein umgekehrtes Wörterbuch, das die Werte der Aufzählungsmitglieder den entsprechenden Aufzählungsmitgliedern _value2member_map_
.
Wörterbuchsuchen sind langsam, da jede Suche zu einer Hash-Berechnung und einer Hash-Tabellensuche führt, was diese Wörterbücher zu nicht optimalen Basisstrukturen für die Aufzählungsklasse macht. Sogar der Mitgliederabruf selbst (wie in
StdEnum.MEMBER
) ist eine Dictionary-Suche.
Unser Weg
Bei der Entwicklung unserer Enum-Implementierung haben wir die hübschen C-sprachigen Aufzählungen und die schönen erweiterbaren Java-Enums berücksichtigt. Die Hauptmerkmale, die wir in unserer Implementierung wollten:
- Eine Aufzählung muss so statisch wie möglich sein. Mit "statisch" meinen wir: Wenn etwas einmalig und zur Deklarationszeit berechnet werden konnte, sollte es;
- Eine Enum-Klasse kann nicht in Unterklassen unterteilt werden (muss eine „letzte“ Klasse sein), wenn eine Unterklasse neue Enum-Mitglieder definiert. Dies gilt für die Standard-Bibliotheksimplementierung, mit der Ausnahme, dass Unterklassen verboten sind, auch wenn keine neuen Mitglieder definiert wurden.
- Eine Enumeration sollte umfangreiche Erweiterungsmöglichkeiten haben (zusätzliche Attribute, Methoden usw.).
Das einzige Mal, dass wir Dictionary-Lookups verwenden, ist eine umgekehrte Zuordnung zum Enum-Member. Alle anderen Berechnungen werden nur einmal während der Klassendeklaration durchgeführt (wobei Metaklassen-Hooks zum Anpassen der Typerstellung verwendet werden).
Im Gegensatz zur Standard-Bibliotheksimplementierung behandeln wir den ersten Wert nach dem Zeichen
=
in der Klassendeklaration als Member-Wert:
A = 1, 'One'
in der Standardbibliothek. Das gesamte Tupel
1, "One"
wird als
value
behandelt
A: 'MyEnum' = 1, 'One'
In unserer Implementierung wird nur
1
als
value
behandelt
Weitere Beschleunigung wird durch die Verwendung von
__slots__
wann immer dies möglich ist. In den mit
__slots__
deklarierten Python-Datenmodellklassen
__slots__
es kein
__dict__
Attribut, das
__dict__
enthält (Sie können also kein Attribut zuweisen, das nicht in
__slots__
). Darüber hinaus werden Attribute, die in
__slots__
definiert
__slots__
mit konstanten Offsets für den
__slots__
auf C-Ebene aufgerufen. Dies ist ein schneller Attributzugriff, da Hash-Berechnungen und Hashtable-Scans vermieden werden.
Was sind die zusätzlichen Vorteile?
FastEnum ist mit keiner Version von Python vor 3.6 kompatibel, da das in Python 3.6 eingeführte Eingabemodul übermäßig verwendet wird. Man könnte davon ausgehen, dass die Installation eines Backported-
typing
Moduls von PyPI helfen würde. Die Antwort lautet: nein. Die Implementierung verwendet PEP-484 für einige Funktions- und Methodenargumente und Rückgabewerttyphinweise, sodass eine Version vor Python 3.5 aufgrund von Syntaxinkompatibilität nicht unterstützt wird.
__new__
verwendet die allererste Codezeile in
__new__
der Metaklasse die PEP-526-Syntax für Variablentyphinweise. Also wird Python 3.5 auch nicht funktionieren. Es ist möglich, die Implementierung auf ältere Versionen zu portieren, obwohl wir in Qrator Labs in der Regel Tipping verwenden, wenn dies möglich ist, da dies die Entwicklung komplexer Projekte erheblich erleichtert. Und hey! Sie möchten sich nicht an Pythons vor 3.6 halten, da es keine Inkompatibilitäten mit Ihrem vorhandenen Code gibt (vorausgesetzt, Sie verwenden Python 2 nicht), obwohl in Asyncio im Vergleich zu 3.5 viel Arbeit geleistet wurde.
Das wiederum macht spezielle Importe wie
auto
im Gegensatz zur Standardbibliothek überflüssig. Sie geben für alle Enum-Mitglieder einen Hinweis mit Ihrem Enum-Klassennamen ein und geben dabei überhaupt keinen Wert an. Der Wert wird dann automatisch für Sie generiert. Obwohl Python 3.6 für die Arbeit mit FastEnum ausreicht, wird darauf hingewiesen, dass die Standardgarantie für die Deklarationsreihenfolge für Wörterbücher nur in Python 3.7 eingeführt wurde. Wir kennen keine nützlichen Appliances, bei denen die Reihenfolge der automatisch generierten Werte wichtig ist (da wir davon ausgehen, dass der selbst generierte Wert nicht der Wert ist, den ein Programmierer interessiert). Beachten Sie jedoch, dass Sie gewarnt sind, wenn Sie sich weiterhin an Python 3.6 halten.
Diejenigen, die ihre Aufzählung ab 0 (Null) anstelle von Standard 1 benötigen, können dies mit einem speziellen Enum-Deklarationsattribut
_ZERO_VALUED
, das aus der resultierenden Enum-Klasse "gelöscht" wird.
Es gibt jedoch einige Einschränkungen: Alle Namen von Aufzählungsmitgliedern müssen KAPITALISIERT sein. Andernfalls werden sie nicht von der Metaklasse erfasst und nicht als Aufzählungsmitglieder behandelt.
Sie können jedoch eine Basisklasse für Ihre Aufzählungen deklarieren (bedenken Sie, dass die Basisklasse die Aufzählungsmetaklasse selbst verwenden kann, sodass Sie nicht für alle Unterklassen eine Metaklasse bereitstellen müssen): Sie können darin gemeinsame Logik (Attribute und Methoden) definieren Klasse, darf aber keine Enum-Mitglieder definieren (damit die Klasse nicht "finalisiert" wird). Sie können diese Klasse dann in beliebig viele Aufzählungsdeklarationen unterteilen, um die gesamte Logik zu erhalten.
Aliase Wir werden sie in einem separaten Thema erklären (implementiert in 1.2.5)
Aliase und wie sie helfen könnten
Angenommen, Sie haben Code, der Folgendes verwendet:
package_a.some_lib_enum.MyEnum
Und das MyEnum wird so deklariert:
class MyEnum(metaclass=FastEnum): ONE: 'MyEnum' TWO: 'MyEnum'
Nun haben Sie sich für ein Refactoring entschieden und möchten Ihre Enumeration in ein anderes Paket verschieben. Sie erstellen so etwas:
package_b.some_lib_enum.MyMovedEnum
Wo MyMovedEnum wie folgt deklariert ist:
class MyMovedEnum(MyEnum): pass
Jetzt Sie können mit der Phase "Verfall" für den gesamten Code beginnen, in dem Ihre Aufzählungen verwendet werden. Sie leiten die direkte Verwendung von
MyEnum
um, um
MyEnum
zu verwenden (letzteres hat alle seine Mitglieder in
MyEnum
). Sie geben in Ihren Projektdokumenten an, dass
MyEnum
veraltet ist und zu einem späteren Zeitpunkt aus dem Code entfernt wird. Zum Beispiel in der nächsten Version. Stellen Sie sich vor, Ihr Code speichert Ihre Objekte mithilfe von pickle mit Aufzählungsattributen. Zu diesem Zeitpunkt verwenden Sie
MyMovedEnum
in Ihrem Code, aber intern sind alle Ihre Enum-Mitglieder weiterhin die
MyEnum
Instanzen. Ihr nächster Schritt wäre, die Deklarationen von
MyEnum
und
MyMovedEnum
sodass
MyMovedEnum
jetzt keine Unterklasse von
MyEnum
und alle ihre Mitglieder selbst deklariert. Andererseits würde
MyEnum
keine Mitglieder deklarieren, sondern nur ein Alias (eine Unterklasse) von
MyMovedEnum
.
Und das schließt es. Beim Neustart Ihrer Laufzeiten auf der Unpickle-Stufe werden alle Ihre Enum-Werte in
MyMovedEnum
und an diese neue Klasse gebunden. In dem Moment, in dem Sie sicher sind, dass alle Ihre gekapselten Objekte mit dieser Klassenorganisationsstruktur nicht (erneut)
MyEnum
sind, können Sie eine neue Version
MyEnum
, in der
MyEnum
zuvor als veraltet markiert und aus Ihrer Codebasis
MyEnum
kann.
Wir empfehlen Ihnen, es zu versuchen!
github.com/QratorLabs/fastenum ,
pypi.org/project/fastenum . Alle Credits gehen an den FastEnum-Autor
santjagocorkez .