Trying to understand oop in python I came into this situation that puzzles me, and I wasn't able to find a satisfactory explanation... I was building a Countable class, which has a counter attribute that counts how many instances of the class have been initialized. I want this counter to be increased also when a subclass (or subsubclass) of the given class is initialized. Here is my implementation:
class Countable(object):
counter = 0
def __new__(cls, *args, **kwargs):
cls.increment_counter()
count(cls)
return object.__new__(cls, *args, **kwargs)
@classmethod
def increment_counter(cls):
cls.counter += 1
if cls.__base__ is not object:
cls.__base__.increment_counter()
where count(cls) is there for debugging purposes, and later i write it down.
Now, let's have some subclasses of this:
class A(Countable):
def __init__(self, a='a'):
self.a = a
class B(Countable):
def __init__(self, b='b'):
self.b = b
class B2(B):
def __init__(self, b2='b2'):
self.b2 = b2
def count(cls):
print('@{:<5} Countables: {} As: {} Bs: {} B2s: {}'
''.format(cls.__name__, Countable.counter, A.counter, B.counter, B2.counter))
when I run a code like the following:
a = A()
a = A()
a = A()
b = B()
b = B()
a = A()
b2 = B2()
b2 = B2()
I obtain the following output, which looks strange to me:
@A Countables: 1 As: 1 Bs: 1 B2s: 1
@A Countables: 2 As: 2 Bs: 2 B2s: 2
@A Countables: 3 As: 3 Bs: 3 B2s: 3
@B Countables: 4 As: 3 Bs: 4 B2s: 4
@B Countables: 5 As: 3 Bs: 5 B2s: 5
@A Countables: 6 As: 4 Bs: 5 B2s: 5
@B2 Countables: 7 As: 4 Bs: 6 B2s: 6
@B2 Countables: 8 As: 4 Bs: 7 B2s: 7
Why at the beginning both the counter of A and B is incrementing, despite I am calling only A()? And why after the first time I call B() it behaves like expected?
I already found out that to have a behavior like I want it is sufficient to add counter = 0 at each subclass, but I was not able to find an explanation of why it behaves like that.... Thank you!
I added few debug prints, and for simplicity limited class creation to two. This is pretty strange:
>>> a = A()
<class '__main__.A'> incrementing
increment parent of <class '__main__.A'> as well
<class '__main__.Countable'> incrementing
@A Counters: 1 As: 1 Bs: 1 B2s: 1
>>> B.counter
1
>>> B.counter is A.counter
True
>>> b = B()
<class '__main__.B'> incrementing
increment parent of <class '__main__.B'> as well
<class '__main__.Countable'> incrementing
@B Counters: 2 As: 1 Bs: 2 B2s: 2
>>> B.counter is A.counter
False
How come when B() is not initialized yet, it points to the same variable as A.counter but after creating single object it is a different one?
The problem with your code is that subclasses of
Countabledon't have their owncounterattribute. They're merely inheriting it fromCountable, so whenCountable'scounterchanges, it looks like the child class'scounterchanges as well.Minimal example:
If
Ahad its owncounterattribute, everything would work as expected:But if all of these classes share the same
counter, why do we see different numbers in the output? That's because you actually add thecounterattribute to the child class later, with this code:This is equivalent to
cls.counter = cls.counter + 1. However, it's important to understand whatcls.counterrefers to. Incls.counter + 1,clsdoesn't have its owncounterattribute yet, so this actually gives you the parent class'scounter. Then that value is incremented, andcls.counter = ...adds acounterattribute to the child class that hasn't existed until now. It's essentially equivalent to writingcls.counter = cls.__base__.counter + 1. You can see this in action here:So what's the solution to this problem? You need a metaclass. This gives you the possibility to give each
Countablesubclass its owncounterattribute when it is created: