我什么时候应该继承 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 时要小心,无论是否有 metaclass subclassing:

考虑到所有这些,我什么时候需要 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 需要多个值,键是数字的,以便按顺序排列多个值。