首页 > 解决方案 > 如何创建允许同义词的 Python Enum?

问题描述

我正在规范化现有的、混乱的数据,我想创建一个Enum允许成员规范名称的同义词,这样如果有人在实例化枚举时使用同义词值,他们就会得到规范的值。IE。

class TrainOutcome(enum.Enum):
    PASSED = "PASSED"
    SUCCESS = "PASSED" # Deprecated synonym for "PASSED"
    FAILED = "FAILED"
    STARTED = "STARTED"

这执行得很好,但生成的枚举没有按预期运行:

>>> TrainOutcome("PASSED")
<TrainOutcome.PASSED: 'PASSED'>

# I want to get <TrainOutcome.PASSED: 'PASSED'> here as well
>>> TrainOutcome("SUCCESS")
ValueError: 'SUCCESS' is not a valid TrainOutcome

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.8/enum.py", line 309, in __call__
    return cls.__new__(cls, value)
  File "/usr/lib/python3.8/enum.py", line 600, in __new__
    raise exc
  File "/usr/lib/python3.8/enum.py", line 584, in __new__
    result = cls._missing_(value)
  File "/usr/lib/python3.8/enum.py", line 613, in _missing_
    raise ValueError("%r is not a valid %s" % (value, cls.__name__))
ValueError: 'SUCCESS' is not a valid TrainOutcome

尽管该__members__属性似乎完全按照我希望的方式映射事物:

>>> TrainOutcome.__members__
mappingproxy({'PASSED': <TrainOutcome.PASSED: 'PASSED'>, 'SUCCESS': <TrainOutcome.PASSED: 'PASSED'>, 'FAILED': <TrainOutcome.FAILED: 'FAILED'>, 'STARTED': <TrainOutcome.STARTED: 'STARTED'>})
>>> TrainOutcome['SUCCESS']
<TrainOutcome.PASSED: 'PASSED'>
>>> TrainOutcome['PASSED']
<TrainOutcome.PASSED: 'PASSED'>

如何创建枚举,以便构造函数接受并返回与索引类型相同的值?

编辑:现有的具有重复值的 Python 枚举不能回答我的问题,因为本质上它试图实现与我所追求的相反。那里的 OP 想让结果值更加明显,我想让它们不那么明显。实际上,理想的解决方案是根本没有同义词成员(因为我使用的是生成Enum的 SQLAlchemy 上下文,它查看成员名称,而不是它们的值),并且只是在构建期间默默地替换为,"SUCCESS"但是在调用的枚举上"PASSED"定义自定义似乎不起作用。__init__super()

编辑: 这个问题和答案提供了迄今为止最简单的解决方案:使用aenum.MultiValueEnum.

否则,这是一个本土解决方案,似乎符合您在 Python 3.6+ 中应该如何做的精神,在某种程度上受到@Green Cloak Guy 的回答的启发:

class EnumSynonymMixin:
    """
    Enum mixin which provides the ability to define synonyms,
    ie. values which can be passed into an enum's constructor, that
    name the same member as one of the defined values, without adding
    any extra members (useful for using with SQLAlchemy's Enum mapping)

    For example:

    class MyEnum(EnumSynonymMixin, enum.Enum):
        FOO = "FOO"
        BAR = "BAR"

        @classmethod
        def synonyms(cls):
            return {"MYFOO": "FOO"}
    
    >>> MyEnum("MYFOO")
    <MyEnum.FOO: 'FOO'>
    """
    @classmethod
    def synonyms(cls):
        """Override to provide a dictionary of synonyms for values that can be
        passed to the constructor"""
        return {}

    @classmethod
    def _missing_(cls, val):
        synonyms = cls.synonyms()
        if val in synonyms:
            return cls.__members__[synonyms[val]]
        return super()._missing(val)


class TrainOutcome(EnumSynonymMixin, enum.Enum):
    PASSED = "PASSED"
    FAILED = "FAILED"
    STARTED = "STARTED"

    @classmethod
    def synonyms(cls):
        return {"SUCCESS": "PASSED"}

标签: pythonenums

解决方案


这应该做你想要的。本质上,类包装器的TrainOutcome(value)行为类似于TrainOutcome[value]前者会产生错误(如您所描述的情况,您试图用“SUCCESS”调用它)。它通过拦截调用__new__()并替换第一个参数来做到这一点。

根据对您问题的评论,您可能不应该这样做 - 我几乎没有理由想到为什么TrainOutcome['SUCCESS']不能满足您的需求。

def callActsLikeGetitem(c):
    oldnew = c.__new__
    def newwrapper(cls, *args, **kwargs):
        try:
            return oldnew(cls, *args, **kwargs)
        except ValueError:
            if len(args) > 0:
                args = (cls[args[0]].name, *args[1:])
            return oldnew(cls, *args, **kwargs)
    c.__new__ = newwrapper
    return c

@callActsLikeGetitem
class TrainOutcome(enum.Enum):
    PASSED = "PASSED"
    SUCCESS = "PASSED" # Deprecated synonym for "PASSED"
    FAILED = "FAILED"
    STARTED = "STARTED"

TrainOutcome("SUCCESS")
# <TrainOutcome.PASSED: 'PASSED'>

推荐阅读