Integration with Python
Operator overloading : comparison
Object equality
class Customer: def __init__(self, name, balance): self.name, self.balance = name, balance cust1 = Customer("John Fisher", 2100) cust2 = Customer("John Fisher", 2100) cust1 == cust2 > False
Variables are references
class Customer: def __init__(self, name, balance, id): self.name, self.balance = name, balance self.id = id cust1 = Customer("John Fisher", 2100, 123) cust2 = Customer("John Fisher", 2100, 123) cust1 == cust2 > False print(cust1) <__main__.Customer at 0x16549e8e78> print(cust2) <__main__.Customer at 0x16549e7e51>
- Due to the fact how the objects and variables representing them are stored
- printing the value of the (customer) object –> prints the memory allocation chunk
- the value = reference to the memory chunk ))> so when comparing we are comparing the memory chunks
Custom comparison
import numpy as np # two diff arrays containing the same data arr1 = np.array([1,2,3]) arr2 = np.array([1,2,3]) arr1 == arr2 > True
- here numpy arrays –> Python considers them equal (as other Python objects like DataFrames
- How to enforce -> special method
__eq__()
Overloading __eq__()
class Customer: def __init__(self, id, name): self.id, self.name = id, name # wil be called when == is used def __eq__(self, other): print("__eq__() is called") # returns True is all attributes match return (self.id == other.id) and \ (self.name == other.name)
__eq__()
is called when 2 objects of a class are compared using==
- accepts 2 args,
self
andother
– object to compare - returns a Boolean
Comparison of objects
# Two equal objects cust1 = Customer(213, 'John') cust2 = Customer(213, 'John') cust1 == cust2 > __eq__() is called > True # two unequal object cust1 = Customer(213, 'John') cust2 = Customer(123, 'John') cust1 == cust2 > __eq__() is called > False
- here the __eq__ method is called and the comparison returns ‘True’ in the 1st case, False in the second case
Other comparisons
Operator | Method |
== |
__eq__() |
!= |
__ne__() |
>= |
__ge__() |
<= |
__le__() |
> |
__gt__() |
< |
__lt__() |
- __hash__() to use objects as dictionary keys and in sets
Exercises
class BankAccount: def __init__(self, number, balance=0): self.balance = balance self.number = number def withdraw(self, amount): self.balance -= amount def __eq__(self, other): return self.number == other.number acct1 = BankAccount(123, 1000) acct2 = BankAccount(123, 1000) acct3 = BankAccount(456, 1000) print(acct1 == acct2) > True print(acct1 == acct3) > False
class Phone: def __init__(self, number): self.number = number def __eq__(self, other): return self.number == other.number class BankAccount: def __init__(self, number, balance=0): self.number, self.balance = number, balance def withdraw(self, amount): self.balance -= amount def __eq__(self, other): return ((self.number == other.number) & (type(self) == type(other))) acct = BankAccount(873555333) pn = Phone(873555333) print(acct == pn) > False
Operator overloading : string representation
Printing an object
class Customer: def __init__(self, name, balance): self.name, self.balance = name, balance cust = Customer("John", 300) print(cust) > <__main__.Customer at 0x1f46e8e9984> # ------- import numpy as np arr = np.array([1,2,3]) print(arr) > [1 2 3]
- There are 2 classes that can be a representation
__str__()
print(obj)
,str(obj)
- informal, for end user
- string representation
print(np.array([1,2,3]))
–>[1 2 3]
__repr__()
repr(obj)
, printing in console- formal, for developer
- reprocducible representation
- fallback for
print()
when__str__()
is not available repr(np.array([1,2,3]))
–>np.array([1,2,3])
Implementation: str
class Customer: def __init__(self, name, balance): self.name, self.balance = name, balance def __str__(self): cust_str = """ Customer: name: {name} balance: {balance} """.format(name = self.name, \ balance = self.balance) return cust_str cust = Customer("Kev", 125) print(cust) > Customer: name: Kev balance: 125
- the triple ” –> indication for multiline print
- {} to fill in the values
Implementation: repr
class Customer: def __init__(self, name, balance): self.name, self.balance = name, balance def __repr__(self): return "Customer('{name}',{balance})".format(name = self.name, balance = self.balance) cust = Customer("Kev", 125) cust > Customer("Kev", 125)
- surround the string arguments with quotation marks in the __repr__() output
String formatting review
my_num = 5 my_str = "Hello" f = "my_num is {}, and my_str is \"{}\".".format(my_num, my_str) print(f) > my_num is 5, and my_str is "Hello".
Exercise
class Employee: def __init__(self, name, salary=30000): self.name, self.salary = name, salary def __str__(self): s = "Employee name: {name}\nEmployee salary: {salary}".format(name=self.name, salary=self.salary) return s def __repr__(self): return "Employee('{name}', {salary})".format(name=self.name, salary=self.salary) emp1 = Employee("Amar Howard", 30000) print(repr(emp1)) emp2 = Employee("Carolyn Ramirez", 35000) print(repr(emp2)) > Employee('Amar Howard', 30000) > Employee('Carolyn Ramirez', 35000)
Exceptions
Exception handling
-
- a = 1 –> a / 0 ==> ZeroDivisionError
- a = 1 –> a + “Hi” ==> TypeError
- a = [1,2,3] –> a[5] ==> IndexError
- a = 1 –> a + b ==> NameError
- Prevent the program from terminating when a exception is raised
- Use the try – except – finally :
try: # try running code except ExceptionNameHere: # Run this code if ExceptionNameHere happens except AnotherExceptionNameHere: # Run this code if AnotherExceptionNameHere happens ... finally: # optional # run this code no matter what
Raising exceptions
def make_list_of_ones(length): if length <= 0: raise ValueError('Invalid Length') # stops program and raises error return [1] * length make_list_of_ones(-1) > ValueError: Invalid Length
Exceptions are classes
- standard exceptions are inherited from BaseException or Exception
- overview : https://docs.python.org/3/library/exceptions.html#exception-hierarchy
Custom exceptions
- Inherits from Exception or one of its subclasses
- Usually an empty class
class BalanceError(Exception): pass class Customer: def __init__(self, name, balance): if balance < 0: raise BalanceError("Balance must be positive") else: self.name, self.balance = name, balance cust = Customer('Larry', -10) > BalanceError: Balance must be positive cust > NameError : name 'cust' is not defined
- the exception interrupted the constructor –> object not created
Catching custom exceptions
try: cust = Customer('Larry', -10) except BalanceError: cust = Customer('Larry', 0)
Excercises
def invert_at_index(x, ind): try: return 1/x[ind] except ZeroDivisionError: print("Cannot divide by zero!") except IndexError: print("Index out of range!") a = [5,6,0,7] # Works okay print(invert_at_index(a, 1)) > 0.1666666666 # Potential ZeroDivisionError print(invert_at_index(a, 2)) > Cannot divide by zero! # Potential IndexError print(invert_at_index(a, 5)) > Index out of range!
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
It’s better to list the except blocks in the increasing order of specificity, i.e. children before parents, otherwise the child exception will be called in the parent except block.