Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Associating string representations with an Enum that uses integer values?

I'm trying to create an enum that has integer values, but which can also return a display-friendly string for each value. I was thinking that I could just define a dict mapping values to strings and then implement __str__ and a static constructor with a string argument, but there's a problem with that...

(Under different circumstances I could have just made the underlying data type for this Enum a string rather than an integer, but this is being used as a mapping for an enum database table, so both the integer value and the string are meaningful, the former being a 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 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 seems that the issue is that an Enum will use all class variables as the enum values, which causes them to return objects of the Enum type, rather than their underlying type.

A few workarounds I can think of include:

  1. Referring to the dict as Fingers._display_strings.value. (However then Fingers.__display_strings becomes a valid enum value!)
  2. Making the dict a module variable instead of a class variable.
  3. Duplicating the dict (possibly also breaking it down into a series of if statements) in the __str__ and from_string functions.
  4. Rather than make the dict a class variable, define a static method _get_display_strings to return the dict, so it doesn't become an enum value.

Note that the initial code above and workaround 1. uses the underlying integer values as the dict keys. The other options all require that the dict (or if tests) are defined somewhere other than directly in the class itself, and so it must qualify these values with the class name. So you could only use, e.g., Fingers.THUMB to get an enum object, or Fingers.THUMB.value to get the underlying integer value, but not just THUMB. If using the underlying integer value as the dict key, then you must also use it to look up the dict, indexing it with, e.g., [Fingers.THUMB.value] rather than just [Fingers.THUMB].

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

like image 482
David Scarlett Avatar asked May 09 '17 06:05

David Scarlett


People also ask

Can we assign string to enum?

We can use Enum. GetName static method to convert an enum value to a string. The following code uses Enum. GetName static method that takes two arguments, the enum type and the enum value.

Can we use integer in enum?

No, we can have only strings as elements in an enumeration.

How do you find the enum value of an integer?

To get the value of enum we can simply typecast it to its type. In the first example, the default type is int so we have to typecast it to int. Also, we can get the string value of that enum by using the ToString() method as below.


4 Answers

This can be done with the stdlib Enum, but is much easier with aenum1:

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 do look-ups via the string value then implement the new class method _missing_value_ (just _missing_ in the 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 stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.

like image 107
Ethan Furman Avatar answered Oct 22 '22 19:10

Ethan Furman


Maybe I am missing the 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 is what you want.

like image 31
BoarGules Avatar answered Oct 22 '22 20:10

BoarGules


The python docs have a somewhat abstract example here, from which I was able to come up with this solution

I have added an explanation inline, as comments.

# we could also do class Finger(IntEnum) it's equivalent.
class Finger(int, Enum):
    def __new__(cls, value, label):
        # Initialise an instance of the Finger enum class 
        obj = int.__new__(cls, value)
        # Calling print(type(obj)) returns <enum 'Finger'>
        # If we don't set the _value_ in the Enum class, an error will be raised.
        obj._value_ = value
        # Here we add an attribute to the finger class on the fly.
        # One may want to use setattr to be more explicit; note the python docs don't do this
        obj.label = label
        return obj

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

    @classmethod
    def from_str(cls, input_str):
        for finger in cls:
            if finger.label == input_str:
                return finger
        raise ValueError(f"{cls.__name__} has no value matching {input_str}")

So let's test it.

In [99]: Finger(1)
Out[99]: <Finger.THUMB: 1>

In [100]: Finger.from_str("thumb")
Out[100]: <Finger.THUMB: 1>

In [101]: Finger.THUMB
Out[101]: <Finger.THUMB: 1>

In [102]: Finger.THUMB.label
Out[102]: 'thumb'

The last test here is quite important, the __str__ method is automatically created depending on the inheritance class Finger(int, Enum).

If this was instead class Finger(str, Enum) and obj = int.__new__(cls, value) became obj = str.__new__(cls, value) all the checks above would work but the call to __str__ would've raised an error.

In [103]: f"Finger.THUMB"
Out[103]: '1'

An example of it being used in python3 standard library http.HTTPStatus

like image 35
Mark Avatar answered Oct 22 '22 19:10

Mark


Another solution I came up with is, since both the integers and the strings are meaningful, was to make the Enum values (int, str) tuples, as follows.

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 object's repr will display the tuple rather than just the int, and the complete tuple must be used to create Fingers objects, not just the int. I.e. 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 complex workaround for that involves subclassing Enum's metaclass to implement a custom __call__. (At least overriding __repr__ is much simpler!)

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 value but a different string (e.g. INDEX = (2, 'index') and POINTER = (2, 'pointer')) would not evaluate as the same Finger object, whereas with a plain int Enum, Finger.POINTER is Finger.INDEX would evaluate to True.

like image 36
David Scarlett Avatar answered Oct 22 '22 20:10

David Scarlett