In Python, do objects have their own copies of class variables?

Viewed 750

This is a question which confuses every Python Intermediate learner, so please give a brief (and idiot-friendly) answer.

I wanted to create a variable which increments the variable population by 1 when a new object is created.

class Human:

    population = 0

    # initialization.
    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender

    
    def increment_pop(self):
        self.population += 1

# class Human ends.

person = Human('Tom', 22, 'M')
person.increment_pop()
person.increment_pop()
print('Population : ', person.population)

person2 = Human('Anna', 24, 'F')
person2.increment_pop()
print('Population : ', person2.population)

print(Human.population)

Output :

Population :  2
Population :  1
0

So both the object and the class has the variable population? What is the difference between the variable population and the variables inside the init() method?

I know that only instance variables are inside the init() method.

2 Answers

It's a bit more complex than that. There are both class variables and instance variables, but what's happening in your example is that the class variable is being overridden by the instance variable.

Consider the following:

>>> class Foobar:
...     my_list = []
... 
>>> foo = Foobar()
>>> Foobar.my_list.append('hello')
>>> foo.my_list.append('world')
>>> foo.my_list
['hello', 'world']

As you can see the Foobar class and foo instance share a variable here. So no, instantiating a class does not "copy" all the class variables. Consider this, however:

>>> foo.my_list = ['spam', 'eggs']
>>> foo.my_list
['spam', 'eggs']
>>> Foobar.my_list
['hello', 'world']

Now we have two variables, one that's a class variable, the other that's an instance variable. They are no longer the same.

If you wanted to always use class variables, the most elegant thing would be to use class methods. Eg,

class Human:

    population = 0

    # initialization.
    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender

    @classmethod
    def increment_pop(cls):
        cls.population += 1

Now because increment_pop is a class method, it will always operate on the class, not the instance. However, the population attribute will be available to instances and as long as they don't overwrite it, it will be the same as the class variable.

A few important things to keep in mind:

  1. Primitives, like integers and strings, are immutable. When you do foo += 1, you're creating a new instance of an integer and replacing the old one. You're not modifying it in place.

  2. Instances can access class variables and class methods with self.whatnot. But if you want to be unambiguous, you can also reference them as ClassName.whatnot.

  3. Class methods can be called from instances, similarly, like self.class_method().

Other notes on Python variables

Let's back up here and consider how Python resolves a request for something like my_human.population on the instance:

  1. First, Python looks for an instance attribute named population. If it exists, that's the value you get.
  2. Second, Python looks for a class attribute named population. If that exists, that's the value you get.

So, when you have no assigned population on your instance, and you access self.population, since no instance attribute exists with that name, you get the class attribute.

However, once you assign an instance attribute to your object, that second step above never happens.

You can inspect this at runtime, too:

>>> class Human:
...     population = 0
...
>>> my_human = Human()
>>> 'population' in Human.__dict__
True
>>> 'population' in my_human.__dict__
False

So population only exists on the Human class, not on the instance. When we access Human.population, it finds the class attribute:

>>> my_human.population
0

But what happens if I create an instance attribute?

>>> Human.population = 100
>>> my_human.population = 50
>>> Human.population
100
>>> my_human.population
50

Now, because the instance has an attribute matching the name population, when you access my_human.population, it never looks up what's on the class.

self references a specific instance of a class. When you call increment_pop, you are using self.population, which is causing the increment only of that instance. Because in python you do not need to call initializers, increment_pop is created a new variable called self.population, where the class variable population should be global.

Change increment_pop to have cls.population rather than self.population and it should work as intended. Note that in the function definition you'll need the decorator @classmethod and the parameter cls.

Related