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

Is it possible to override __new__ in an enum to parse strings to an instance?

我想把字符串解析成python枚举。通常可以实现一个解析方法来实现这一点。几天前,我发现了一个新的方法,它可以根据给定的参数返回不同的实例。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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脚本时,会收到以下消息:

1
2
3
4
5
[...]
  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:

此枚举用于URI分析,特别是用于分析架构。所以我的URI看起来像这样

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

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

1
type = Types(splitList[0])

类型现在应包含具有3个可能值(未知、源、netlist)的枚举类型的值。

如果我允许在枚举的成员列表中使用别名,就不可能重复枚举的值alias free。


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

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

在这种情况下,凌驾于__call__之上可能是多余的。相反,您可以轻松地使用名称别名:

1
2
3
4
5
6
7
8
class Types(enum.Enum):
    Unknown = 0

    Source = 1
    src = 1

    NetList = 2
    nl = 2

这里,Types.nl是一个别名,将返回与Types.Netlist相同的对象。然后通过名称访问成员(使用Types[..]索引访问);因此Types['nl']工作并返回Types.Netlist

您认为不可能迭代枚举的值alias free的断言是错误的。迭代显式不包括别名:

Iterating over the members of an enum does not provide the aliases

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

演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
>>> 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;我将使用异常处理:

1
2
3
4
try:
    scheme = Types[scheme]
except KeyError:
    scheme = Types.Unknown

超越__call__

如果要将字符串视为值,并使用调用而不是项访问,则这是重写元类的__call__方法的方法:

1
2
3
4
5
6
7
8
9
10
11
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

演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> 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支持名称别名,您似乎需要值别名。重写__call__可以提供一个传真,但是我们可以通过将值别名的定义放入枚举类本身来做得更好。例如,如果指定重复的名称会给您提供值别名呢?

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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)

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

1
2
3
4
5
6
7
8
class Types(enum.Enum, metaclass=ValueAliasEnumMeta):
    Unknown = 0

    Source = 1
    Source = 'src'

    NetList = 2
    NetList = 'nl'

演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> 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>


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

我的意思是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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)

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

更新

下面是一个表驱动的版本,它有助于消除一些重复的编码,否则会涉及到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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()}
Types._str_to_value = dict(Types._str_to_value) # convert to regular dict (optional)

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()
    print(list(Types))  # iterate values

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


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

"必须使用实例myEnum作为第一个参数调用未绑定方法()(改为获取Enummeta实例)"

我可以通过改变:

1
2
3
4
# 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:

1
2
3
4
5
# 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中都进行了正确的测试。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
from enum import Enum

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

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

1
2
3
4
>>> Types.src
<Types.src: 1>
>>> Types.Source
<Types.src: 1>

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

1
2
3
4
5
6
7
8
>>> Types.__str__ = lambda self: self.name
>>> Types.__format__ = lambda self, _: self.name
>>> str(Types.Unknown)
'??'
>>> '{}'.format(Types.Source)
'src'
>>> Types['src']
<Types.src: 1>

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


Is it possible to override __new__ in a python enum to parse strings to an instance?

总之,是的。正如Martineau所说明的,在类被声明之后,可以替换__new__方法(它的原始代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)

而且正如他的演示代码所示,如果您不十分小心,您将打破其他事情,如酸洗,甚至基本成员逐值查找:

1
2
3
4
5
6
7
8
9
--> 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以获得我们想要的东西的聪明方法:

1
2
3
4
5
6
7
8
9
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子类中,通过创建一个新的类方法也可以很好地处理它。

您需要的类是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

在行动中:

1
2
3
4
5
6
7
8
9
--> for obj in Types:
...   print(obj)
...
??
src
nl

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

如果您确实需要值别名,甚至可以在不使用元类黑客的情况下处理它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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'))

这给了我们:

1
2
3
[<Types.Unknown: 0>, <Types.Source: 1>, <Types.NetList: 2>]
Types.Source
Types.Source