Classes

OOP in Python

Class

Classes created with operator class:

class A:
    attr = 10

When a class definition is entered, a new namespace is created, and used as the local scope — thus, all assignments to local variables go into this new namespace. In particular, function definitions bind the name of the new function here

Class objects support two kinds of operations: attribute references and instantiation.

Simple OOP examples

Here is the simplest class for Cat and two objects of this class:

🪄 Code:

class Cat:
    pass 

agata = Cat()
blanka = Cat()

print(agata, id(agata))
print(blanka, id(blanka))

📟 Output:

<__main__.Cat object at 0x7ff57dd25360> 140692354650976
<__main__.Cat object at 0x7ff57dd25540> 140692354651456

Cat class and object is "empty" - doesn't define any attributes and methods. They even don't have their proper name.

Let's add names:

🪄 Code:

agata.name = "Agata"

print(agata.name)

📟 Output:

Agata

We can assign attributes during initialization of the object - it would simplify things a lot.

The method starts with __ ("dunder") is called a magic method. __init__ is one example, __str__ - method that defines a string representation of the object - is another.

self in examples below is the reference to the object itself so we could use it in the function code - for example during str(agata) Python will call agata.__str__() which will need to return a string including agata.name, and it does this using the reference self.

🪄 Code:

class Cat:
    def __init__(self, name="Stray"):
        self.name = name
        
    def __str__(self):
        return f'<Cat "{self.name}">'
    
    def __repr__(self):
        return f'Cat("{self.name}")'
        
agata = Cat("Agata")
street_cat = Cat()

print(agata)
print(street_cat)
print(repr(agata))

📟 Output:

<Cat "Agata">
<Cat "Stray">
Cat("Agata")

Magic methods are not the only ones we can define - in fact we can add any method that would describe some action involving an object.

Here we also will set a class attribute accessible from all children.

🪄 Code:

class Cat:
    default_sound = "Meow"
    
    def __init__(self, name="Stray"):
        self.name = name
        #self.default_sound = "Rrrr"  # We can override "global" class attribute with "local" instance attribute
        
    def __str__(self):
        return f'<Cat "{self.name}">'
    
    def sound(self, times=3):
        return " ".join([self.default_sound] * times)
    
blanka = Cat("Blanka")
print(blanka.sound())

📟 Output:

Meow Meow Meow

Examples with more methods/attributes:

class Bus:
    """ Sample Bus class """
    # Class attributes
    buses_count = 0         # IDEA: change with classmethod property -- get len(buses)
    people_transferred = 0  # IDEA: change with dict()
    money_collected = 0     # IDEA: change with dict()
    buses = []
 
    # Instance Initializer
    def __init__(self, name="Some Bus", rate=7):
        # instance attribute
        self.name = name
        self.people_transferred = 0
        self.rate = rate
        self.money_collected = 0
        self.__class__.buses_count += 1   # self.__class__ instead of hardcoding Bus
        self.__class__.buses.append(self) # self.__class__ instead of hardcoding Bus
 
    # Instance method
    def transfer(self, num=1):
        self.people_transferred += num
        self.__class__.people_transferred += num
        self.money_collected += self.rate * num
        self.__class__.money_collected += self.rate * num
        
    def __str__(self):
        return f"<Bus '{self.name}'>"
 
    def info(self): # change to __str__
        data = dict(
            name=self.name, total_buses=Bus.buses_count, rate=self.rate,
            num=self.people_transferred, total=Bus.people_transferred
        )
        return "Bus '{name}' (rate: {rate}€) (total: {total_buses}), " \
               "transferred {num} from {total} ppl".format(**data)
    
    def __repr__(self):
        return f"Bus('{self.name}', {self.rate})"
b = Bus("Bus #40")
b.transfer(100) # --> Bus.transfer(b, 100)
b

📟 Output:

Bus('Bus #40', 7)

🪄 Code:

print(b)

📟 Output:

<Bus 'Bus #40'>

🪄 Code:

b.transfer(50)
print(b.info()) # --> Bus.info(b)

📟 Output:

Bus 'Bus #40' (rate: 7€) (total: 1), transferred 150 from 150 ppl

🪄 Code:

b2 = Bus("Tram #1", 8)
b2.transfer(50)
print(b2.info())

📟 Output:

Bus 'Tram #1' (rate: 8€) (total: 2), transferred 50 from 200 ppl

🪄 Code:

print(f"Bus.people_transferred = {Bus.people_transferred}")
print(f"Bus.money_collected = {Bus.money_collected}")
print(f"Bus.buses_count = {Bus.buses_count}")
print(f"Bus.buses = {Bus.buses}")

📟 Output:

Bus.people_transferred = 200
Bus.money_collected = 1450
Bus.buses_count = 2
Bus.buses = [Bus('Bus #40', 7), Bus('Tram #1', 8)]

Creation of an instance of the class - like calling a function (in fact it is exactly like this - firstly we calling magic method __new__() then __init__()

🪄 Code:

bus_317 = Bus("# 317")
bus_317.transfer(20)
print(bus_317.info())

📟 Output:

Bus '# 317' (rate: 7€) (total: 3), transferred 20 from 220 ppl

🪄 Code:

b.transfer(23)
bus_317.transfer()
bus_317.transfer(55)  
print(bus_317.info())

📟 Output:

Bus '# 317' (rate: 7€) (total: 3), transferred 76 from 299 ppl

Class variables and instance variables were changed:

🪄 Code:

print(Bus.people_transferred)
print(bus_317.people_transferred)

📟 Output:

299
76

Inheritance

Inheritance from some class (called "base" or super class) allows to create a new class "borrowing" all attributes and methods definitions as they were in the super class. It allows to:

  1. Reuse the code (DRY principle).

  2. Create abstractions.

For example here is the class for the Robot:

class Robot:
    sounds = ["Beeep", "Bzzzt", "Oooooh"]
    
    def __init__(self, name, weigth=1000):
        self.weigth = weigth
        self.name = name
        
    def __str__(self):
        """Info about this robot"""
        return "Robot {obj.name} ({obj.weigth} kg)".format(obj=self)
 
    def say(self):
        """Say something"""
        import random 
        return f"{self.name} says: {random.choice(self.sounds)}"

It is completely usable:

🪄 Code:

bip = Robot("Bip 1.0")
print(bip)
print(bip.say())

📟 Output:

Robot Bip 1.0 (1000 kg)
Bip 1.0 says: Oooooh

We can re-use this class to create a robot from Futurama using the inheritance. For this we need to specify base/super class in parenthesis during new class definition.

class BendingRobot(Robot):
    sounds = ["Kill all humans", "Kiss my shiny metal face", "Oh, your God!",
              "Oh wait you’re serious. Let me laugh even harder."]

🪄 Code:

bender = BendingRobot("Bender")
print(bender)
print(bender.say())

📟 Output:

Robot Bender (1000 kg)
Bender says: Oh, your God!

As we can we still can use say method defined in the base class.

Multiple Inheritance

Python supports a limited form of multiple inheritance as well. A class definition with multiple base classes looks like this:

🪄 Code:

class A:
    a = "a from A"
    
class B(A):
    x = "x from B"
    
class C(A):
    a = "a from C"
    x = "x from C",
    
class D(B, C):  # change to D(C, B) and check...
    pass

d = D()
print(D.__mro__) # D.mro()
print(d.a, d.x)

📟 Output:

(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
a from C x from B

Let's enhance our Robot example by inheriting from two classes at once.

🪄 Code:

class Mail:
    def send_message(self, msg):
        print(f"*** SENDING MESSAGE: <<<{msg}>>>  ***")
        
Mail().send_message("Test")

📟 Output:

*** SENDING MESSAGE: <<<Test>>>  ***

🪄 Code:

class BendingMailingRobot(Robot, Mail):
    sounds = ["Kill all humans", "Kiss my shiny metal face", "Oh, your God!",
              "Oh wait you’re serious. Let me laugh even harder."]
    
bender2_0 = BendingMailingRobot("Bender 2.0")
bender2_0.send_message(bender2_0.say())

📟 Output:

*** SENDING MESSAGE: <<<Bender 2.0 says: Kiss my shiny metal face>>>  ***

More advanced example:

🪄 Code:

class BendingMailingRobot(BendingRobot, Mail):
    def mail(self):
        return self.send_message(self.say())
    
bender3_0 = BendingMailingRobot("Bender 3.0")
bender3_0.mail()

📟 Output:

*** SENDING MESSAGE: <<<Bender 3.0 says: Oh, your God!>>>  ***

And even more advanced example (with overloading of the existing send_message method with super() function covered later):

🪄 Code:

class BendingMailingRobot3(BendingRobot, Mail):
    def send_message(self):
        return super().send_message(self.say())

bender4_0 = BendingMailingRobot3("Bender 4.0")
bender4_0.send_message()

📟 Output:

*** SENDING MESSAGE: <<<Bender 4.0 says: Kiss my shiny metal face>>>  ***

Methods

Methods can be:

  • instance methods

  • class methods

  • static methods

Instance method

By default all methods (except of __new__ are instance methods).

The method that should be called with the instance as it's first argument. This method is bound to instance so if calling as it's method passing instance is not required. Example:

🪄 Code:

class Example:
    def cool_method(self):
        print(f"I am instance method, my instance is: {self}")
        
ex = Example()
ex.cool_method()
# Example.cool_method(ex)  # <-- same 

print(ex.cool_method)

📟 Output:

I am instance method, my instance is: <__main__.Example object at 0x7ff57df07610>
<bound method Example.cool_method of <__main__.Example object at 0x7ff57df07610>>

Class methods

The method with class as the first argument. Useful to run some code without need of creating the instace.

To mark the method as class method it is required to use builtin decorator @classmethod

🪄 Code:

class Example:
    attr = 5
    
    @classmethod
    def class_method(cls):
        print("CLS:", cls.attr)
        
    def instance_method(self):
        print("INSTANCE:", self.attr)
        
ex = Example()
ex.attr = 25
ex.class_method()
ex.instance_method()
Example.class_method() # Example.class_method(Example)

📟 Output:

CLS: 5
INSTANCE: 25
CLS: 5

Let's add some class method to Bus class. Please note that we don't need any existing buses to call general_info method on Bus2:

🪄 Code:

class Bus2(Bus):
    # creating isolated class attributes to not use the ones from base Bus class
    buses_count = 0
    buses = []
    people_transferred = 0
    money_collected = 0
    
    @classmethod
    def general_info(cls):
        print(f"* People_transferred = {cls.people_transferred}")
        print(f"* Money_collected = {cls.money_collected}")
        print(f"* Total buses = {cls.buses_count}")
        print(f"* Buses = {cls.buses}")
        
    @classmethod
    def remove_bus(cls, bus):
        try:
            cls.buses.remove(bus)
            cls.buses_count -= 1
            print(f"{bus} removed")
        except ValueError:
            print("No such bus registered!")
            
Bus2.general_info()

📟 Output:

* People_transferred = 0
* Money_collected = 0
* Total buses = 0
* Buses = []

Let's add some buses, check the general info and remove buses:

🪄 Code:

bus = Bus2()
bus.transfer(100)
Bus2.general_info()

📟 Output:

* People_transferred = 100
* Money_collected = 700
* Total buses = 1
* Buses = [Bus('Some Bus', 7)]

Removing also works good:

🪄 Code:

Bus2.remove_bus(bus)
Bus2.remove_bus(bus)

📟 Output:

<Bus 'Some Bus'> removed
No such bus registered!

Static method

This method doesn't require to pass instance/class at all.

To mark the method as static method it is required to use builtin decorator @staticmethod

🪄 Code:

class Example:
    @staticmethod
    def cool_method():
        Example.attr = 123123
        return "I am a static method, I don't have access to anything :("

    @staticmethod
    def other_stat_method(str_):
        return str_[::-1]
        
ex = Example()
print(ex.cool_method())
print(Example.cool_method())
print(ex.other_stat_method("Radar"))
print(Example.attr)

📟 Output:

I am a static method, I don't have access to anything :(
I am a static method, I don't have access to anything :(
radaR
123123

We can use static method for creating some helper function for `Bus` class, let's add simple one for calculating money collected:

class Bus:
    ...
    @staticmethod
    def calc_money(ppl, rate):
        return ppl * rate
    
    def transfer(self, num=1):
        self.people_transferred += num
        self.__class__.people_transferred += num
        self.money_collected += self.calc_money(num, self.rate)
        self.__class__.money_collected += self.calc_money(num, self.rate)
    ...

Old and New classes

This chapter is only viable for Python 2 - as in Python 3 there are no such distinguishing as "old/new" classes.

Before Python 2.5 the format for creating a class was:

class Old:
    pass

This was resulted in various problems with MRO and types. So some code redesigned, for this new format introduced:

class New(object):
    pass

Differences:

  1. classobj type as builtin types

  2. Correct horizontal-then-vertical MRO (earlier it was vertical)

  3. No __new__() method in old class

  4. Additional functionality:

    1. __slots__ (Python 3.x)

    2. __getattribute__

    3. __getattr__ and __getattribute__ don't look for magic methods in instance

Last updated