Class dictionary inheritance with metaclasses

I am setting a property of a class fields

using a metaclass:

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 = {}

      

The following instance produces the expected results:

class SubClass(MyBaseClass):
    param1 = 1 # meets <my_condition>

>>> SubClass.fields
{param1: 1}

      

But if I now subclass SubClass

, fields

empty:

class SubSubClass(SubClass):
   pass

>>> SubSubClass.fields 
{}

      

How can I update the classdict of all classes in the inheritance hierarchy so that the variable is fields

updated from the base classes?

+3


source to share


2 answers


You need to store the fields

superclasses somehow , for example by iterating over "bases" and using them fields

as a starting point:

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)

      



And it works for SubClass

and SubSubClass

:

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

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

      

+4


source


I suggest turning fields

into a property descriptor that fetches all content _fields

from parent classes. This way, you can also more easily customize what happens when name conflicts, etc.

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() }

      

Using:

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

class SubClass(MyBaseClass):
    param1 = 1

>>> SubClass.fields
{param1: 1}

class SubSubClass(SubClass):
   pass

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

      

Usage now SomeChildClass.fields

always refers to the metaclass property. The third argument getattr

allows classes with no attribute _fields

(for example object

) to fail.



Using a descriptor also has the advantage that a child class cannot accidentally override an attribute fields

:

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

      

You can also create a setter if needed and use it in the method __init__

(i.e. revert to using fields

instead _fields

) so that the rest of the class is implementation agnostic:

    @fields.setter
    def fields(cls, mapping):
        try:
            cls._fields.update(**mapping)
        except AttributeError:
            cls._fields = dict(**mapping)

      

+1


source







All Articles