我什么时候应该继承 EnumMeta 而不是 Enum?
When should I subclass EnumMeta instead of Enum?
在 this article Nick Coghlan talks about some of the design decisions that went in to the PEP 435 Enum
type 中,以及如何 EnumMeta
被子class 以提供不同的 Enum
体验。
然而,我给出的关于使用 metaclass 的建议(我是 stdlib Enum
的主要作者)是,如果没有真正充分的理由,就不应该这样做——比如不能够使用 class 装饰器或隐藏任何丑陋的专用功能来完成您的需求;在我自己的工作中,我已经能够通过在创建 Enum
时使用 __new__
、__init__
、and/or 普通 class/instance 方法来做我需要的任何事情class:
Enum
with attributes
class constants that are not Enum
members
然后有一个警示故事,在研究 Enum
时要小心,无论是否有 metaclass subclassing:
- Is it possible to override
__new__
in an enum to parse strings to an instance?
考虑到所有这些,我什么时候需要 fiddle 和 EnumMeta
本身?
到目前为止,我所看到的关于 subclassing EnumMeta
的最佳(也是唯一)案例来自以下四个问题:
我们将在此处进一步研究动态成员案例。
首先,看一下不使用 subclass 时所需的代码 EnumMeta
:
stdlib方式
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":"Afghanistan","alpha-2":"AF","country-code":"004"},
{"name":"Åland Islands","alpha-2":"AX","country-code":"248"},
{"name":"Albania","alpha-2":"AL","country-code":"008"},
{"name":"Algeria","alpha-2":"DZ","country-code":"012"}]
这里是 JSONEnumMeta
class:
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__
returns一个普通的dict
EnumMeta.__prepare__
用于获取 _EnumDict
的实例——这是获取 one
的正确方法
带有前导下划线的键首先传递给真正的 _EnumDict
,因为在处理枚举成员时可能需要它们
枚举成员的顺序与它们在文件中的顺序相同
1 披露:我是 Python stdlib Enum
, the enum34
backport, and the Advanced Enumeration (aenum
) 库的作者。
2 这需要aenum 2.0.5+
.
3 如果您的 Enum
需要多个值,键是数字的,以便按顺序排列多个值。
在 this article Nick Coghlan talks about some of the design decisions that went in to the PEP 435 Enum
type 中,以及如何 EnumMeta
被子class 以提供不同的 Enum
体验。
然而,我给出的关于使用 metaclass 的建议(我是 stdlib Enum
的主要作者)是,如果没有真正充分的理由,就不应该这样做——比如不能够使用 class 装饰器或隐藏任何丑陋的专用功能来完成您的需求;在我自己的工作中,我已经能够通过在创建 Enum
时使用 __new__
、__init__
、and/or 普通 class/instance 方法来做我需要的任何事情class:
Enum
with attributesclass constants that are not
Enum
members
然后有一个警示故事,在研究 Enum
时要小心,无论是否有 metaclass subclassing:
- Is it possible to override
__new__
in an enum to parse strings to an instance?
考虑到所有这些,我什么时候需要 fiddle 和 EnumMeta
本身?
到目前为止,我所看到的关于 subclassing EnumMeta
的最佳(也是唯一)案例来自以下四个问题:
我们将在此处进一步研究动态成员案例。
首先,看一下不使用 subclass 时所需的代码 EnumMeta
:
stdlib方式
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":"Afghanistan","alpha-2":"AF","country-code":"004"},
{"name":"Åland Islands","alpha-2":"AX","country-code":"248"},
{"name":"Albania","alpha-2":"AL","country-code":"008"},
{"name":"Algeria","alpha-2":"DZ","country-code":"012"}]
这里是 JSONEnumMeta
class:
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__
returns一个普通的dict
EnumMeta.__prepare__
用于获取_EnumDict
的实例——这是获取 one 的正确方法
带有前导下划线的键首先传递给真正的
_EnumDict
,因为在处理枚举成员时可能需要它们枚举成员的顺序与它们在文件中的顺序相同
1 披露:我是 Python stdlib Enum
, the enum34
backport, and the Advanced Enumeration (aenum
) 库的作者。
2 这需要aenum 2.0.5+
.
3 如果您的 Enum
需要多个值,键是数字的,以便按顺序排列多个值。