1. Operator Overloading: comparison#
How can we compare two custom objects?
Below we have two objects of the
Sensor
class that have the same data. if we Python ask if these objects are equal , the answer isno
.class Sensor: def __init__ (self, name, serial_number): self.name , self.serial_number = name, serial_number sensor1 = Sensor("Temperature",123) sensor2 = Sensor("Temperature",123) sensor1 == sensor2
False
Important
Python doesn’t consider two objects with the same data equal by default.
When we print both sensors, we receive an output with
Sensor at
and a string (a number in hexadecimal). When an object is created, Python allocates a chunk of memory to that object, and the variable that the object is assigned to actually contains just the reference to the memory chunk.In other words, the code below is saying to Python: Allocate a chunk of memory for a sensor object, and label it
sensor1
. Then, allocate another chunk of memory, and label itsensor2
.Note
When we compare variables
sensor1
andsensor2
, we are actually comparing references, not the data. Becausesensor1
andsensor2
point to different chuncks in memory, they are not considered equal.sensor1 = Sensor("Temperature",123) sensor2 = Sensor("Temperature",123) print(sensor1) print(sensor2)
<main.Sensor object at 0x7fecb45d2790>
<main.Sensor object at 0x7fecb45d2a60>
1.1. The __eq__()
method#
The
__eq__()
method is implicitly called whenever two objects of the same class are compared to each other.We can re-define this method to execute custom comparison code. The methods should accept two arguments, referring to the objects to be compared. They are usually
self
and other by convention. It should always return aBoolean
valueTrue
orFalse
.The following code has a basic
Sensor
class withid
andname
attributes, and we redefine the__eq__
method to returnTrue
if the values of all attributes are equal.class Sensor: def __init__(self, id, name): self.id, self.name = id, name # The following is called when `==` is used def __eq__(self, other): print ("__eq__() is called") # Return `True` if all attributes match return (self.id == other.id) and \ (self.name == other.name) sensor3 = Sensor(456, "Conductivity") sensor4 = Sensor(456, "Conductivity") sensor3 == sensor4
__eq__()
is called
True
1.2. Other Comparison Operators#
Python allows you to implement all the comparison operators in your custom class using special methods like
__eq__
.
Operator |
Method |
Description |
Example |
---|---|---|---|
== |
|
returns True if two operands are equal, otherwise False. |
a == b |
!= |
|
returns True if two operands are not equal, otherwise False. |
a != b |
> |
|
returns True if left operand is greater than the right operand, otherwise False |
a > b |
< |
|
returns True if left operand is smaller than the right operand, otherwise False |
a < b |
>= |
|
returns True if left operand is greater than or equal to the right operand, otherwise False |
a >= b |
<= |
|
returns True if left operand is smaller than or equal to the right operand, otherwise False. |
a <= b |
1.2.1. Example - Comparison Operators 1#
class Data:
id = 0
def __init__(self, i):
self.id = i
def __eq__(self, other):
print('== operator overloaded')
if isinstance(other, Data):
return True if self.id == other.id else False
else:
return False
def __ne__(self, other):
print('!= operator overloaded')
if isinstance(other, Data):
return True if self.id != other.id else False
else:
return False
def __gt__(self, other):
print('> operator overloaded')
if isinstance(other, Data):
return True if self.id > other.id else False
else:
return False
def __lt__(self, other):
print('< operator overloaded')
if isinstance(other, Data):
return True if self.id < other.id else False
else:
return False
def __le__(self, other):
print('<= operator overloaded')
if isinstance(other, Data):
return True if self.id <= other.id else False
else:
return False
def __ge__(self, other):
print('>= operator overloaded')
if isinstance(other, Data):
return True if self.id >= other.id else False
else:
return False
d1 = Data(10)
d2 = Data(7)
print(f'd1 == d2 = {d1 == d2}')
print(f'd1 != d2 = {d1 != d2}')
print(f'd1 > d2 = {d1 > d2}')
print(f'd1 < d2 = {d1 < d2}')
print(f'd1 <= d2 = {d1 <= d2}')
print(f'd1 >= d2 = {d1 >= d2}')
1.2.2. Example - Comparison Operators 2#
It is a good practice to check the class of objects passed to
__eq__()
method to make sure the comparison makes sense.For example, consider the following two classes:
Temperature class |
Conductivity Class |
---|---|
class Temperature :
def __init__(self, number):
self.number = number
def __eq__(self, other):
return self.number == other.number
temp = Temperature(1453)
print(type(temp))
|
class Conductivity:
def __init__(self, number):
self.number = number
def __eq__(self, other):
return self.number == other.number
cond = Conductivity(1453)
print(type(cond))
|
Running
temp == cond
will returnTrue
, even though we are comparing a temperature sensor number with a conductivity sensor number. This is wrong! We need to compare potatoes with potatoes =)
temp = Temperature(1453)
cond = Conductivity(1453)
print(temp == cond)
True
class Conductivity:
def __init__(self, number):
self.number = number
def __eq__(self, other):
return True if (self.number == other.number) and \
isinstance(other, Conductivity) else False
class Temperature:
def __init__(self, number):
self.number = number
def __eq__(self, other):
return True if (self.number == other.number) and \
isinstance(other, Temperature) else False
# ----------------------------------- %
temp1 = Temperature(1453)
cond1 = Conductivity(1453)
print (temp1.number, cond1.number)
print(f'temp1 == cond1 = {temp1 == cond1}')
# ------------------------------------ %
temp2 = Temperature(1453)
print (temp1.number, temp1.number)
print(f'temp1 == temp2 = {temp1 == temp2}')
# ------------------------------------ %
temp3 = Temperature(3985)
print (temp1.number, temp3.number)
print(f'temp1 == temp3 = {temp1 == temp3}')
# ------------------------------------ %
cond2 = Conductivity(5050)
print (cond1.number, cond2.number)
print(f'cond1 == cond2 = {cond1 == cond2}')
2. Operator Overloading: string representation#
There are two special methods that we can define in a class that will return a printable representation of an object.
2.1. The __str__()
method#
This method is executed when we call
print
orstr
on an objectThis method must return the String object. If we don’t implement
__str__()
function for a class, then built-in object implementation is used that actually calls__repr__()
: function.
2.2. The __repr__()
method#
This method is executed when we cal
repr
on the object, or when we print it in the console without calling print explicitly.
Note
If both the functions return strings, which is supposed to be the object representation, what’s the difference?
The __str__
function is supposed to return a human-readable format, which is good for logging or to display some
information about the object. Whereas, the __repr__
function is supposed to return an “official” string representation
of the object, which can be used to construct the object again.
import datetime
now = datetime.datetime.now()
print(now.__str__())
print(now.__repr__())
Let’s check what happens if we don’t define the functions for a custom object
2.3. Example - String Representation 1#
class Sensor:
def __init__(self, sensor_name, serial_number ):
self.sensor_name = sensor_name
self.serial_number = serial_number
p = Sensor('Temperature', 1416)
print(p.__str__())
print(p.__repr__())
Note
As you can see, the default implementation is useless.
2.4. Example - String Representation 2#
class Sensor:
def __init__(self, sensor_name, serial_number ):
self.sensor_name = sensor_name
self.serial_number = serial_number
# INFORMAL
def __str__(self):
sensor_str = """ Sensor:
sensor_name: {sensor_name}
serial_number: {serial_number}
""".format(sensor_name = self.sensor_name, \
serial_number = self.serial_number)
return sensor_str
# FORMAL
def __repr__(self):
return "Sensor('{sensor_name}', {serial_number})"\
.format(sensor_name = self.sensor_name, serial_number = self.serial_number)
p = Sensor('Temperature', 1416)
print(p)
print(p.__str__())
print(str(p))
print(p.__repr__())
print(repr(p))
3. Exceptions#
Exceptions are used to prevent the program from terminating.
3.1. Raising an Exception#
We can use raise to throw an exception if a condition occurs.
x = 10
if x > 5 :
raise Exception('x should not exceed . The value of x was {}'.format(x))
Error
Exception Traceback (most recent call last)
/tmp/ipykernel_48/555160632.py in
1 x = 10
2 if x > 5 :
—-> 3 raise Exception(‘x should not exceed . The value of x was {}’.format(x))
Exception: x should not exceed . The value of x was 10
The program comes to a halt and displays our exception to screen, offering clues about what went wrong.
3.2. Custom Exceptions#
class SalaryError(ValueError): pass
class BonusError(SalaryError): pass
class Employee:
MIN_SALARY = 30000
MAX_BONUS = 5000
def __init__(self, name, salary = 30000):
self.name = name
if salary < Employee.MIN_SALARY:
raise SalaryError("Salary is too low!")
self.salary = salary
# Rewrite using exceptions
def give_bonus(self, amount):
if amount > Employee.MAX_BONUS:
raise BonusError("The bonus amount is too high!")
elif self.salary + amount < Employee.MIN_SALARY:
raise SalaryError("The salary after bonus is too low!")
else:
self.salary += amount
emp1 = Employee("Fernando", 30000)
emp1.give_bonus(1000)
print(emp1.salary)
emp1.give_bonus(6000)
print(emp1.salary)
Error
31000
BonusError Traceback (most recent call last)
/tmp/ipykernel_67/3863838099.py in
26 emp1.give_bonus(1000)
27 print(emp1.salary)
—> 28 emp1.give_bonus(6000)
29 print(emp1.salary)
/tmp/ipykernel_67/3863838099.py in give_bonus(self, amount)
15 def give_bonus(self, amount):
16 if amount > Employee.MAX_BONUS:
—> 17 raise BonusError(“The bonus amount is too high!”)
18
19 elif self.salary + amount < Employee.MIN_SALARY:
BonusError: The bonus amount is too high!