Python Unit - 5
Python Unit - 5
Inheritance
Inheritance in Python
It refers to defining a new class with little or no modification to an existing class.
The new class is called derived (or child) class and the one from which it inherits is called
the base (or parent) class.
It provides reusability of a code. We don’t have to write the same code again and again.
Also, it allows us to add more features to a class without modifying it.
It is transitive in nature, which means that if class B inherits from another class A, then all
the subclasses of B would automatically inherit from class A.
Derived class inherits features from the base class where new features can be added to it. This
results in re-usability of code.
Inheritance is represented using the Unified Modeling Language or UML in the following way:
Classes are represented as boxes with the class name on top. The inheritance relationship is
represented by an arrow from the derived class pointing to the base class. The word extends is
usually added to the arrow.
d = Dog()
d.bark()
d.speak()
Polymorphism in Python
What is Polymorphism: The word polymorphism means having many forms. In programming,
polymorphism means same function name (but different signatures) being uses for different
types.
# Driver code
print(add(2, 3))
print(add(2, 3, 4))
def flight(self):
print("Most of the birds can fly but some cannot")
class penguin(Bird):
def flight(self):
print("Penguins do not fly")
obj_bird = Bird()
obj_parr = parrot()
obj_peng = penguin()
obj_bird.intro()
obj_bird.flight()
obj_parr.intro()
obj_parr.flight()
obj_peng.intro()
obj_peng.flight()
Example
class Animal:
def speak(self):
print("speaking")
class Dog(Animal):
def speak(self):
print("Barking")
d = Dog()
d.speak()
Types of Inheritance
There are five types of inheritance in python:
1. Single Inheritance
2. Multiple Inheritance
3. Multilevel Inheritance
4. Hierarchical Inheritance
5. Hybrid Inheritance
In multiple inheritance, the features of all the base classes are inherited into the derived class.
class Base2:
pass
Example:
class Calculation1:
def Summation(self, a, b):
return a + b;
class Calculation2:
def Multiplication(self, a, b):
return a * b;
d = Derived()
print(d.Summation(10, 20))
print(d.Multiplication(10, 20))
print(d.Divide(10, 20))
In multilevel inheritance, features of the base class and the derived class are inherited into the
new derived class.
class Derived1(Base):
pass
class Derived2(Derived1):
Example:
class Animal:
def speak(self):
print("Animal Speaking")
d = DogChild()
d.bark()
d.speak()
d.eat()
Deriving a class from other derived classes that are in turn derived from the same base class is
called multi-path inheritance.
As shown in the above figure, the derived class has two immediate base classes - Derived Class I
and Derived Class 2. Both these base classes are themselves derived from the Base Class, thereby
class ECA(Student):
def ECA_score(self):
print('ECA Score......60% and above')
R = Result()
R.Eligibility()
Thus, we see that diamond relationships exist when at least one of the parent classes can be
accessed through multiple paths from the bottommost class. Diamond relationship is very
common in Python as all classes inherit from the object and in case of multiple inheritance there
is more than one path to reach the object. To prevent, base classes from being accessed more
than once, the dynamic algorithm (C3 and the MRO) linearizes the search order in such a way
that the left-to-right ordering specified in each class is preserved and each parent is called only
once (also known as monotonic).
So technically, all other classes, either built-in or user-defined, are derived classes and all objects
are instances of the object class.
# Output: True
print(issubclass(list,object))
# Output: True
print(isinstance(5.5,object))
# Output: True
print(isinstance("Hello",object))
In the multiple inheritance scenario, any specified attribute is searched first in the current class.
If not found, the search continues into parent classes in depth-first, left-right fashion without
searching the same class twice.
Example
class Base1:
pass
class Base2:
pass
So, in the above example of MultiDerived class the search order is [MultiDerived, Base1, Base2,
object]. This order is also called linearization of MultiDerived class and the set of rules used to
find this order is called Method Resolution Order (MRO).
MRO must prevent local precedence ordering and also provide monotonicity. It ensures that a
class always appears before its parents. In case of multiple parents, the order is the same as
tuples of base classes.
MRO of a class can be viewed as the __mro__ attribute or the mro() method. The former returns
a tuple while the latter returns a list.
>>> MultiDerived.__mro__
(<class '__main__.MultiDerived'>,
<class '__main__.Base1'>,
<class '__main__.Base2'>,
<class 'object'>)
>>> MultiDerived.mro()
[<class '__main__.MultiDerived'>,
<class '__main__.Base1'>,
<class '__main__.Base2'>,
<class 'object'>]
Here is a little more complex multiple inheritance example and its visualization along with the
MRO.
# Demonstration of MRO
class X:
pass
class Y:
pass
class Z:
pass
print(M.mro())
# Output:
# [<class '__main__.M'>, <class '__main__.B'>,
# <class '__main__.A'>, <class '__main__.X'>,
# <class '__main__.Y'>, <class '__main__.Z'>,
# <class 'object'>]
class AbstractClassExample(ABC):
pass
Since an abstract class is an incomplete class, users are not allowed to create its object. To use
such a class, programmers must derive it keeping in mind that they would only be either using or
overriding the features. specified in that class.
Therefore, we see that an abstract class just serves as a template for other classes by defining a
list of methods that the classes must implement. It makes no sense to instantiate an abstract
class because all the method definitions are empty and must be implemented in a subclass.
Interfaces:
The abstract class is thus an interface definition. In inheritance, we say that a class implements
an interface if it inherits from the class which specifies that interface. In Python, we use the
NotImplementedError to restrict the instantiation of a class. Any class that has the
NotImplementedError inside method definitions cannot be instantiated. Consider the
program given in the following example which creates an abstract class Fruit. Two other classes,
Mango and Orange are derived from Fruit that implements all the methods defined in Fruit. Then
we create the objects of the derived classes to access the methods defined in these classes.
Example:
class Fruit:
def taste(self):
raise NotImplementedError()
def rich_in(self):
return "Vitamin C"
def colour(self):
return "Orange"
Alphanso = Mango()
print(Alphanso.taste(), Alphanso.rich_in(), Alphanso.colour())
Org = Orange()
print (Org.taste(), Org.rich_in(), Org.colour ())
Syntax Errors:
Syntax errors, also known as parsing errors, are perhaps the most common kind of complaint you
get while you are still learning Python:
Example:
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^
SyntaxError: invalid syntax
Logic Errors:
Logic errors are those that appear once the application is in use. They are most often faulty
assumptions made by the developer, or unwanted or unexpected results in response to user
actions.
For example, a mistyped key might provide incorrect information to a method, or you may
assume that a valid value is always supplied to a method when that is not the case.
Exceptions:
Even if a statement or expression is syntactically correct, it may cause an error when an attempt
is made to execute it. Errors detected during execution are called exceptions and are not
>>> 10 * (1/0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly
The last line of the error message indicates what happened. Exceptions come in different types,
and the type is printed as part of the message: the types in the example are
ZeroDivisionError, NameError and TypeError. The string printed as the exception type is
the name of the built-in exception that occurred. This is true for all built-in exceptions, but need
not be true for user-defined exceptions (although it is a useful convention). Standard exception
names are built-in identifiers (not reserved keywords).
The rest of the line provides detail based on the type of exception and what caused it.
The preceding part of the error message shows the context where the exception occurred, in the
form of a stack traceback. In general it contains a stack traceback listing source lines;
however, it will not display lines read from standard input.
Handling Exceptions:
It is possible to write programs that handle selected exceptions. Look at the following example,
which asks the user for input until a valid integer has been entered, but allows the user to
interrupt the program (using Control-C or whatever the operating system supports); note that a
user-generated interruption is signaled by raising the KeyboardInterrupt exception.
while True:
try:
x = int(input("Please enter a number: "))
break
except ValueError:
print("Oops! That was no valid number. Try again...")
Syntax:
try:
Statement 1
Statement 2
except ZeroDivisionError:
Perform alternative
Arithmetic operations
except FileNotFoundError:
Use local file instead of remote file
If try with multiple except blocks available then based on raised exception the corresponding
except block will be executed.
Example:
try:
x = int(input("Enter First Number: "))
y = int(input("Enter Second Number: "))
print(x / y)
except ZeroDivisionError :
print("Can't Divide with Zero")
except ValueError:
print("please provide int value only")
If try with multiple except blocks available, then the order of these except blocks is important.
Python interpreter will always consider from top to bottom until matched except block
identified.
Example:
try:
x = int(input("Enter First Number: "))
y = int(input("Enter Second Number: "))
print(x / y)
except ArithmeticError :
print("ArithmeticError")
Syntax:
except (Exception1,Exception2,exception3,..):
or
except (Exception1,Exception2,exception3,..) as msg :
Parenthesis are mandatory and this group of exceptions internally considered as tuple.
Example:
try:
x = int(input("Enter First Number: "))
y = int(input("Enter Second Number: "))
print(x / y)
except (ZeroDivisionError, ValueError) as msg:
print("Plz Provide valid numbers only and problem is: ", msg)
finally block:
It is not recommended to maintain clean up code (Resource De-allocating Code or Resource
releasing code) inside try block because there is no guarantee for the execution of every
statement inside try block always.
It is not recommended to maintain clean up code inside except block, because if there is no
exception then except block won't be executed.
Hence we required some place to maintain clean up code which should be executed always
irrespective of whether exception raised or not raised and whether exception handled or not
handled. Such type of best place is nothing but finally block.
Syntax:
try:
Risky Code
except:
Handling Code
finally:
Cleanup code
The specialty of finally block is it will be executed always whether exception raised or not raised
and whether exception handled or not handled.
Output
try
finally
Output
try
except
finally
Output
try
finally
ZeroDivisionError: division by zero (Abnormal Termination)
Note: Exceptions in the else clause are not handled by the preceding except clauses.
Example:
# program to print the reciprocal of even numbers
try:
Raising Exceptions
As a Python developer you can choose to throw an exception if a condition occurs.
To throw (or raise) an exception, use the raise keyword.
Example:
Raise an error and stop the program if x is lower than 0:
x = -1
if x < 0:
raise Exception("Sorry, no numbers below zero")
Example:
The raise keyword is used to raise an exception.
We can define what kind of error to raise, and the text to print to the user.
x = "hello"
Built- in Exceptions
In Python, all exceptions must be instances of a class that derives from BaseException. In a try
statement with an except clause that mentions a particular class, that clause also handles any
exception classes derived from that class.
The built-in exceptions can be generated by the interpreter or built-in functions. Except where
mentioned, they have an “associated value” indicating the detailed cause of the error. This may
be a string or a tuple of several items of information (e.g., an error code and a string explaining
the code). The associated value is usually passed as arguments to the exception class’s
constructor.
The built-in exception classes can be subclassed to define new exceptions; programmers are
encouraged to derive new exceptions from the Exception class or one of its subclasses, and
not from BaseException.
Programmer is responsible to define these exceptions and Python not having any idea about
these. Hence we have to raise explicitly based on our requirement by using "raise" keyword.
Syntax:
class classname(predefined_exception_class_name):
def __init__(self,arg):
self.msg=arg
Example:
class TooYoungException(Exception):
def __init__(self, arg):
self.msg = arg
class TooOldException(Exception):
def __init__(self, arg):
self.msg = arg
Example 2:
class SalaryNotInRangeError(Exception):
"""Exception raised for errors in the input salary.
Attributes:
salary -- input salary which caused the error
message -- explanation of the error
"""
Note:
The raise keyword is best suitable for customized exceptions but not for pre defined
exceptions
This means that while evaluating an expression with operators, Python looks at the operands
around the operator. If the operands are of built-in types, Python calls a built-in routine. In case,
C1 = Complex()
C1.setValue(1, 2)
C2 = Complex()
C2.setValue(3, 4)
C3 = Complex()
C3 = C1 + C2
C3.display()
OUTPUT:
TypeError: unsupported operand type(s) for +: 'instance' and 'instance'
So, the reason for this error is simple. + operator does not work on user-defined objects. Now, to
do the same concept, we will add an operator overloading function in our code.
C1 = Complex()
C1.setValue(1, 2)
C2 = Complex()
C2.setValue(3, 4)
C3 = Complex()
C3 = C1 + C2
print("RESULT = ")
C3.display()
In the program, when we write C1 + C2, the __add__() function is called on C1 and C2 is passed
as an argument. Remember that, user-defined classes have no + operator defined by default. The
only exception is when you inherit from an existing class that already has the + operator defined.
Note: The __add__() method returns the new combined object to the caller
The program given below compares two Book objects. Although the class Book has three,
attributes, comparison is done based on its price. However, this is not mandatory. You can
compare two objects based on any of the attributes.
B1 = Book()
B1.set("OOP with C++", "Oxford University Press", 525)
B2 = Book()
B2.set("Let us C++", "BPB", 300)
if B1 > B2:
print("This book has more knowledge so I will buy")
B1.display()