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?
Classes called child classes or subclasses inherit methods and variables from parent classes or base classes. We can think of a parent class called Parent that has class variables for last_name , height , and eye_color that the child class Child will inherit from the Parent .
Inheritance allows us to define a class that inherits all the methods and properties from another class. Parent class is the class being inherited from, also called base class. Child class is the class that inherits from another class, also called derived class.
Class variable − A variable that is shared by all instances of a class. Class variables are defined within a class but outside any of the class's methods. Class variables are not used as frequently as instance variables are.
Here's an example – car, bus, bike – all of these come under a broader category called Vehicle. That means they've inherited the properties of class vehicles i.e all are used for transportation. We can represent this relationship in code with the help of inheritance.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With