Disons que j'ai un mappage préexistant en tant que dictionnaire:

value_map = {'a': 1, 'b': 2}

Je peux créer une classe enum à partir de ceci comme ceci:

from enum import Enum
MyEnum = Enum('MyEnum', value_map)

Et l'utiliser comme ça

a = MyEnum.a
print(a.value)
>>> 1
print(a.name)
>>> 'a'

Mais ensuite, je veux définir quelques méthodes pour ma nouvelle classe enum:

def double_value(self):
    return self.value * 2

Bien sûr, je peux le faire:

class MyEnum(Enum):
    a = 1
    b = 2
    @property
    def double_value(self):
        return self.value * 2

Mais comme je l'ai dit, je dois utiliser un dictionnaire de mappage de valeurs prédéfini, donc je ne peux pas le faire. Comment cela peut il etre accompli? J'ai essayé d'hériter d'une autre classe définissant cette méthode comme un mixin, mais je n'ai pas pu le comprendre.

3
waszil 18 mars 2019 à 14:43

2 réponses

Meilleure réponse

Vous pouvez passer un type de base avec des méthodes mixin dans l'API fonctionnelle, avec l'argument type :

>>> import enum
>>> value_map = {'a': 1, 'b': 2}
>>> class DoubledEnum:
...     @property
...     def double_value(self):
...         return self.value * 2
...
>>> MyEnum = enum.Enum('MyEnum', value_map, type=DoubledEnum)
>>> MyEnum.a.double_value
2

Pour une approche entièrement fonctionnelle qui n'utilise jamais d'instruction class, vous pouvez créer le mix-in de base avec le type() :

DoubledEnum = type('DoubledEnum', (), {'double_value': property(double_value)})
MyEnum = enum.Enum('MyEnum', value_map, type=DoubledEnum)

Vous pouvez également utiliser la métaclasse enum.EnumMeta() de la même manière, comme Python le ferait lorsque vous créez une sous-classe class MyEnum(enum.Enum): ... :

  1. Créez un dictionnaire de classe à l'aide de la métaclasse __prepare__ crochet
  2. Appelez la métaclasse en passant le nom de la classe, les bases ((enum.Enum,) ici) et le dictionnaire de classe créé à l'étape 1.

La sous-classe de dictionnaire personnalisé que enum.EnumMeta utilise n'est pas vraiment conçue pour une réutilisation facile ; il implémente un hook __setitem__ pour enregistrer les métadonnées, mais ne remplace pas la méthode dict.update(), nous devons donc faire preuve d'un peu de prudence lors de l'utilisation de votre dictionnaire value_map :

import enum

def enum_with_extras(name, value_map, bases=enum.Enum, **extras):
    if not isinstance(bases, tuple):
        bases = bases,
    if not any(issubclass(b, enum.Enum) for b in bases):
        bases += enum.Enum,
    classdict = enum.EnumMeta.__prepare__(name, bases)
    for key, value in {**value_map, **extras}.items():
        classdict[key] = value
    return enum.EnumMeta(name, bases, classdict)

Passez ensuite double_value=property(double_value) à cette fonction (avec le nom enum et le dictionnaire value_map) :

>>> def double_value(self):
...     return self.value * 2
...
>>> MyEnum = enum_with_extras('MyEnum', value_map, double_value=property(double_value))
>>> MyEnum.a
<MyEnum.a: 1>
>>> MyEnum.a.double_value
2

Vous êtes par ailleurs autorisé à créer des sous-classes d'une énumération sans membres (tout ce qui est un descripteur n'est pas un membre, donc fonctions, propriétés, méthodes de classe, etc.), vous pouvez donc pouvoir définir d'abord une énumération sans membres :

class DoubledEnum(enum.Enum):
    @property
    def double_value(self):
        return self.value * 2

Qui est une classe de base acceptable à la fois dans l'API fonctionnelle (par exemple enum.Enum(..., type=DoubledEnum)) et pour l'approche de métaclasse que j'ai codée en enum_with_extras().

4
Martijn Pieters 18 mars 2019 à 12:56

Vous pouvez créer une nouvelle méta-classe (en utilisant une méta-métaclasse ou une fonction d'usine, comme je le fais ci-dessous) qui dérive de enum.EnumMeta (la métaclasse pour les énumérations) et ajoute simplement les membres avant de créer la classe

import enum
import collections.abc


def enum_metaclass_with_default(default_members):
    """Creates an Enum metaclass where `default_members` are added"""
    if not isinstance(default_members, collections.abc.Mapping):
        default_members = enum.Enum('', default_members).__members__

    default_members = dict(default_members)

    class EnumMetaWithDefaults(enum.EnumMeta):
        def __new__(mcs, name, bases, classdict):
            """Updates classdict adding the default members and
            creates a new Enum class with these members
            """

            # Update the classdict with default_members
            # if they don't already exist
            for k, v in default_members.items():
                if k not in classdict:
                    classdict[k] = v

            # Add `enum.Enum` as a base class

            # Can't use `enum.Enum` in `bases`, because
            # that uses `==` instead of `is`
            bases = tuple(bases)
            for base in bases:
                if base is enum.Enum:
                    break
            else:
                bases = (enum.Enum,) + bases

            return super(EnumMetaWithDefaults, mcs).__new__(mcs, name, bases, classdict)

    return EnumMetaWithDefaults


value_map = {'a': 1, 'b': 2}


class MyEnum(metaclass=enum_metaclass_with_default(value_map)):
    @property
    def double_value(self):
        return self.value * 2


assert MyEnum.a.double_value == 2

Une autre solution était d'essayer directement de mettre à jour locals(), car il est remplacé par un mappage qui crée des valeurs d'énumération lorsque vous essayez d'affecter des valeurs.

import enum


value_map = {'a': 1, 'b': 2}


def set_enum_values(locals, value_map):
    # Note that we can't use `locals.update(value_map)`
    # because it's `locals.__setitem__(k, v)` that
    # creates the enum value, and `update` doesn't
    # call `__setitem__`.
    for k, v in value_map:
        locals[k] = v


class MyEnum(enum.Enum):
    set_enum_values(locals(), value_map)

    @property
    def double_value(self):
        return self.value * 2


assert MyEnum.a.double_value == 2

Cela semble assez bien défini, et a = 1 va très probablement être le même que locals()['a'] = 1, mais cela pourrait changer à l'avenir. La première solution est plus robuste et moins hacky (et je ne l'ai pas testée dans d'autres implémentations Python, mais elle fonctionne probablement de la même manière)

1
Artyer 18 mars 2019 à 12:59