Schnellere Aufzählung

tl; dr


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

... 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 # this is a direct access to the attribute value by the pointer that the object holds for that 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 # that is what you write in your code one_value_attribute = StdEnum.ONE.value one_value = one_value_attribute.__get__(StdEnum.ONE) 

 # and this is what really __get__ does (python 3.7 implementation): 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) 

 # since DynamicClassAttribute is a decorator on Enum methods `name` and `value` the final row of __get__() ends up with: @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_ 

Der gesamte Ablauf könnte also als folgender Pseudocode dargestellt werden:
 def get_func(enum_member, attrname): # this is also a __dict__ lookup so hash + hashtable scan also occur 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 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 .

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


All Articles