在枚举中重写__new__以解析字符串为实例可能吗?
我想把字符串转换成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:
这个枚举是在解析URI时使用的,特别是用来解析协议部分。所以我的URI看起来像这样:
nl:PoC.common.config
<schema>:<namespace>[.<subnamespace>*].entity
在进行简单的字符串分割操作后,我会把URI的第一部分传给枚举的创建。
type = Types(splitList[0])
现在,type应该包含一个枚举类型的值,这个枚举有三个可能的值(Unknown, Source, NetList)。
如果我允许在枚举的成员列表中使用别名,那么就无法不带别名地遍历枚举的值了。
5 个回答
我觉得解决你问题最简单的方法就是使用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()
调用的。
我没有足够的积分来评论被接受的答案,但在使用 enum34 包的 Python 2.7 中,运行时会出现以下错误:
“未绑定的方法 <lambda>() 必须以 MyEnum 实例作为第一个参数调用(而不是 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 中都测试通过了。
在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)
而且正如他的演示代码所示,如果你不够小心,你可能会破坏其他功能,比如序列化(pickling),甚至基本的按值查找成员:
--> 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
子类中优雅地处理。
你想要的类是:
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>
如果你真的需要值别名,即使这样也可以在不使用 metaclass 黑科技的情况下处理:
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
在你的 enum.Enum
类型中,__new__
方法是用来创建枚举值的新实例的,比如 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>
是的,你可以重写一个枚举(enum
)子类的 __new__()
方法来实现解析功能,但要小心哦。为了避免在两个地方都写整数编码,你需要在类定义之后单独定义这个方法,这样你就可以引用枚举中定义的符号名称。
我说的意思是:
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()}