Schnelle ENUM

tl; dr


github.com/QratorLabs/fastenum
pip 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:
 # /path/to/package/static.py: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4 ... 

... 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 #        #   ,      one_value_attribute = StdEnum.ONE.value one_value = one_value_attribute.__get__(StdEnum.ONE) 

 #   ,  __get__     (  python3.7): def __get__(self, instance, ownerclass=None): if instance is None: if self.__isabstractmethod__: return self raise AttributeError() elif self.fget is None: raise AttributeError("unreadable attribute") return self.fget(instance) 

 #   DynamicClassAttribute     `name`  `value`   __get__()  : @DynamicClassAttribute def name(self): """The name of the Enum member.""" return self._name_ @DynamicClassAttribute def value(self): """The value of the Enum member.""" return self._value_ 

Somit kann die gesamte Abfolge von Aufrufen durch den folgenden Pseudocode dargestellt werden:
 def get_func(enum_member, attrname): #        __dict__,        -     return getattr(enum_member, f'_{attrnme}_') def get_name_value(enum_member): name_descriptor = get_descriptor(enum_member, 'name') if enum_member is None: if name_descriptor.__isabstractmethod__: return name_descriptor raise AttributeError() elif name_descriptor.fget is None: raise AttributeError("unreadable attribute") return get_func(enum_member, 'name') 

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.

Source: https://habr.com/ru/post/de480614/


All Articles