Why is super() not behaving like I expected when assigning to a class variable of the base class?

Question:

I am attempting to experiment with classes so I can better understand what they do. I wanted to build a counter which records the number of instances of a class (MyClass):

class ObjectCounter: # I want this to count the number of objects in each class
    myclass_obj_count = 0

class MyClass(ObjectCounter):
    def __init__(self):
        super().myclass_obj_count += 1 # AttributeError: 'super' object has no attribute 'myclass_obj_count'

m1 = MyClass()
m2 = MyClass()
m3 = MyClass()
print(ObjectCounter.myclass_obj_count)

Since that didn’t work, I looked online for someone trying to do the same thing. Here is some code I found online. This works as expected, and I feel like I have a basic understanding of how this works. This is a better solution to the task I was attempting, but I’m not satisfied because I want to know how super() works.

class geeks:
    counter = 0

    def __init__(self):
        geeks.counter += 1

g1 = geeks()
g2 = geeks()
g3 = geeks()
print(geeks.counter) # this gives an expected result

Therefore, I tried this instead:

class ObjectCounter: # I want this to count the number of objects in each class
    myclass_obj_count = 0

    def add_myclass(self):
        self.myclass_obj_count += 1

class MyClass(ObjectCounter):
    def __init__(self):
        super().add_myclass()

my_class_1 = MyClass()
my_class_2 = MyClass()
my_class_3 = MyClass()
print(ObjectCounter.myclass_obj_count) # expected output: 3

Instead of getting the expected output of 3, I got an output of 0. Why is this happening?

Asked By: h4zard

||

Answers:

First, be aware of the += operator; it’s implementation is quite subtle:

a += b

becomes

a = a.__iadd__(b)

This perhaps strange definition allows python to support it even for immutable types (like strings).

Note what happens when used for a class variable that is referred to by the alias self

class ObjectCounter: # I want this to count the number of objects in each class
    myclass_obj_count = 0
    def add_myclass(self):
        self.myclass_obj_count += 1
        # effectively becomes:
        # self.myclass_obj_count = self.myclass_obj_count.__iadd__(1)

This will introduce an instance variable of the same name, shadowing the class variable.
You don’t even need the subclass to test this:

>>> x = ObjectCounter()
>>> x.add_myclass()
>>> x.add_myclass()
>>> x.add_myclass()
>>> x.myclass_obj_count
3
>>> ObjectCounter.myclass_obj_count
0

Referring to the class variable directly instead of using self fixes this

    def add_myclass(self):
        ObjectCounter.myclass_obj_count += 1

I’m hesitant to give definite answers of what happens under the hood when class variables, super() and assignments are used, other than it just doesn’t work. Perhaps because it would be quite ambiguous of whether or not we are defining class variables or new instance variables.
super() won’t let you assign to either;

class ObjectCounter:
    myclass_obj_count = 0
    def __init__(self):
        self.x = 'test'


class MyClass(ObjectCounter):
    def __init__(self):
        super().__init__()
        print(super().myclass_obj_count) # reading works just fine
        print(type(super()))  # this isn't actually exactly the same as "ObjectCounter"
        super().myclass_obj_count = 123  # no good
        super().x = 'foo'  # also no good.

All in all, for any assignment to class variables you can use the class name itself.

Answered By: Mikael Öhman
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.