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 Countable
don't have their own counter
attribute. They're merely inheriting it from Countable
, so when Countable
's counter
changes, it looks like the child class's counter
changes as well.
Minimal example:
class Countable:
counter = 0
class A(Countable):
pass # A does not have its own counter, it shares Countable's counter
print(Countable.counter) # 0
print(A.counter) # 0
Countable.counter += 1
print(Countable.counter) # 1
print(A.counter) # 1
If A
had its own counter
attribute, everything would work as expected:
class Countable:
counter = 0
class A(Countable):
counter = 0 # A has its own counter now
print(Countable.counter) # 0
print(A.counter) # 0
Countable.counter += 1
print(Countable.counter) # 1
print(A.counter) # 0
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 the counter
attribute to the child class later, with this code:
cls.counter += 1
This is equivalent to cls.counter = cls.counter + 1
. However, it's important to understand what cls.counter
refers to. In cls.counter + 1
, cls
doesn't have its own counter
attribute yet, so this actually gives you the parent class's counter
. Then that value is incremented, and cls.counter = ...
adds a counter
attribute to the child class that hasn't existed until now. It's essentially equivalent to writing cls.counter = cls.__base__.counter + 1
. You can see this in action here:
class Countable:
counter = 0
class A(Countable):
pass
# Does A have its own counter attribute?
print('counter' in A.__dict__) # False
A.counter += 1
# Does A have its own counter attribute now?
print('counter' in A.__dict__) # True
So what's the solution to this problem? You need a metaclass. This gives you the possibility to give each Countable
subclass its own counter
attribute when it is created:
class CountableMeta(type):
def __init__(cls, name, bases, attrs):
cls.counter = 0 # each class gets its own counter
class Countable:
__metaclass__ = CountableMeta
# in python 3 Countable would be defined like this:
#
# class Countable(metaclass=CountableMeta):
# pass
class A(Countable):
pass
print(Countable.counter) # 0
print(A.counter) # 0
Countable.counter += 1
print(Countable.counter) # 1
print(A.counter) # 0