13

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?

4
  • I can't reproduce your output. My output for B2s is always the same as Bs. Commented Sep 15, 2017 at 11:14
  • I edited your question with simplified example of the issue. This is an interesting question, hope someone can shed some light on the process Commented Sep 15, 2017 at 11:17
  • @Rawing you are right, I pasted the output of another example... now I fix it! Commented Sep 15, 2017 at 11:23
  • Are you aware that python has __subclasses__ which will give you the subclasses of a class? stackoverflow.com/a/3862957/7432 Commented Sep 15, 2017 at 11:51

1 Answer 1

19

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
Sign up to request clarification or add additional context in comments.

6 Comments

I would just add that in Python3.6+, one can also use __init_subclass__() hook for the same purpose (adding a counter attribute to each subclass).
Or (in Python 2.7.x+ and 3.x) use a class decorator.
however, after the creation of the first object is completed (a = A()), I get id(Countable.counter) == id(A.counter). Why does this happen if the assignment creates a new class variable for class A??
@blue_note ints are immutable, so it doesn't make much sense to check their id. What matters is not if both classes share the same int instance, it's whether A has its own attribute that shadows Countable's. If ints were mutable, then both classes sharing the same int instance would be a problem, but they're not. See also this question for more info on comparing the ids of ints.
@blue_note Yes, it (CPython) does that for ints between -5 and 256.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.