Reputation: 81
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
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