tl; dr
github.com/QratorLabs/fastenumpip install fast-enum
Que sont les énumérations
(Si vous pensez le savoir, faites défiler la page jusqu'à la section «Enums dans la bibliothèque standard»).
Imaginez que vous devez décrire un ensemble de tous les états possibles pour les entités de votre modèle de base de données. Vous utiliserez probablement un tas de constantes définies comme des attributs au niveau du module:
... ou en tant qu'attributs de niveau classe définis dans leur propre classe:
class MyModelStates: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4
Cela vous aide à vous référer à ces états par leurs noms mnémoniques, tandis qu'ils persistent dans votre stockage en tant que simples entiers. Par cela, vous vous débarrassez des nombres magiques dispersés dans votre code et le rendez plus lisible et auto-descriptif.
Mais, la constante au niveau du module et la classe avec les attributs statiques souffrent de la nature inhérente des objets python: ils sont tous mutables. Vous pouvez accidentellement attribuer une valeur à votre constante au moment de l'exécution, et c'est un gâchis pour déboguer et restaurer vos entités cassées. Ainsi, vous souhaiterez peut-être rendre votre ensemble de constantes immuable, ce qui signifie que le nombre de constantes déclarées et les valeurs auxquelles elles sont mappées ne doivent pas être modifiées au moment de l'exécution.
Pour cela, vous pouvez essayer de les organiser en tuples nommés avec
namedtuple()
, par exemple:
MyModelStates = namedtuple('MyModelStates', ('INITIAL', 'PROCESSING', 'PROCESSED', 'DECLINED', 'RETURNED')) EntityStates = MyModelStates(0, 1, 2, 3, 4)
Cependant, cela ne semble toujours pas trop compréhensible: en plus de cela, les objets
namedtuple
ne sont pas vraiment extensibles. Disons que vous avez une interface utilisateur qui affiche tous ces états. Vous pouvez ensuite utiliser vos constantes basées sur un module, votre classe avec les attributs ou des tuples nommés pour les rendre (les deux derniers sont plus faciles à rendre pendant que nous y sommes). Mais votre code ne fournit aucune possibilité de donner à l'utilisateur une description adéquate pour chaque état que vous avez défini. De plus, si vous prévoyez d'implémenter la prise en charge multilingue et i18n dans votre interface utilisateur, vous constaterez que remplir toutes les traductions de ces descriptions devient une tâche incroyablement fastidieuse. Les valeurs d'état correspondantes peuvent ne pas nécessairement avoir de descriptions correspondantes, ce qui signifie que vous ne pouvez pas simplement mapper tous vos états
INITIAL
sur la même description dans
gettext
. Au lieu de cela, votre constante devient ceci:
INITIAL = (0, 'My_MODEL_INITIAL_STATE')
Votre classe devient alors ceci:
class MyModelStates: INITIAL = (0, 'MY_MODEL_INITIAL_STATE')
Et enfin, votre
namedtuple
devient ceci:
EntityStates = MyModelStates((0, 'MY_MODEL_INITIAL_STATE'), ...)
Eh bien, assez bien, il s'assure maintenant que la valeur de l'état et le talon de traduction sont mappés aux langues prises en charge par votre interface utilisateur. Mais maintenant, vous remarquerez peut-être que le code qui utilise ces mappages est devenu un gâchis. Chaque fois que vous essayez d'attribuer une valeur à votre entité, vous devez également ne pas oublier d'extraire la valeur à l'index 0 du mappage 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. Gardez à l'esprit que les deux premières approches utilisant des constantes et des attributs de classe, respectivement, souffrent toujours de mutabilité.
Et puis les énumérations arrivent sur scène
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')
Voilà. Vous pouvez maintenant facilement parcourir l'énumération dans votre moteur de rendu (syntaxe Jinja2):
{% for state in MyEntityState %} <option value=”{{ state.val }}”>{{ _(state.description) }}</option> {% endfor %}
L'énumération est immuable pour les deux ensembles de membres (vous ne pouvez pas définir un nouveau membre au moment de l'exécution, ni supprimer un membre déjà défini) et les valeurs de membre qu'ils conservent (vous ne pouvez pas réaffecter de valeurs d'attribut ou supprimer un attribut).
Dans votre code, vous attribuez simplement des valeurs à vos entités comme ceci:
my_entity.state = MyEntityStates.INITIAL.val
Eh bien, assez clair. Auto-descriptif. Assez extensible. C'est pour cela que nous utilisons Enums.
Pourquoi est-ce plus rapide?
Mais l'ENUM par défaut est plutôt lent, alors nous nous sommes demandé - pourrions-nous le rendre plus rapide?
En fait, nous le pouvons. A savoir, il est possible de le faire:
- 3 fois plus rapide sur l'accès des membres
- ~ 8,5 fois plus rapide sur l'accès aux attributs (
name
, value
) - 3 fois plus rapide sur l'accès enum par valeur (appel sur la classe enum
MyEnum(value)
) - 1,5 fois plus rapide sur l'accès enum par nom (
MyEnum[name]
type MyEnum[name]
)
Les types et les objets sont dynamiques en Python. Mais Python a les outils pour limiter la nature dynamique des objets. Avec leur aide, on peut obtenir une amélioration significative des performances en utilisant
__slots__
ainsi qu'en évitant d'utiliser des descripteurs de données lorsque cela est possible sans croissance de complexité significative ou si vous pouvez bénéficier de la vitesse.
Machines à sous
Par exemple, on pourrait utiliser une déclaration de classe avec
__slots__
- dans ce cas, les instances de classe n'auraient qu'un ensemble restreint d'attributs: les attributs déclarés dans
__slots__
et tous les
__slots__
des classes parentes.
Descripteurs
Par défaut, l'interpréteur Python renvoie directement une valeur d'attribut d'un objet:
value = my_obj.attribute
Selon le modèle de données Python, si la valeur d'attribut d'un objet est elle-même un objet qui implémente le protocole de descripteur de données, cela signifie que lorsque vous essayez d'obtenir cette valeur, vous obtenez d'abord l'attribut en tant qu'objet, puis une méthode spéciale
__get__
est appelé sur cet attribut-objet en passant l'objet gardien lui-même comme argument:
obj_attribute = my_obj.attribute obj_attribute_value = obj_attribute.__get__(my_obj)
Énumérations dans la bibliothèque standard
Au moins les attributs de
name
et de
value
de l'implémentation Enum standard sont déclarés en tant que
types.DynamicClassAttribute
. Cela signifie que lorsque vous essayez d'obtenir le
name
(ou la
value
) d'un membre, le flux est le suivant:
one_value = StdEnum.ONE.value
Ainsi, le flux complet pourrait être représenté comme le pseudo-code suivant:
def get_func(enum_member, attrname):
Nous avons créé un script simple qui illustre la conclusion 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 avoir exécuté le script, il a créé cette image pour nous:

Cela prouve que chaque fois que vous accédez au
name
et à la
value
attributs de stdlib enum, il appelle un descripteur. Ce descripteur se termine à son tour par un appel à la propriété
def name(self)
de stdlib enum décorée avec le descripteur.
Eh bien, vous pouvez comparer cela à 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()
Qui sort cette image:

C'est ce qui se fait réellement dans l'implémentation Enum standard chaque fois que vous accédez aux attributs de
name
et de
value
de vos membres Enum. Et c'est pourquoi notre implémentation est plus rapide.
L'implémentation de la classe Enum par la bibliothèque standard Python utilise des tonnes d'appels de protocole de descripteur. Lorsque nous avons essayé d'utiliser l'énumération standard dans nos projets, nous avons remarqué combien d'appels de protocole de descripteur pour les attributs de
name
et de
value
des membres
Enum
ont été invoqués. Et parce que les énumérations étaient utilisées de manière excessive dans tout le code, les performances résultantes étaient médiocres.
De plus, la classe d'énumération standard contient quelques attributs d'aide «protégés»:
_member_names_
- une liste qui contient tous les noms des membres enum;_member_map_
- un OrderedDict qui mappe le nom d'un membre enum au membre lui-même;_value2member_map_
- un dictionnaire inversé qui mappe les valeurs des membres enum aux membres enum correspondants.
Les recherches de dictionnaire sont lentes car chacune mène à un calcul de hachage et à une recherche de table de hachage, ce qui rend ces structures de base non optimales pour la classe enum. Même la récupération de membre elle-même (comme dans
StdEnum.MEMBER
) est une recherche de dictionnaire.
Notre chemin
Lors du développement de notre implémentation Enum, nous avons gardé à l'esprit ces jolies énumérations en langage C et les magnifiques énumérations Java extensibles. Les principales fonctionnalités que nous souhaitions dans notre implémentation:
- un Enum doit être aussi statique que possible; par «statique», nous voulons dire: si quelque chose pouvait être calculé une fois et au moment de la déclaration, il le devrait;
- une Enum ne peut pas être sous-classée (doit être une classe «finale») si une sous-classe définit de nouveaux membres enum - cela est vrai pour l'implémentation de bibliothèque standard, à l'exception que la sous-classe est interdite même si aucun nouveau membre n'est défini;
- un Enum devrait avoir de vastes possibilités d'extensions (attributs supplémentaires, méthodes, etc.).
La seule fois où nous utilisons des recherches de dictionnaire est dans une
value
mappage inversée vers un membre Enum. Tous les autres calculs sont effectués une seule fois pendant la déclaration de classe (où les crochets de métaclasses sont utilisés pour personnaliser la création de type).
Contrairement à l'implémentation de bibliothèque standard, nous traitons la première valeur après le signe
=
dans la déclaration de classe comme valeur membre:
A = 1, 'One'
dans la bibliothèque standard énumère le tuple entier
1, "One"
est traité comme une
value
A: 'MyEnum' = 1, 'One'
dans notre implémentation, seul
1
est traité comme une
value
Une accélération supplémentaire est obtenue en utilisant
__slots__
chaque fois que possible. Dans le modèle de données Python, les classes déclarées avec
__slots__
n'ont pas d'attribut
__dict__
qui contient des attributs d'instance (vous ne pouvez donc pas attribuer d'attribut non mentionné dans
__slots__
). De plus, les attributs définis dans
__slots__
accessibles avec des décalages constants au pointeur d'objet de niveau C. Il s'agit d'un accès aux attributs à grande vitesse car il évite les calculs de hachage et les analyses de table de hachage.
Quels sont les avantages supplémentaires?
FastEnum n'est compatible avec aucune version de Python antérieure à 3.6, car il utilise de manière excessive le module de
typing
introduit dans Python 3.6; On pourrait supposer que l'installation d'un module de
typing
rétroporté à partir de PyPI serait utile. La réponse est non. L'implémentation utilise PEP-484 pour certaines fonctions et méthodes et les arguments de type de valeur de retour, de sorte que toute version antérieure à Python 3.5 n'est pas prise en charge en raison d'une incompatibilité de syntaxe. Mais là encore, la toute première ligne de code dans
__new__
de la métaclasse utilise la syntaxe PEP-526 pour les
__new__
de type variable. Donc Python 3.5 ne fera pas non plus. Il est possible de porter l'implémentation vers des versions plus anciennes, bien que nous, dans Qrator Labs, ayons tendance à utiliser l'indicateur de type chaque fois que cela est possible, car cela aide grandement à développer des projets complexes. Et bon! Vous ne voulez pas vous en tenir à n'importe quel python avant 3.6 car il n'y a pas d'incompatibilité en arrière avec votre code existant (en supposant que vous n'utilisez pas Python 2) bien que beaucoup de travail ait été fait en asyncio par rapport à 3.5.
Cela, à son tour, rend inutiles les importations spéciales comme
auto
, contrairement à la bibliothèque standard. Vous tapez-hint tous vos membres Enum avec votre nom de classe Enum, ne fournissant aucune valeur du tout - et la valeur serait générée automatiquement pour vous. Bien que python 3.6 soit suffisant pour fonctionner avec FastEnum, soyez averti que l'ordre standard de garantie de déclaration du dictionnaire n'a été introduit que dans python 3.7. Nous ne connaissons aucun appareil utile où l'ordre de valeur généré automatiquement est important (car nous supposons que la valeur générée elle-même n'est pas la valeur dont un programmeur se soucie). Néanmoins, considérez-vous averti si vous restez avec python 3.6;
Ceux qui ont besoin que leur énumération commence à 0 (zéro) au lieu de 1 par défaut peuvent le faire avec un attribut de déclaration d'énumération spécial
_ZERO_VALUED
, cet attribut est "effacé" de la classe Enum résultante;
Il y a cependant quelques limitations: tous les noms de membres enum doivent être MAJUSCULÉS sinon ils ne seront pas récupérés par la métaclasse et ne seront pas traités comme des membres enum;
Cependant, 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 enum elle-même, vous n'avez donc pas besoin de fournir de métaclasse à toutes les sous-classes): vous pouvez définir une logique commune (attributs et méthodes) dans ce mais ne peut pas définir de membres enum (de sorte que la classe ne sera pas "finalisée"). Vous pouvez ensuite sous-classer cette classe dans autant de déclarations d'énumération que vous le souhaitez et cela vous fournira toute la logique commune;
Alias Nous les expliquerons dans un sujet séparé (implémenté en 1.2.5)
Alias et comment ils pourraient aider
Supposons que vous ayez du code qui utilise:
package_a.some_lib_enum.MyEnum
Et que MyEnum est déclaré comme ceci:
class MyEnum(metaclass=FastEnum): ONE: 'MyEnum' TWO: 'MyEnum'
Maintenant, vous avez décidé de refactoriser et de déplacer votre énumération dans 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
Maintenant. Vous êtes prêt à commencer l'étape de "dépréciation" pour tout le code qui utilise vos énumérations. Vous détournez les utilisations directes de
MyEnum
pour utiliser
MyMovedEnum
(ce dernier a tous ses membres
MyEnum
dans
MyEnum
). Vous
MyEnum
dans vos documents de projet que
MyEnum
est obsolète et sera supprimé du code à un moment donné dans le futur. Par exemple, dans la prochaine version. Considérez que votre code enregistre vos objets avec des attributs enum en utilisant pickle. À ce stade, vous utilisez
MyMovedEnum
dans votre code, mais en interne, tous vos membres enum sont toujours les instances
MyEnum
. Votre prochaine étape serait d'échanger les déclarations de
MyEnum
et
MyMovedEnum
afin que
MyMovedEnum
ne soit plus une sous-classe de
MyEnum
et de déclarer tous ses membres lui-même;
MyEnum
, d'autre part, ne déclarerait aucun membre mais deviendrait juste un alias (sous-classe) de
MyMovedEnum
.
Et cela le conclut. Au redémarrage de vos runtimes sur une étape de décapage, toutes vos valeurs d'énumération seront redirigées vers
MyMovedEnum
et seront reliées à cette nouvelle classe. Au moment où vous êtes sûr que tous vos objets marinés ont été non (re) marinés avec cette structure d'organisation de classe, vous êtes libre de faire une nouvelle version, où précédemment marqué comme obsolète, votre
MyEnum
peut être déclaré obsolète et effacé de votre base de code.
Nous vous encourageons à l'essayer!
github.com/QratorLabs/fastenum ,
pypi.org/project/fast-enum . Tous les crédits vont à l'auteur de
FastEnum santjagocorkez .