Class Variables and Methods

In this section we will learn about class variables and class methods. We saw in the previous section that you can have multiple instances of a class. Each of the instances are objects which have their own properties or attributes specific to that instance. Other times though we want attributes that are common to the class and shared by all the instances of the class. These are called class variables and class methods. Let’s look at an example with a TransitBus class to learn these concepts.

class TransitBus:
    def __init__(self, identifier):
        self.identifier = identifier
        self.num_passengers=0
    
    def add_passengers(self, number):
        self.num_passengers += number
    
    def remove_passengers(self, number):
        self.num_passengers -= number 
bus1 = TransitBus(1)
bus2 = TransitBus(2)
bus3 = TransitBus(3)

bus1.add_passengers(5)
bus2.add_passengers(4)
bus3.add_passengers(13)
bus2.remove_passengers(1)
bus2.identifier
2
bus2.num_passengers
3

Above we created three instances of the TransitBus class. Each object is an instance of a bus. Each bus has it’s own identifier and has a certain number of passengers. All the buses can add and remove passengers with their add_passengers and remove_passengers methods.

Class Variables

What if we wanted to keep track of the total number of buses in the system as well as the total number of passengers? These would be properties specific to the TransitBus class in general. They are not specific to any particular instance or bus. Also, it would be nice if each bus had the ability to get this information. This can be achieved with class variables. They are variables that are shared across all instances of the class. Let’s define two new class variables in the TransitBus class, total_num_passengers and total_num_buses.

class TransitBus:
    total_num_passengers = 0
    total_num_buses = 0
    
    def __init__(self, identifier):
        self.identifier = identifier
        self.num_passengers=0
    
    def add_passengers(self, number):
        self.num_passengers += number
    
    def remove_passengers(self, number):
        self.num_passengers -= number 

The first thing to note is where in the code class variables are defined. They are defined outside the __init__. To access class variables you do not even need an instance of the class.

TransitBus.total_num_buses
0
TransitBus.total_num_passengers
0

You can not do this with instance variables such as identifier and num_passengers. Using TransitBus.identifier or TransitBus.num_passengers would raise an AttributeError. This is because we need an instance (an actual bus) of the TransitBus first to access the instance variables num_passengers and identifier.

Let’s create a new bus instance and see how that particular instance can access the class variables.

print('bus1 info')
b1 = TransitBus(1)
b1.add_passengers(10)
b1.remove_passengers(3)
print(b1.identifier)
print(b1.num_passengers)

print('bus2 info')
b2 = TransitBus(2)
b2.add_passengers(20)
b2.remove_passengers(15)
print(b2.identifier)
print(b2.num_passengers)
bus1 info
1
7
bus2 info
2
5

Each instance of a class has access to the class variables.

b1.total_num_buses
0
b1.total_num_passengers
0
b2.total_num_buses
0
b2.total_num_passengers
0

We need to update the TransitBus logic so that TransitBus.total_num_buses and TransitBus.total_num_passengers are updated each time a new bus is created and each time passengers get on or get off the bus. Notice that we use the TransitBus name to access the class variables. For example we used TransitBus.total_num_buses. You can also simply use self.total_num_buses to access the class variables too. Sometimes it can be more clear to the reader or maintainer of the code to use the class name to reference the class variables.

class TransitBus:
    total_num_passengers = 0
    total_num_buses = 0
    
    def __init__(self, identifier):
        self.identifier = identifier
        self.num_passengers=0
        TransitBus.total_num_buses += 1
    
    def add_passengers(self, number):
        self.num_passengers += number
        TransitBus.total_num_passengers += number
    
    def remove_passengers(self, number):
        self.num_passengers -= number
        TransitBus.total_num_passengers -= number
print('bus1 info')
b1 = TransitBus(1)
b1.add_passengers(10)
b1.remove_passengers(3)
print(b1.identifier)
print(b1.num_passengers)

print('bus2 info')
b2 = TransitBus(2)
b2.add_passengers(20)
b2.remove_passengers(15)
print(b2.identifier)
print(b2.num_passengers)
bus1 info
1
7
bus2 info
2
5

After running the above code there should be a total of 2 buses in the system as well as 12 total passengers.

TransitBus.total_num_buses
2
TransitBus.total_num_passengers
12

Class variables have the same value across every instance of the class.

print(b1.total_num_buses)
print(b1.total_num_passengers)
print(b2.total_num_buses)
print(b2.total_num_passengers)
2
12
2
12

Let’s create a new bus and see what happens to the totals.

b3 = TransitBus(3)
b3.add_passengers(35)
TransitBus.total_num_buses
3
TransitBus.total_num_passengers
47
print(b1.total_num_buses)
print(b1.total_num_passengers)
print(b2.total_num_buses)
print(b2.total_num_passengers)
print(b3.total_num_buses)
print(b3.total_num_passengers)
3
47
3
47
3
47

Now what if we wanted to create a 100 buses in the system and keep track of them all in the system? It would not make much sense to create bus1, bus2, ... bus100 variables. Instead, we could just create 100 instances of the TransitBus class and keep track of the different buses in a class variable. In this case we will keep track of each bus in the system within a dictionary. For example, if the transit system had three buses in the system this transit_system dictionary could look something like this:

{'1': {'num_passengers': 5}, '2': {'num_passengers': 10}, '3': {'num_passengers': 20}}
{'1': {'num_passengers': 5},
 '2': {'num_passengers': 10},
 '3': {'num_passengers': 20}}

So it is a dictionary with the keys as the bus identifiers and the values with the information about the bus. Let’s update the logic of the TransitBus class by adding this new class variable transit_system.

class TransitBus:
    total_num_passengers = 0
    total_num_buses = 0
    transit_system = {}
    
    def __init__(self, identifier):
        self.identifier = identifier
        self.num_passengers=0
        TransitBus.total_num_buses += 1
        self.update_bus_in_system()
    
    def update_bus_in_system(self):
        TransitBus.transit_system[self.identifier] = {'num_passengers': self.num_passengers}
    
    def add_passengers(self, number):
        self.num_passengers += number
        TransitBus.total_num_passengers += number
        self.update_bus_in_system()
    
    def remove_passengers(self, number):
        self.num_passengers -= number
        TransitBus.total_num_passengers -= number
        self.update_bus_in_system()
TransitBus.total_num_buses
0
TransitBus.total_num_passengers
0
TransitBus.transit_system
{}
b1 = TransitBus(1)
b2 = TransitBus(2)
b3 = TransitBus(3)
TransitBus.transit_system
{1: {'num_passengers': 0}, 2: {'num_passengers': 0}, 3: {'num_passengers': 0}}
b1.add_passengers(10)
b2.add_passengers(20)
b3.add_passengers(30)
TransitBus.transit_system
{1: {'num_passengers': 10},
 2: {'num_passengers': 20},
 3: {'num_passengers': 30}}
b1.remove_passengers(5)
b2.remove_passengers(5)
b3.remove_passengers(5)
TransitBus.transit_system
{1: {'num_passengers': 5},
 2: {'num_passengers': 15},
 3: {'num_passengers': 25}}
print(b1.num_passengers)
print(b1.total_num_buses)
print(b1.total_num_passengers)
5
3
45

Class Methods

We will add a class method called transit_stats. A class method is a method that does need any particular instance of the class to be called. You use the @classmethod decorator and use cls instead of self. See the code below to see what is going on. We will also add a reset_system class method which removes all the buses from the system.

class TransitBus:
    total_num_passengers = 0
    total_num_buses = 0
    transit_system = {}
    
    def __init__(self, identifier):
        self.identifier = identifier
        self.num_passengers=0
        TransitBus.total_num_buses += 1
        self.update_bus_in_system()
    
    def update_bus_in_system(self):
        TransitBus.transit_system[self.identifier] = {'num_passengers': self.num_passengers}
    
    def add_passengers(self, number):
        self.num_passengers += number
        TransitBus.total_num_passengers += number
        self.update_bus_in_system()
    
    def remove_passengers(self, number):
        self.num_passengers -= number
        TransitBus.total_num_passengers -= number
        self.update_bus_in_system()
    
    @classmethod
    def transit_stats(cls):
        avg_person_per_bus = cls.total_num_passengers / cls.total_num_buses
        num_empty_buses = len([v for (k,v) in cls.transit_system.items() if not v['num_passengers']])
        return {'total_num_passengers': cls.total_num_passengers,
               'total_num_buses': cls.total_num_buses,
               'num_empty_buses': num_empty_buses,
               'avg_person_per_bus': avg_person_per_bus}
    
    @classmethod
    def reset_system(cls):
        cls.total_num_passengers = 0
        cls.total_num_buses = 0
        cls.transit_system = {}
        
    

Lets create 2000 buses in the system where each bus has a random number of people between 0 and 45. Then we will remove a random number of passengers. First we will reset the transit system.

TransitBus.reset_system()
import random
for i in range(1, 2001):
    bus = TransitBus(identifier=i)
    bus.add_passengers(random.randint(0,45))
    bus.remove_passengers(random.randint(0, bus.num_passengers))

Here are the first 10 buses in the system.

[(k,v) for k,v in TransitBus.transit_system.items() if k in range(1,11)]
[(1, {'num_passengers': 19}),
 (2, {'num_passengers': 16}),
 (3, {'num_passengers': 6}),
 (4, {'num_passengers': 0}),
 (5, {'num_passengers': 31}),
 (6, {'num_passengers': 4}),
 (7, {'num_passengers': 17}),
 (8, {'num_passengers': 2}),
 (9, {'num_passengers': 33}),
 (10, {'num_passengers': 2})]

And now we can get the transit stats.

TransitBus.transit_stats()
{'total_num_passengers': 22057,
 'total_num_buses': 2000,
 'num_empty_buses': 202,
 'avg_person_per_bus': 11.0285}

Although the TransitBus class is another simple example, hopefully it gets you more familiar with the idea of using classes and also the difference between instance variables and methods and class variable and methods. Feel free to play around with the TransitBus class some more on your own and improve it or add new functionality.