9  Object Oriented Programming

9.1 Classes and Objects

In python we deal with so-called objects. An object is an instance, that is a particular realization, of a class.
Here’s an analogy: You can think of a class as the instructions to build a car, say a FiatUno:

class FiatUno:
    def __init__(self, color="white", radio=False):
        self.color = color
        self.radio = radio
        self.radio_is_on = False
    def turn_on_radio(self):
        self.radio_is_on = True
        print(f"Radio is on!")
    def turn_off_radio(self):
        self.radio_is_on = False
        print(f"Radio is off!")
Note

By convention, we use CamelCase to name classes.

Each actual car is an object, i.e. an instance of a class:

car1 = FiatUno()

We can make another car:

car2 = FiatUno()

Like two cars can be the same model and color, they always have different license plates.
The same happens in python:

id(car1)
139856081602784
id(car2)
139856081603648

Each object has attributes:

car1.color, car1.radio
('white', False)

9.2 Methods

Different car models have different functionalities, like turning the radio on.
In python it’s the same, the functions attached to an object are called methods and we call them with this dot notation:

car1.turn_on_radio()  # it's just a function, so we call it with ()
Radio is on!
car1.turn_off_radio()
Radio is off!

Where is the self argument of the functions?
The first argument of a method will be always passed to the method in the call and it is the object itself (thus the convention to call it self).
That’s a bit meta an a bit confusing, but don’t worry, you’ll get a feel of it by using the classes/objects.

9.3 Dunder Methods

We saw above the __init__ method. This methods are special and there is a bunch of them already present on all classes:

dir(FiatUno)
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'turn_off_radio',
 'turn_on_radio']

These “dunder” methods define the behavior of the object in several ways. For example, the __str__ method defines the string representation of the object, which is used when we call print on an object. When we do print(something), python actually goes to the method __str__ of something and calls that one. Let’s see it in action:

class A:
    def __init__(self, msg="hello"):
        self.msg = msg
    def __str__(self):
        return self.msg + ", nice to meet you"
a = A()
print(a)
hello, nice to meet you

It’s good to know that these dunder methods exist and some libraries will require the user changing them. But in general, you should not worry about them.

Tip

Don’t overwrite dunder methods unless you really, really need it and you’re sure what you’re doing – it is pretty easy to make mistakes that lead to difficult to debug bugs.

9.4 Inheritance

Tip

Avoid using inheritance. We show it here just for you to know it exists and that some libraries use it. You can totally live without it.

Classes can be organized in a hierarchy:

class Car:
    def __init__(self):
        self.n_wheels = 4 
        
class FiatPalio(Car):
    def turn_on_radio(self):
        print("Radio is on")
        
class FiatTipo(Car):
    def turn_on_ac(self):
        print("Pretty hot in here")

car3 = FiatPalio()
car4 = FiatTipo()
car3.turn_on_radio()
Radio is on
car3.n_wheels
4
car4.turn_on_ac()
Pretty hot here
car4.n_wheels
4

In that case Car is called the “parent” class and the FiatPalio the “child” or “subclass”.

Notice that both car3 and car4 inherited the attribute n_wheels from Car.

A class can have more than one parent:

class Parent1:
    n_arms = 2
        
class Parent2:
    n_legs = 2
        
class Parent3:
    n_eyes = 2

class Child(Parent1, Parent2, Parent3):
    def list_body_parts(self):
        print("arms: ", self.n_arms)
        print("legs: ", self.n_legs)
        print("eyes: ", self.n_eyes)
child = Child()
child.list_body_parts()
arms:  2
legs:  2
eyes:  2

Multiple inheritance is also tricky and you should avoid it. It’s just good to know it exists to understand code from out there, for example, the popular scikit-learn library uses this a lot to propagate methods and avoid code duplication.