# 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 attributesclass method(
cls, arguments
): can access the class (via cls) but cannot access the instance (via self). Use@classmethod
decoratorstatic 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 vectorssubstraction operator(
-
) : implement__sub__
method to return a new vector as difference of the two vectorscompare two vectors (
==
) : implement__eq__
method to compare (coordinate by coordinate) the vector to anotherstr 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 parentWhen the child implements its own
__init__
, it must invokesuper().__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?
For given class instance, look for a method name in current class definition
If not present, look for method name up the hierarchy (that is, in parent, then grandparent, etc.)
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)")