我啥时候应该继承 EnumMeta 而不是 Enum?
Posted
技术标签:
【中文标题】我啥时候应该继承 EnumMeta 而不是 Enum?【英文标题】:When should I subclass EnumMeta instead of Enum?我什么时候应该继承 EnumMeta 而不是 Enum? 【发布时间】:2017-09-29 12:42:43 【问题描述】:在this article 中,Nick Coghlan 谈到了涉及PEP 435 Enum
type 的一些设计决策,以及如何将EnumMeta
子类化以提供不同的Enum
体验。
但是,我给出的关于使用元类的建议(并且我是主要的 stdlib Enum
作者)是,如果没有非常好的理由,不应该这样做——例如无法完成你需要的类装饰器,或隐藏任何丑陋的专用函数;在我自己的工作中,我可以在创建Enum
类时使用__new__
、__init__
和/或普通类/实例方法来做任何我需要的事情:
Enum
with attributes
Handling missing members
class constants that are not Enum
members
然后有一个警示故事,即在研究 Enum
时要小心,有和没有元类子类化:
__new__
in an enum to parse strings to an instance?
考虑到所有这些,我什么时候需要摆弄EnumMeta
本身?
【问题讨论】:
【参考方案1】:迄今为止我见过的最好的(也是唯一的)子类化EnumMeta
的案例来自以下四个问题:
A more pythonic way to define an enum with dynamic members
Prevent invalid enum attribute assignment
Create an abstract Enum class
Invoke a function when an enum member is accessed
我们将在此处进一步研究动态成员案例。
首先看一下不继承EnumMeta
时需要的代码:
标准库方式
from enum import Enum
import json
class BaseCountry(Enum):
def __new__(cls, record):
member = object.__new__(cls)
member.country_name = record['name']
member.code = int(record['country-code'])
member.abbr = record['alpha-2']
member._value_ = member.abbr, member.code, member.country_name
if not hasattr(cls, '_choices'):
cls._choices =
cls._choices[member.code] = member.country_name
cls._choices[member.abbr] = member.country_name
return member
def __str__(self):
return self.country_name
Country = BaseCountry(
'Country',
[(rec['alpha-2'], rec) for rec in json.load(open('slim-2.json'))],
)
aenum
方式 12
from aenum import Enum, MultiValue
import json
class Country(Enum, init='abbr code country_name', settings=MultiValue):
_ignore_ = 'country this' # do not add these names as members
# create members
this = vars()
for country in json.load(open('slim-2.json')):
this[country['alpha-2']] = (
country['alpha-2'],
int(country['country-code']),
country['name'],
)
# have str() print just the country name
def __str__(self):
return self.country_name
上面的代码对于一次性枚举来说很好——但是如果从 JSON 文件创建枚举对你来说很常见呢?想象一下,如果你可以这样做:
class Country(JSONEnum):
_init_ = 'abbr code country_name' # remove if not using aenum
_file = 'some_file.json'
_name = 'alpha-2'
_value =
1: ('alpha-2', None),
2: ('country-code', lambda c: int(c)),
3: ('name', None),
如你所见:
_file
是要使用的 json 文件的名称
_name
是名称应使用的路径
_value
是映射路径到值的字典3
_init_
指定不同值组件的属性名称(如果使用aenum
)
JSON 数据取自 https://github.com/lukes/ISO-3166-Countries-with-Regional-Codes -- 这是一个简短的摘录:
["name":"阿富汗","alpha-2":"AF","country-code":"004",
"name":"奥兰群岛","alpha-2":"AX","country-code":"248",
"name":"Albania","alpha-2":"AL","country-code":"008",
"name":"阿尔及利亚","alpha-2":"DZ","country-code":"012"]
这是JSONEnumMeta
类:
class JSONEnumMeta(EnumMeta):
@classmethod
def __prepare__(metacls, cls, bases, **kwds):
# return a standard dictionary for the initial processing
return
def __init__(cls, *args , **kwds):
super(JSONEnumMeta, cls).__init__(*args)
def __new__(metacls, cls, bases, clsdict, **kwds):
import json
members = []
missing = [
name
for name in ('_file', '_name', '_value')
if name not in clsdict
]
if len(missing) in (1, 2):
# all three must be present or absent
raise TypeError('missing required settings: %r' % (missing, ))
if not missing:
# process
name_spec = clsdict.pop('_name')
if not isinstance(name_spec, (tuple, list)):
name_spec = (name_spec, )
value_spec = clsdict.pop('_value')
file = clsdict.pop('_file')
with open(file) as f:
json_data = json.load(f)
for data in json_data:
values = []
name = data[name_spec[0]]
for piece in name_spec[1:]:
name = name[piece]
for order, (value_path, func) in sorted(value_spec.items()):
if not isinstance(value_path, (list, tuple)):
value_path = (value_path, )
value = data[value_path[0]]
for piece in value_path[1:]:
value = value[piece]
if func is not None:
value = func(value)
values.append(value)
values = tuple(values)
members.append(
(name, values)
)
# get the real EnumDict
enum_dict = super(JSONEnumMeta, metacls).__prepare__(cls, bases, **kwds)
# transfer the original dict content, _items first
items = list(clsdict.items())
items.sort(key=lambda p: (0 if p[0][0] == '_' else 1, p))
for name, value in items:
enum_dict[name] = value
# add the members
for name, value in members:
enum_dict[name] = value
return super(JSONEnumMeta, metacls).__new__(metacls, cls, bases, enum_dict, **kwds)
# for use with both Python 2/3
JSONEnum = JSONEnumMeta('JsonEnum', (Enum, ), )
几点说明:
JSONEnumMeta.__prepare__
返回一个普通的dict
EnumMeta.__prepare__
用于获取_EnumDict
的实例——这是获取实例的正确方法
带有前导下划线的键首先传递给真正的_EnumDict
,因为在处理枚举成员时可能需要它们
枚举成员的顺序与它们在文件中的顺序相同
1 披露:我是Python stdlib Enum
、enum34
backport 和Advanced Enumeration (aenum
) 库的作者。
2 这需要aenum 2.0.5+
。
3 如果您的Enum
需要多个值,则这些键是数字以保持多个值的顺序。
【讨论】:
我无法理解的是为什么使用 EnumMeta 而不是经典继承? @jossefaz:你的意思是为什么不添加一个Enum.from_json()
类方法?你当然可以。以上是关于我啥时候应该继承 EnumMeta 而不是 Enum?的主要内容,如果未能解决你的问题,请参考以下文章
我啥时候应该使用 CROSS APPLY 而不是 INNER JOIN?
我啥时候应该在子进程中使用`wait`而不是`communicate`?
我啥时候应该使用 StringComparison.InvariantCulture 而不是 StringComparison.CurrentCulture 来测试字符串是不是相等?