将字符串表示与使用整数值的枚举相关联?

时间:2017-05-09 06:01:00

标签: python python-3.x enums

我尝试创建一个具有整数值的枚举,但也可以为每个值返回一个显示友好的字符串。我以为我可以定义一个dict映射值到字符串,然后实现__str__和一个带有字符串参数的静态构造函数,但是这有问题......

(在不同的情况下,我可能只是将此Enum的基础数据类型设为字符串而不是整数,但这被用作枚举数据库表的映射,因此整数值和字符串都是有意义的,前者是主键。)

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 + '"')

转换为字符串时,出现以下错误:

>>> 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

似乎问题是Enum将使用所有类变量作为枚举值,这会导致它们返回Enum类型的对象,而不是它们的基础类型。

我能想到的一些解决方法包括:

  1. 将该字典称为Fingers._display_strings.value。 (但是Fingers.__display_strings成为有效的枚举值!)
  2. 使dict成为模块变量而不是类变量。
  3. if__str__函数中复制dict(可能还会将其分解为一系列from_string语句)。
  4. 不是让dict成为类变量,而是定义一个静态方法_get_display_strings来返回dict,因此它不会成为枚举值。
  5. 请注意,上面的初始代码和变通方法1.使用基础整数值作为dict键。其他选项都要求dict(或if测试)在类本身以外的地方定义,因此必须使用类名限定这些值。因此,您只能使用Fingers.THUMB来获取枚举对象,或Fingers.THUMB.value来获取基础整数值,但不仅仅是THUMB。如果使用基础整数值作为dict键,那么您还必须使用它来查找dict,使用例如[Fingers.THUMB.value]而不仅仅是[Fingers.THUMB]对其进行索引。

    所以,问题是,为Enum实现字符串映射的最佳或最Pythonic方法是什么,同时保留基础整数值?

7 个答案:

答案 0 :(得分:12)

这可以使用stdlib Enum完成,但aenum 1 更容易:

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

如果您希望能够通过字符串值进行查找,那么请在stdlib中实现新的类方法_missing_value_(仅_missing_):

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 披露:我是Python stdlib Enumenum34 backportAdvanced Enumeration (aenum)图书馆的作者。

答案 1 :(得分:3)

也许我在这里忽略了这一点,但如果你定义

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

然后在Python 3.6中你可以做到

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

我认为这就是你想要的。

答案 2 :(得分:1)

我提出的另一个解决方案是,因为整数和字符串都有意义,所以要使Enum值为(int, str)元组,如下所示。

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 + '"')

但是,这意味着Fingers对象的repr将显示元组而不仅仅是int,并且必须使用完整的元组来创建Fingers个对象,而不仅仅是int。即您可以f = Fingers((1, 'thumb')),但不能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

更复杂的解决方法是将Enum的元类子类化为实现自定义__call__。 (至少覆盖__repr__要简单得多!)

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])

此实现与普通int枚举之间的一个区别是具有相同整数值但不同字符串(例如INDEX = (2, 'index')POINTER = (2, 'pointer'))的值不会评估为相同Finger对象,而使用普通的int枚举,Finger.POINTER is Finger.INDEX将评估为True

答案 3 :(得分:1)

python 文档有一些abstract example here,我从中可以想出这个解决方案

我已内联添加了解释,作为注释。

# 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}")

让我们测试一下。

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'

这里的最后一个测试很重要,__str__ 方法是根据继承 class Finger(int, Enum) 自动创建的。

如果改为 class Finger(str, Enum) 并且 obj = int.__new__(cls, value) 变为 obj = str.__new__(cls, value),上述所有检查都可以工作,但对 __str__ 的调用会引发错误。

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

答案 4 :(得分:0)

好问题但是,这意味着Fingers对象的repr将显示元组而不仅仅是int,并且必须使用完整的元组来创建Fingers对象,而不仅仅是int。即你可以做到

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

但不是

f = Fingers(1)

答案 5 :(得分:0)

我遇到了同样的问题,我想显示GUI ComboBox的字符串(在PyGTK中)。 我不知道解决方案的Pythonic性(甚至是一个词?),但我使用了以下方法:

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

从您的代码中使用它们:

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

答案 6 :(得分:0)

尽管这不是OP所要求的,但是当您不在乎该值是否为int时,这仍然是一个不错的选择。您可以将该值用作人类可读的字符串。

来源:https://docs.python.org/3/library/enum.html

忽略值 在许多用例中,人们并不关心枚举的实际值是多少。定义这种简单枚举的方法有几种:

使用auto的实例作为值 使用对象的实例作为值 使用描述性字符串作为值 使用元组作为值,并使用自定义 new ()将intu值替换为元组 使用这些方法中的任何一种都向用户表示这些值并不重要,并且还使人们能够添加,删除或重新排序成员,而不必重新编号其余成员。

无论选择哪种方法,都应提供一个repr()来隐藏(不重要的)值:

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

使用描述性字符串 使用字符串作为值看起来像:

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

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