dd95
dd95

Reputation: 239

All subclasses of a class with a custom metaclass share the same attributes even though they shouldn't

I have defined a metaclass MyMeta and a custom attribute called MyAttributeType. Then I create 4 classes, the first, class MyObjectA has the metaclass attribute set to MyMeta, class MyObjectB inherits from MyObjectA and has a few attributes, then both MyObjectC and MyObjectD inherit from MyObjectB and they both have some extra attribute.

In the metaclass I add those attributes to a list, so I would expect that list to contain only the attributes of the class itself and of its parents, instead they all get all the attributes.

I think it is easier to look at the code, below is a minimal test, you can copy and run it and should work. Far below is the output vs the expected output.

class MyAttributeType(object):

    def __init__(self, name=None, node=None):
        self._name = name
        self._node = node

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    @property
    def node(self):
        return self._node

    @node.setter
    def node(self, value):
        self._node = value


class MyMeta(type):

    def __new__(mcs, clsname, clsbases, clsattributes):
        result_dict = {"_attributes": []}

        for base in clsbases:
            try:
                for key, value in base.__dict__.items():
                    if isinstance(value, property) or isinstance(value, MyAttributeType):
                        result_dict[key] = value
                result_dict["_attributes"] = base.__dict__["_attributes"]
            except KeyError:
                pass

        for key, value in clsattributes.items():
            if isinstance(value, MyAttributeType):

                if key.startswith("_"):
                     key = key[1:]

                result_dict["_{0}".format(key)] = value
                result_dict["_attributes"].append(value)

                value.name = key

                result_dict[key] = value

            else:
                result_dict[key] = value

        inst = super(MyMeta, mcs).__new__(mcs, clsname, clsbases, result_dict)
        return inst


class MyObjectA(object):

    __metaclass__ = MyMeta

    _name = None
    _attributes = []

    def __init__(self, name):
        self._name = name

        for attr in self._attributes:
            attr.node = self._name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    @property
    def attributes(self):
        return [attr.name for attr in self._attributes]


class MyObjectB(MyObjectA):

    attr1 = MyAttributeType()
    attr2 = MyAttributeType()
    attr3 = MyAttributeType()


class MyObjectC(MyObjectB):

    attr4 = MyAttributeType()
    attr5 = MyAttributeType()
    attr6 = MyAttributeType()


class MyObjectD(MyObjectB):

    attr7 = MyAttributeType()
    attr8 = MyAttributeType()
    attr9 = MyAttributeType()


a = MyObjectA("testA")
b = MyObjectB("testB")
c = MyObjectC("testC")
d = MyObjectD("testD")

print a.attributes
print b.attributes
print c.attributes
print d.attributes

Output:

['attr2', 'attr3', 'attr1', 'attr6', 'attr5', 'attr4', 'attr7', 'attr8', 'attr9']
['attr2', 'attr3', 'attr1', 'attr6', 'attr5', 'attr4', 'attr7', 'attr8', 'attr9']
['attr2', 'attr3', 'attr1', 'attr6', 'attr5', 'attr4', 'attr7', 'attr8', 'attr9']
['attr2', 'attr3', 'attr1', 'attr6', 'attr5', 'attr4', 'attr7', 'attr8', 'attr9']

Expected:

[]
['attr2', 'attr3', 'attr1']
['attr2', 'attr3', 'attr1', 'attr6', 'attr5', 'attr4']
['attr2', 'attr3', 'attr1', 'attr7', 'attr8', 'attr9']

Upvotes: 0

Views: 149

Answers (1)

MisterMiyagi
MisterMiyagi

Reputation: 52159

You need to share _attributes of the base classes as a copy:

            result_dict["_attributes"] = base.__dict__["_attributes"][:]

Sharing a list without copying it means changes are visible on all references. You are currently doing the equivalent of this:

>>> base_attributes = [] 
>>> child_attributes = base_attributes
>>> child_attributes.append(2)
>>> base_attributes
[2]

Use [:] to create a copy of all elements on assignment:

>>> base_attributes = [] 
>>> child_attributes = base_attributes[:]
>>> child_attributes.append(2)
>>> base_attributes
[]

Upvotes: 2

Related Questions