Automatically setting an enum member's value to its name

后端 未结 2 1427
时光取名叫无心
时光取名叫无心 2020-12-17 09:02

I\'ve been messing around with python\'s enum library and have come across a conundrum. In the docs, they show an example of an auto-numbering enum, wherein something is def

相关标签:
2条回答
  • 2020-12-17 09:43

    Perhaps you are looking for the name attribute which is automatically provided by the Enum class

    >>> class Animal(Enum):
    ...     ant = 1
    ...     bee = 2
    ...     cat = 3
    ...     dog = 4
    ...
    
    >>> Animal.ant.name == "ant"
    True
    

    Though if you really want to shoot yourself in the foot. And I'm sure this will introduce a whole world of gotchas (I've eliminated the most obvious one).

    from enum import Enum, EnumMeta, _EnumDict
    
    class AutoStrEnumDict(_EnumDict):
        def __setitem__(self, key, value):
            super().__setitem__(key, key)
    
    class AutoStrEnumMeta(EnumMeta):
        @classmethod
        def __prepare__(metacls, cls, bases):
            return AutoStrEnumDict()
        def __init__(self, name, bases, attrs):
            super().__init__(name, bases, attrs)
            # override Enum.__str__
            # can't put these on the class directly otherwise EnumMeta overwrites them
            # should also consider resetting __repr__, __format__ and __reduce_ex__
            if self.__str__ is not str.__str__:
                self.__str__ = str.__str__
    
    class AutoStrNameEnum(str, Enum, metaclass=AutoStrEnumMeta):
        pass
    
    class Animal(AutoStrNameEnum):
        horse = ()
        dog = ()
    
    print(Animal.horse)
    assert Animal.horse == "horse"
    assert str(Animal.horse) == "horse" 
    # and not equal to "Animal.horse" (the gotcha mentioned earlier)
    
    0 讨论(0)
  • 2020-12-17 10:01

    Update: 2017-03-01

    In Python 3.6 (and Aenum 2.01) Flag and IntFlag classes have been added; part of that was a new auto() helper that makes this trivially easy:

    >>> class AutoName(Enum):
    ...     def _generate_next_value_(name, start, count, last_values):
    ...         return name
    ...
    >>> class Ordinal(AutoName):
    ...     NORTH = auto()
    ...     SOUTH = auto()
    ...     EAST = auto()
    ...     WEST = auto()
    ...
    >>> list(Ordinal)
    [<Ordinal.NORTH: 'NORTH'>, <Ordinal.SOUTH: 'SOUTH'>, <Ordinal.EAST: 'EAST'>, <Ordinal.WEST: 'WEST'>]
    

    Original answer

    The difficulty with an AutoStr class is that the name of the enum member is not passed into the code that creates it, so it is unavailable for use. Another wrinkle is that str is immutable, so we can't change those types of enums after they have been created (by using a class decorator, for example).

    The easiest thing to do is use the Functional API:

    Animal = Enum('Animal', [(a, a) for a in ('horse', 'dog')], type=str)
    

    which gives us:

    >>> list(Animal)
    [<Animal.horse: 'horse'>, <Animal.dog: 'dog'>]
    
    >>> Animal.dog == 'dog'
    True
    

    The next easiest thing to do, assuming you want to make a base class for your future enumeration use, would be something like my DocEnem:

    class DocEnum(Enum):
        """
        compares equal to all cased versions of its name
        accepts a doctring for each member
        """
        def __new__(cls, *args):
            """Ignores arguments (will be handled in __init__)"""
            obj = object.__new__(cls)
            obj._value_ = None
            return obj
    
        def __init__(self, doc=None):
            # first, fix _value_
            self._value_ = self._name_.lower()
            self.__doc__ = doc
    
        def __eq__(self, other):
            if isinstance(other, basestring):
                return self._value_ == other.lower()
            elif not isinstance(other, self.__class__):
                return NotImplemented
            return self is other
    
        def __hash__(self):
            # keep DocEnum hashable
            return hash(self._value_)
    
        def __ne__(self, other):
            return not self == other
    

    and in use:

    class SpecKind(DocEnum):
        REQUIRED = "required value"
        OPTION = "single value per name"
        MULTI = "multiple values per name (list form)"
        FLAG = "boolean value per name"
        KEYWORD = 'unknown options'
    

    Note that unlike the first option, DocEnum members are not strs.


    If you want to do it the hard way: subclass EnumMeta and fiddle with the new Enum's class dictionary before the members are created:

    from enum import EnumMeta, Enum, _EnumDict
    
    class StrEnumMeta(EnumMeta):
        def __new__(metacls, cls, bases, oldclassdict):
            """
            Scan through `oldclassdict` and convert any value that is a plain tuple
            into a `str` of the name instead
            """
            newclassdict = _EnumDict()
            for k, v in oldclassdict.items():
                if v == ():
                    v = k
                newclassdict[k] = v
            return super().__new__(metacls, cls, bases, newclassdict)
    
    class AutoStrEnum(str, Enum, metaclass=StrEnumMeta):
        "base class for name=value str enums"
    
    class Animal(AutoStrEnum):
        horse = ()
        dog = ()
        whale = ()
    
    print(Animal.horse)
    print(Animal.horse == 'horse')
    print(Animal.horse.name, Animal.horse.value)
    

    Which gives us:

    Animal.horse
    True
    horse horse
    

    1 Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.

    0 讨论(0)
提交回复
热议问题