关于python:动态子类化Enum基类

Dynamically subclass an Enum base class

我已经设置了一个元类和基类对,用于创建必须解析的几种不同文件类型的行规范。

我决定使用枚举,因为同一文件中不同行的许多单独部分通常具有相同的名称。枚举使区分它们变得容易。此外,规范是刚性的,不需要添加更多的成员,也不需要在以后扩展行规范。

规范类按预期工作。但是,我在动态创建它们时遇到一些困难:

1
2
>>> C1 = LineMakerMeta('C1', (LineMakerBase,), dict(a = 0))
AttributeError: 'dict' object has no attribute '_member_names'

有办法解决这个问题吗?下面的示例工作得很好:

1
2
3
4
5
6
7
8
class A1(LineMakerBase):
    Mode = 0, dict(fill=' ', align='>', type='s')
    Level = 8, dict(fill=' ', align='>', type='d')
    Method = 10, dict(fill=' ', align='>', type='d')
    _dummy = 20 # so that Method has a known length

A1.format(**dict(Mode='DESIGN', Level=3, Method=1))
# produces '  DESIGN 3         1'

元类基于enum.EnumMeta,如下所示:

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
import enum

class LineMakerMeta(enum.EnumMeta):
   "Metaclass to produce formattable LineMaker child classes."
    def _iter_format(cls):
       "Iteratively generate formatters for the class members."
        for member in cls:
            yield member.formatter
    def __str__(cls):
       "Returns string line with all default values."
        return cls.format()
    def format(cls, **kwargs):
       "Create formatted version of the line populated by the kwargs members."
        # build resulting string by iterating through members
        result = ''
        for member in cls:
            # determine value to be injected into member
            try:
                try:
                    value = kwargs[member]
                except KeyError:
                    value = kwargs[member.name]
            except KeyError:
                value = member.default
            value_str = member.populate(value)
            result = result + value_str
        return result

基本类如下:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
class LineMakerBase(enum.Enum, metaclass=LineMakerMeta):
   """A base class for creating Enum subclasses used for populating lines of a file.

    Usage:

    class LineMaker(LineMakerBase):
        a = 0,      dict(align='>', fill=' ', type='f'), 3.14
        b = 10,     dict(align='>', fill=' ', type='d'), 1
        b = 15,     dict(align='>', fill=' ', type='s'), 'foo'
        #   ^-start ^---spec dictionary                  ^--default
   """

    def __init__(member, start, spec={}, default=None):
        member.start = start
        member.spec = spec
        if default is not None:
            member.default = default
        else:
            # assume value is numerical for all provided types other than 's' (string)
            default_or_set_type = member.spec.get('type','s')
            default = {'s': ''}.get(default_or_set_type, 0)
            member.default = default
    @property
    def formatter(member):
       """Produces a formatter in form of '{0:<format>}' based on the member.spec
        dictionary. The member.spec dictionary makes use of these keys ONLY (see
        the string.format docs):
            fill align sign width grouping_option precision type"""

        try:
            # get cached value
            return '{{0:{}}}'.format(member._formatter)
        except AttributeError:
            # add width to format spec if not there
            member.spec.setdefault('width', member.length if member.length != 0 else '')
            # build formatter using the available parts in the member.spec dictionary
            # any missing parts will simply not be present in the formatter
            formatter = ''
            for part in 'fill align sign width grouping_option precision type'.split():
                try:
                    spec_value = member.spec[part]
                except KeyError:
                    # missing part
                    continue
                else:
                    # add part
                    sub_formatter = '{!s}'.format(spec_value)
                    formatter = formatter + sub_formatter
            member._formatter = formatter
            return '{{0:{}}}'.format(formatter)
    def populate(member, value=None):
       "Injects the value into the member's formatter and returns the formatted string."
        formatter = member.formatter
        if value is not None:
            value_str = formatter.format(value)
        else:
            value_str = formatter.format(member.default)
        if len(value_str) > len(member) and len(member) != 0:
            raise ValueError(
                    'Length of object string {} ({}) exceeds available'
                    ' field length for {} ({}).'
                    .format(value_str, len(value_str), member.name, len(member)))
        return value_str
    @property
    def length(member):
        return len(member)
    def __len__(member):
       """Returns the length of the member field. The last member has no length.
        Length are based on simple subtraction of starting positions."""

        # get cached value
        try:
            return member._length
        # calculate member length
        except AttributeError:
            # compare by member values because member could be an alias
            members = list(type(member))
            try:
                next_index = next(
                        i+1
                        for i,m in enumerate(type(member))
                        if m.value == member.value
                        )
            except StopIteration:
                raise TypeError(
                       'The member value {} was not located in the {}.'
                       .format(member.value, type(member).__name__)
                       )
            try:
                next_member = members[next_index]
            except IndexError:
                # last member defaults to no length
                length = 0
            else:
                length = next_member.start - member.start
            member._length = length
            return length


此行:

1
C1 = enum.EnumMeta('C1', (), dict(a = 0))

失败,错误消息完全相同。EnumMeta__new__方法希望最后一个参数是enum._EnumDict的一个实例。_EnumDictdict的一个子类,提供了一个名为_member_names的实例变量,当然常规dict没有。当您通过枚举创建的标准机制时,这一切都是在幕后正确发生的。这就是为什么你的另一个例子工作得很好。

此行:

1
C1 = enum.EnumMeta('C1', (), enum._EnumDict())

无错误运行。不幸的是,Enumdict的构造函数被定义为不带参数,因此您不能像您显然希望的那样用关键字初始化它。

在返回到python3.3的枚举的实现中,下面的代码块出现在EnumMeta的构造函数中。您可以在LineMakerMeta类中执行类似的操作:

1
2
3
4
5
6
def __new__(metacls, cls, bases, classdict):
    if type(classdict) is dict:
        original_dict = classdict
        classdict = _EnumDict()
        for k, v in original_dict.items():
            classdict[k] = v

在正式实现中,在python3.5中,if语句和随后的代码块由于某种原因而消失。所以以东十一〔8〕必须对神以东十一〔3〕诚实,我不明白这是为什么。在任何情况下,Enum的实现都是非常复杂的,并且处理了大量的角落案例。

我知道这不是对你问题的一个简单的回答,但我希望它能给你指出一个解决办法。


创建您的LineMakerBase类,然后这样使用:

1
C1 = LineMakerBase('C1', dict(a=0))

元类不应该像您试图使用它的方式那样使用。查看这个答案以获取何时需要元类子类的建议。

关于您的代码的一些建议:

两次尝试/除了在format中,似乎更清楚如下:

1
2
3
4
5
6
7
    for member in cls:
        if member in kwargs:
            value = kwargs[member]
        elif member.name in kwargs:
            value = kwargs[member.name]
        else:
            value = member.default

此代码:

1
2
# compare by member values because member could be an alias
members = list(type(member))

  • list(member.__class__)会更清楚
  • 有错误的评论:listing一个Enum类永远不会包含别名(除非你已经覆盖了EnumMeta的那部分)
  • 现在您没有复杂的__len__代码,只要您是EnumMeta的子类,就应该扩展__new__以自动计算长度一次:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # untested
    def __new__(metacls, cls, bases, clsdict):
        # let the main EnumMeta code do the heavy lifting
        enum_cls = super(LineMakerMeta, metacls).__new__(cls, bases, clsdict)
        # go through the members and calculate the lengths
        canonical_members = [
               member
               for name, member in enum_cls.__members__.items()
               if name == member.name
               ]
        last_member = None
        for next_member in canonical_members:
            next_member.length = 0
            if last_member is not None:
                last_member.length = next_member.start - last_member.start


    动态创建Enum子类的最简单方法是使用Enum本身:

    1
    2
    3
    4
    5
    6
    7
    8
    >>> from enum import Enum
    >>> MyEnum = Enum('MyEnum', {'a': 0})
    >>> MyEnum
    <enum 'MyEnum'>
    >>> MyEnum.a
    <MyEnum.a: 0>
    >>> type(MyEnum)
    <class 'enum.EnumMeta'>

    至于您的自定义方法,如果使用常规函数可能会更简单,这正是因为Enum实现非常特殊。