关于Python的:当我应该收藏指正enummeta而不是enum?

When should I subclass EnumMeta instead of Enum?

在本文中,Nick Coghlan讨论了一些涉及到PEP 435 Enum类型的设计决策,以及如何将EnumMeta子类化以提供不同的Enum体验。

然而,关于使用元类,我给出的建议(我是主要的stdlib Enum作者)是,在没有真正好的理由的情况下,不应该这样做——比如不能用类修饰器完成你所需要的,或者用一个专门的函数来隐藏任何丑陋之处;在我自己的工作中,我能够做任何我只需要你做的事情。创建Enum类时,使用__new____init__和/或普通类/实例方法:

  • 带属性的Enum

  • 处理缺少的成员

  • 不是Enum成员的类常量

还有一个值得注意的故事,就是在深入研究Enum时要小心,无论是否使用元类子类:

  • 是否可以重写枚举中的__new__以将字符串解析到实例?

考虑到这些,我什么时候需要摆弄EDOCX1[1]本身呢?


迄今为止,我所看到的最好(也是唯一)的EnumMeta子类化案例来自这两个问题:

  • 用动态成员定义枚举的一种更为python的方法

  • python枚举防止无效的属性分配

我们将在这里进一步研究动态成员案例。

首先,看一下在不将EnumMeta子类化时所需的代码:

STDLIB方式

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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文件创建枚举对您来说很常见呢?想象一下,如果你可以这样做:

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

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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 Enumenum34backport和advanced enumeration(aenum库的作者。

2这就需要aenum 2.0.5+

3如果您的Enum需要多个值,则键是数字以保持多个值的顺序。