10

我想将字符串解析为 python 枚举。通常人们会实现一种解析方法来做到这一点。几天前,我发现了__new__方法,它能够根据给定的参数返回不同的实例。

这是我的代码,它不起作用:

import enum
class Types(enum.Enum):
  Unknown = 0
  Source = 1
  NetList = 2

  def __new__(cls, value):
    if (value == "src"):  return Types.Source
#    elif (value == "nl"): return Types.NetList
#    else:                 raise Exception()

  def __str__(self):
    if (self == Types.Unknown):     return "??"
    elif (self == Types.Source):    return "src"
    elif (self == Types.NetList):   return "nl"

当我执行我的 Python 脚本时,我收到以下消息:

[...]
  class Types(enum.Enum):
File "C:\Program Files\Python\Python 3.4.0\lib\enum.py", line 154, in __new__
  enum_member._value_ = member_type(*args)
TypeError: object() takes no parameters

如何返回枚举值的正确实例?

编辑1:

此 Enum 用于 URI 解析,特别是用于解析模式。所以我的 URI 看起来像这样

nl:PoC.common.config
<schema>:<namespace>[.<subnamespace>*].entity

所以在一个简单的 string.split 操作之后,我会将 URI 的第一部分传递给枚举创建。

type = Types(splitList[0])

type 现在应该包含具有 3 个可能值(Unknown、Source、NetList)的枚举类型的值

如果我允许在枚举的成员列表中使用别名,就不可能自由地迭代枚举的值别名。

4

5 回答 5

13

__new__您的类型上的方法用于enum.Enum创建枚举的新实例,因此Types.Unknown,Types.Source等单例实例。枚举调用(例如Types('nl')由 处理EnumMeta.__call__,您可以对其进行子类化。

使用名称别名适合您的用例

在这种情况下,压倒__call__一切可能是矫枉过正。相反,您可以轻松地使用名称别名

class Types(enum.Enum):
    Unknown = 0

    Source = 1
    src = 1

    NetList = 2
    nl = 2

Types.nl是一个别名,将返回与相同的对象Types.Netlist。然后您按名称访问成员(使用Types[..]索引访问);所以Types['nl']有效并返回Types.Netlist

您关于无法免费迭代枚举值别名的断言是不正确的。迭代明确不包括别名

遍历枚举成员不提供别名

别名是Enum.__members__有序字典的一部分,如果您仍然需要访问这些。

一个演示:

>>> import enum
>>> class Types(enum.Enum):
...     Unknown = 0
...     Source = 1
...     src = 1
...     NetList = 2
...     nl = 2
...     def __str__(self):
...         if self is Types.Unknown: return '??'
...         if self is Types.Source:  return 'src'
...         if self is Types.Netlist: return 'nl'
... 
>>> list(Types)
[<Types.Unknown: 0>, <Types.Source: 1>, <Types.NetList: 2>]
>>> list(Types.__members__)
['Unknown', 'Source', 'src', 'NetList', 'nl']
>>> Types.Source
<Types.Source: 1>
>>> str(Types.Source)
'src'
>>> Types.src
<Types.Source: 1>
>>> str(Types.src)
'src'
>>> Types['src']
<Types.Source: 1>
>>> Types.Source is Types.src
True

这里唯一缺少的是将未知模式转换为Types.Unknown; 我会为此使用异常处理:

try:
    scheme = Types[scheme]
except KeyError:
    scheme = Types.Unknown

覆盖__call__

如果您想将您的字符串视为值,并使用调用而不是项目访问,这就是您覆盖__call__元类方法的方式:

class TypesEnumMeta(enum.EnumMeta):
    def __call__(cls, value, *args, **kw):
        if isinstance(value, str):
            # map strings to enum values, defaults to Unknown
            value = {'nl': 2, 'src': 1}.get(value, 0)
        return super().__call__(value, *args, **kw)

class Types(enum.Enum, metaclass=TypesEnumMeta):
    Unknown = 0
    Source = 1
    NetList = 2

演示:

>>> class TypesEnumMeta(enum.EnumMeta):
...     def __call__(cls, value, *args, **kw):
...         if isinstance(value, str):
...             value = {'nl': 2, 'src': 1}.get(value, 0)
...         return super().__call__(value, *args, **kw)
... 
>>> class Types(enum.Enum, metaclass=TypesEnumMeta):
...     Unknown = 0
...     Source = 1
...     NetList = 2
... 
>>> Types('nl')
<Types.NetList: 2>
>>> Types('?????')
<Types.Unknown: 0>

请注意,我们在这里将字符串值转换为整数,并将其余部分留给原始 Enum 逻辑。

完全支持别名

因此,enum.Enum支持名称别名,您似乎需要别名。覆盖__call__可以提供一种复制,但是我们可以通过将值别名的定义放入枚举类本身来做得更好。例如,如果指定重复名称会给您提供值别名怎么办?

您必须提供一个子类,enum._EnumDict因为它是防止名称被重用的类。我们假设第一个枚举值是默认值:

class ValueAliasEnumDict(enum._EnumDict):
     def __init__(self):
        super().__init__()
        self._value_aliases = {}

     def __setitem__(self, key, value):
        if key in self:
            # register a value alias
            self._value_aliases[value] = self[key]
        else:
            super().__setitem__(key, value)

class ValueAliasEnumMeta(enum.EnumMeta):
    @classmethod
    def __prepare__(metacls, cls, bases):
        return ValueAliasEnumDict()

    def __new__(metacls, cls, bases, classdict):
        enum_class = super().__new__(metacls, cls, bases, classdict)
        enum_class._value_aliases_ = classdict._value_aliases
        return enum_class

    def __call__(cls, value, *args, **kw):
        if value not in cls. _value2member_map_:
            value = cls._value_aliases_.get(value, next(iter(Types)).value)
        return super().__call__(value, *args, **kw)

然后,您可以在枚举类中定义别名和默认值:

class Types(enum.Enum, metaclass=ValueAliasEnumMeta):
    Unknown = 0

    Source = 1
    Source = 'src'

    NetList = 2
    NetList = 'nl'

演示:

>>> class Types(enum.Enum, metaclass=ValueAliasEnumMeta):
...     Unknown = 0
...     Source = 1
...     Source = 'src'
...     NetList = 2
...     NetList = 'nl'
... 
>>> Types.Source
<Types.Source: 1>
>>> Types('src')
<Types.Source: 1>
>>> Types('?????')
<Types.Unknown: 0>
于 2014-06-08T10:44:58.743 回答
10

是的,如果你小心的话,你可以重写子类的__new__()方法enum来实现解析方法,但是为了避免在两个地方指定整数编码,你需要在类之后单独定义方法,所以你可以引用枚举定义的符号名称。

这就是我的意思:

import enum

class Types(enum.Enum):
    Unknown = 0
    Source = 1
    NetList = 2

    def __str__(self):
        if (self == Types.Unknown):     return "??"
        elif (self == Types.Source):    return "src"
        elif (self == Types.NetList):   return "nl"
        else:                           raise TypeError(self)

def _Types_parser(cls, value):
    if not isinstance(value, str):
        # forward call to Types' superclass (enum.Enum)
        return super(Types, cls).__new__(cls, value)
    else:
        # map strings to enum values, default to Unknown
        return { 'nl': Types.NetList,
                'ntl': Types.NetList,  # alias
                'src': Types.Source,}.get(value, Types.Unknown)

setattr(Types, '__new__', _Types_parser)


if __name__ == '__main__':

    print("Types('nl') ->",  Types('nl'))   # Types('nl') -> nl
    print("Types('ntl') ->", Types('ntl'))  # Types('ntl') -> nl
    print("Types('wtf') ->", Types('wtf'))  # Types('wtf') -> ??
    print("Types(1) ->",     Types(1))      # Types(1) -> src

更新

这是一个更加表格驱动的版本,它消除了一些否则会涉及的重复编码:

from collections import OrderedDict
import enum

class Types(enum.Enum):
    Unknown = 0
    Source = 1
    NetList = 2
    __str__ = lambda self: Types._value_to_str.get(self)

# Define after Types class.
Types.__new__ = lambda cls, value: (cls._str_to_value.get(value, Types.Unknown)
                                        if isinstance(value, str) else
                                    super(Types, cls).__new__(cls, value))

# Define look-up table and its inverse.
Types._str_to_value = OrderedDict((( '??', Types.Unknown),
                                   ('src', Types.Source),
                                   ('ntl', Types.NetList),  # alias
                                   ( 'nl', Types.NetList),))
Types._value_to_str = {val: key for key, val in Types._str_to_value.items()}


if __name__ == '__main__':

    print("Types('nl')  ->", Types('nl'))   # Types('nl')  -> nl
    print("Types('ntl') ->", Types('ntl'))  # Types('ntl') -> nl
    print("Types('wtf') ->", Types('wtf'))  # Types('wtf') -> ??
    print("Types(1)     ->", Types(1))      # Types(1)     -> src

    print(list(Types))  # -> [<Types.Unknown: 0>, <Types.Source: 1>, <Types.NetList: 2>]

    import pickle  # Demostrate picklability
    print(pickle.loads(pickle.dumps(Types.NetList)) == Types.NetList)  # -> True

请注意,在 Python 3.7+ 中,常规字典是有序的,因此OrderedDict不需要在上面的代码中使用,它可以简化为:

# Define look-up table and its inverse.
Types._str_to_value = {'??': Types.Unknown,
                       'src': Types.Source,
                       'ntl': Types.NetList,  # alias
                       'nl': Types.NetList}
Types._value_to_str = {val: key for key, val in Types._str_to_value.items()}
于 2014-06-08T19:37:26.630 回答
5

是否可以在 python 枚举中覆盖__new__以将字符串解析为实例?

一句话,是的。正如 martineau 所示,您可以在类被实例化__new__替换该方法(他的原始代码):

class Types(enum.Enum):
    Unknown = 0
    Source = 1
    NetList = 2
    def __str__(self):
        if (self == Types.Unknown):     return "??"
        elif (self == Types.Source):    return "src"
        elif (self == Types.NetList):   return "nl"
        else:                           raise TypeError(self) # completely unnecessary

def _Types_parser(cls, value):
    if not isinstance(value, str):
        raise TypeError(value)
    else:
        # map strings to enum values, default to Unknown
        return { 'nl': Types.NetList,
                'ntl': Types.NetList,  # alias
                'src': Types.Source,}.get(value, Types.Unknown)

setattr(Types, '__new__', _Types_parser)

并且正如他的演示代码所示,如​​果您不是非常小心,您将破坏其他事情,例如酸洗,甚至是基本的成员按值查找:

--> print("Types(1) ->", Types(1))  # doesn't work
Traceback (most recent call last):
  ...
TypeError: 1
--> import pickle
--> pickle.loads(pickle.dumps(Types.NetList))
Traceback (most recent call last):
  ...
TypeError: 2

Martijn 展示了一种增强EnumMeta以获得我们想要的东西的聪明方法:

class TypesEnumMeta(enum.EnumMeta):
    def __call__(cls, value, *args, **kw):
        if isinstance(value, str):
            # map strings to enum values, defaults to Unknown
            value = {'nl': 2, 'src': 1}.get(value, 0)
        return super().__call__(value, *args, **kw)

class Types(enum.Enum, metaclass=TypesEnumMeta):
    ...

但这使我们有重复的代码,并针对 Enum 类型工作。

唯一缺少对您的用例的基本 Enum 支持的能力是能够将一个成员作为默认成员,但即使这样也可以Enum通过创建新的类方法在普通子类中优雅地处理。

你想要的课程是:

class Types(enum.Enum):
    Unknown = 0
    Source = 1
    src = 1
    NetList = 2
    nl = 2
    def __str__(self):
        if self is Types.Unknown:
            return "??"
        elif self is Types.Source:
            return "src"
        elif self is Types.NetList:
            return "nl"
    @classmethod
    def get(cls, name):
        try:
            return cls[name]
        except KeyError:
            return cls.Unknown

并在行动中:

--> for obj in Types:
...   print(obj)
... 
??
src
nl

--> Types.get('PoC')
<Types.Unknown: 0>

如果你真的需要值别名,即使这样也可以在不求助于元类黑客的情况下处理:

class Types(Enum):
    Unknown = 0, 
    Source  = 1, 'src'
    NetList = 2, 'nl'
    def __new__(cls, int_value, *value_aliases):
        obj = object.__new__(cls)
        obj._value_ = int_value
        for alias in value_aliases:
            cls._value2member_map_[alias] = obj
        return obj

print(list(Types))
print(Types(1))
print(Types('src'))

这给了我们:

[<Types.Unknown: 0>, <Types.Source: 1>, <Types.NetList: 2>]
Types.Source
Types.Source
于 2014-06-08T15:43:51.707 回答
2

我没有足够的代表来评论接受的答案,但在 Python 2.7 中使用 enum34 包,在运行时会发生以下错误:

“必须使用实例 MyEnum 作为第一个参数调用未绑定方法 <lambda>()(改为获取 EnumMeta 实例)”

我能够通过更改来纠正这个问题:

# define after Types class
Types.__new__ = lambda cls, value: (cls._str_to_value.get(value, Types.Unknown)
                                    if isinstance(value, str) else
                                    super(Types, cls).__new__(cls, value))

到以下内容,用 staticmethod() 包装 lambda:

# define after Types class
Types.__new__ = staticmethod(
    lambda cls, value: (cls._str_to_value.get(value, Types.Unknown)
                        if isinstance(value, str) else
                        super(Types, cls).__new__(cls, value)))

此代码在 Python 2.7 和 3.6 中都经过正确测试。

于 2017-05-01T16:38:57.210 回答
2

我认为迄今为止最简单的解决您的问题的方法是使用Enum该类的函数式 API,这在选择名称时提供了更多的自由,因为我们将它们指定为字符串:

from enum import Enum

Types = Enum(
    value='Types',
    names=[
        ('??', 0),
        ('Unknown', 0),
        ('src', 1),
        ('Source', 1),
        ('nl', 2),
        ('NetList', 2),
    ]
)

这将创建一个具有名称别名的枚举。注意列表中条目的顺序names。第一个将被选为默认值(也为 返回name),其他的被视为别名,但两者都可以使用:

>>> Types.src
<Types.src: 1>
>>> Types.Source
<Types.src: 1>

要将name属性用作返回值,str(Types.src)我们将默认版本替换为Enum

>>> Types.__str__ = lambda self: self.name
>>> Types.__format__ = lambda self, _: self.name
>>> str(Types.Unknown)
'??'
>>> '{}'.format(Types.Source)
'src'
>>> Types['src']
<Types.src: 1>

请注意,我们还替换了__format__由 调用的方法str.format()

于 2017-04-08T22:18:30.357 回答