简体   繁体   中英

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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM