Python OOP: Four Pillars of OOP in Python 3 for Begineers (Udemy)
04-07-2020
Noun (name) --> Class
Adjective (properties) --> Attributes
Verb (action) --> Methods
Methods are functions that are defined within a class. 'self' is a default parameter that the methods accept and is used to access the attributes of the class
class ScientistA:
name = "Dave"
title = "Researcher"
exptperweek = 6
def metgoal(self):
if self.exptperweek >=5:
print('Performed all experiments')
else:
print('Didn''t perform all experiments')
In order to access the methods and attributes for the class, we need to create an object for the class
labMemberOne = ScientistA()
labMemberTwo = ScientistA()
print(labMemberOne.name, labMemberTwo.name)
Dave Dave
So, although we successfully wrote a class and created two objects, it's clear that we need further improvements to make it more useful. For instance, we'd want to assign different attributes to each object.
Key takeaway: Classes are blueprints and Objects are the execution of that blueprint
Class attributes:
These properties remain the same for all objects of the class. 'workplace' below.
Instance attributes:
These properties are specific to each instance of the class. 'name', 'title', 'exptperweek' below.
class ScientistB:
workplace = "Janelia"
def metgoal(self,name,title,exptperweek):
self.name = name
self.title = title
self.exptperweek =exptperweek
if self.exptperweek >=5:
print('Performed all experiments')
else:
print('Didn''t perform all experiments')
def thankmember(self):
print('Thank you for your research contributions',self.name)
# Create an instance of the class
labMemberOne = ScientistB()
print('Employee at', labMemberOne.workplace)
labMemberOne.metgoal('Tim','Research Scientist',5)
labMemberOne.thankmember()
Employee at Janelia Performed all experiments Thank you for your research contributions Tim
These methods use the 'self' parameter to access and modify the instance attributes of your class
These methods don't take the default 'self' parameter. These are defined using a decorator called @staticmethod. Decorators take a method and extend the functionality of the method
class ScientistC:
workplace = "Janelia"
def metgoal(self,name,title,exptperweek):
self.name = name
self.title = title
self.exptperweek =exptperweek
if self.exptperweek >=5:
print('Performed all experiments')
else:
print('Didn''t perform all experiments')
@staticmethod
def thankmember():
print('Thank you for your research contributions!')
labMemberOne = ScientistC()
print('Employee at', labMemberOne.workplace)
labMemberOne.metgoal('Julia','Research Scientist',5)
labMemberOne.thankmember()
Employee at Janelia Performed all experiments Thank you for your research contributions!
class ScientistD:
workplace = "Janelia"
def employeeDetails(self,name,title):
self.name = name
self.title = title
def metgoal(self,exptperweek):
self.exptperweek =exptperweek
if self.exptperweek >=5:
print('Performed all experiments')
else:
print('Didn''t perform all experiments')
def thankmember(self):
print('Thank you for your research contributions',self.name)
labMember = ScientistD()
Say, you were to run the following:
labMember.metgoal(5)
labMember.thankmember()
Since the method in which the instance attribute 'self.name' was defined isn't called, it will throw an 'AttributeError'
However, if you first call the method where the attribute 'self.name' is defined, then it works without this error
labMember.employeeDetails('Tanya','Research Scientist')
labMember.metgoal(5)
labMember.thankmember()
Performed all experiments Thank you for your research contributions Tanya
So, this makes it clear that we need to have a mechanism that initializes all the attributes of our object of our class before they are to be used. Python has a special method called the init method for this
__init__
method¶class ScientistE:
workplace = "Janelia"
def __init__(self,name,title):
self.name = name
self.title = title
def metgoal(self,exptperweek):
self.exptperweek =exptperweek
if self.exptperweek >=5:
print('Performed all experiments')
else:
print('Didn''t perform all experiments')
def thankmember(self):
print('Thank you for your research contributions,',self.name,'!')
member = ScientistE('Draymond','Research Scientist')
print(member.name,'is a', member.title, 'at', member.workplace)
member.metgoal(5)
member.thankmember()
Draymond is a Research Scientist at Janelia Performed all experiments Thank you for your research contributions, Draymond !
Abstraction is the act of only giving out the most essential information while the details are hidden or encapsulated. In other words, abstraction presents a simplified picture of the complexities that are behind the curtain. Encapsulation is the process of hiding the code and data into a single unit to achieve abstraction.
For example, when you apply 'append' to your list, 'append' is the level of abstraction provided to you to add an element to the list. The internals of the 'append' command isn't exposed to the user as it has been encapsulated in the list class.
class Car:
def __init__(self):
# A dictionary to map the type of car and price per day
self.carFare = {'Hatchback': 30, 'Sedan': 50, 'SUV': 100}
def displayFareDetails(self):
print("Cost per day: ")
print("Hatchback: $",self.carFare['Hatchback'])
print("Sedan: $", self.carFare['Sedan'])
print("SUV: $", self.carFare['SUV'])
def calculateFare(self, typeOfCar, numberOfDays):
self.typeOfCar = typeOfCar
self.numberOfDays = numberOfDays
# Calculate the fare depending upon the type of car and number of days as requested by the user
print("Type of vehicle:", self.typeOfCar)
print("Number of days:", self.numberOfDays)
print("Here is your total price: $", self.carFare[self.typeOfCar] * self.numberOfDays)
print("Thank you!")
car = Car()
car.displayFareDetails()
Cost per day: Hatchback: $ 30 Sedan: $ 50 SUV: $ 100
car.calculateFare('SUV', 3)
Type of vehicle: SUV Number of days: 3 Here is your total price: $ 300 Thank you!
Inheritance is a way to form new classes using classes that have already been defined. The newly formed classes are called derived classes, and the classes that we derive from are called base classes. Important benefits of inheritance are code reuse and reduction in complexity of a program. The derived classes (descendants) override or extend the functionality of base classes (ancestors).
class Apple: #base class
manufacturer = "Apple Inc"
contactWebsite = "www.apple.com/contact"
def contactDetails(self):
print("To contact us, log on to", self.contactWebsite)
class Macbook(Apple): #derived class
def __init__(self,year,product):
self.year = year
self.product = product
def productDetails(self):
print("This device is a {} manufactured in {} by {}.".format(self.product, self.year, self.manufacturer))
macbook = Macbook(2009,"Macbook Pro")
macbook.productDetails() # Calling the method in the derived class
macbook.contactDetails() # Method in the base class is inherited by the derived class, so calling that method directly
This device is a Macbook Pro manufactured in 2009 by Apple Inc. To contact us, log on to www.apple.com/contact
The order in which the base classes are defined in the derived class matters. So, if both classes use the same attribute, then the attribute from the first class is used (for instance, 'name' below)
class OperatingSystem: #first base class
name = "Mac OS Mojave"
class Apple: #second base class
manufacturer = "Apple Inc"
contactWebsite = "www.apple.com/contact"
name = "Apple"
def contactDetails(self):
print("To contact {}, log on to {}".format(self.name,self.contactWebsite))
class Macbook(OperatingSystem,Apple): #derived class with two base classes
def __init__(self,year,product):
self.year = year
self.product = product
def productDetails(self):
print("This device is a {} manufactured in {} by {}. The OS on this machine is {}."
.format(self.product, self.year, self.manufacturer,self.name))
macbook2 = Macbook(2019,"Macbook Air")
macbook2.productDetails()
macbook2.contactDetails()
This device is a Macbook Air manufactured in 2019 by Apple Inc. The OS on this machine is Mac OS Mojave. To contact Mac OS Mojave, log on to www.apple.com/contact
When you have classes at multiple levels. For instance, let's say there are 3 classes- classA, classB, and classC. Suppose classB inherits from classA, and classC inherits from classB. Then, classC will now have access to all the methods and attributes of both classB and classA.
class Grandfather:
surname = "Jordan"
origin = "North Carolina"
class Father(Grandfather):
city = "Chapel Hill"
class Child(Father):
school = "UNC"
sibblings = 3
def __init__(self,name):
self.name = name
def moreInfo(self):
print("{} {} is from {} and grew up in {}. She attended {}. She has {} sibblings who attend the same school."
.format(self.name, self.surname, self.origin, self.city, self.school, self.sibblings))
child = Child("Maya")
child.moreInfo()
Maya Jordan is from North Carolina and grew up in Chapel Hill. She attended UNC. She has 3 sibblings who attend the same school.
Public: public attributes and methods of a class are accessible from within the class, derived classes, as well as from anywhere outside the derived classes
Protected: protected attributes and methods of a class are accessible from within the class and the derived classes
Private: private attributes and methods of a class are accessible only from within the class-- not even the derived classes have access to this information
Unlike other programming languages (C++, Java etc), Python doesn't force access specifiers. However, they are strongly encouraged through a naming convention that has been followed over the years
class Vehicle:
numberofWheels = 4 #public
_color = "Blue" #protected i.e., use in this class and derived classes
__yearOfManufacture = 2019 #private i.e., use only in this class
class Car(Vehicle):
def __init__(self):
print("Car color (Protected attribute acquired from the base class):",self._color)
car = Car()
print(car.numberofWheels)
print(car._color)
Car color (Protected attribute acquired from the base class): Blue 4 Blue
If we try to access the private attribute of the base class (Vehicle) from the derived class (Car), it'll throw an attribute error:
print(car.__yearOfManufacture)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-23-e98671bca5f4> in <module>()
----> 1 print(car.__yearOfManufacture)
AttributeError: 'Car' object has no attribute '__yearOfManufacture'
vehicle = Vehicle()
print("Public Attribute:",vehicle.numberofWheels)
print("Protected Attribute:",vehicle._color)
Public Attribute: 4 Protected Attribute: Blue
Let's create an object tbat belongs to the base class (Vehicle). Now, you'll notice that the public and protected classes can be accessed. However, the private class still throws an AttributeError.
print(vehicle.__yearOfManufacture)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-35-475c338e1691> in <module>()
----> 1 print(vehicle.__yearOfManufacture)
AttributeError: 'Vehicle' object has no attribute '__yearOfManufacture'
The above AttributeError was thrown because Python internally mangles the private data as _Class__PrivateData. So, if you must access the private data (strongly discouraged), you can do so by calling _Class_PrivateData
print(vehicle._Vehicle__yearOfManufacture)
2019
Polymorphism is the process of using an operator or function in different ways for different data input. In practical terms, polymorphism means that if class B inherits from class A, it doesn't have to inherit everything about class A. It can override the base class methods and do some of those things differently.
In Python, Polymorphism allows us to define methods in the child class with the same name as defined in their parent class.
# Overriding
class A:
def explore(self):
print("explore() method from class A")
class B(A):
def explore(self):
print("Override method from class A and use explore() method from class B")
b = B()
a = A()
b.explore()
a.explore()
Override method from class A and use explore() method from class B explore() method from class A
As seen above, if the object associated with the base class is used, then the base class method is used. However, if the object associated with the derived class is used, then the method in this derived class is used instead (i.e., base class method overriden).
super()
method¶If you want to access the overridden method of the base class from the derived class, you can do so using the super() method
class AA:
def explore(self):
print("explore() method from class A")
class BB(AA):
def explore(self):
super().explore() # calling the parent class explore() method
print("Override method from class A and use explore() method from class B")
bb = BB()
bb.explore()
explore() method from class A Override method from class A and use explore() method from class B
class Thorlabs:
def __init__(self):
self._glass = "BK7" # protected attribute
self._diameter = "1 inch"
self._coating = "AR coating"
class Lens(Thorlabs):
def __init__(self):
# Here we are calling the init of our base class to initialise the default parameters
super().__init__()
self.__manufacturer = "Thorlabs" # private attribute
def setGlass(self, glass):
self._glass = glass
def displaySpecs(self):
print("Lens diameter: {} Glass: {} Coating: {} Manufactured by {}"
.format(self._diameter, self._glass, self._coating, self.__manufacturer))
@staticmethod
def glassTypes():
print('N-BK7, UV fused silica, calcium fluoride, magnesium fluoride, zinc selenide, germanium, silicon')
lens = Lens()
print("Would you like to change the type of glass from BK7? Y/N")
userChoice = input()
if userChoice is 'Y':
print("Glass options:")
lens.glassTypes()
print("Enter the type of glass")
glass = input()
lens.setGlass(glass)
lens.displaySpecs()
Would you like to change the type of glass from BK7? Y/N Y Glass options: N-BK7, UV fused silica, calcium fluoride, magnesium fluoride, zinc selenide, germanium, silicon Enter the type of glass UV fused silica Lens diameter: 1 inch Glass: UV fused silica Coating: AR coating Manufactured by Thorlabs