ENUM rapide

tl; dr


github.com/QratorLabs/fastenum
pip install fast-enum 

Pourquoi l'énumération est nécessaire


(si vous savez tout - allez à la section "Enumérations dans la bibliothèque standard")

Imaginez que vous devez décrire un ensemble de tous les états possibles des entités dans votre propre modèle de base de données. Très probablement, vous prendrez un tas de constantes définies directement dans l'espace de noms du module:
 # /path/to/package/static.py: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4 ... 

... ou comme attributs de classe statiques:
 class MyModelStates: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4 

Cette approche aidera à faire référence à ces états par des noms mnémoniques, tandis que dans votre référentiel, ce seront des entiers ordinaires. Ainsi, vous vous débarrassez simultanément des nombres magiques dispersés dans différentes parties du code, ce qui le rend plus lisible et informatif.

Cependant, à la fois la constante du module et la classe avec des attributs statiques souffrent de la nature intrinsèque des objets Python: ils sont tous mutables (mutables). Vous pouvez accidentellement attribuer une valeur à votre constante au moment de l'exécution, et le débogage et la restauration des objets cassés est une aventure distincte. Ainsi, vous souhaiterez peut-être rendre le faisceau de constantes inchangé dans le sens où le nombre de constantes déclarées et leurs valeurs auxquelles elles sont mappées ne changeront pas pendant l'exécution du programme.

Pour ce faire, vous pouvez essayer de les organiser en tuples nommés à l'aide de namedtuple() , comme dans l'exemple:
 MyModelStates = namedtuple('MyModelStates', ('INITIAL', 'PROCESSING', 'PROCESSED', 'DECLINED', 'RETURNED')) EntityStates = MyModelStates(0, 1, 2, 3, 4) 

Mais cela ne semble pas très net et lisible, et les objets namedtuple , à leur tour, ne sont pas très extensibles. Supposons que vous ayez une interface utilisateur qui affiche tous ces états. Vous pouvez utiliser vos constantes dans des modules, une classe avec des attributs ou des tuples nommés pour les rendre (les deux derniers sont plus faciles à rendre puisque nous en parlons). Mais un tel code ne permet pas de fournir à l'utilisateur une description adéquate de chaque état que vous définissez. De plus, si vous prévoyez d'implémenter le multilinguisme et la prise en charge i18n dans votre interface utilisateur, vous vous rendrez compte à quelle vitesse terminer toutes les traductions de ces descriptions devient une tâche incroyablement fastidieuse. Faire correspondre les noms d'états ne signifie pas nécessairement faire correspondre la description, ce qui signifie que vous ne pouvez pas simplement mapper tous vos états INITIAL à la même description dans gettext . Au lieu de cela, votre constante prend la forme suivante:
 INITIAL = (0, 'My_MODEL_INITIAL_STATE') 

Ou votre classe devient comme ça:
 class MyModelStates: INITIAL = (0, 'MY_MODEL_INITIAL_STATE') 

Enfin, le tuple nommé se transforme en:
 EntityStates = MyModelStates((0, 'MY_MODEL_INITIAL_STATE'), ...) 

Déjà pas mal - maintenant, il garantit que la valeur d'état et le talon de traduction sont affichés dans les langues prises en charge par l'interface utilisateur. Mais vous remarquerez peut-être que le code utilisant ces mappages est devenu un gâchis. Chaque fois, en essayant d'attribuer une valeur d'entité, vous devez extraire la valeur avec l'index 0 de l'affichage que vous utilisez:

 my_entity.state = INITIAL[0] 
ou
 my_entity.state = MyModelStates.INITIAL[0] 
ou
 my_entity.state = EntityStates.INITIAL[0] 

Et ainsi de suite. N'oubliez pas que les deux premières approches qui utilisent respectivement des constantes et des attributs de classe souffrent de mutabilité.

Et les transferts viennent à notre aide


 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') 

C’est tout. Maintenant, vous pouvez facilement parcourir la liste dans votre rendu (syntaxe Jinja2):
 {% for state in MyEntityState %} <option value=”{{ state.val }}”>{{ _(state.description) }}</option> {% endfor %} 

Une énumération est immuable à la fois pour un ensemble d'éléments - vous ne pouvez pas définir un nouveau membre d'une énumération au moment de l'exécution et vous ne pouvez pas supprimer un membre déjà défini, et pour les valeurs des éléments qu'elle stocke - vous ne pouvez pas [ré] attribuer des valeurs d'attribut ou supprimer un attribut.

Dans votre code, vous affectez simplement des valeurs à vos entités, comme ceci:
 my_entity.state = MyEntityStates.INITIAL.val 

Tout est suffisamment clair, informatif et extensible. C'est pour cela que nous utilisons les énumérations.

Comment pourrions-nous l'accélérer?


L'énumération à partir de la bibliothèque standard est plutôt lente, alors nous nous sommes demandé - pouvons-nous l'accélérer? Il s'est avéré que nous pouvons, à savoir, la mise en œuvre de notre énumération:

  • Trois fois plus rapide sur l'accès au dénombrement des membres;
  • ~ 8.5 plus rapide lors de l'accès à l'attribut ( name , value ) d'un membre;
  • 3 fois plus rapide lors de l'accès à un membre par valeur (appelez le constructeur de l'énumération MyEnum(value)) ;
  • 1,5 fois plus rapide lors de l'accès à un membre par son nom (comme dans le MyEnum[name] ).

Les types et les objets en Python sont dynamiques. Mais il existe des outils pour limiter la nature dynamique des objets. Vous pouvez obtenir une amélioration significative des performances avec __slots__ . Il est également possible de gagner en vitesse si vous évitez d'utiliser des descripteurs de données lorsque cela est possible - mais vous devez envisager la possibilité d'une augmentation significative de la complexité de l'application.

Machines à sous


Par exemple, vous pouvez utiliser une déclaration de classe à l'aide de __slots__ - dans ce cas, toutes les instances de classes n'auront qu'un ensemble limité de propriétés déclarées dans __slots__ et tous les __slots__ classes parentes.

Descripteurs


Par défaut, l'interpréteur Python renvoie directement la valeur de l'attribut de l'objet (en même temps, nous stipulons que dans ce cas, la valeur est également un objet Python, et non, par exemple, non signé longtemps long en termes de langage C):
value = my_obj.attribute # , .

Selon le modèle de données Python, si la valeur d'attribut est un objet qui implémente le protocole de descripteur, alors en essayant d'obtenir la valeur de cet attribut, l'interpréteur trouvera d'abord une référence à l'objet auquel la propriété fait référence, puis appellera la méthode spéciale __get__ , qui sera transmise à notre objet d'origine en tant que argument:
 obj_attribute = my_obj.attribute obj_attribute_value = obj_attribute.__get__(my_obj) 

Énumérations dans la bibliothèque standard


Au moins, les propriétés de name et de value des membres de l'implémentation d'énumération standard sont déclarées en tant que types.DynamicClassAttribute . Cela signifie que lorsque vous essayez d'obtenir les valeurs de name et de value , les événements suivants se produisent:

 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_ 

Ainsi, toute la séquence d'appels peut être représentée par le pseudo-code suivant:
 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') 

Nous avons écrit un script simple démontrant la sortie décrite ci-dessus:
 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() 

Et après exécution, le script nous a donné l'image suivante:


Cela montre que chaque fois que vous accédez aux attributs de name et de value des membres d'énumération à partir de la bibliothèque standard, un handle est appelé. Ce descripteur, à son tour, se termine par un appel de la classe Enum partir de la bibliothèque standard de la méthode def name(self) , décorée avec le descripteur.

Comparez avec notre 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() 

Ce qui peut être vu dans l'image suivante:


Tout cela se produit vraiment dans l'implémentation d'énumération standard chaque fois que vous accédez aux propriétés de name et de value de leurs membres. C'est aussi la raison pour laquelle notre implémentation est plus rapide.

L'implémentation des énumérations dans la bibliothèque standard Python utilise de nombreux appels à des objets qui implémentent le protocole de descripteur de données. Lorsque nous avons essayé d'utiliser l'implémentation d'énumération standard dans nos projets, nous avons immédiatement remarqué combien de descripteurs de données étaient appelés pour le name et la value .
Et puisque les énumérations ont été utilisées de manière assez extensive dans tout le code, les performances résultantes étaient faibles.

De plus, la classe Enum standard contient plusieurs attributs auxiliaires «protégés»:
  • _member_names_ - une liste contenant tous les noms des membres de l'énumération;
  • _member_map_ - OrderedDict , qui mappe le nom d'un membre d'énumération à sa valeur;
  • _value2member_map_ - un dictionnaire contenant une correspondance dans la direction opposée: les valeurs des membres de l'énumération aux membres correspondants de l'énumération.

La recherche dans le dictionnaire est lente, car chaque appel conduit au calcul de la fonction de hachage (à moins, bien sûr, que le résultat soit mis en cache séparément, ce qui n'est pas toujours possible pour le code non managé) et à la recherche dans la table de hachage, ce qui fait de ces dictionnaires une base non optimale pour les énumérations. Même la recherche de membres d'énumération (comme dans StdEnum.MEMBER ) elle-même est une recherche de dictionnaire.

Notre approche


Nous avons créé notre implémentation d'énumérations en tenant compte des énumérations élégantes en C et des belles énumérations extensibles en Java. Les principales fonctions que nous voulions mettre en œuvre à domicile étaient les suivantes:

  • l'énumération doit être aussi statique que possible; «Statique» signifie ici ce qui suit - si quelque chose ne peut être calculé qu'une seule fois et pendant l'annonce, alors il doit être calculé à ce moment (et seulement à ce moment);
  • il est impossible d'hériter d'une énumération (il doit s'agir d'une classe "finale") si la classe héritière définit de nouveaux membres de l'énumération - cela est vrai pour l'implémentation dans la bibliothèque standard, à l'exception que l'héritage y est interdit, même si la classe héritière ne définit pas de nouveaux membres;
  • l'énumération doit avoir amplement de possibilités d'extension (attributs supplémentaires, méthodes, etc.)

Nous utilisons la recherche par dictionnaire dans le seul cas - c'est le mappage inverse de la valeur de value à un membre d'énumération. Tous les autres calculs sont effectués une seule fois au cours de la déclaration de classe (où les métaclasses sont utilisées pour configurer la création de type).
Contrairement à la bibliothèque standard, nous traitons uniquement la première valeur après le signe = dans la déclaration de classe en tant que valeur membre:
A = 1, 'One' dans la bibliothèque standard, l'ensemble du tuple 1, "One" considéré comme la valeur de la value ;
A: 'MyEnum' = 1, 'One' dans notre implémentation, seul 1 considéré comme une value .

Une accélération supplémentaire est obtenue grâce à l'utilisation de __slots__ lorsque cela est possible. Dans les classes Python déclarées à l'aide de __slots__ , l'attribut __dict__ n'est pas créé pour les __dict__ , qui contient le mappage des noms d'attribut à leurs valeurs (par conséquent, vous ne pouvez déclarer aucune propriété de l'instance qui n'est pas mentionnée dans __slots__ ). De plus, les valeurs des attributs définis dans __slots__ sont accessibles avec un décalage constant dans le pointeur d'instance d'objet. Il s'agit d'un accès haut débit aux propriétés car il évite les calculs de hachage et les analyses de table de hachage.

Quels sont les jetons supplémentaires?


FastEnum n'est compatible avec aucune version de Python antérieure à 3.6 car il utilise universellement les annotations de type implémentées dans Python 3.6. On peut supposer que l'installation du module de saisie à partir de PyPi vous aidera. La réponse courte est non. L'implémentation utilise PEP-484 pour les arguments de certaines fonctions, méthodes et pointeurs vers le type de retour, donc toute version antérieure à Python 3.5 n'est pas prise en charge en raison d'une incompatibilité de syntaxe. Mais, encore une fois, la toute première ligne de code dans la métaclasse __new__ utilise la syntaxe PEP-526 pour indiquer le type de variable. Donc, Python 3.5 ne fonctionnera pas non plus. Vous pouvez porter l'implémentation vers des versions plus anciennes, bien que nous, chez Qrator Labs, ayons tendance à utiliser des annotations de type chaque fois que cela est possible, car cela aide beaucoup au développement de projets complexes. Enfin, finalement! Vous ne voulez pas rester bloqué dans Python avant la version 3.6, car dans les versions plus récentes il n'y a pas d'incompatibilité avec votre code existant (à condition que vous n'utilisiez pas Python 2), et beaucoup de travail a été fait dans l'implémentation de asyncio par rapport à 3.5, sur notre avis, mérite une mise à jour immédiate.

Ceci, à son tour, rend inutile l'importation spéciale d' auto , contrairement à la bibliothèque standard. Vous indiquez simplement que le membre de l'énumération sera une instance de cette énumération sans fournir de valeur du tout - et la valeur sera générée automatiquement pour vous. Bien que Python 3.6 soit suffisant pour travailler avec FastEnum, gardez à l'esprit que la préservation de l'ordre des clés dans les dictionnaires a été introduite uniquement dans Python 3.7 (et nous n'avons pas utilisé OrderedDict séparément pour le cas 3.6). Nous ne connaissons aucun exemple où l'ordre de valeurs généré automatiquement est important, car nous supposons que si le développeur a confié à l'environnement la tâche de générer et d'attribuer une valeur à un membre d'énumération, la valeur elle-même n'est pas si importante pour lui. Cependant, si vous n'êtes toujours pas passé à Python 3.7, nous vous avons prévenu.

Ceux qui ont besoin que leurs énumérations commencent à 0 (zéro) au lieu de la valeur par défaut (1) peuvent le faire en utilisant un attribut spécial lors de la déclaration de l'énumération _ZERO_VALUED , qui ne sera pas stockée dans la classe résultante.

Cependant, il existe certaines restrictions: tous les noms des membres de l'énumération doivent être écrits en majuscules, sinon ils ne seront pas traités par la métaclasse.

Enfin, vous pouvez déclarer une classe de base pour vos énumérations (gardez à l'esprit que la classe de base peut utiliser la métaclasse elle-même, vous n'avez donc pas besoin de fournir la métaclasse à toutes les sous-classes) - définissez simplement la logique générale (attributs et méthodes) dans cette classe et ne définissez pas les membres de l'énumération (donc la classe ne sera pas "finalisée"). Après, vous pouvez déclarer autant de classes héritées de cette classe que vous le souhaitez, et les héritiers eux-mêmes auront la même logique.

Les alias et comment ils peuvent aider


Supposons que vous ayez du code en utilisant:
 package_a.some_lib_enum.MyEnum 

Et que la classe MyEnum est déclarée comme suit:
 class MyEnum(metaclass=FastEnum): ONE: 'MyEnum' TWO: 'MyEnum' 

Maintenant, vous avez décidé de refactoriser et de transférer la liste vers un autre package. Vous créez quelque chose comme ceci:
 package_b.some_lib_enum.MyMovedEnum 

Où MyMovedEnum est déclaré comme ceci:
 class MyMovedEnum(MyEnum): pass 

Vous êtes maintenant prêt pour l'étape à laquelle le transfert situé à l'ancienne adresse est considéré comme obsolète. Vous réécrivez les importations et les appels de cette énumération afin que le nouveau nom de cette énumération (son alias) soit maintenant utilisé - vous pouvez être sûr que tous les membres de cette énumération d'alias sont réellement déclarés dans la classe avec l'ancien nom. Dans la documentation de votre projet, vous déclarez que MyEnum obsolète et sera supprimé du code à l'avenir. Par exemple, dans la prochaine version. Supposons que votre code stocke vos objets avec des attributs contenant des membres d'énumération à l'aide de pickle . À ce stade, vous utilisez MyMovedEnum dans votre code, mais en interne, tous les membres d'énumération sont toujours des instances de MyEnum . Votre prochaine étape consiste à échanger les déclarations de MyEnum et MyMovedEnum afin que MyMovedEnum ne MyMovedEnum pas une sous-classe de MyEnum et MyEnum tous ses membres; MyEnum , d'autre part, ne déclare désormais aucun membre, mais devient juste un alias (sous-classe) de MyMovedEnum .

C’est tout. Lorsque vous redémarrez vos applications à l'étape de unpickle tous les membres de l'énumération seront à nouveau déclarés en tant MyMovedEnum de MyMovedEnum et deviendront associés à cette nouvelle classe. Au moment où vous êtes sûr que tous vos objets stockés, par exemple, dans la base de données, ont été désérialisés (et éventuellement sérialisés à nouveau et stockés dans le référentiel) - vous pouvez publier une nouvelle version, dans laquelle elle était précédemment marquée comme classe obsolète MyEnum peut être déclaré plus inutile et supprimé de la base de code.

Essayez-le par vous-même: github.com/QratorLabs/fastenum , pypi.org/project/fast-enum .
Les pros du karma vont à l'auteur FastEnum - santjagocorkez .

UPD: Dans la version 1.3.0, il est devenu possible d'hériter des classes existantes, par exemple, int , float , str . Les membres de telles énumérations réussissent le test d'égalité à un objet propre avec la même valeur ( IntEnum.MEMBER == int(value_given_to_member) ) et, bien sûr, qu'ils sont des instances de ces classes héritées. Ceci, à son tour, permet au membre de l'énumération héritée de int d'être un argument direct à sys.exit() tant que code retour de l'interpréteur python.

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


All Articles