Lesson 7. Intro To OO
Lesson 7. Intro To OO
Intro to OO
1
• Following an OO approach we analyze the problem in terms of:
1. Entities that appear in our problem (classes/objects)
2. Characteristics of those entities or information we need to store related to them (data at-
tributes or fields)
3. Things that those entities can do (methods)
• This is known as encapsulation (variables + methods operating on them): we put together
data and functions working over them. OO is also inheritance and polymorphism (we’ll see
them later in this topic)
2 Prelude
As seen above, in OO we put together data and functions related to the entities of a given problem.
Let start focusing in data by having a look of the enemies of our game. As a first approach, the
data we need to store about each enemy could be: x position, y position, direction, image… Which
data structure can be used to store it? It could be a list, but it is better to use a dictionary:
Data of an enemy {'x': 10, 'y': 10, 'direction': 'left', 'image': 'enemy.png'}
We could even thing on adding more functions, like one to change the direction of the enemies:
2
[4]: def change_direction(enemy : dict):
""" Changes the value of the 'direction' key of an enemy dictionary. As␣
↪dictionaries are
mutable, the value of the ditionary used to invoke the parameter changes
:param enemy: a dictionary with the information of an enemy"""
if enemy["direction"] == "right":
enemy["direction"] = "left"
else:
enemy["direction"] = "right"
[5]: change_direction(enemy3)
print("Data of an enemy", enemy3)
Data of an enemy {'x': 10, 'y': 10, 'direction': 'right', 'image': 'enemy.png'}
3 Classes
• The former example is not using OO, we can do something better using OO.
• As a first approach a class can be seen as a kind of generic dictionary.
• A class is like an skeleton to create objects: it is like a generic definition of the variables
(which we will call attributes or fields) and the functions (which we will call methods) that
an object will have. In other words, a class is a generic definition of all the objects of a given
type.
• A whole program will have two different kinds of ‘programs’: classes where fields and methods
of the objects are defined and implemented, and a main program where objects belonging to
those classes are used to build the algorithm. We define classes and work with objects.
• To define a new class we use class NameOfTheClass:
• Naming conventions for class names: upper camel case (see in the above line an example of
a correct class name)
• It is recommended to put the class in its own file, do not use the same file for different classes
nor include any other code in that file
• Once the class is defined I need to add attributes and methods to it. For the attributes the
recommended way is to use a special method named the init method.
• Most generic schema of a class: (except for the init method anything could be optional)
class NameOfTheClass:
""" Class documentation """
3
@property
def attribute_name(self):
""" Properties and setters are used to check the values of the attributes """
@attribute.setter
def attribute_name(self, new_value):
""" Properties and setters are used to check the values of the attributes """
• Attributes are special variables declared inside the init by appending self.
to their names. Name rules for attributes names: same as for variables
(self.this_is_a_valid_attribute_name)
• As a general rule it must receive a parameter for each of the attributes of the object.
• But if the value of any attribute can be calculated using other attributes or it has
always a fixed initial value or it is random, a parameter for that attribute is not
needed.
• As in any other method, the parameters can have by default values (optional), which will be
the initial values of the attributes if no parameter is given when invoking the init method.
4
def __init__(self, day:int, m: str, year: int):
""" This method both declares the attributes of the class and
receives the initial value for all them
:param day: the day
:param m: the month
:param year: the year
"""
# Attributes must be declared with self.name_of_attribute
# In the init we usually just copy the value of each parameter into its
# corresponding attribute
# We use self.name to store each of the data
self.day = day
self.month = m
self.year = year
Exercise: Create a class for the enemy dictionaries seen above where the direction will be randomly
chosen.
5 Objects
• Classes just define the characteristics of the objects, to use a class we need to create an object.
• An object is a variable of a Class. In other words, an object is a variable whose type is a
class.
• To create an object: variable_name = ClassName(values_for_the_attributes). When
doing it, we are really invoking the init method.
• Values of the fields of the new object must be specified when creating it.
• To access or change the value of an attribute of an object I use dot notation:
object.attribute to see the value or object.attribute = value to change its value.
• What happens if we try to access a non-existing field? -> Error
5
• What happens if we try to assign a value to a non-existing field? -> The field is created (as
in dictionaries)
• Do not create fields on the fly. It is a bad programming practice as all the objects of a class
must have the same fields. It is very difficult to manage programs where objects of a class
can have different attributes. Totally forbidden this course.
[9]: # This is the way to declare a variable belonging to the Date class
# Any variable belonging to a class is called an object: my_date is
# an object of the Date class
my_date = Date(day = 12, m = "october", year = 2022)
# To see the values to the attributes I use the so called "dot notation"
# object.attribute
# To recover the value of the day
print("The day is", my_date.day)
# Assigning a new value for the day
my_date.day = 22
print("The day is", my_date.day)
# For the month
my_date.month = "November"
print("The month is", my_date.month)
# For the year
my_date.year = 2021
print("The year is", my_date.year)
# This is another object of the Date class, notice we usually give values
# to parameters by their position, we don't use the names
another_date = Date(22, "october", 2022)
# We specified day attribute is an integer, but this is only a
# suggestion, anything can be stored into it. Don't do it!!!
another_date.day = "hello"
print("The day is", another_date.day)
The day is 12
The day is 22
The month is November
The year is 2021
The day is hello
right
6
• The Class must be in our folder. To be able to import from another folder, we should create
packages (we will not cover that but material is uploaded to Aula Global)
[8]: # Creating objects with values for the attributes that make no sense
d = Date("hello", 4, "bye")
print(d.day, d.month, d.year)
# And I can also do it after creating them
d2 = Date(1, "november", 2022)
7
d2.day = "hello how are you?"
print(d2.day, d2.month, d2.year)
hello 4 bye
hello how are you? november 2022
• Setters and properties are the way Python has to avoid the user of a class to give wrong
values to the attributes.
• To use setters/properties in Python we need to do the following:
1. In the init we create the attribute in the regular way self.attribute = value.
We don’t perform any checking of the value. We will also use the attribute in the
regular way in any other method except for the property and the setter methods
that we will create for each attribute.
2. For each attribute for which we want to control the values, we create a new
method with the same name of the attribute. This magic method is called setter.
Its header will be def attribute(self, value) and it must be decorated with
@attribute.setter. Inside this method I should check that the type of value
is correct and also that its value is correct. If they are not right, exceptions will
be risen. If the value is correct I will assign it to the attribute, but instead of
doing self.attribute = value I must put two underscores before the name of
the attribute: self.__attribute = value. If I don’t do it I will have an error.
This magic method will be automatically invoked any time I try to change the
value of the attribute, either in the class or in the main program.
3. If I created a setter like in the previous point, I am forced to create a property. A
property is also a magic method. It must have the name of the attribute too and
will receive only self as parameter (def attribute(self)). It must be decorated
with @property (exactly like this). In this special method all I can do is to return
the value of the attribute adding two underscores before the name of the attribute:
self.__attribute. This will allow to read the value of the attribute but not to
change it. The property must be placed before the setter.
• In @property and @setter I can also use self._attribute (only one underscore) instead of
self.__attribute but if I do so, in addition to attribute, _attribute will be also visible
outside, which is a non-desired side effect.
• We’ll see later that we can have a property without a setter, but setters can never be alone,
if a setter is created a property must be defined first.
[1]: # A Date class with setters and properties to avoid giving a wrong value to the␣
↪fields
class Date:
"""A class to store a date information. We will use properties/setters to
check the values provided are good. It has also by default values for
the parameters"""
8
""" This method both declares the attributes of the class and
receives the initial value for all them
:param day: the day
:param m: the month
:param year: the year
"""
# No checking is done here, everything is delegated to the setters
# As I have a setter for year, this is totally equivalent to an␣
↪invocation
#We create properties and setters also for day and month
@property
def month(self) -> str:
""" :return : the name of the month in small caps"""
return self.__month
9
@month.setter
def month(self, month: str):
# Notice that inside a method we can use local variables as we did with
# functions. Do not use self. with them or they will become attributes
# and they will be visible outside the class. We only want this variable
# to be used inside this method.
days_in_month = {'january':31, 'february': 28, "march": 31, "april":30,
"may": 31, "june": 30, "july": 31, "august": 31,
"september": 30, "october": 31, "november": 30,
"december":31}
if type(month) != str:
raise TypeError("The month must be a string")
elif month.lower() in days_in_month:
self.__month = month.lower()
else:
raise ValueError("Valid months are " + str(list(days_in_month)))
@property
def day(self) -> int:
return self.__day
@day.setter
def day(self, day: int):
# We cannot use a variable defined in another method, so we need to␣
↪repeat
days_in_month['february'] = 29
if type(day) != int:
raise TypeError("The day must be an integer")
elif day > 0 and day <= days_in_month[self.month]:
self.__day = day
else:
if self.month == 2:
raise ValueError("February has only 28 days in " + str(self.
↪year))
else:
raise ValueError(self.month + " has only " +␣
↪str(days_in_month[self.month])
10
+ " days")
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[2], line 3
1 # If we try to create a wrong month, we get an error
2 # The same would happen for day or year
----> 3 my_date = Date(18, "movember", 2021)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[3], line 3
1 my_date = Date(18,"november",2021)
2 # This will also raise an error
----> 3 my_date.day = 33
98 + " days")
11
ValueError: november has only 30 days
7 Methods
• A method is a function that I declare inside a class
• Methods have always at least one parameter: the self parameter (we can call it whichever
name we want, but self is the recommended one)
• This first and compulsory parameter of a method is a special one, because it represents the
whole object. It contains a link to the attributes of the object so we can work with them
inside the method
• To invoke a method I use object_name.method_name(parameters without considering
the self)
12
# As I have a setter for year, this is totally equivalent to an␣
invocation
↪
#We create properties and setters also for day and month
@property
def month(self) -> str:
""" :return : the name of the month in small caps"""
return self.__month
@month.setter
def month(self, month: str):
# Notice that inside a method we can use local variables as we did with
# functions. Do not use self. with them or they will become attributes
# and they will be visible outside the class. We only want this variable
# to be used inside this method.
13
days_in_month = {'january':31, 'february': 28, "march": 31, "april":30,
"may": 31, "june": 30, "july": 31, "august": 31,
"september": 30, "october": 31, "november": 30,
"december":31}
if type(month) != str:
raise TypeError("The month must be a string")
elif month.lower() in days_in_month:
self.__month = month.lower()
else:
raise ValueError("Valid months are " + str(list(days_in_month)))
@property
def day(self) -> int:
return self.__day
@day.setter
def day(self, day: int):
# We cannot use a variable defined in another method, so we need to␣
↪repeat
days_in_month['february'] = 29
if type(day) != int:
raise TypeError("The day must be an integer")
elif day > 0 and day <= days_in_month[self.month]:
self.__day = day
else:
if self.month == 2:
raise ValueError("February has only 28 days in " + str(self.
↪year))
else:
raise ValueError(self.month + " has only " +␣
↪str(days_in_month[self.month])
+ " days")
14
days_in_month = {'january':31, 'february': 28, "march": 31, "april":30,
"may": 31, "june": 30, "july": 31, "august": 31,
"september": 30, "october": 31, "november": 30,
"december":31}
# I change the days of February if leap year
if self.year % 4 == 0 and (self.year % 100 != 0 or self.year % 400 ==␣
0):
↪
days_in_month['february'] = 29
return days_in_month[self.month]
30
Exercise: create properties, setters and a change_direction method for the Enemy class.
@property
def x(self) -> int:
return self.__x
@property
def y(self) -> int:
return self.__y
@property
def direction(self) -> str:
return self.__direction
15
@property
def image(self) -> str:
return self.__image
@x.setter
def x(self, x: int):
if type(x) != int:
raise TypeException ("The coordinate x must be an int")
elif x < 0:
raise ValueException("No negative coordinates allowed")
else:
self.__x = x
@y.setter
def y(self, y: int):
if type(y) != int:
raise TypeException ("The coordinate y must be an int")
elif y < 0:
raise ValueException("No negative coordinates allowed")
else:
self.__y = y
@direction.setter
def direction(self, direction: str):
if type(direction) != str:
raise TypeError("The direction must be string")
elif direction.lower() != "left" and direction.lower() != "right":
raise ValueError("The valid directions are 'left' or 'right'")
else:
self.__direction = direction.lower()
@image.setter
def image(self, image: str):
if type(image) != str:
raise TypeError("The image must be a file name (string)")
else:
self.__image = image
def change_direction(self):
""" Changes the value of the 'direction' attribute of an enemy"""
if self.direction == "right":
self.direction = "left"
else:
self.direction = "right"
16
[5]: enem = Enemy(20,30)
print("The direction is", enem.direction)
enem.change_direction()
print("The direction is", enem.direction)
17
""" This special method will return the value of the year
:return : the year"""
# Here I must return my attribute preceded by __
# If I don't do it it will not work
return self.__year
#We create properties and setters also for day and month
@property
def month(self) -> str:
""" :return : the name of the month in small caps"""
return self.__month
@month.setter
def month(self, month: str):
# Notice that inside a method we can use local variables as we did with
# functions. Do not use self. with them or they will become attributes
# and they will be visible outside the class. We only want this variable
# to be used inside this method.
days_in_month = {'january':31, 'february': 28, "march": 31, "april":30,
"may": 31, "june": 30, "july": 31, "august": 31,
"september": 30, "october": 31, "november": 30,
"december":31}
if type(month) != str:
raise TypeError("The month must be a string")
elif month.lower() in days_in_month:
self.__month = month.lower()
else:
raise ValueError("Valid months are " + str(list(days_in_month)))
@property
def day(self) -> int:
18
return self.__day
@day.setter
def day(self, day: int):
# We cannot use a variable defined in another method, so we need to␣
↪repeat
days_in_month['february'] = 29
if type(day) != int:
raise TypeError("The day must be an integer")
elif day > 0 and day <= days_in_month[self.month]:
self.__day = day
else:
if self.month == 2:
raise ValueError("February has only 28 days in " + str(self.
↪year))
else:
raise ValueError(self.month + " has only " +␣
↪str(days_in_month[self.month])
+ " days")
days_in_month['february'] = 29
return days_in_month[self.month]
19
be called like this, no changes in name, no extra parameters.
It must return the string I want to be shown when printing"""
sufix = "th"
if self.day == 1 or self.day == 21 or self.day == 31:
sufix = "st"
elif self.day == 2 or self.day == 22:
sufix = "nd"
elif self.day == 3 or self.day == 23:
sufix = "rd"
return self.month + " " + str(self.day) + sufix +", " + str(self.year)
20
False
False
Exercise: Create magic methods for the Enemy class
@property
def x(self) -> int:
return self.__x
@property
def y(self) -> int:
return self.__y
@property
def direction(self) -> str:
return self.__direction
@property
def image(self) -> str:
return self.__image
@x.setter
def x(self, x: int):
if type(x) != int:
raise TypeException ("The coordinate x must be an int")
elif x < 0:
raise ValueException("No negative coordinates allowed")
else:
self.__x = x
21
@y.setter
def y(self, y: int):
if type(y) != int:
raise TypeException ("The coordinate y must be an int")
elif y < 0:
raise ValueException("No negative coordinates allowed")
else:
self.__y = y
@direction.setter
def direction(self, direction: str):
if type(direction) != str:
raise TypeError("The direction must be string")
elif direction.lower() != "left" and direction.lower() != "right":
raise ValueError("The valid directions are 'left' or 'right'")
else:
self.__direction = direction.lower()
@image.setter
def image(self, image: str):
if type(image) != str:
raise TypeError("The image must be a file name (string)")
else:
self.__image = image
def change_direction(self):
""" Changes the value of the 'direction' attribute of an enemy"""
if self.direction == "right":
self.direction = "left"
else:
self.direction = "right"
else:
raise NotImplementedError("Only enemies can be compared")
22
[47]: enem1 = Enemy(10,10)
enem2 = Enemy(10,10)
print(enem1)
print(enem2)
print(enem1 == enem2)
23
# As we need the month and the year to check the values of the day,
# the day attribute must be declared after the year and month ones
self.day = day
#This is a read only field that we can read inside the class or outside it
@property
def leap_year(self) -> bool:
""" The value of leap_year is calculated using other fields"""
return self.year % 4 == 0 and (self.year % 100 != 0 or self.year % 400␣
↪== 0)
#We create properties and setters also for day and month
@property
def month(self) -> str:
""" :return : the name of the month in small caps"""
return self.__month
@month.setter
def month(self, month: str):
# Notice that inside a method we can use local variables as we did with
# functions. Do not use self. with them or they will become attributes
# and they will be visible outside the class. We only want this variable
24
# to be used inside this method.
days_in_month = {'january':31, 'february': 28, "march": 31, "april":30,
"may": 31, "june": 30, "july": 31, "august": 31,
"september": 30, "october": 31, "november": 30,
"december":31}
if type(month) != str:
raise TypeError("The month must be a string")
elif month.lower() in days_in_month:
self.__month = month.lower()
else:
raise ValueError("Valid months are " + str(list(days_in_month)))
@property
def day(self) -> int:
return self.__day
@day.setter
def day(self, day: int):
# We cannot use a variable defined in another method, so we need to␣
↪repeat
else:
raise ValueError(self.month + " has only " +␣
↪str(days_in_month[self.month])
+ " days")
25
days_in_month = {'january':31, 'february': 28, "march": 31, "april":30,
"may": 31, "june": 30, "july": 31, "august": 31,
"september": 30, "october": 31, "november": 30,
"december":31}
# I change the days of February if leap year
if self.leap_year:
days_in_month['february'] = 29
return days_in_month[self.month]
26
[22]: # Creating the object and printing it
obj = Date(14, "november", 2022)
print(obj)
# We can read the value of leap_year
print(obj.leap_year)
# But if we try to set it, we have an error
obj.leap_year = 12
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[22], line 7
5 print(obj.leap_year)
6 # But if we try to set it, we have an error
----> 7 obj.leap_year = 12
Exercise. Convert the image attribute of the Enemy class into a read only one
@property
def x(self) -> int:
return self.__x
@property
def y(self) -> int:
return self.__y
27
@property
def direction(self) -> str:
return self.__direction
@property
def image(self) -> str:
""" A read only attribute without setter"""
return "image.png"
@x.setter
def x(self, x: int):
if type(x) != int:
raise TypeException ("The coordinate x must be an int")
elif x < 0:
raise ValueException("No negative coordinates allowed")
else:
self.__x = x
@y.setter
def y(self, y: int):
if type(y) != int:
raise TypeException ("The coordinate y must be an int")
elif y < 0:
raise ValueException("No negative coordinates allowed")
else:
self.__y = y
@direction.setter
def direction(self, direction: str):
if type(direction) != str:
raise TypeError("The direction must be string")
elif direction.lower() != "left" and direction.lower() != "right":
raise ValueError("The valid directions are 'left' or 'right'")
else:
self.__direction = direction.lower()
def change_direction(self):
""" Changes the value of the 'direction' attribute of an enemy"""
if self.direction == "right":
self.direction = "left"
else:
self.direction = "right"
28
return "Enemy at " + str(self.x) + ", " + str(self.y) + " moving to the␣
↪ " + str(self.direction)
else:
raise NotImplementedError("Only enemies can be compared")
29
• If I need a variable to be used only internally but by more than one method, it
should be declared as a private attribute
• If variable is going to be used only by one method, it should be declared as a local
variable
• I can also have private methods def __method(self, ...) they can be used inside the class
but not outside it.
30
# As this method's name is equal to the previous one, to avoid replacing
# the previous one, it needs to be decorated with @attribute.setter
@year.setter
def year(self, year: int):
""" This method allows to change the value of the year
:param year: the value of the year, if 0 is given an error is raisen"""
# If the type is not correct we raise an exception
if type(year) != int:
raise TypeError("The year must be an int")
# Here I need to use __year again
elif year != 0:
self.__year = year
# We also change the value of the days of February
if self.leap_year:
self.__days_in_month['february'] = 29
else:
self.__days_in_month['february'] = 28
else:
# In any other case, I raise an exception
raise ValueError("Year must be not equal to 0")
#We create properties and setters also for day and month
@property
def month(self) -> str:
""" :return : the name of the month in small caps"""
return self.__month
#This is a read only field that we can read inside the class or outside it
@property
def leap_year(self) -> bool:
""" The value of leap_year is calculated using other fields"""
return self.year % 4 == 0 and (self.year % 100 != 0 or self.year % 400␣
↪== 0)
@month.setter
def month(self, month: str):
if type(month) != str:
raise TypeError("The month must be a string")
elif month.lower() in self.__days_in_month:
self.__month = month.lower()
else:
raise ValueError("Valid months are " + str(list(self.
↪__days_in_month)))
@property
def day(self) -> int:
31
return self.__day
@day.setter
def day(self, day: int):
if type(day) != int:
raise TypeError("The day must be an integer")
elif day > 0 and day <= self.__days_in_month[self.month]:
self.__day = day
else:
if self.month == 2:
raise ValueError("February has only 28 days in " + str(self.
↪year))
else:
raise ValueError(self.month + " has only " + str(self.
↪__days_in_month[self.month])
+ " days")
32
""" This is the method Python will invoke if I try to compare
two Date objects. In addition to self it must receive the other
object. It must return True or False"""
# If the type of the other object is not Date we will raise
# a NotImplemented excpetion
if type(another) == Date:
# Two dates are equal if the values of all their attributes are
# equal
return (self.day == another.day and self.month == another.month
and self.year == another.year)
else:
raise NotImplementedError("== can only compare Date objects")
10 Composition
• In OOP there are two ways to create a class using another class: composition and inheritance.
• In composition one of the attributes of a class is an object of another class
33
# birthday must be an object of Date class
# we should add a property and a setter for it
self.birthday = birthday
@birthday.setter
def birthday(self, bd):
if type(bd) != Date:
raise TypeError("The birthday must be a date")
else:
self.__birthday = bd
else:
raise NotImplementedError()
34
1st, 2002 (non leap) equal? False
11 Inheritance
• Second way to create a new class from an existing one (different from composition)
• We want to create a new class that has everything that another class has plus some new
attributes and new methods and maybe we would like also to modify the implementation of
some of the methods.
• Inheritance is like copying the code of one class (called mother class or super class) into
another one (named child class or subclass), but it has some advantages:
• No need to copy the code, it is done automatically
• Any change done to the mother class will be automatically inherited by the child
class (no need to copy it again)
• If I want to add new attributes I need to put them in the new init method and rely on the
inherited one for the old ones using super()
• In general if I want to change a method I need to overwrite it, but if I want I can use the old
version (the one from the mother class) to do it. In this case the keyword super() is used
@property
def price(self) -> float:
return self.__price
@price.setter
def price(self, price: float):
if type(price) != float and type(price) != int:
raise TypeError("The price must be a number")
elif price < 0:
raise ValueError("Do you really want a negative price?")
else:
self.__price = price
35
""" Two products are equal if they have the same name, despite of the␣
↪price"""
if type(other) == Product:
return self.name == other.name
else:
raise NotImplementedError("Cannot compare " + str(type(self)) + "␣
↪and " + str(type(other)))
toy: 34.99€
toy: 17.5€
36
apples: 1.2€
apples: 0.6€
apples: 0€
apples: 1€
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[75], line 13
11 fp.price = 1
12 print(fp)
---> 13 fp.price = -1
[82]: # Subclass that adds a new method and some attributes to the superclass
# It will overwrite __init__ and __str_ using the equivalent methods
# of the superclass
class FreshProduct(Product):
@property
def expiration(self) -> Date:
return self.__expiration
@expiration.setter
def expiration(self, d):
if type(d) != Date:
raise TypeError("The expiration must be a date")
else:
37
self.__expiration = d
# Overwriting the eq
def __eq__(self, other) -> bool:
""" Two Fresh Products are equal if they have the same name and␣
↪expiration date"""
if type(other) == FreshProduct:
return super().__eq__(other) and self.expiration == other.expiration
else:
raise NotImplementedError("Cannot compare " + str(type(self)) + "␣
↪and " + str(type(other)))
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
Cell In[79], line 3
1 fp5 = FreshProduct("banana", 3.2, Date(12, "january", 2023))
2 # They should be equal, but an exception appears!!!
----> 3 print("Are", fp4, "and", fp5, "equal?", fp4 == fp5)
4 # Why? Because the eq of Product checks that the type of the other must␣
↪be Product
38
Cell In[77], line 44, in FreshProduct.__eq__(self, other)
42 """ Two Fresh Products are equal if they have the same name and␣
↪expiration date"""
43 if type(other) == FreshProduct:
---> 44 return super().__eq__(other) and self.expiration == other.expiration
45 else:
46 raise NotImplementedError("Cannot compare " + str(type(self)) + "␣
↪and " + str(type(other)))
12 Polymorphism
• Is the fact that an object of the child class is also an object of the mother class
• We can see it with the difference between type() and isinstance()
39
[80]: # Rewriting the Product class to use isinstance in the eq instead of type
class Product:
""" This class represents a product in an online store"""
def __init__(self, name: str, price: float):
self.name = name
self.price = price
@property
def price(self) -> float:
return self.__price
@price.setter
def price(self, price: float):
if type(price) != float and type(price) != int:
raise TypeError("The price must be a number")
elif price < 0:
raise ValueError("Do you really want a negative price?")
else:
self.__price = price
if isinstance(other, Product):
return self.name == other.name
else:
raise NotImplementedError("Cannot compare " + str(type(self)) + "␣
↪and " + str(type(other)))
[83]: # Now the if I run again the cell where FreshProduct is declared, its eq works␣
↪properly
Are banana: 1.2€ expires on january 12th, 2023 (non leap) and banana: 3.2€
expires on january 12th, 2023 (non leap) equal? True
40
13 Multiple inheritance
• In Python a class can inherit from several classes: class A(B,C,D)
• If I use super() it looks first for the method at B, if it does not find it, it will look at C, and
so on
• If there are attributes with the same name I will keep only the one that changed last
• If I have several methods with the same name, I keep the one of the first mother class in the
list of mother classes
• But I can also invoke that method of any other mother class by writing:
MotherClass.method(object, parameters)
41