Introduction to Object Oriented Programming
Programming languages are built upon certain models to ensure the code behaves predictably. Python primarily follows what is known as an object oriented paradigm or model. Object-oriented programming or OOP, relies heavily on simplicity and reusability to improve workflow.
Programming paradigms are a strategy for reducing code complexity and determining the flow of execution. There are several different paradigms such as declarative, procedural, object-oriented, function, logic, event-driven, flow-driven more.
These paradigms are not mutually exclusive. Programs and programming languages can opt for multiple paradigms.
Python is primarily object-oriented, but itâs also procedural and functional. In simple terms, a paradigm can be defined as a style of writing a program.
OOP is one of the most widely used paradigms today due to the growing popularity of languages that use it, such as Java, Python, C++, and more.
NOTE
But the OOP is ability to translate real-world problems into code is arguably the biggest factor in its success.
OOP has high modularity, which makes code easier to understand, makes it reusable, adds layers of abstraction and allows for code blocks to be moved between projects.
Some key components of OOP
- classes
- objects
- methods
Classes
A class is a logical code block that contains attributes and behavior. In Python, a class is defined with the class keyword. The attributes can be variables and the behavior can be functions inside of it.
Objects
You can create instances from these classes which are called objects. In other words, a class provides a blueprint for creating an object.
The state of an object comprises its attributes and behavior, and each one has a unique identifier to distinguish it from other instances. The attributes and behavior of the class are what define the state of the object.
Methods
Which are the functions defined inside a class that determine the behavior of an object instance.
The concepts that OOP hinges upon
Inheritance
Which is the creation of a new class by deriving from an existing one. The original is called the parent or superclass, or any derivatives are referred to as the subclass or child class.
Polymorphism
Itâs a word that means having many forms. In the context of Python, polymorphism means that a single function can act differently depending on the object or the causes.
Example
Using â+â with numbers, it will add numbers together, but with strings, concatenation will be done.
Encapsulation
Broadly, this means that Python can bind methods and variables from direct access by wrapping them within a single unit of scope, such as a class.
Encapsulation helps prevent unwanted modifications, in effect, reducing the occurrence of errors and outputs.
Abstraction
This refers to the ability to hide implementation details to make data safer and more secure.
NOTE
Note that Python does not support abstraction directly and uses inheritance to achieve it.
Some others
- Method overloading
- Method overriding
- Constructors
- âŠ
OOP Principles
This reading introduces you to the OOP principles in more detail using some examples.
The object oriented paradigm was introduced in the 1960s by Alan Kay. At the time, the paradigm was not the best computing solution given the small scalability of software developed then. As the complexity of software and real-life applications improved, object oriented principles became a better solution.
You previously encountered the four main pillars of object oriented programming. These are: encapsulation, polymorphism, inheritance and abstraction. Letâs look at a few examples that demonstrate how these principles translate when using Python.
Encapsulation
The idea of encapsulation is to have methods and variables within the bounds of a given unit. In the case of Python, this unit is called a class. And the members of a class become locally bound to that class. These concepts are better understood with scope, such as global scope (which in simple terms is the files I am working with), and local scope (which refers to the method and variables that are âlocalâ to a class). Encapsulation thus helps in establishing these scopes to some extent.
For example, the Little Lemon company may have different departments such as inventory, marketing and accounts. And you may be required to deal with the data and operations for each of them separately. Classes and objects help in encapsulating and in turn restrict the different functionalities.
Encapsulation is also used for hiding data and its internal representation. The term for this is information hiding. Python has a way to deal with it, but it is better implemented in other programming languages such as Java and C++. Access modifiers represented by keywords such as public, private and protected are used for information hiding. The use of single and double underscores for this purpose in Python is a substitute for this practice. For example, letâs examine an example of protected members in Python.
self._a is a protected member and can be accessed by the class and its subclasses.
Private members in Python are conventionally used with preceding double underscores: __. self.__b is a private member of the class Alpha and can only be accessed from within the class Alpha.
It should be noted that these private and protected members can still be accessed from outside of the class by using public methods to access them or by a practice known as name mangling. Name mangling is the use of two leading underscores and one trailing underscore, for example:
_class__identifier
Class is the name of the class and identifier is the data member that I want to access.
Polymorphism
Polymorphism refers to something that can have many forms. In this case, a given object. Remember that everything in Python is inherently an object, so when I talk about polymorphism, it can be an operator, method or any object of some class. I can illustrate the case for polymorphism using built-in functions and operations, for example:
The output is:
In the example, I have used the same operator () to perform on a string, integer and a list. You can see the () operator behaves differently in all three cases.
Letâs examine one more example.
The output is:
The len() function is able to take variable inputs. In the example above it is a string and a list that provides the output in integer format.
Inheritance
Inheritance in Python will be covered later in the course, but the basic template for it is as follows:
As the structure of inheritance gets more complicated, Python adheres to something called the Method Resolution Order (MRO) that determines the flow of execution. MRO is a set of rules, or an algorithm, that Python uses to implement monotonicity, which refers to the order or sequence in which the interpreter will look for the variables and functions to implement. This also helps in determining the scope of the different members of the given class.
Abstraction
Abstraction can be seen both as a means for hiding important information as well as unnecessary information in a block of code. The core of abstraction in Python is the implementation of something called abstract classes and methods, which can be implemented by inheriting from something called the abc module. âabcâ here stands for abstract base class. It is first imported and then used as a parent class for some class that becomes an abstract class. Its simplest implementation can be done as below.
Python classes and instances
Classes have the ability to combine data and functionality, which is a very useful feature when you are coding.
You may have also heard of classes discussed in terms of attributes and behaviors. In general, attributes refer to variables declared in a class, while behaviors are associated with the methods in the class.
Creating a class creates a new type of object from which you can create instances.
NOTE
An important thing to keep in mind is that everything in Python is an object or derived from the object class.
The pass keyword plays the role of a placeholder when nothing needs to be executed. In practice, this tells Python that I wonât do anything with this class just yet.
- Class definition
- Creating a new instance
- Initializing the new instance
Since everything in Python is an object, it makes sense to follow naming conventions to make things less confusing later.
There is a third type of object called the method object, which you can use the column method whenever itâs needed.
Classes mainly perform two kinds of operations, attribute references and instantiation.
There is None
, because there is no return value in the function.
Exercise: Define a Class
Learning Objectives
You have encountered the basic principles of Object Oriented programming and in some preliminary ways demonstrated how the different principles can be put into practice with the help of classes, the building blocks of OOP. Let us now look at the structure of these classes.
Here you will learn how to create classes and objects with the help of examples. Letâs first look at the basic members of a class. These can be the attributes or the data members, the methods, and additionally the comments that you can include. These members can be shown with the help of an example below. Let us imagine you want to make a class of some house. You begin by creating a class for it.
Example 1
In the code above, you start with a multiline comment, which alternatively can also be called a docstring (''' enclosed comments ''' ). In the next line you have the class definition, followed by a couple of data members or attributes: num_rooms
and bathrooms
. This is then followed by a function definition, which is empty except for the pass
 keyword that basically signals Python to continue execution without throwing an error. The last line in the code block is the single-line comment preceded by #
.
The code completely defines the class and functions present inside it, but it is effectively not useful unless you call or instantiate it. You can do this by one of the two ways: Calling the class directly Instantiating an object of that class
You can add a few lines of code below your code that will call the variable num_rooms on the house object and the House class after we create a house object from House class:
The effective output for this will be:
To follow up with this example, add few more lines to this code and see the output, this time after you have updated the num_rooms variable called on house object to 7:
The new output this time will be:
What has happened in the code above is, you have created an instance of a class called house
 and then modified the attribute for that instance with a value of 7. It updates the value of the instance attribute, but not the class attribute. So the num_rooms attribute of the class remains unchanged as 5, but the instance attribute associated with house object changes to 7. Letâs now insert an alternate piece of code in this.
This time, instead of an instance attribute, you will modify the class attribute by directly calling it over the class as follows:
The output for it will be:
You will notice that the changes on a class attribute will affect even the instances that you will create over it. Also note the use of the keywork self in this example. self
 is a convention in Python, and you may use any other word in its place, but as a practice, it is easy to recognize. self
 here is passed inside the method cost_evaluation()
 as it is an instance method and facilitates the method to point to any instance of the House when that method is called. It should be noted how any number of parameters can be passed to these instance methods but the first one is always the reference to the instance of that class.
You can interact and run the entire program that you just saw in the code block below:
Define a Class - solution
Solution code
Instantiate a custom Object
INFO
Code reusability is the use of existing code to build new software. Reusability is a core programming concept.
Two special methods in python
new
cls
cls
here is not a keyword, but rather a convention. It acts as a placeholder for passing the class as its first argument, which will be used for creating the new empty object.
init
Which is similar to what is known as a constructor in some other programming languages.
It takes the objects created using the new method along with other arguments to initialize the new object being created.
self
The
self
keyword here is another convention. It has no function itself but serves as a placeholder for self-reference by the instance object.
NOTE
I find that despite passing the same function and variable items, the two instances produce different contents.
Exercise: Instantiate a custom Object
This is your first experience creating classes and objects in Python. You will be following a sequential process where you will create a class, define its state by creating variables and functions to define its attributes and behavior, and then instantiate it using some variable. Finally, you will use the class members to get the desired output.
Follow the steps to build and run your program in the environment provided at the bottom of the reading.
Step 1
1.1 Define a class called MyFirstClass
.
1.2 Add a print statement inside it such as âWho wrote this?â.
Step 2
Create a string variable named index
and initialize it with a string âAuthor-Bookâ.
Step 3
3.1 Define a function called hand_list()
with the help of def
keyword.
3.2 Pass the parameter  self
to it. And then pass two parameters, philosopher
 and book
to it.
Step 4
4.1 Write a print statement using the print()
function and pass the class variable by accessing it.
Hint: Class variables are accessed directly by calling it over the class name using dot notation.
4.2 Write a print statement that will give output such as:Â âPlato wrote the book: Republicâ
 where âPlatoâ is the philosopher and âRepublicâ is the book.
Hint: You can make use of the built-in (+
) concatenation operator to join these strings.
Step 5
5.1 Create and instantiate an object of that class, called whodunnit
5.2 Call method hand_list()
over this object âwhodunnitâ and pass two values to it namely âSun Tzuâ and âThe Art of Warâ.
Use the following code block to build your program:
Instantiate a custom Object - solution
Solution code
Instance methods
Parent classes vs. child classes
When instantiating objects from a class, you may find that the class is missing some properties that you use frequently. In that case, you could decide to make a new class that replicates the first one, but also adds a few more properties. It would be cumbersome to write everything from scratch, but thanks to inheritance, you donât have to.
Everything in python is an object. It specifically means that every class Python inherits from a built-in base class called objects, which is found in built-ins dot objects.
In other words, a class declaration such as Someclass with empty parentheses implies some class with object as its arguments.
When speaking of class derivation, the originating class is known as the parent class, super class, or base class.
The class which inherits from it is the child class, subclass, or derived class.
Any named pairing is acceptable.
NOTE
Child class extends the attributes and behaviors of its parent class.
This allows you to do two things:
- You can add new properties to the child class.
- You can modify inherited properties in the child class without affecting the parents.
NOTE
Any changes in the parent class will also affect any child classes.
Example
TIP
By calling the Employees class, the super method has automatically been applied to access the variables there and initialize them within the Supervisors class.
Inheritance and Multiple Inheritance
Letâs say there two classes, namely class A and class B. If you have to perform simple inheritance, it can be done as follows:
If class A is the parent class and class B is inheriting from it, then class A is passed inside class B as a parameter. This will allow class B to directly access the attributes and methods inside class A.
Multiple inheritance
You have learned about single inheritance so far, but Python also gives us the ability to perform multiple inheritance between classes.
Here is a simple example of how that can be done.
The output is 2 because C derives from the immediate super class of C, and thatâs B.
The case above is an example of multi-level inheritance where the derived class C inherits from base class B. The class B is in turn a derived class of base class C. Class B here is an intermediary derived class. There are three levels of inheritance in this case, but it could be extended as long as I want, though it may become impractical after a while.
Built-in functions
There are two built-in functions that can come in handy when trying to find the relationship between different classes and objects: issubclass() and isinstance().
The first one, issubclass ()Â is demonstrated below.
Two classes are passed as arguments to this function and a Boolean result is returned. The above example can be extended as follows.
The output is:
This illustrates how the child class is passed as the first argument. To avoid confusion, this can be read as: âIs B subclass of A?â You can see the result is âTrueâ in the second case where child B is the subclass.
Another built-in function similar to this one is isinstance() thatdetermines if some object is an instance of some class. So if I write:
The output that I will get is âTrueâ.
Now that you know how classes can be extended from other classes, letâs look at another useful built-in function called the super() function.
The super() function is a built-in function that can be called inside the derived class and gives access to the methods and variables of the parent classes or sibling classes. Sibling classes are the classes that share the same parent class. When you call the super() function, you get an object that represents the parent class in return.
The super() function plays an important role in multiple inheritance and helps drive the flow of the code execution. It helps in managing or determining the control of from where I can draw the values of my desired functions and variables.
If you change anything inside the parent class, there is a direct retrieval of changes inside the derived class. This is mainly used in places where you need to initialize the functionalities present inside the parent class in the child class as well. You can then add additional code in the child class.
Here is an example.
The output is:
In the code above, if I had commented out the line for super() function, the output is:
This happened because when you initialize the child class, you donât initialize the base class with it. super() function helps you to achieve this and add the initialization of base class with the derived class.
Exercise: Classes and object exploration
In this reading, you will explore the behaviour of functions, objects and classes in Python and how the flow of execution of different program statements works to enable better understanding.
You will perform minor modifications in the given code to observe how it changes the output.
First, set up a file called class_explore.py
that contains the following piece of code. Alternatively, you can use the interactive environment here.
Now, modify the code as per the instructions below and observe the changes.
Step 1: Run the code and observe its output. Take note of every line in the output and how it is different from the output you expected.
Algorithmically we can view the program consisting of the following:
Class definition of A
1.1 Constructor for A
1.2 Definition of local method alpha()
Instantiating object a over class A
Calling method alpha() over object of class A
Class definition of B
Constructor for B
5.1 Calling method alpha() over object of class A
5.2 Instantiating object a over class B *.
Additional print statements distributed through the code.
Step 2: Comment out lines #13, 14, 21, 24, 27 and 28. Run the code again.
The output is:
Even if you have commented out the creation of instances for both classes A and B, the output still shows âPrint inside Aâ and âPrint inside Bâ and also the value of variable d, which is 5. How is that possible?
Itâs because statements inside a class body get executed irrespective of the instance creation. You will also see how the print statement âInside class Aâ, which is inside the constructor, is not executed because itâs inside a function.
The value of d=5 being printed demonstrates that the namespace and scope of the variable is determined by the interpreter before you create any instance of the class or call any function inside it. If you observe the list you get by calling the dir() function, youâll note that the last entry is the alpha() function added to the namespace of A.
Step 3: Now remove the comment for lines 21 and 24.
If you run the code at this point, it will throw an error, âNameError: name âaâ is not definedâ. Take note of how you have passed the object a to the constructor of class B and the code still worked fine earlier. Only when you tried to âuseâ object a, did you get an error because it has not been instantiated. In other words, Python still does not know what âaâ means. The same will happen if you remove commenting next to line 28.
To make the code work, now remove the # in front of line 14 and run it again.
The output is:
Step 4: Remove the commenting for line 27 and 28.
The variable c of class A is modified over object a inside class B. Even though the instance of class B is still not created, the value of a.c is still getting updated, even outside the class, as evidenced by the final line in the output which shows the output is 2.
Step 5: Finally remove all the remaining comments and run the code one more time.
Here are a few observations.
- When you try and print the âobjectâ of class A as in lines 21 and 28, you get the address of the object instead of the contents.
- Note how the address of object a is the same both inside class B and in the global scope of the program. It remains the same irrespective of from where it is called.
- The alpha() function is called twice in the program, but you still get the output as 2 every time and not 3. Thatâs because the value is updated only temporarily and not assigned to anything.
Revise items about classes, function calls and scope in case of confusion.
Abstract classes and methods
If you have an abstract class, you can ensure the functionality of every class that is derived from it. For example, a vehicle could be an abstract class. You canât create a vehicle, but you can derive a car, a tractor, or a boat from a vehicle.
The methods we put in the abstract class are guaranteed to be present in the derived class because they must be implemented.
If a vehicle has a turn on engine method, then we assure that any method calls to a derived class that is looking for turn on engine will find it.
This could be for reasons of interoperability, consistency, and avoiding code duplication in general.
Abstract classes
In object oriented programming, the abstract class is a type of class for which you cannot create an instance. Python also does not support abstraction directly. You need to impose a module just to define an abstract class. Furthermore, methods in an abstract class needs to be defined before they can be implemented.
Why use abstract classes?
One of the key advantages is the ability to hide the details of implementation without sacrificing functionality.
Abstract Class
Implementation in abstract classes can be done in two ways:
- One is that, as base abstract classes lack implementation of their own, the methods must be implemented by the derived class.
- Another possibility is that the super function can be used.
The module is known as the abstract base class or ABC, and needs to be imported with some code.
After that, you can create a class called SomeAbstractClass and pass in the ABC module so that it inherits that class.
The next step is to import the abstract method decorator inside the same module.
Decorator
A decorator is a function that takes another function as its arguments and gives a new function as its output. Itâs denoted by the add sign.
For now, itâs enough to know that decorators are like helper functions that add functionality to an already existing function.
Finally, here youâll define an abstractmethod which cannot be called on an object of this class. You will be able to call this method over objects of classes that inherit from this class.
Abstract method
Similarly, we can define abstract methods with the help of what we call an abstract method decorator present inside the same module.
Any given abstract class can consist of one or more abstract methods. However, a class that has abstract class as its parents cannot be instantiated unless you override all the abstract methods present in at first.
Example
Imagine a scenario in which an employer wants to collect donations from employees for a charitable cause.
NOTE
Thereâs no implementation to this method here.
Method Resolution Order
Up to this point, youâve explored class relationships that were relatively straight forward. But what happens when things get complex? How will you know which classes inherit from which, fortunately method resolution order or MRO provides rules that can help make sense of that.
Python has many types of inheritance. The categorization types are based on the number of parents and child classes as well as the hierarchical order, including simple inheritance.
There are broadly four types of inheritance:
- Simple Inheritance
- Multiple Inheritance : which involves a child class inheriting from more than one parent.
- Multi Level Inheritance : which is inheritance taking place on several levels.
- Hierarchical Inheritance : which concerns how several sub classes inherit from a common parent.
- Hybrid Inheritance : you could say that there is fifth type called hybrid inheritance, which mixes characteristics of the others.
As these inheritance types demonstrate inheritance becomes increasingly complex as the number of classes in a project grow and become more interdependent.
MRO
Method Resolution Order
MRO determines the order in which a given method or attributes is passed through in a search of the hierarchy of classes for its resolution, or in other words, from where it belongs.
The order of the resolution is called linearization of a class, and MRO defines the rules thatâs follows.
The default order in python is bottom to top, left to right when imagining the inheritance of these python classes in a tree structure.
- Z
- Y
- X
But things become much more complicated when more levels are added to the hierarchy. So developers rely on algorithms to build MROs.
Old style classes used in depth-first search algorithm or DFS from python version three onwards, python versions have moved to the new style of classes that rely on the C3 linearization algorithm.
C3 Linearization Algorithm
The implementation of the C three linearization algorithm is complex.
- The algorithm follows monotonicity, which broadly means that an inherited property cannot skip over direct parent classes.
- It also follows the inheritance graph of the class
- and the super class is visited only after visiting the methods of the local classes.
mro()
Imagine the class A has a variable num with value of five and then Class B also has a number variable with a value of nine Here, the morrow function tells you quickly that class C will inherit the nine value from class B.
help()
It provides a much more detailed output with MRO information at the top.
It also contains information about the data descriptors and types used inside the code.
Working with Methods: Examples
You have learned how to use objects, classes and the methods inside them. You have covered these both in cases where there is only one class, as well as when there are multiple classes. You also explored how multiple inheritance works in Python and the role Method Resolution Order(MRO) plays in determining the call for the method.
The following examples demonstrate how the function call is resolved in cases of multiple inheritance in different scenarios. Note that all the functions have the same names in all of the examples.
Example 1
Output:
Class C inherits from classes B and A. When I donât find any function a() inside class C, I should search for classes B and A and its important that I do it in that order.
I will now add one more level to this and note the output.
Example 2
Output:
Class D inherits from class C, which in turn inherits from classes A and B. Class D accesses the immediate superclass of class D, which is class C and resolves the value of the variable once itâs found in that superclass.
Now letâs say I comment out the declaration inside class C.
And replace it with the pass
 keyword to keep the code functional.
Since there was no value present inside class C either, the function call above would go to A. That is because class C will point to class A as having higher precedence while inheriting.
Now letâs take another example of a similar scenario.
Example 3
The output is:
Note that this throws an error. In the code above, class D inherits from both class A and class C.
Class C is its immediate superclass, but since this is multiple inheritance, the rules are more complicated and it also has to check the classes passed to it for precedence.
In this particular case, class D is unable to resolve the order that should be followed, while resolving the value for the variable in cases where the variable is not present in the class of the given object.
It results in a TypeError because itâs unable to create method resolution order (MRO). MRO is Pythonâs way of resolving the order of precedence of classes while dealing with inheritance.
Letâs examine one final example.
Example 4
Output:
The code here is simple. class F directly inherits from its immediate superclass and the first class that is passed to it. The second line then demonstrates the return from the mro() function.
The examples in this reading demonstrate how code in which multiple inheritance is used, can get complicated and very messy, very fast. Multiple inheritance, with all the advantages and flexibility that it provides, should only be used once you have a strong command of Python as a language to avoid creating âspaghetti codeâ thatâs difficult to understand and update.
Exercise: Working with Methods
- Guess the output for the following block of code and try running the code once you have a solution in mind:
- Guess the output for the following block of code and try running the code once you have a solution in mind:
- Guess the output for the following block of code and try running the code once you have a solution in mind:
Working with Methods - solution
Solutions:
- Output Example 1:
- Output Example 2:
- Output Example 3:
Additional resources
The following resources will be helpful as additional references in dealing with different concepts related to the topics you have covered in this lesson.
OOPMROPythonIngeritanceAbstractionClassParadigmPython
Previous one â 6.Functional programming | Next one â 8.Modules