Summary
Object oriented Programming (OOP) is a programming style characterized by the identification of classes of objects closely linked with the methods (functions). OOP offers the potential to evolve programming to a higher level of abstraction. It also provides flexibility for codebase through inheritance and polymorphism. This notebook presents fundamental of OOP in Python.
Python functions and data files to run this notebook are in my Github page.
Object-oriented Programming (OOP)¶
- Procedural Programming
- Codes are as a separate sequence of steps
- It is great for data analysis
- Thinking in sequence like getting up in the morning having breakfast go to work.
- However, OOP may not be efficient when there are many objects (go to school, play, watch TV, take plane..)
- Object-Oriented Programming
- They are great to build frameworks and tools
- To make a maintainable and reusable codes
- Code are considered as interactions of objects
- Objects as data structures
Object
=state
such as phone, email.. of customers (called attributes too) +behavior
such as place of order, cancel order... of customers (called methods too)- Encapsulation: bundling data with code operating on it
Classes are blueprints for objects that are outlining possible states and behaviors
- Objects in Python
- Everything in Python is an object
- Every object has a class
- we can use type() to find the class of object
See illustrations below for classes and objects for car:
import numpy as np
a = np.array([4,7,9,5])
print(type(a))
<class 'numpy.ndarray'>
print(f"the class of for {10} is {type(10)}")
print(f"the class of for Hello is {type('Hello')}")
print(f"the class of for {np.mean} is {type(np.mean)}")
the class of for 10 is <class 'int'> the class of for Hello is <class 'str'> the class of for <function mean at 0x0000022CC085B430> is <class 'function'>
- Attributes and Methods
- State <=> attributes
- Behavior <=> methods
- We can use obj. to access attributes and methods
# shape attribute
a = np.array([5,1,0,4])
a.shape
(4,)
# reshape method
a = np.array([5,1,0,4])
a.reshape(2,2)
array([[5, 1], [0, 4]])
Now we can rephrase object as Object = attributes + methods:
- attributes <=> variables
- methods <=> function()
# reshape method
a = np.array([5,1,0,4])
# we can list all attributes and methods
dir(a)
['T', '__abs__', '__add__', '__and__', '__array__', '__array_finalize__', '__array_function__', '__array_interface__', '__array_prepare__', '__array_priority__', '__array_struct__', '__array_ufunc__', '__array_wrap__', '__bool__', '__class__', '__complex__', '__contains__', '__copy__', '__deepcopy__', '__delattr__', '__delitem__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__iand__', '__ifloordiv__', '__ilshift__', '__imatmul__', '__imod__', '__imul__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__ior__', '__ipow__', '__irshift__', '__isub__', '__iter__', '__itruediv__', '__ixor__', '__le__', '__len__', '__lshift__', '__lt__', '__matmul__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmatmul__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__setitem__', '__setstate__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__xor__', 'all', 'any', 'argmax', 'argmin', 'argpartition', 'argsort', 'astype', 'base', 'byteswap', 'choose', 'clip', 'compress', 'conj', 'conjugate', 'copy', 'ctypes', 'cumprod', 'cumsum', 'data', 'diagonal', 'dot', 'dtype', 'dump', 'dumps', 'fill', 'flags', 'flat', 'flatten', 'getfield', 'imag', 'item', 'itemset', 'itemsize', 'max', 'mean', 'min', 'nbytes', 'ndim', 'newbyteorder', 'nonzero', 'partition', 'prod', 'ptp', 'put', 'ravel', 'real', 'repeat', 'reshape', 'resize', 'round', 'searchsorted', 'setfield', 'setflags', 'shape', 'size', 'sort', 'squeeze', 'std', 'strides', 'sum', 'swapaxes', 'take', 'tobytes', 'tofile', 'tolist', 'tostring', 'trace', 'transpose', 'var', 'view']
Type hints¶
Type hinting is a formal solution to statically indicate the type of a value within your Python code. It was introduced in Python 3.5. Here’s an example of adding type information to a function. You annotate the arguments and the return value:
Here is an example to represent how to convert one type to another.
a_string_variable = "Hello, world!"
print(type(a_string_variable))
<class 'str'>
a_string_variable = 42
print(type(a_string_variable))
print(id(a_string_variable))
print(hash(a_string_variable))
<class 'int'> 2390776311376 42
Here are the two steps, shown side by side, showing how the variable is moved from object to object:
The various properties are part of the object, not the variable. When we check the type of a variable with type()
, we see the type of the object the variable currently references. The variable doesn't have a type of its own; it's nothing more than a name. Similarly, asking for the id()
of a variable shows the ID of the object the variable refers to. So obviously, the name a_string_variable is a bit misleading if we assign the name to an integer object.
Here is an example of type hints in Python:
def say_greet(name: str) -> str:
return "Hello, " + name
Here is another example:
def headline(text: str, align: bool = True) -> str:
if align:
return f"{text.title()}\n{'-' * len(text)}"
else:
return f" {text.title()} ".center(50, "o")
print(headline("check python type ", align=False))
ooooooooooooooo Check Python Type ooooooooooooooo
text='my name is mehdi'
Use normal rules for colons, that is, no space before and one space after a colon (text: str).
Use spaces around the = sign when combining an argument annotation with a default value (align: bool = True).
Use spaces around the -> arrow (def headline(...) -> str).
Another example is below:
def main(val) -> None:
print(odd(val))
def odd(n: int) -> bool:
return n % 2 != 0
if __name__ == "__main__":
main(11)
True
The main()
function doesn't have a return type; mypy
suggests including -> None to make the absence of a return value perfectly explicit.
Introduction for Classes in Python¶
- Class definition in Python can be started as
class <name>:
- The code inside the
class
should be indented - Empty class can be created by using
pass
class CustInfo:
pass
- to create an object
class name+()
should be used
cus1 = CustInfo()
cus2 = CustInfo()
cus1 and cus2 are objects
a = CustInfo()
print(a)
b = CustInfo()
print(b)
<__main__.CustInfo object at 0x0000022CC1B5EDC0> <__main__.CustInfo object at 0x0000022CC1B5EF10>
This code instantiates two objects from the new class, assigning the object variable names a
and b
. Creating an instance of a class is a matter of typing the class name, followed by a pair of parentheses. It looks much like a function call; calling a class will create a new object
. When printed, the two objects tell us which class they are and what memory address they live at. Memory addresses aren't used much in Python code.
a is b
False
How to Add Method to a Class¶
- The method is the function within class
self
should be applied as first argument for method definitionself
should be ignored when calling method on an object
class CustInfo:
def idtfy(self, name):
print("I am Customer " + name)
cust_name = CustInfo()
cust_name.idtfy("Mehdi")
I am Customer Mehdi
- In class definition,
self
is a substitute for a particular object
The one difference, syntactically, between methods of classes and functions outside classes is that methods have one required argument. This argument is conventionally named self
; I've never seen a Python programmer use any other name for this variable (convention is a very powerful thing). There's nothing technically stopping you, however, from calling it this or even Martha, but it's best to acknowledge the social pressure of the Python community codified in PEP 8 and stick with self
.
Pay attention to the difference between a class and an object in this discussion. We can think of the method as a function attached to a class. The self parameter refers to a specific instance of the class.
When you call the method on two different objects, you are calling the same method twice, but passing two different objects as the self parameter.
What happens if we forget to include the self argument in our class definition? Python will bail with an error message, as follows:
class Point:
def reset():
pass
p = Point()
p.reset()
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) ~\AppData\Local\Temp\ipykernel_18944\1841119531.py in <module> 4 5 p = Point() ----> 6 p.reset() TypeError: reset() takes 0 positional arguments but 1 was given
How to Add Attribute to a Class¶
class CustInfo:
# set the name attribute of an object as name
def name_assign(self, name):
# Create an attribute by assigning a value
self.name = name # When set_name is called, .name will created
cust = CustInfo() # Name does not exist yet
cust.name_assign("Mehdi Rezvandehy") # Create a name and assign to Mehdi Rezvandehy
print(cust.name)
Mehdi Rezvandehy
class CustInfo:
def name_assign(self, name):
self.name = name
# Using .name from the object it*self*
def idtfy(self):
print("I am Customer " + self.name)
cust = CustInfo()
cust.name_assign("Mehdi Rezvandehy")
cust.idtfy()
I am Customer Mehdi Rezvandehy
# We can also use __init__ as below for creating attributes
class CustInfo:
def __init__(self, name):
self.name = name
def idtfy(self):
print("I am Customer " + self.name)
cust = CustInfo("Mehdi Rezvandehy")
cust.idtfy()
I am Customer Mehdi Rezvandehy
How do we pass multiple arguments to a method?
Let's add a new method that allows us to move a point to an arbitrary position, not just to the origin. We can also include a method that accepts another Point object as input and returns the distance between them:
import math
class Point:
def move(self, x: float, y: float) -> None:
self.x = x
self.y = y
def reset(self) -> None:
self.move(0, 0)
def calculate_distance(self, other: "Point") -> float:
return math.hypot(self.x - other.x, self.y - other.y)
We've defined a class with two attributes, x
, and y
, and three separate methods, move()
, reset()
, and calculate_distance()
.
The move()
method accepts two arguments, x
and y
, and sets the values on the self
object. The reset()
method calls the move()
method, since a reset is just a move to a specific known location.
The calculate_distance()
method computes the Euclidean distance between two points. Here's an example of using this class definition. This shows how to call a method with arguments: include the arguments inside the parentheses and use the same dot notation to access the method name within the instance. We just picked some random positions to test the methods. The test code calls each method and prints the results on the console:
set1 = Point()
set2 = Point()
set1.reset()
set2.move(9, 0)
print(set2.calculate_distance(set1))
assert set2.calculate_distance(set1) == set1.calculate_distance(
set2)
9.0
The assert
statement is a marvelous test tool; the program will bail if the expression after assert evaluates to False (or zero, empty, or None). In this case, we use it to ensure that the distance is the same.
"init" Constructor¶
- Within a class, methods are function
self
as the first argument, we can use other keys but it is not recommended.- Attributes are defined by the assignment
- Attributes in class can be referred via
self.---
We can add data to object when we created it by constructor __init__()
, which is called every time an object is created.
class CustInfo:
def __init__(self, name, balance=100): # set defalt value for balance as $100
self.name = name
self.balance = balance
print("The __init__ method was called")
cust = CustInfo("Mehdi Rezvandehy",20) #<--- __init__ is implicitly called
print(cust.name)
print(cust.balance)
The __init__ method was called Mehdi Rezvandehy 20
Reading attributes in the constructor are:
- We can easier know all the attributes
- When the object is created, attributes are also created
- It makes your code more usable and maintainable
Best practice when dealing with classes
- Attributes should be initialized in
__init__()
- Naming for classes can have both lower and upper case e.g.
UseCase
. We do not normally have_
for classes; however, function and attribute should always have lower casekernel_density_estimate
- Although
self
can be replaced with any word, it should be kept asself
- It is always recommended to use docstrings for class:
Type Hints¶
Type hinting is a formal solution to statically indicate the type of a value within your Python code. It was introduced in Python 3.5. Here’s an example of adding type information to a function. You annotate the arguments and the return value:
Hints are optional. They don't do anything at runtime. There are tools, however, that can examine the hints to check for consistency. If we don't want to make the two arguments required, we can use the same syntax Python functions use to provide default arguments. The keyword argument syntax appends an equals sign after each variable name. If the calling object does not provide this argument, then the default argument is used instead. The variables will still be available to the function, but they will have the values specified in the argument list. Here's an example:
Here is an example to represent how to convert one type to another.
a_string_variable = "Hello, world!"
print(type(a_string_variable))
<class 'str'>
a_string_variable = 42
print(type(a_string_variable))
print(id(a_string_variable))
print(hash(a_string_variable))
<class 'int'> 2390776311376 42
Here are the two steps, shown side by side, showing how the variable is moved from object to object:
The various properties are part of the object, not the variable. When we check the type of a variable with type()
, we see the type of the object the variable currently references. The variable doesn't have a type of its own; it's nothing more than a name. Similarly, asking for the id()
of a variable shows the ID of the object the variable refers to. So obviously, the name a_string_variable is a bit misleading if we assign the name to an integer object.
Here is an example of type hints in Python:
def say_greet(name: str) -> str:
return "Hello, " + name
Here is another example:
def headline(text: str, align: bool = True) -> str:
if align:
return f"{text.title()}\n{'-' * len(text)}"
else:
return f" {text.title()} ".center(50, "o")
print(headline("check python type ", align=False))
ooooooooooooooo Check Python Type ooooooooooooooo
text='my name is mehdi'
Use normal rules for colons, that is, no space before and one space after a colon (text: str).
Use spaces around the = sign when combining an argument annotation with a default value (align: bool = True).
Use spaces around the -> arrow (def headline(...) -> str).
Another example is below:
def main(val) -> None:
print(odd(val))
def odd(n: int) -> bool:
return n % 2 != 0
if __name__ == "__main__":
main(11)
True
Type hints within Python class:
class Point:
def __init__(self, x: float = 0, y: float = 0) -> None:
self.move(x, y)
Explain with docstrings
¶
class NewClass:
"""This is a docstrings within the class"""
pass
Python can be an extremely easy-to-read programming language; some might say it is self-documenting. However, when carrying out object-oriented programming, it is important to write API documentation that clearly summarizes what each object and method does.
Python supports this through the use of docstrings
. Each class, function, or method header can have a standard Python string as the first indented line inside the definition (the line that ends in a colon).
docstrings
are Python strings enclosed within apostrophes (') or quotation marks ("). Often, docstrings
are quite long and span multiple lines (the style guide suggests that the line length should not exceed 80 characters), which can be formatted as multi-line strings, enclosed in matching triple apostrophe (''') or triple quote (""") character.
A docstring should clearly and concisely summarize the purpose of the class or method it is describing. It should explain any parameters whose usage is not immediately obvious, and is also a good place to include short examples of how to use the API. Any caveats or problems an unsuspecting user of the API should be aware of should also be noted.
One of the best things to include in a docstring is a concrete example.
class Point:
"""
Represents a point in two-dimensional geometric coordinates
>>> p_0 = Point()
>>> p_1 = Point(3, 4)
>>> p_0.calculate_distance(p_1)
5.0
"""
def __init__(self, x: float = 0, y: float = 0) -> None:
"""
Initialize the position of a new point. The x and y
coordinates can be specified. If they are not, the
point defaults to the origin.
:param x: float x-coordinate
:param y: float y-coordinate
"""
self.move(x, y)
def move(self, x: float, y: float) -> None:
"""
Move the point to a new location in 2D space.
:param x: float x-coordinate
:param y: float x-coordinate
"""
self.x = x
self.y = y
def reset(self) -> None:
"""
Reset the point back to the geometric origin: 0, 0
"""
self.move(0, 0)
def calculate_distance(self, other: "Point") -> float:
"""
Calculate the Euclidean distance from this point
to a second point passed as a parameter.
:param other: Point instance
:return: float distance
"""
return math.hypot(self.x - other.x, self.y - other.y)
Retrieved from Lott and Phillips,Python Object-Oriented Programming
Instance Level Data (Instance attribute)¶
The class below is employment information with name and salary as instance attributes. self
binds to an instance.
class EmployeeInfo:
def __init__(self, name, Salary=100): # set defalt value for Salary as $100
self.name = name
self.Salary = Salary
print("The __init__ method was called")
cust = EmployeeInfo("Mehdi Rezvandehy",20) #<--- __init__ is implicitly called
print(cust.name)
print(cust.Salary)
The __init__ method was called Mehdi Rezvandehy 20
Class Level Data (Class attribute)¶
- Among all instances of a class, the data will be shared
- Class attributes are defined in the body of
class
- It is considered as "Global variable" within the class
- It does not need self
class MyClass:
# Define a class attribute
value = 10
class Staff:
# Define a class attribute
income_min = 50000 # no need to have self
def __init__(self, name, salary):
self.name = name
# Use class name to access class attribute
if salary >= Staff.income_min:
self.salary = salary
else:
self.salary = Staff.income_min
For the code above:
- income_min is shared among all instances
- No need to have
self
for class attribute - income_min can be accessed any where in the class by
ClassName.income_min
# call instance level attribute
emp1 = Staff("Mehdi",60000)
print(emp1.income_min)
50000
# call class level attribute
emp1 = Staff("Mehdi",60000)
print(emp1.salary)
60000
Class methods (@classmethod Decorator)¶
Decorators are a very powerful and useful tool in Python since it allows programmers to modify the behaviour of a function or class. Decorators allow us to wrap another function in order to extend the behaviour of the wrapped function, without permanently modifying it. But before diving deep into decorators let us understand some concepts that will come in handy in learning the decorators.
Decorator can modify the behaviour:
# A Python example to demonstrate that
# decorators can be useful attach data
# A decorator function to attach
# data to func
def attach_data(func):
func.data = 3
return func
@attach_data
def add (x, y):
return x + y
# Driver code
# This call is equivalent to attach_data()
# with add() as parameter
print(add(2, 3))
print(add.data)
5 3
add()
returns sum of x and y passed as arguments but it is wrapped by a decorator function, calling add(2, 3)
would simply give sum of two numbers but when we call add.data
then add
function is passed into then decorator function attach_data
as argument and this function returns add
function with an attribute data
that is set to 3 and hence prints it.
Python decorators are a powerful tool to remove redundancy.
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_whee():
print("Whee!")
say_whee()
Something is happening before the function is called. Whee! Something is happening after the function is called.
So, @my_decorator
is just an easier way of saying say_whee = my_decorator(say_whee)
. It’s how you apply a decorator to a function.
We can use decorator for Python class as well.
Classmethod
is sharing a function of class for every instanceClassmethod
can't use instance-level data
class MyClass:
@classmethod # a class method is declared by using decorator
def my_awesome_method(cls, args): # cls argument refers to the class
pass
# No instance attribute can be used :(
The @classmethod
decorator is a built-in function decorator which is an expression that gets evaluated after your function is defined. The result of that evaluation shadows your function definition.
A class method receives the class as the implicit first argument, just like an instance method receives the instance.
# Python program to demonstrate
# use of a class method and static method.
from datetime import date
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# a class method to create a person object by birth year.
@classmethod
def fromBirthYear(cls, name, year):
return cls(name, date.today().year - year) # return the object
person1 = Person('mayank', 21)
person2 = Person.fromBirthYear('mayank', 1980)
print(person1.age)
print(person2.age)
21 42
@classmethod
Characteristics are:
- Declares a class method.
- The first parameter must be
cls
, which can be used to access class attributes. - The class method can only access the class attributes but not the instance attributes.
- It can return an object of the class.
class Student:
name = 'unknown' # class attribute
def __init__(self):
self.age = 20 # instance attribute
@classmethod
def tostring(cls):
print('Student Class Attributes: name=',cls.name)
Student.tostring()
Student Class Attributes: name= unknown
For the above code, the Student
class contains a class attribute name
and an instance attribute age
. The tostring()
method is decorated with the @classmethod
decorator that makes it a class method.
Alternative Constructors¶
- We can only have one
__init__
class Staff:
income_min = 40000
def __init__(self, name, salary=40000):
self.name = name
if salary >= Staff.income_min:
self.salary = salary
else:
self.salary = Staff.income_min
##############
@classmethod
def read_file(cls, filename):
with open(filename, "r") as f_cont:
for line in f_cont:
name = line.rstrip()
return cls(name)
Employee data is downloaded from data.txt.
# Create a staff without calling Staff()
staff = Staff.read_file("./Data/data.txt")
type(staff)
__main__.Staff
print(staff.name)
2014 Employee Smith,John 2000
Class Inheritance¶
In the programming world, duplicate code is considered evil. We should not have multiple copies of the same, or similar, code in different places. When we fix a bug in one copy and fail to fix the same bug in another copy, we've caused no end of problems for ourselves.
Moreover, if someone has already written a code, we can reuse without writing it from scratch. Since modules (numpy, Scipy, Pnadas..) are great for fixed functionality, OOP is great for customizing functionality.
New class functionality = Old class functionality + extra
Technically, each class we create uses legacy. All Python classes are subclasses of a special integrated class called an object.
class MyChild(object):
# Do stuff here
pass
This is inheritance! In Python 3, all classes automatically inherit from object if we don't explicitly provide a different superclass. The superclasses
, or parent classes
, in the relationship are the classes that are being inherited from, object
in this example.
MyParent
: class whose functionality is being extended/inheritedMyChild
: class that will inherit the functionality and add more
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
self.balance -= amount
return(self.balance)
# Empty class inherited from BankAccount
class SavingsAccount(BankAccount):
pass
Child class should have all of the parent data
# Constructor inherited from BankAccount
savings_acct = SavingsAccount(1000)
type(savings_acct)
__main__.SavingsAccount
# Attribute inherited from BankAccount
savings_acct.balance
1000
# Method inherited from BankAccount
savings_acct.withdraw(300)
700
A SavingsAccount is a BankAccount (possibly with special features).
savings_acct = SavingsAccount(1000)
isinstance(savings_acct, SavingsAccount)
True
isinstance(savings_acct, BankAccount)
True
How to Customize Functionality via Inheritance¶
The code below inherits all instances from BankAccount
but does not do any thins in saving account.
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
self.balance -= amount
return(self.balance)
# Empty class inherited from BankAccount
class SavingsAccount(BankAccount):
pass
The code below shows SavingAccount
class that inherits instances from BankAccount
.
class SavingsAccount(BankAccount):
# SavingsAccount constructor with an additional parameter
# inherets balance from BankAccount
def __init__(self, balance, interest_rate):
# Parent constructor is called using ClassName.__init__(). self should always be included
BankAccount.__init__(self, balance) #
# More functionality is added
self.interest_rate = interest_rate
Explanation of code above:
Constructor of the parent class is run first by
Parent.__init__(self,...)
interest_rate is added as more functionality
Parent constructors do not need to be called
Now we can make a saving account that inherits balance from BankAccount
but has interest_rate functionality from SavingsAccount
.
# Construct the object using the new constructor
account = SavingsAccount(5000, 0.02)
account.interest_rate
0.02
account.balance
5000
Methods can be added to class as usual; the data can be used from both the parent and the child class.
class SavingsAccount(BankAccount):
def __init__(self, balance, interest_rate):
# Inherent balance from BankAccount
BankAccount.__init__(self, balance)
# interest_rate from SavingsAccount
self.interest_rate = interest_rate
# Add new function
def interest_compute(self, n_periods = 1):
return self.balance * ( (1 + self.interest_rate) ** n_periods - 1)
save_account = SavingsAccount(5000,0.02)
save_account.interest_compute()
100.00000000000009
Now we can make a checking account as below:
class CheckingAccount(BankAccount):
def __init__(self, balance, limit):
BankAccount.__init__(self, balance)
self.limit = limit
def deposit(self, amount):
self.balance += amount
return self.balance
def withdraw(self, amount, fee=10):
if fee <= self.limit:
return BankAccount.withdraw(self, amount - fee)
else:
return BankAccount.withdraw(self, amount - self.limit)
acct_check = CheckingAccount(1200,limit=15)
# withdraw $200 from CheckingAccount
acct_check.withdraw(15, fee=20)
1200
acct_bank = BankAccount(5000)
# withdraw from BankAccount
acct_bank.withdraw(150)
4850
Example 1 (Basic inheritance)¶
How do we apply inheritance in practice? The simplest and most obvious use of inheritance is to add functionality to an existing class. Let's start with a contact manager that tracks the names and email addresses of several people (Contact
class).
from __future__ import annotations
class Contact_info:
all_contacts: List["Contact"] = []
def __init__(self, name: str, email: str) -> None:
self.name = name
self.email = email
Contact_info.all_contacts.append(self)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"{self.name!r}, {self.email!r}"
f")"
)
cont_1 = Contact_info("mehdi", "dusty@example.com")
cont_2 = Contact_info("ali", "ali@itmaybeahack.com")
Contact_info.all_contacts
[Contact_info('mehdi', 'dusty@example.com'), Contact_info('ali', 'ali@itmaybeahack.com')]
Two instances of the Contact
class are created and assigned them to variables cont_1
and cont_2
. When we looked at the Contact_info.all_contacts
class variable, we saw that the list has been updated to track the two objects. But what if some of our contacts are also suppliers that we need to order supplies from?
class Supplier(Contact_info):
def order(self, order: "Order") -> None:
print(
"This item "
f"'{order}' order to '{self.name}'")
s = Supplier("shopper", "hoss@email.com")
s.order('pencil')
This item 'pencil' order to 'shopper'
s.all_contacts
[Contact_info('mehdi', 'dusty@example.com'), Contact_info('ali', 'ali@itmaybeahack.com'), Supplier('shopper', 'hoss@email.com')]
Example 2 (Overriding and Super)¶
Inheritance is great for adding new behavior to existing classes, but what if we want to change behavior? The Contact_info
class allows only a name and an email address. But if we want to add a phone number for our close friends?, this class cannot do it. We can do this by overriding the init() method. Overriding means altering or replacing a method of the superclass with a new method in the subclass.
#class Friend(Contact_info):
# def __init__(self, name: str, email: str, phone: str) -> None:
# self.name = name
# self.email = email
# self.phone = phone
class Friend(Contact_info):
def __init__(self, name: str, email: str, phone: str) -> None:
super().__init__(name, email)
self.phone = phone
What we really need is a way to execute the original __init__()
method on the Contact_info
class from inside our new class. This is what the super()
function does; it returns the object as if it was actually an instance of the parent class
, allowing us to call the parent method directly:
f = Friend("samiar", "samiar@yahoo.com", "7589-1822")
Contact_info.all_contacts
[Contact_info('mehdi', 'dusty@example.com'), Contact_info('ali', 'ali@itmaybeahack.com'), Supplier('shopper', 'hoss@email.com'), Friend('samiar', 'samiar@yahoo.com')]
Another simple example is below:
class Parent:
def __init__(self, txt):
self.message = txt
def printmessage(self):
print(self.message)
class Child(Parent):
def __init__(self, txt):
super().__init__(txt)
x = Child("Hello, and welcome!")
x.printmessage()
Hello, and welcome!
So, the super()
function is used to give access to methods and properties of a parent.
Another example:
class Mammal:
def __init__(self, mammalName):
self.mammalName=mammalName
print(mammalName, 'is a warm-blooded animal.')
def leg(self):
print(self.mammalName, 'has four legs')
class Dog(Mammal):
def __init__(self,mammalName):
print('Dog has four legs.')
super().__init__(mammalName) # supper is more common. If we use super, we should use for entire code.
# or we can use
# Mammal.__init__(self,mammalName)
d1 = Dog('cat')
Dog has four legs. cat is a warm-blooded animal.
d1.leg()
cat has four legs
class Dog(Mammal):
def __init__(self,mammalName):
print('Dog has four legs.')
Mammal.__init__(self,mammalName)
d1 = Dog('cat')
Dog has four legs. cat is a warm-blooded animal.
String Representation¶
Comparison Objects for Classes¶
class CustomerInfo:
def __init__(self, name, balance):
self.name, self.balance = name, balance
customer1 = CustomerInfo("Mehdi Rezvandehy", 5000)
customer2 = CustomerInfo("Mehdi Rezvandehy", 5000)
customer1==customer2
False
Classes are not equal because Python assigns a chunk of memory for each class when it is created.
print(f'customer1 is {customer1} \ncustomer2 is {customer2}')
customer1 is <__main__.CustomerInfo object at 0x0000022CC1C4A6D0> customer2 is <__main__.CustomerInfo object at 0x0000022CC1C4A0D0>
However, if we compare two arrays with the same elements, Python gives true.
array1 = np.array([4,3,1,9])
array2 = np.array([4,3,1,9])
array1 == array2
array([ True, True, True, True])
Overloading eq()
$__eq__()$ is called when objects of a class are compared using ==
$__eq__()$ accepts 2 arguments,
self
andother
It returns a Boolean (True or False)
class Customer:
def __init__(self, id, name, balance):
self.id, self.name, self.balance = id, name, balance
# Will be called for == is used
def __eq__(self, other):
# Returns True if all attributes match
return (self.id == other.id) and \
(self.name == other.name) and \
(self.balance == other.balance)
# Will be called for != is used
def __ne__(self, other):
# Returns True !=
return (self.id != other.id) and \
(self.name != other.name) and \
(self.balance != other.balance)
# Will be called for >= is used
def __ge__(self, other):
# Returns True >=
return (self.id >= other.id) and \
(self.balance >= other.balance)
# Will be called for <= is used
def __le__(self, other):
# Returns True <=
return (self.id <= other.id) and \
(self.balance <= other.balance)
# Object equality with
customer1 = Customer(178, "Mehdi Rezvandehy",1205)
customer2 = Customer(178, "Mehdi Rezvandehy",1205)
customer1 == customer2
True
- Other comparison operators
Operator | Method |
---|---|
!= | __ne__() |
== | __eq__() |
>= | __ge__() |
<= | __le__() |
> | __gt__() |
< | __lt__() |
# Object equality with
customer1 = Customer(195, "Mehdi Rezvandehy",1209)
customer2 = Customer(178, "Ali Hatami",1205)
customer1 != customer2
True
# Object >=
customer1 = Customer(195, "Mehdi Rezvandehy",1209)
customer2 = Customer(178, "Ali Hatami",1205)
customer1 >= customer2
True
# Object <=
customer1 = Customer(195, "Mehdi Rezvandehy",1209)
customer2 = Customer(198, "Ali Hatami",1305)
customer1 <= customer2
True
Compare
__str__()
versus__repr__()
*
__str__()
is informal, for end user while__repr__()
is formal and used for developer*
__str__()
is used for string representation while__repr__()
is used for reproducible representation*
__repr__()
is fallback forprint()
str(np.array([4,5,6]))
'[4 5 6]'
repr(np.array([4,5,6]))
'array([4, 5, 6])'
class Customer_info:
def __init__(self, name, balance):
self.name, self.balance = name, balance
def __str__(self):
cust_str = f"""
Customer:
name: {self.name}
balance: {self.balance}
"""
return cust_str
#def __repr__(self):
# cust_str = f"""
# Customer:
# name: {self.name}
# balance: {self.balance}
# """
# return cust_str
cust = Customer_info("Mehdi Rezvandehy", 5000)
# Will implicitly call __str__()
print(cust)
Customer: name: Mehdi Rezvandehy balance: 5000
class Customer_info:
def __init__(self, name, balance):
self.name, self.balance = name, balance
#def __str__(self):
# cust_str = f"""
# Customer:
# name: {self.name}
# balance: {self.balance}
# """
# return cust_str
def __repr__(self):
cust_str = f"""
Customer:
name: {self.name}
balance: {self.balance}
"""
return cust_str
cust = Customer_info("Mehdi Rezvandehy", 5000)
cust
Customer: name: Mehdi Rezvandehy balance: 5000
__repr__
does not need print
function while __str__
needs.
Exceptions¶
- Exception prevents the program from terminating when an exception is raised.
try
-except
...except
-finally
- We can raise an exception by
raise ExceptionNameHere('Error message here')
def sqrt(value):
if value <= 0:
raise ValueError("Invalid Value!")
return np.sqrt(value)
- Exceptions are classes
- standard exceptions are inherited from
BaseException
orException
- standard exceptions are inherited from
- Inherit from
Exception
or one of its subclasses - Usually an empty class
class BalanceValueError(Exception):
pass
class Customer:
def __init__(self, name, balance):
if balance < 0 :
raise BalanceValueError("Balance should ne positive!")
else:
self.name, self.balance = name, balance
customer_bal = Customer("Mehdi Rezvandehy", -20)
--------------------------------------------------------------------------- BalanceValueError Traceback (most recent call last) ~\AppData\Local\Temp\ipykernel_18944\180780449.py in <module> ----> 1 customer_bal = Customer("Mehdi Rezvandehy", -20) ~\AppData\Local\Temp\ipykernel_18944\137691722.py in __init__(self, name, balance) 5 def __init__(self, name, balance): 6 if balance < 0 : ----> 7 raise BalanceValueError("Balance should ne positive!") 8 else: 9 self.name, self.balance = name, balance BalanceValueError: Balance should ne positive!
- Object is not created since Exception interrupted the constructor.
Now let's look at the tail side of the exception coin. If we encounter an exception situation, how should our code react to or recover from it? We handle exceptions by wrapping any code that might throw one (whether it is exception code itself, or a call to any function or method that may have an exception raised inside it) inside a try...except clause. The most basic syntax looks like this:
def handler() -> None:
try:
never_returns()
print("Never executed")
except Exception as ex:
print(f"I caught an exception: {ex!r}")
print("Executed after the exception")
handler()
I caught an exception: NameError("name 'never_returns' is not defined") Executed after the exception
from typing import Union
def funny_division(divisor: float) -> Union[str, float]:
try:
return 100 / divisor
except ZeroDivisionError:
return "Zero is not a good idea!"
print(funny_division(0))
Zero is not a good idea!
print(funny_division("hello"))
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) ~\AppData\Local\Temp\ipykernel_18944\3660904025.py in <module> ----> 1 print(funny_division("hello")) ~\AppData\Local\Temp\ipykernel_18944\2507124677.py in funny_division(divisor) 2 def funny_division(divisor: float) -> Union[str, float]: 3 try: ----> 4 return 100 / divisor 5 except ZeroDivisionError: 6 return "Zero is not a good idea!" TypeError: unsupported operand type(s) for /: 'int' and 'str'
def funnier_division(divisor: int) -> Union[str, float]:
try:
if divisor == 13:
raise ValueError("13 is an unlucky number")
return 100 / divisor
except (ZeroDivisionError, TypeError):
return "Enter a number other than zero"
print(funnier_division(10))
10.0
some_exceptions = [ValueError, TypeError, IndexError, None]
for choice in some_exceptions:
try:
print(f"\nRaising {choice}")
if choice:
raise choice("An error")
else:
print("no exception raised")
except ValueError:
print("Caught a ValueError")
except TypeError:
print("Caught a TypeError")
except Exception as e:
print(f"Caught some other error: {e.__class__.__name__}")
else:
print("This code called if there is no exception")
finally:
print("This cleanup code is always called")
Raising <class 'ValueError'> Caught a ValueError This cleanup code is always called Raising <class 'TypeError'> Caught a TypeError This cleanup code is always called Raising <class 'IndexError'> Caught some other error: IndexError This cleanup code is always called Raising None no exception raised This code called if there is no exception This cleanup code is always called
While obscure, the finally
clause is executed after the return
statement inside a try clause. While this can be exploited for post-return
processing, it can also be confusing to folks reading the code.
Designing for Inheritance and Polymorphism¶
Polymorphism means using a unified interface to operate on objects of different classes. In fact, different behaviors happen depending on which subclass is being used, without having to explicitly know what the subclass actually is.
- Only Interface Matters
def withdraw_batch(accounts_list,value):
for accnt in accounts_list:
accnt.withdraw(value)
a, b, c = BankAccount(1000), CheckingAccount(5000,limit=10), SavingsAccount(3000,interest_rate=0.1)
a, b, c
(<__main__.BankAccount at 0x22cc1c768e0>, <__main__.CheckingAccount at 0x22cc1c76280>, <__main__.SavingsAccount at 0x22cc1c76910>)
withdraw_batch([a,b,c],500) # Will use BankAccount.withdraw(), # then CheckingAccount.withdraw(),
# then SavingsAccount.withdraw()
withdraw_batch()
doesn't need to check the object to know whichwithdraw()
to call
Liskov substitution principle (LSP)¶
Liskov substitution principle (LSP): Base class should be interchangeable with any of its subclasses without altering any properties of the program.
Syntactically:
- function signatures are compatible
- arguments, returned values
Semantically:
- the state of the object and the program remains consistent
- subclass method doesn't strengthen input conditions
- subclass method doesn't weaken output conditions
- no additional exceptions
# Compare withdraw of BankAccount with withdraw of CheckingAccount
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
self.balance -= amount
return(self.balance)
##############################################
class CheckingAccount(BankAccount):
def __init__(self, balance, limit):
BankAccount.__init__(self, balance)
self.limit = limit
def deposit(self, amount):
self.balance += amount
return self.balance
def withdraw(self, amount, fee=10):
if fee <= self.limit:
return BankAccount.withdraw(self, amount - fee)
else:
return BankAccount.withdraw(self, amount - self.limit)
Violating LSP by Syntactic incompatibility
BankAccount.withdraw()
requires 1 parameter, butCheckingAccount.withdraw()
requires 2
Violating LSP by Subclass strengthening input conditions
BankAccount.withdraw()
accepts any amount, butCheckingAccount.withdraw()
assumes that the amount is limited
Other Violating LSP
- Changing additional attributes in subclass's method
- Throwing additional exceptions in subclass's method
More details to LSP¶
The Liskov substitution principle states that a child class must be substitutable for its parent class. Liskov substitution principle aims to ensure that the child class can assume the place of its parent class without causing any errors. The example below shows meeting LSP retrieved from here.
from abc import ABC, abstractmethod
class Notification(ABC):
@abstractmethod
def notify(self, message):
pass
class Email(Notification):
def __init__(self, email):
self.email = email
def notify(self, message):
print(f'Send "{message}" to {self.email}')
class SMS(Notification):
def __init__(self, phone):
self.phone = phone
def notify(self, message):
print(f'Send "{message}" to {self.phone}')
class Contact:
def __init__(self, name, email, phone):
self.name = name
self.email = email
self.phone = phone
class NotificationManager:
def __init__(self, notification):
self.notification = notification
def send(self, message):
self.notification.notify(message)
if __name__ == '__main__':
contact = Contact('John Doe', 'john@test.com', '(408)-888-9999')
sms_notification = SMS(contact.phone)
email_notification = Email(contact.email)
notification_manager = NotificationManager(sms_notification)
notification_manager.send('Hello John')
notification_manager.notification = email_notification
notification_manager.send('Hi John')
Send "Hello John" to (408)-888-9999 Send "Hi John" to john@test.com
notify
method in parent class Notification
is the same as the child classes Email
and SMS
.
Another example, imagine a program that plays audio files. However, the process of decompressing and extracting an audio file is very different for different types of files. While .wav
files are stored uncompressed, .mp3, .wma,
and .ogg
files all utilize totally different compression algorithms.
We can use inheritance with polymorphism to simplify the design. Each type of file can be represented by a different subclass of AudioFile
, for example, WavFile
and MP3File
. Each of these would have a play()
method that would be implemented differently for each file to ensure that the correct extraction procedure is followed. The media player object would never need to know which subclass of AudioFile
it is referring to; it just calls play()
and polymorphically lets the object take care of the actual details of playing. Let's look at a quick skeleton showing how this might work:
from pathlib import Path
class AudioFile:
ext: str
def __init__(self, filepath: Path) -> None:
if not filepath.suffix == self.ext:
raise ValueError("Invalid file format")
self.filepath = filepath
class MP3File(AudioFile):
ext = ".mp3"
def play(self) -> None:
print(f"playing {self.filepath} as mp3")
class WavFile(AudioFile):
ext = ".wav"
def play(self) -> None:
print(f"playing {self.filepath} as wav")
class OggFile(AudioFile):
ext = ".ogg"
def play(self) -> None:
print(f"playing {self.filepath} as ogg")
All audio files check to ensure that a valid extension was given upon initialization. If the filename doesn't end with the correct name, it raises an exception.
The __init__()
method in the parent class is able to access the ext
class variable from different subclasses. That's polymorphism at work.
In addition, each subclass of AudioFile
implements play()
in a different way. This is also polymorphism in action. The media player can use the exact same code to play a file, no matter what type it is; it doesn't care what subclass of AudioFile it is looking at.
Retrieved from Lott and Phillips,Python Object-Oriented Programming
p_1 = MP3File(Path("you are my sunshine.mp3"))
p_1.play()
p_2 = WavFile(Path("my only sunshine.wav"))
p_2.play()
p_3 = WavFile(Path("my only sunshine.wrd"))
playing you are my sunshine.mp3 as mp3 playing my only sunshine.wav as wav
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) ~\AppData\Local\Temp\ipykernel_18944\2042668637.py in <module> 3 p_2 = WavFile(Path("my only sunshine.wav")) 4 p_2.play() ----> 5 p_3 = WavFile(Path("my only sunshine.wrd")) ~\AppData\Local\Temp\ipykernel_18944\1497370230.py in __init__(self, filepath) 4 def __init__(self, filepath: Path) -> None: 5 if not filepath.suffix == self.ext: ----> 6 raise ValueError("Invalid file format") 7 self.filepath = filepath 8 class MP3File(AudioFile): ValueError: Invalid file format
filepath=Path("you are my sunshine.mp3")
filepath.suffix
'.mp3'
Naming Convention¶
we are all adults. All class data is public
However, we can use restricting access using 1- naming conventions, 2- use
@property
, 3- Overriding__getattr__()
and__setattr__()
Naming Convention for Internal Attributes¶
obj._attr_name
, obj._func_name()
when name start with a single
_
, it is "internal".It should not be part of public API.
If you are a class user, you should NOT touch this.
If you are a class developer: you can use it for helper functions, implementation details ...
df._var1_to_mean
(attribute),measure._m_to_in()
(function)
Encapsulation, Naming convention for pseudo-private attributes¶
obj.__attr_name
, obj.__func_name()
when name start with a double
__
but not end with, it is "private".There is no inheritance
If you are a class user, you should NOT touch this
If you are a class developer: you can use it for helper functions, implementation details ...
df.__var1_to_mean
(attribute),measure.__m_to_in()
(function)
Trailing and leading `__` are only used for built-in Python methods (`__int__()`, `__repr__()`)
Here is an example of encapsulation:
class Speed:
def __init__(self):
self.speed=10
self.__new_speed=80
s=Speed()
print(s.speed)
print(s.__new_speed)
10
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) ~\AppData\Local\Temp\ipykernel_18944\3665847376.py in <module> 6 s=Speed() 7 print(s.speed) ----> 8 print(s.__new_speed) AttributeError: 'Speed' object has no attribute '__new_speed'
We do not have access to private value but we can modify it so this is a big problem. To resolve this we use getter and setter.
class Speed:
def __init__(self):
self.speed=10
self.__new_speed=80
# this is a getter (we are getting a private value)
def get_new_speed(self):
return self.__new_speed
# this is a setter (we are changing or modifying a private value)
def set_new_speed(self, new_speed):
self.__new_speed=new_speed
s=Speed()
print(s.speed)
print(s.get_new_speed())
s.set_new_speed(100)
print(s.get_new_speed())
10 80 100
Here is another example for private function:
class Example:
def __init__(self):
self.x=10
self._y=50
self.__z=100
def public_method(self):
print(self.x)
print(self._y)
print(self.__z)
def __private_method(self):
print("Inside Private Method")
s=Example()
print(s.x)
print(s._y)
print(s.__z)
10 50
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) ~\AppData\Local\Temp\ipykernel_18944\3519030824.py in <module> 17 print(s.x) 18 print(s._y) ---> 19 print(s.__z) AttributeError: 'Example' object has no attribute '__z'
print(s.public_method())
print(s.__private_method())
10 50 100 None
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) ~\AppData\Local\Temp\ipykernel_18944\3341620232.py in <module> 1 print(s.public_method()) ----> 2 print(s.__private_method()) AttributeError: 'Example' object has no attribute '__private_method'
class Example:
def __init__(self):
self.x=10
self._y=50
self.__z=100
def public_method(self):
print(self.x)
print(self._y)
print(self.__z)
self.__private_method()
def __private_method(self):
print("Inside Private Method")
s=Example()
s.public_method()
10 50 100 Inside Private Method
Here are more details:
class Employee_info:
def __init__(self,name,salary):
self.name, self.salary=name,salary
#
def name(self,name):
self.name=name
#
def salary(self, salary):
self.salary= salary
#
def _raise(self, amount):
self.salary=self.salary + amount
em_info=Employee_info('mehdi rezvandehy',3500)
print(em_info.name)
em_info.salary=em_info.salary+6500
print(em_info.salary)
mehdi rezvandehy 10000
We can add and subtract any value to a attribute. This might be problem because attribute might be string or the final result may not make sense (for example negative salary). The question is how to protect an attribute.
In Python, property()
is a built-in function that creates and returns a property
object. Let's say that this class is part of your program. You are modeling a house with a House class (at the moment, the class only has a price instance attribute defined):
class House:
def __init__(self, price):
self.price = price
This instance attribute is public because its name doesn't have a leading underscore. Since the attribute is currently public, it is very likely that you and other developers in your team accessed and modified the attribute directly in other parts of the program using dot notation, like this:
obj.price # Access value
obj.price = 40000 # Modify value
So far everything is working great, right? But let's say that you are asked to make this attribute protected (non-public) and validate the new value before assigning it. Specifically, you need to check if the value is a positive float. How would you do that? Let's see.
At this point, if you decide to add getters
and setters
, you and your team will probably panic?. This is because each line of code that accesses or modifies the value of the attribute will have to be modified to call the getter
or setter
, respectively. Otherwise, the code will break ⚠️.
obj.get_price() # Changed from obj.price
obj.set_price(40000) # Changed from obj.price = 40000
But... Properties come to the rescue! With @property, you and your team will not need to modify any of those lines because you will able to add getters
and setters
"behind the scenes" without affecting the syntax that you used to access or modify the attribute when it was public.
If you decide to use @property
, your class will look like the example below:
class House:
def __init__(self, price):
self._price = price
@property # (or getter)
def price(self):
return self._price
@price.setter
def price(self, new_price):
if new_price > 0 and isinstance(new_price, float):
self._price = new_price
else:
print("Please enter a valid price")
@price.deleter
def price(self):
del self._price
Specifically, we can define three methods for a property:
- A
getter
- to access the value of the attribute. - A
setter
- to set the value of the attribute. - A
deleter
- to delete the instance attribute.
Price is now "Protected"
Please note that the price attribute is now considered "protected" because we added a leading underscore to its name in self._price
:
self._price = price
In Python, by convention, when you add a leading underscore to a name, you are telling other developers that it should not be accessed or modified directly outside of the class. It should only be accessed through intermediaries (getters and setters) if they are available.
Getter¶
Here we have the getter method:
@property
def price(self):
return self._price
Notice the syntax:
@property
- Used to indicate that we are going to define a property. Notice how this immediately improves readability because we can clearly see the purpose of this method.
def price(self)
- The header. Notice how thegetter
is named exactly like the property that we are defining:price
. This is the name that we will use to access and modify the attribute outside of the class. The method only takes one formal parameter,self
, which is a reference to the instance.
- return
self._price
- This line is exactly what you would expect in a regulargetter
. The value of the protected attribute is returned.
class House:
def __init__(self, price):
self._price = price
@property # (or getter)
def price(self):
return self._price
house = House(50000.0) # Create instance
house.price # Access value
50000.0
50000.0
The value for price cannot be changed because of being considered as "protected" due to having a leading underscore to its name
Setter¶
@price.setter
def price(self, new_price):
if new_price > 0 and isinstance(new_price, float):
self._price = new_price
else:
print("Please enter a valid price")
@price.setter
- Used to indicate that this is the setter method for the price property. Notice that we are not using@property.setter
, we are using@price.setter
. The name of the property is included before.setter
.
def price(self, new_price):
- The header and the list of parameters. Notice how the name of the property is used as the name of the setter. We also have a second formal parameter (new_price
), which is the new value that will be assigned to the price attribute (if it is valid).
- Finally, we have the body of the setter where we validate the argument to check if it is a positive float and then, if the argument is valid, we update the value of the attribute. If the value is not valid, a descriptive message is printed. You can choose how to handle invalid values according the needs of your program.
isinstance(568.0, float)
True
class House:
def __init__(self, price):
self._price = price
@property # (or getter)
def price(self):
return self._price
@price.setter
def price(self, new_price):
if new_price > 0 and isinstance(new_price, float):
self._price = new_price
else:
print("Please enter a valid price")
house = House(40000.0) # Create instance
house.price = 35000.0 # Update value
house.price # Access value
35000.0
house = House(50000.0)
house.price = -50
Please enter a valid price
This proves that the setter method is working as an intermediary. It is being called "behind the scenes" when we try to update the value, so the descriptive message is displayed when the value is not valid.
Deleter¶
@price.deleter
def price(self):
del self._price
@price.deleter
- Used to indicate that this is the deleter method for the price property. Notice that this line is very similar to @price.setter
, but now we are defining the deleter method, so we write @price.deleter
.
def price(self):
- The header. This method only has one formal parameter defined, self.
del self._price
- The body, where we delete the instance attribute.
This is an example of the use of the deleter method with @property
:
class House:
def __init__(self, price):
self._price = price
@property # (or getter)
def price(self):
return self._price
@price.setter
def price(self, new_price):
if new_price > 0 and isinstance(new_price, float):
self._price = new_price
else:
print("Please enter a valid price")
@price.deleter
def price(self):
del self._price
# Create instance
house = House(50000.0)
# The instance attribute exists
house.price
50000.0
# Delete the instance attribute
del house.price
# The instance attribute doesn't exist
house.price
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) ~\AppData\Local\Temp\ipykernel_18944\1766094459.py in <module> 3 4 # The instance attribute doesn't exist ----> 5 house.price ~\AppData\Local\Temp\ipykernel_18944\3631519707.py in price(self) 6 @property # (or getter) 7 def price(self): ----> 8 return self._price 9 10 @price.setter AttributeError: 'House' object has no attribute '_price'
Composition and Aggregation¶
Sometimes a class can be inherited to another class since the relationships is not parent-child, for example Books and Library are very close but we cannot say book is a library! Another example is salary and employee with very high relationship. In this case, we cannot use inheritance since they do not have that relationship. Composition (strong association) is used when two classes are highly depended to each other. Salary is part of Employee content is part of container.
class Salary:
def __init__(self, pay, reward):
self.pay = pay
self.reward = reward
def annual_salary(self):
return (self.pay*12)+self.reward
class Employee:
def __init__(self, name, position, pay, reward):
self.name = name
self.position = position
self.final_salary = Salary(pay, reward)
def final_salary_m(self):
return self.final_salary.annual_salary()
emp = Employee("Mehdi","Data Sientist", 10000,1000)
print (emp.final_salary_m())
121000
Another important parameter is aggregation (weak association) when there is a has relationship. For example, Bank has a Employee, Library has a book.
class Salary:
def __init__(self, pay, reward):
self.pay = pay
self.reward = reward
def annual_salary(self):
return (self.pay*12)+self.reward
class Employee:
def __init__(self, name, position, sal):
self.name = name
self.position = position
self.final_salary = sal
def final_salary_m(self):
return self.final_salary.annual_salary()
sal = Salary (100000,10000)
emp = Employee("Mehdi","Data Sientist", sal)
print (emp.final_salary_m())
1210000
Abstract Class¶
Sometimes we do not want to create an object from our parent class, we just want to allow only creating object from subclass. In this case, abstract class can be used.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod # decorator
def area(self):
pass
@abstractmethod # decorator
def perimeter(self):
pass
class Square(Shape):
def __init__(self,side):
self.__side= side
shape_obj = Shape()
square_obj = Square(10)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) ~\AppData\Local\Temp\ipykernel_18944\649407628.py in <module> 13 self.__side= side 14 ---> 15 shape_obj = Shape() 16 square_obj = Square(10) TypeError: Can't instantiate abstract class Shape with abstract methods area, perimeter
class Shape(ABC):
@abstractmethod # decorator
def area(self):
pass
@abstractmethod # decorator
def perimeter(self):
pass
class Square(Shape):
def __init__(self,side):
self.__side = side
def area(self):
return self.__side * self.__side
# def perimeter(self):
# return 4*self.__side
shape_obj = Square(10)
print(shape_obj.area())
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) ~\AppData\Local\Temp\ipykernel_18944\739178187.py in <module> 17 # return 4*self.__side 18 ---> 19 shape_obj = Square(10) 20 print(shape_obj.area()) TypeError: Can't instantiate abstract class Square with abstract method perimeter
class Shape(ABC):
@abstractmethod # decorator
def area(self):
pass
@abstractmethod # decorator
def perimeter(self):
pass
class Square(Shape):
def __init__(self,side):
self.__side = side
def area(self):
return self.__side * self.__side
def perimeter(self):
return 4*self.__side
shape_obj = Square(10)
print(shape_obj.area())
100
__name__ == __main__
¶
If the python interpreter is running source file as the main program, it sets the special __name__
variable to have a value “__main__”
. If this file is being imported from another module, __name__
will be set to the module’s name. Module’s name is available as value to __name__
global variable.
# Python program to execute
# main directly
print ("Always executed")
if __name__ == "__main__":
print ("Executed when invoked directly")
else:
print ("Executed when imported")
Always executed Executed when invoked directly
# Python function
def my_function():
print ("I am inside function")
# We can test function by calling it.
my_function()
I am inside function
if __name__ == "__main__":
my_function()
import myscript
myscript.my_function()
I am inside function I am inside module
Every Python module has it’s name defined and if this is ‘main’, it implies that the module is being run standalone by the user.
If script is imported as a module in another script, the name is set to the name of the script/module.
- Home
-
- Prediction of Movie Genre by Fine-tunning GPT
- Fine-tunning BERT for Fake News Detection
- Covid Tweet Classification by Fine-tunning BART
- Semantic Search Using BERT
- Abstractive Semantic Search by OpenAI Embedding
- Fine-tunning GPT for Style Completion
- Extractive Question-Answering by BERT
- Fine-tunning T5 Model for Abstract Title Prediction
- Image Captioning by Fine-tunning ViT
- Build Serverless ChatGPT API
- Statistical Analysis in Python
- Clustering Algorithms
- Customer Segmentation
- Time Series Forecasting
- PySpark Fundamentals for Big Data
- Predict Customer Churn
- Classification with Imbalanced Classes
- Feature Importance
- Feature Selection
- Text Similarity Measurement
- Dimensionality Reduction
- Prediction of Methane Leakage
- Imputation by LU Simulation
- Histogram Uncertainty
- Delustering to Improve Preferential Sampling
- Uncertainty in Spatial Correlation
-
- Machine Learning Overview
- Python and Pandas
- Main Steps of Machine Learning
- Classification
- Model Training
- Support Vector Machines
- Decision Trees
- Ensemble Learning & Random Forests
- Artificial Neural Network
- Deep Neural Network (DNN)
- Unsupervised Learning
- Multicollinearity
- Introduction to Git
- Introduction to R
- SQL Basic to Advanced Level
- Develop Python Package
- Introduction to BERT LLM
- Exploratory Data Analysis
- Object Oriented Programming in Python
- Natural Language Processing
- Convolutional Neural Network
- Publications