A glimpse into classes#
Let’s have a brief look how classes work in Python.
Classes in Python#
In Python, everything is an object, every object has a class.
i = 1
print(i.__class__)
s = "hello"
print(s.__class__)
<class 'int'>
<class 'str'>
A class collects attributes and functions together. Here is an example of a class definition for a car:
class Car:
    """A class representing a car"""
    def __init__(self, year, fuel="electric"):
        self.year = year
        self.fuel = fuel
        self.speed = 0
    def start(self):
        if self.fuel == "electric":
            print("sssss")
        else:
            print("Wohmmm")
    def accelerate(self, new_speed):
        self.speed = new_speed
This class contains three methods:
- The - __init__method is the constructor function which is called when a new class object is instantiated.
- The - startmethod is a user-defined function.
- The - acceleratemethod is also a user-defined function.
Note: All instance methods take a self variable as first argument. This variable represents the car object itself. We can use the self variable to access information about the object.
Using our Python class#
We can create a new instance of the class:
mycar = Car(year=2007, fuel="Bensin")
print(mycar)
<__main__.Car object at 0x7f0b4c2041c0>
Instantiating the class calls the __init__ constructor and returns a new object, an instance of our class:
We can call the user-defined methods on our object:
mycar.start()
mycar.accelerate(new_speed=100)
Wohmmm
Note: When calling instance methods, the self argument in the declaration is not included.
We can also access the member attributes of the object:
print(mycar.year)
print(mycar.fuel)
print(mycar.speed)
2007
Bensin
100
Magic class methods#
- Python classes can implement some magic methods. 
- If implemented, Python calls these magic methods on certain operations. 
- Here are some operations defined by magic methods: 
str(a)            # calls a.__str__(), called also with print(a)!
len(a)            # calls a.__len__()
c = a*b           # calls c = a.__mul__(b)
a = a+b           # calls a = a.__add__(b)
a += c            # calls a.__iadd__(c)
d = a[3]          # calls d = a.__getitem__(3)
a[3] = 0          # calls a.__setitem__(3, 0)
f = a(1.2, True)  # calls f = a.__call__(1.2, True)
if a:             # calls if a.__len__()> 0: or if a.__nonzero__():
a == b            # calls a.__eq__(self, b)
A complete list is available here: Python reference - Data model
Example#
Currently, when we try to print our Car object the output is not very informative:
print(mycar)
<__main__.Car object at 0x7f0b4c2041c0>
Let’s extend our car class with the magic __str__ method:
class Car:
    """A class representing a car"""
    def __init__(self, year, fuel="electric"):
        self.year = year
        self.fuel = fuel
        self.speed = 0
    def __str__(self):
        return f"This is a car from {self.year} driving on {self.fuel} at speed {self.speed}"
This magic __str__ method is now called when we convert the car object to a string:
mycar = Car(2007, "bensin")
str(mycar)
'This is a car from 2007 driving on bensin at speed 0'
Subclasses#
Remember: A class collects attributes and functions that belong together.
Subclasses can be used to specialize and extend existing classes.
Technical notes on subclasses:
- Python supports single and multiple inheritance 
- Python has a similar class concept as in Java and C++, but: - All functions are virtual 
- No private/protected variables (the effect can be “simulated”) 
- This makes class programming easier and faster than in C++ and Java: 
 
class Car:
    """A class representing a car"""
    def __init__(self, color):
        self.color = color
        self.sound = "Brooom"
    def start(self):
        print(self.sound)
Implementing a subclass#
Subclasses can be used to inherit the member functions/attributes from its Base class.
It is useful to create specialisation of a class without code duplication.
Let’s create a subclass ElectricCar that inherits the properties from the Car class:
class ElectricCar(Car):
    def __init__(self, color):
        super().__init__(color)  # Calls the parent constructor
        # self.color and self.sound are now class members
        self.sound = "Sssss"  # Change the sound
    def stop(self):  # Add a new method to ElectricCars
        print("Engine stopped")
If a subclass method is not implemented, Python will try to call the function of the parent class:
ecar = ElectricCar("white")
ecar.start()
ecar.stop()
Extending default types#
Everything is an object! Everything has a class!
Let’s try counting occurrences of strings in a sequence and storing them in a dict:
counts = dict()
for word in ["a", "b", "c", "a", "b"]:
    if word not in counts:
        # we have to check every time if we've seen this before!
        counts[word] = 0
    counts[word] += 1
counts
{'a': 2, 'b': 2, 'c': 1}
We can define a custom subclass of dict that overrides __getitem__ so that it returns a default value when a key hasn’t been seen before
class DefaultDict(dict):
    """A dictionary that returns a value for undefined keys"""
    def __init__(self, default_item):
        self.default_item = default_item
    def __getitem__(self, key):
        if key not in self:
            self[key] = self.default_item
        return super().__getitem__(key)
counts = DefaultDict(default_item=0)
for word in ["a", "b", "c", "a", "b"]:
    counts[word] += 1
counts
{'a': 2, 'b': 2, 'c': 1}
counts’ class is our DefaultDict,
but because it’s a subclass, isinstance returns True
for any parent class of the checked object:
print(counts.__class__)
print(isinstance(counts, dict))
<class '__main__.DefaultDict'>
True
Unit tests#
Unit testing is a software testing method by which individual units of source code are tested determine if they are fit to use. Unit testing your code will help you
- avoid bugs 
- avoid breaking old code when you make changes 
- provide documentation in the form of a minimal working example 
Example: Let’s say we have written a function add which adds two integers.
def test_two_plus_two():
    # Test that 2 + 2 = 4
    assert add(2,2) == 4, 'addition not returning correct answer'
Features:
- Function name should start or end with the word ‘test’ 
- Function should not take any arguments 
- Test function should not be randomized in any way 
Unit tests can be systematically run with e.g. pytest. For more information see the Pytest home-page