# Define helper function
from pytest import approx
def error_message(actual, expected):
    return f'Actual {actual} != Expected {expected}'

Lecture 6 - Classes in Python#

REMEMBER:

  • use getter/setter or decorators for good coding practice

    • A decorator is a funtion that takes another function and extends its behavior without explicitly modifying it. In Python, decorating a function can be simply done using the @ symbol

  • use single leading underscore to indicate attributes/methods for internal use

Initialization of a class

class ExampleClass(object):
    def __init__(self, <list of parameters>):
    ...

Attributes:

  • Class attributes: common among all instances of the object - if class attribute is changed in one, it will affect all other.

    • Defined above __init__

  • Instance attributes: specific to a given instance

    • Defined in __init__

Methods:

  • instance method(self, arguments): a method that can access both class and instance attributes

  • class method(cls, arguments): can access the class (via cls) but cannot access the instance (via self). Use @classmethod decorator

  • static method is a function in a class that is like an ordinary function, that does not depend on any instance (no self handle). Use @staticmethod decorator

A. Create a class for a three-dimensional vector#

Create a class for storing a 3D vector (x, y, z coordinates), and which provides a set of functions for manipulating the coordinates.

The class Vector3D should have the following properties:

  • Instance attributes

    • x stores x coordinate (object attribute)

    • y stores y coordinate (object attribute)

    • z stores z coordinate (object attribute)

  • Ordinary instance methods

    • length() : compute length of vector*

    • normalize() : normalize vector to unit length vector*

      • for square root use math.sqrt function from math library

  • Overloaded operations (special/magic methods)

    • addition operator(+) : implement __add__ method to return a new vector as the sum of the two vectors

    • substraction operator(-) : implement __sub__ method to return a new vector as difference of the two vectors

    • compare two vectors (==) : implement __eq__ method to compare (coordinate by coordinate) the vector to another

    • str method : implement __str__method to make a nice print statement for the vector

  • Static method

    • zerovector() : instantiate a zero vector

# Importing the math library
import math

# Fill in the Vector3D class

class Vector3D(object):
    """
    3D vector class
    
    instance attributes:
        _x (float)
        _y (float)
        _z (float)
    
    class methods:
        length (float): returns Euclidean 2-norm of vector
        normalize (None): scales coordinates so that resulting vector has length 1
        __add__ (Vector3D) returns new vector as vector sum of two vectors
        __sub__ (Vector3D) returns new vector as vector difference of two vectors
        __str__: string representation of vector

    static method:
        zerovec (Vector3D): returns a zero vector
    """
    def __init__(self, x, y, z):
        self._x = x
        self._y = y 
        self._z = z
    
    @property
    def x(self):
        return self._x
    
    @x.setter
    def x(self, new_x):
        self._x = new_x
    
    @property
    def y(self):
        return self._y
    
    @y.setter
    def y(self, new_y):
        self._y = new_y
    
    @property
    def z(self):
        return self._z
    
    @z.setter
    def z(self, new_z):
        self._z = new_z

    def length(self):
        return math.sqrt(self._x * self._x + self._y * self._y + self._z * self._z)
    
    def normalize(self): 
        nfact = 1.0 / self.length()
        self._x *= nfact 
        self._y *= nfact 
        self._z *= nfact
        
    def __add__(self, other):
        x = self._x + other._x 
        y = self._y + other._y 
        z = self._z + other._z
        return Vector3D(x, y, z)
            
    def __sub__(self, other):
        x = self._x - other._x 
        y = self._y - other._y 
        z = self._z - other._z
        return Vector3D(x, y, z)
    
    def __eq__(self, other):
        if (self._x == other._x) and (self._y == other._y) and (self._z == other._z):
            return True
        else:
            return False

    def __str__(self):
        return f'Vector3D({self._x}, {self._y}, {self._z})'
    
    @staticmethod
    def zerovec():
        return Vector3D(0.0,0.0,0.0)
    
# Verify Vector3D constructor
vector = Vector3D(1.0, 2.0, 4.0)

assert vector.x == approx(1), error_message(vector.x, 1)
assert vector.y == approx(2), error_message(vector.y, 2)
assert vector.z == approx(4), error_message(vector.z, 4)
# Verify length method in Vector3D class 
vector = Vector3D(1.0, 2.0, 4.0)

assert vector.length() == approx(math.sqrt(21)), error_message(vector.length(), math.sqrt(21))
# Verify normalize method in Vector3D class 
vector = Vector3D(1.0, 2.0, 4.0)

vector.normalize()

assert vector.length() == approx(1), error_message(vector.length(), 1)
#Verify addition operator for two vectors 
avector = Vector3D(1.0, 2.0, 4.0)
bvector = Vector3D(2.0, 3.3, 0.9)

cvector = avector + bvector

assert cvector.x == approx(3.0), error_message(cvector.x, 3)
assert cvector.y == approx(5.3), error_message(cvector.y, 5.3)
assert cvector.z == approx(4.9), error_message(cvector.z, 4.9)
#Verify substraction operator for two vectors 
avector = Vector3D(1.0, 2.0, 4.0)
bvector = Vector3D(2.0, 0.2, 0.9)

cvector = avector - bvector

assert cvector.x == approx(-1.0), error_message(cvector.x, -1)
assert cvector.y == approx(1.8), error_message(cvector.y, 1.8)
assert cvector.z == approx(3.1), error_message(cvector.z, 3,1)
#Verify comparison of two vectors
avector = Vector3D(1.0, 2.0, 4.0)
bvector = Vector3D(2.0, 0.2, 0.9)

assert (avector == bvector) == False, error_message(avector == bvector, False)
#Verify staticmethod zerovec
zerovector = Vector3D.zerovec()

assert type(zerovector) == Vector3D, error_message (type(zerovector), Vector3D)
assert (zerovector.x == 0.0), error_message(zerovector, 0.0)
assert (zerovector.y == 0.0), error_message(zerovector, 0.0)
assert (zerovector.z == 0.0), error_message(zerovector, 0.0)

Class for currency data storage#

Create a class for storing money in SEK, Euro and Dollars and provides methods for manipulating the amount and type of currency.

The class Money should have the following properties:

  • Class attribute

    • conversion_rate dict for floats that contains the conversion rates SEK_TO_EUR (default to 11.9) and SEK_TO_USD (10.9) (class attributes)

  • Instance attribute

    • currency stores currency type (‘SEK’ or ‘EUR’)

    • amount stores amount of money

  • Ordinary class methods

    • set_conversion_rate(new_conversion_rate) : method for (re)setting the conversion rates. If rate is present, update it. Otherwise add conversion rate.

  • Overloaded operations

    • addition operator(+) : implement addition of money objects - remember that you should only be able to add amounts if they are in same currency (that is, a conversion may be required)

    • substraction operator(-) : implement substraction of money objects - remember that you should only be able to subtract amounts if they are same currency

    • currency type of result must be currency type of first operand in arithmetical expression

  • Class method

    • update_rates: takes a dictionary with new conversion rates and updates the class attribute

class Money(object):
    """
    A class for manipulation of different currencies
    
    class attributes:
        conversion_rate (dict of floats) 
    instance attributes:
        amount (float)
        currency (str)
    class methods:
        set_conversion_rate: resets conversion rate
        __add__: implements addition of Money objects (right side currency converted to left side if different)
        __sub__: implements subtraction of Money objects
    """
    conversion_rate = {"SEK_PER_EUR": 11.9, "SEK_PER_USD": 10.9}

    def __init__(self, amount, currency):
        self._amount = amount
        self._currency = currency
    
    @property
    def amount(self):
        return self._amount
    
    @amount.setter
    def amount(self, new_amount):
        self._amount = new_amount
    
    @property
    def currency(self):
        return self._currency
    
    @currency.setter
    def currency(self, new_currency):
        self._currency = new_currency

    @classmethod
    def set_conversion_rate(cls, rate):
        for key, value in rate.items():
            print(key, value)
            if key in cls.conversion_rate.keys():
                cls.conversion_rate[key] = value
            else:
                print(f"Introduced new conversion rate {key}:{value}")
                cls.conversion_rate.update({key : value})
        
    def __add__(self, other):
        if self._currency == other._currency:
            return Money(self._amount + other._amount, self._currency)
        else:
            rate_key = self._currency + '_PER_' + other._currency
            rate_invkey = other._currency + '_PER_' + self._currency
            if rate_key in Money.conversion_rate.keys():
                return Money(self._amount + other._amount * Money.conversion_rate[rate_key], self._currency)
            elif rate_invkey in Money.conversion_rate.keys():
                return Money(self._amount + other._amount / Money.conversion_rate[rate_invkey], self._currency)
            else:
                raise ValueError("Trying to add Money objects of unknown and different currency")
            
    def __sub__(self, other):
        return self + (-other)
    
    def __neg__(self):
        return Money(-self._amount, self._currency)
    
    def __str__(self):
        return f'{self._amount} {self._currency}'
    
#Verify class attribute
assert Money.conversion_rate["SEK_PER_EUR"] == 11.9, error_message(Money.conversion_rate["SEK_PER_EUR"], 11.9)
assert Money.conversion_rate["SEK_PER_USD"] == 10.9, error_message(Money.conversion_rate["SEK_PER_USD"], 10.9)
#Verify instance attributes
ma = Money(100, 'SEK')
mb = Money(50, 'EUR')
assert ma.amount == 100, error_message(ma.amount, 100)
assert ma.currency == 'SEK', error_message(ma.currency, 'SEK')
assert mb.amount == 50, error_message(mb.amount, 50)
assert mb.currency == 'EUR', error_message(mb.currency, 'EUR')
#Verify addition
ma = Money(100, 'SEK')
mb = Money(50, 'EUR')
mc = ma + mb
assert mc.amount == 695, error_message(mc.amount, 695)
assert mc.currency == 'SEK', error_message(mc.currency, 'SEK')

#Verify addition
ma = Money(100, 'EUR')
mb = Money(50, 'SEK')
mc = ma + mb
assert mc.amount == approx(104.2016806722689), error_message(mc.amount, 104.2)
assert mc.currency == 'EUR', error_message(mc.currency, 'EUR')
#Verify subtraction
ma = Money(100, 'SEK')
mb = Money(50, 'EUR')
md = mb - ma
assert md.amount == approx(41.596638655462186), error_message(md.amount, 41.6)
assert md.currency == 'EUR', error_message(md.currency, 'EUR')
#Set conversion rate
ma = Money(100, 'SEK')
mb = Money(50, 'EUR')
ma.set_conversion_rate({"SEK_PER_EUR":20})
assert ma.conversion_rate["SEK_PER_EUR"] == 20, error_message(ma.conversion_rate, 20)
assert mb.conversion_rate["SEK_PER_EUR"] == 20, error_message(mb.conversion_rate, 20)
SEK_PER_EUR 20

Inheritance, parent and child classes#

A main concept in object-oriented programming is inheritance.

Some benefits of inheritance:

  • Represent the relationship between different objects that exists in the real world. (For instance, humans, cats, dogs, etc. are all animals)

  • Avoid replication of code. We do not need to rewrite similar code piece over and over again

Syntax of simple inheritance:

As a class (by default in Python 3 or explicitly in Python 2) inherits from the built-in object class

class Parent(object): # Parent class inherits from the built-in object class
    <body of code>

class Child(ParentClass): #Child class inherits from the Parent class
    <body of code>

A parent class (also called superclass or baseclass) is a class whose properties are inherited by the child class (also called subclass or derived class).

Ways to inherit from parent class:

  • Simplest case is when child class do not implement its own __init__. Then it will automatically inherit its parent

  • When the child implements its own __init__, it must invoke super().__init__(). super() allows you to refer to the parent class

class Parent:
    def __init__(self):
        print("parent initialization")

    def parent_method(self):
        print('Calling parent method')

class Child(Parent):
    def __init__(self):
        super().__init__(<inputs>) #<------- required when inheritance should be forced
        print("Child initialization")

    def child_method(self):
         print('Calling child method')

Besides inheriting all data attributes and methods from the parent class, the child class can:

  • introduce additional data attributes

  • introduce additional procedural attributes (methods)

  • override methods from the parent class (use the same name for the method in a child class as its parent class in order to override the behavior). This is called polymorphism.

Hierachies:

Child class can have methods with same name as superclass. So, how do we keep track of which one to use?

  1. For given class instance, look for a method name in current class definition

  2. If not present, look for method name up the hierarchy (that is, in parent, then grandparent, etc.)

  3. Use first method up the hierarchy that you found with that method name

class Animal(object): 
    """
    A class to illustrate inheritance
     
    instance attributes:
        name: (str) animal name
        age: (int) animal age
        
    class methods:
        __repr__: formal string representation of Animal"
        __str__: informal string representation of Animal
    """
    animals = []

    def __init__(self, name, age):
        self._name = name
        self._age = age

    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, name):
        self._name = name    

    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, age):
        self._age = age

    def __repr__(self):
        return f"Animal({self.name},{self.age})"
    
    def __str__(self):
        return f"Hello, I am an Animal named {self.name}. I am {self.age} years old."
# Let's now make two child classes derived from the Animal class
# First, write a Cat class. It should introduce another data attribute color
# It should override the __repr__ methods and return f"Cat({self.name}, {self.age}, {self.color})"
# It should override the __str__ method and return f"I am a Cat that can say meow"

class Cat(Animal):
    def __init__(self, name, age, color): #<---- Invoked separate init for child class. Hence I user super()
        super().__init__(name, age)
        self._color = color

    @property
    def color(self):
        return self._color
    
    def __repr__(self): #<------------- Overriding a method
        return f"Cat({self.name}, {self.age}, {self.color})"
    
    def __str__(self): #<------------- Overriding a method
        return f"I am a Cat that can say meow"
# Now to the Person class. 
# It should introduce another data attribute job
# It should override the __repr__ methods and return f"Person({self.name}, {self.age}, {self.job})"
# It should override the __str__ method and return f"My name is {self.name}. I am {self.age} years old and my job is {self.job}"
        
class Person(Animal):
    
    def __init__(self, name, age, job):
        super().__init__(name, age)
        self._job = job
    
    @property
    def job(self):
        return self._job
    
    @job.setter
    def job(self, job):
        self._job = job
    
    def __repr__(self): #<------------- Overriding a method
        return f"Person({self.name}, {self.age}, {self.job})"
    
    def __str__(self): # <-------------- Overriding a method
        return f"My name is {self.name}. I am {self.age} years old and my job is {self.job}"
    
# Let us create a Person instance
kevin = Person("Kevin", 18, "student")

# And a cat instance
carlo = Cat("Carlo", 2, "brown")
assert kevin.name == "Kevin", error_message(kevin.name, "Kevin")
assert kevin.age == 18, error_message(kevin.age, 18)
assert kevin.job == "student", error_message(kevin.job, "student")
assert str(kevin) == "My name is Kevin. I am 18 years old and my job is student", error_message(str(kevin), "My name is Kevin. I am 18 years old and my job is student")
assert repr(kevin) == "Person(Kevin, 18, student)", error_message(repr(kevin), "Person(Kevin, 18, student)")
assert carlo.name == "Carlo", error_message(carlo.name, "Carlo")
assert carlo.age == 2, error_message(carlo.age, 2)
assert carlo.color == "brown", error_message(carlo.color, "brown")
assert str(carlo) == "I am a Cat that can say meow", error_message(str(carlo), "I am a Cat that can say meow")
assert repr(carlo) == "Cat(Carlo, 2, brown)", error_message(repr(carlo), "Cat(Carlo, 2, brown)")