Associate string representations with an Enum that uses integer values?

I'm trying to create an enum that has integer values, but that can also return a display-friendly string for each value. I thought I could just define dict display values ​​for strings and then implement a __str__

static constructor with a string argument as well, but there is a problem with that ...

(In other circumstances, I could just make the underlying datatype for this Enum a string, not an integer, but this is used as a mapping for the enum database table, so integer and string make sense, the first being the primary key.)

from enum import Enum

class Fingers(Enum):
    THUMB = 1
    INDEX = 2
    MIDDLE = 3
    RING = 4
    PINKY = 5

    _display_strings = {
        THUMB: "thumb",
        INDEX: "index",
        MIDDLE: "middle",
        RING: "ring",
        PINKY: "pinky"
        }

    def __str__(self):
        return self._display_strings[self.value]

    @classmethod
    def from_string(cls, str1):
        for val, str2 in cls._display_strings.items():
            if str1 == str2:
                return cls(val)
        raise ValueError(cls.__name__ + ' has no value matching "' + str1 + '"')

      

When converting to a string, I get the following error:

>>> str(Fingers.RING)
Traceback (most recent call last):
  File "<pyshell#0>", line 1, in <module>
    str(Fingers.RING)
  File "D:/src/Hacks/PythonEnums/fingers1.py", line 19, in __str__
    return self._display_strings[self.value]
TypeError: 'Fingers' object is not subscriptable

      

It looks like the problem is that Enum will use all the class variables as enum values, which makes them return objects of type Enum rather than their base type.

Here are some workarounds:

  1. Referring to Fingers._display_strings.value

    how Fingers._display_strings.value

    . (However, then Fingers.__display_strings

    it becomes a valid enum value!)
  2. Make a dict of a module variable instead of a class variable.
  3. Duplicating the dict (possibly breaking it down into a series of statements as well if

    ) in the __str__

    and functions from_string

    .
  4. Instead of making the dict a class variable, define a static method _get_display_strings

    to return the dict so that it doesn't become an enum value.

Note that in the above source code and 1.

workaround, basic integer values ​​are used as dict keys. Other options all require that the DICT (or, if

tests) be defined somewhere other than directly in the class itself, and therefore it must qualify these values ​​with the class name. So you can use, for example, Fingers.THUMB

to get an enum object, or Fingers.THUMB.value

to get a basic integer value, but not simple THUMB

. If you are using an underlying integer value as the dict key, then you should also use it to search for a word, indexing it with, for example, [Fingers.THUMB.value]

and not just [Fingers.THUMB]

.

So the question is, what is the best or most Pythonic way to implement string mapping for an Enum while still keeping the underlying integer value?

+4


source to share


6 answers


This can be done with stdlib Enum

, but much easier with 1 :aenum

from aenum import Enum

class Fingers(Enum):

    _init_ = 'value string'

    THUMB = 1, 'two thumbs'
    INDEX = 2, 'offset location'
    MIDDLE = 3, 'average is not median'
    RING = 4, 'round or finger'
    PINKY = 5, 'wee wee wee'

    def __str__(self):
        return self.string

      

If you want to be able to search through a string value then implement a new class method _missing_value_

(just _missing_

in stdlib):



from aenum import Enum

class Fingers(Enum):

    _init_ = 'value string'

    THUMB = 1, 'two thumbs'
    INDEX = 2, 'offset location'
    MIDDLE = 3, 'average is not median'
    RING = 4, 'round or finger'
    PINKY = 5, 'wee wee wee'

    def __str__(self):
        return self.string

    @classmethod
    def _missing_value_(cls, value):
        for member in cls:
            if member.string == value:
                return member

      


1 Disclosure: I am the author of the Python stdlibEnum

, enum34

backport,
and extended enumeration library ( aenum

)
.

+11


source


I may be missing a point here, but if you define

class Fingers(Enum):
    THUMB = 1
    INDEX = 2
    MIDDLE = 3
    RING = 4
    PINKY = 5

      

then in Python 3.6 you can do



print (Fingers.THUMB.name.lower())

      

which I think you need.

+3


source


Another solution, that I encountered is that since the integer and string values have meaning, it was necessary to make the values of the Enum values (int, str)

, as shown below.

from enum import Enum

class Fingers(Enum):
    THUMB = (1, 'thumb')
    INDEX = (2, 'index')
    MIDDLE = (3, 'middle')
    RING = (4, 'ring')
    PINKY = (5, 'pinky')

    def __str__(self):
        return self.value[1]

    @classmethod
    def from_string(cls, s):
        for finger in cls:
            if finger.value[1] == s:
                return finger
        raise ValueError(cls.__name__ + ' has no value matching "' + s + '"')

      

However, this means that a Fingers

tuple will be displayed in the object view, not just int, and the full set should be used to create objects Fingers

, not just int. That is, you can do f = Fingers((1, 'thumb'))

, but not f = Fingers(1)

.

>>> Fingers.THUMB
<Fingers.THUMB: (1, 'thumb')>
>>> Fingers((1,'thumb'))
<Fingers.THUMB: (1, 'thumb')>
>>> Fingers(1)
Traceback (most recent call last):
  File "<pyshell#25>", line 1, in <module>
    Fingers(1)
  File "C:\Python\Python35\lib\enum.py", line 241, in __call__
    return cls.__new__(cls, value)
  File "C:\Python\Python35\lib\enum.py", line 476, in __new__
    raise ValueError("%r is not a valid %s" % (value, cls.__name__))
ValueError: 1 is not a valid Fingers

      

An even more sophisticated workaround for this involves subclassing the metaclass Enum

to implement a custom one __call__

. (At least overriding is __repr__

much easier!)

from enum import Enum, EnumMeta

class IntStrTupleEnumMeta(EnumMeta):
    def __call__(cls, value, names=None, *args, **kwargs):
        if names is None and isinstance(value, int):
            for e in cls:
                if e.value[0] == value:
                    return e

        return super().__call__(value, names, **kwargs)

class IntStrTupleEnum(Enum, metaclass=IntStrTupleEnumMeta):
    pass

class Fingers(IntStrTupleEnum):
    THUMB = (1, 'thumb')
    INDEX = (2, 'index')
    MIDDLE = (3, 'middle')
    RING = (4, 'ring')
    PINKY = (5, 'pinky')

    def __str__(self):
        return self.value[1]

    @classmethod
    def from_string(cls, s):
        for finger in cls:
            if finger.value[1] == s:
                return finger
        raise ValueError(cls.__name__ + ' has no value matching "' + s + '"')

    def __repr__(self):
        return '<%s.%s %s>' % (self.__class__.__name__, self.name, self.value[0])

      

One difference between this implementation and a plain int Enum is that values ​​with the same integer but a different string (e.g. INDEX = (2, 'index')

and POINTER = (2, 'pointer')

) will not evaluate to the same object Finger

, whereas with a plain int Enum Finger.POINTER is Finger.INDEX

will evaluate to True

...

+1


source


Good question. However, this means that the Fingers object will render a tuple, not just an int, and the full set must be used to create Fingers objects, not just int. That is, you can do

f = Fingers((1, 'thumb')) 

      

but not

f = Fingers(1)

      

0


source


I had the same problem where I wanted to display strings for the ComboBox GUI (in PyGTK). I don't know about Pythonicity (is that even a word?) Solutions, but I used this:

from enum import IntEnum
class Finger(IntEnum):
    THUMB = 1
    INDEX = 2
    MIDDLE = 3
    RING = 4
    PINKY = 5

    @classmethod
    def names(cls):
        return ["Thumb", "Index", "Middle", "Ring", "Pinky"]

    @classmethod
    def tostr(cls, value):
        return cls.names()[value - cls.THUMB]

    @classmethod
    def toint(cls, s):
        return cls.names().index(s) + cls.THUMB

      

Using them from your code:

>>> print(Finger.INDEX)
Finger.INDEX
>>> print(Finger.tostr(Finger.THUMB))
Thumb
>>> print(Finger.toint("Ring"))
4

      

0


source


While this is not what the OP was asking about, it is still one good option for when you don't care if the value is int or not. You can use the value as a human readable string.

Source: https://docs.python.org/3/library/enum.html.

Omitting Values In many use cases, it doesn't matter what the actual value of the enumeration is. There are several ways to define this type of simple enumeration:

use auto instances for value; use object instances as a value; use a descriptive string as a value; use a tuple as a value and a custom new () method to replace a tuple with an int. Using any of these methods means to the user that these values ​​are not important, and also allows you to add, remove, or reorder elements without numbering the rest of the elements.

Whichever method you choose, you must provide repr (), which hides the (unimportant) value as well:

class NoValue(Enum):
     def __repr__(self):
         return '<%s.%s>' % (self.__class__.__name__, self.name)

      

Using a descriptive string Using a string as a value would look like this:

class Color(NoValue):
     RED = 'stop'
     GREEN = 'go'
     BLUE = 'too fast!'

Color.BLUE
<Color.BLUE>
Color.BLUE.value
'too fast!'

      

0


source







All Articles