使用元类继承类字典

6

我正在使用元类设置类属性fields

class MyMeta(type):
    def __new__(mcs, name, bases, clsdict):

        clsdict['fields'] = {k: v
                             for k, v in clsdict.items()
                             if <my_condition>}
        return super(MyMeta, mcs).__new__(mcs, name, bases, clsdict)

class MyBaseClass(metaclass=MyMeta):
    fields = {}

以下实例会得到预期的结果:
class SubClass(MyBaseClass):
    param1 = 1 # meets <my_condition>

>>> SubClass.fields
{param1: 1}

但是如果我现在子类化SubClassfields 就是空的:

class SubSubClass(SubClass):
   pass

>>> SubSubClass.fields 
{}

我该如何更新继承层次结构中所有类的classdict,以便从基类更新fields变量?
2个回答

5

你需要以某种方式保留超类的fields,例如通过迭代“bases”并使用它们的fields作为起点:

class MyMeta(type):
    def __new__(mcs, name, bases, clsdict):
        if 'fields' not in clsdict:
            clsdict['fields'] = {}
        # Initialize "fields" from base classes
        for base in bases:
            try:
                clsdict['fields'].update(base.fields)
            except AttributeError:
                pass
        # Fill in new fields (I included a "trivial" condition here, just use yours instead.)
        clsdict['fields'].update({k: v for k, v in clsdict.items() if k.startswith('param')})
        return super(MyMeta, mcs).__new__(mcs, name, bases, clsdict)

它适用于SubClassSubSubClass

>>> SubClass.fields
{'param1': 1}

>>> SubSubClass.fields
{'param1': 1}

谢谢,就这些了。唯一的问题是:为什么在 clsdict['fields'].update(base.fields) 上要加上 try/except?如果基类缺少 fields,你已经设置了它。 - ProfHase85
2
@ProfHase85: 因为一个子类可以有多个基类,其中一些可能没有“fields”属性。 - martineau

2
我建议将fields转化为一个属性描述符,在其中获取来自父类中_fields的所有内容。这样,当存在名称冲突等情况时,您也可以更轻松地定制其处理方式。
class MyMeta(type):
    def __new__(mcs, name, bases, clsdict):
        # change fields to _fields
        clsdict['_fields'] = {k: v
                             for k, v in clsdict.items()
                             if <my_condition>}
        return super(MyMeta, mcs).__new__(mcs, name, bases, clsdict)
    @property
    def fields(cls):
        # reversed makes most recent key value override parent values
        return {k:v 
                for c in reversed(cls.__mro__) 
                for k,v in getattr(c, '_fields', {}).items() }

使用方法:

class MyBaseClass(metaclass=MyMeta):
    fields = {}

class SubClass(MyBaseClass):
    param1 = 1

>>> SubClass.fields
{param1: 1}

class SubSubClass(SubClass):
   pass

>>> SubSubClass.fields 
{param1: 1} # success

现在,SomeChildClass.fields的使用总是指向元类属性。 getattr的第三个参数允许没有_fields属性(如object)的类静默失败。

使用描述符还有一个好处,可以防止子类意外覆盖fields属性:

>>> SubSubClass.fields = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

如果需要的话,您也可以创建一个setter,并在__init__方法中使用它(即返回使用fields而不是_fields),这样整个类的实现就与具体实现无关。
    @fields.setter
    def fields(cls, mapping):
        try:
            cls._fields.update(**mapping)
        except AttributeError:
            cls._fields = dict(**mapping)

1
这个比被接受的答案更好,因为它提供了更新属性的实时查看。但是你可以直接在元类中使用 cls.fields 属性中设置的 collections.ChainMap。这样一来,在 fields 属性的每次访问中,就不需要创建你所做的字典视图。 - jsbueno

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接