zhenyu wang
zhenyu wang

Reputation: 81

Subclass with class variable inheritance

In a parent class, I defined a class variable and a class method with which to modify the class variable value. I want each child class to use its own variable, not share with its parent.

But the result is not what I expected; in the following example I have two sets of parent class plus child classes, and some code to demonstrate what goes wrong:

class P:
    _X = 0

    @classmethod
    def cm(cls):
        print("In p cm")
        cls._X += 1

class C1(P):
    pass

class C2(P):
    pass

class Image:
    _callbacks = {}

    @classmethod
    def registerDataFormat(cls, fmt, loader):
        if fmt in cls._callbacks.keys():
            print("The %s format has already been registered." % (fmt))
            return False

        cls._callbacks[fmt] = {}
        cls._callbacks[fmt]["loader"] = loader

class HSImage(Image):
    pass

class GT(Image):
    pass

if __name__ == '__main__':
    C1.cm()
    print(C1._X)
    print(P._X)
    C2.cm()
    print(C2._X)
    print(P._X)

    HSImage.registerDataFormat("mat", "loader 1")
    print(HSImage._callbacks)
    print(Image._callbacks)
    GT.registerDataFormat("mat", "loader 2")
    print(GT._callbacks)
    print(Image._callbacks)

Here are the results:

In p cm
1
0
In p cm
1
0
{'mat': {'loader': 'loader 1'}}
{'mat': {'loader': 'loader 1'}}
The mat format has already been registered.
{'mat': {'loader': 'loader 1'}}
{'mat': {'loader': 'loader 1'}}

The first example has the expected result, but not the second, why is the class variable shared with the parent class when I called the class method on a child class in the second set of classes?

My expected results:

In p cm
1
0
In p cm
1
0
{'mat': {'loader': 'loader 1'}}
{}
{'mat': {'loader': 'loader 2'}}
{}

Upvotes: 2

Views: 1652

Answers (1)

Martijn Pieters
Martijn Pieters

Reputation: 1121924

The difference is that you mutated a dictionary. The first, simple example with integers works with immutable integer objects. cls._X += 1 takes the value of _X (from a parent class if necessary), after which the old + 1 operation produces a new integer object that is then assigned back to cls._X. The assignment matters here, as that will take place on the child class.

But you didn't assign anything back to a class with the second case:

cls._callbacks[fmt] = {}
cls._callbacks[fmt]["loader"] = loader

You assigned to a key in the dictionary. The cls._callbacks attribute itself is not altered, it is the same dictionary shared between all the classes. The cls._callbacks reference is looked up, found on the Image base class, after which the dictionary itself is updated by adding the key-value pair. None of the subclasses (HSImage or GT) have the attribute themselves.

You would need to create a copy and assign back the altered copy:

cls._callbacks = {k: dict(v) for k, v in cls._callbacks.items()}
cls._callbacks[fmt] = {'loader': loader}

This creates a copy not just of the outer dictionary but of all the values too, because those are all dictionaries too, before adding the new dictionary for fmt. The copy is then assigned to cls._callbacks, effectively creating a new attribute on a subclass if it wasn't there already.

That's not that efficient of course; the copy is created each time you register a loader. You'd be better of with creating a new _callback dictionary object on each subclass, but that gets tedious and can easily be forgotten. You can instead automate that with the __init_subclass__ method:

class Image:
    def __init_subclass__(cls):
        cls._callbacks = {}

    @classmethod
    def registerDataFormat(cls, fmt, loader):
        if fmt in cls._callbacks:
            print("The {} format has already been registered.".format(fmt))
            return

        cls._callbacks[fmt] = {'loader': loader}

The __init_subclass__ method is called for each subclass you create.

Upvotes: 3

Related Questions