This is Part 2 of the Python crash course “Python Mastery: From Beginner to Expert “. If you haven’t read it yet, check it out before continuing. If you’re all caught up, welcome back! In Part 1, we covered all the basic requirements to be on your way to mastering Python. Now, let’s dive deeper into Object-Oriented Programming (OOP) and get one step closer to becoming a Python expert.
👐Classes and Objects
In object-oriented programming (OOP), a class is a blueprint or template for creating objects that defines a set of attributes and methods that the objects will have. An object is an instance of a class, created using the class as a blueprint.
Classes and objects are fundamental concepts in OOP, as they allow for the organization of code into reusable and modular structures. By creating a class, we can encapsulate related data and behavior into a single unit, making our code more manageable and easier to understand. We can create multiple instances of the same class, each with its own set of data and behavior.
In Python, we create classes using the class
keyword, followed by the name of the class and a colon. The body of the class is indented, and typically contains attributes and methods. To create an object, we use the name of the class followed by parentheses.
Here is an example of a simple class in Python:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
print(f"Hello, my name is {self.name} and I am {self.age} years old.")
In this example, we define a Person
class with two attributes (name
and age
) and one method (greet
). The __init__
method is a special method that is called when an object is created from the class, and is used to initialize the object’s attributes. The self
parameter refers to the object that is being created.
We can create a Person
object like this:
person = Person("Alice", 25)
This creates a Person
object with the name
attribute set to “Alice” and the age
attribute set to 25. We can call the greet
method on the object like this:
person.greet() # Output: "Hello, my name is Alice and I am 25 years old."
Defining a Class
To define a class in Python, we use the class
keyword followed by the name of the class. The first method in a class is called the __init__
method. This is a special method used to initialize the object’s properties.
Here’s an example of a simple class:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
In this example, we have defined a class called Person
. It has two properties, name
and age
. The __init__
method initializes these properties using the arguments passed to it.
The self
parameter refers to the object itself. It is automatically passed to all instance methods in a class and is used to access the object’s properties and methods.
We can create an instance of the Person
class as follows:
person1 = Person("John", 25)
This creates an instance of the Person
class with the name “John” and age 25. We can access the properties of this object using the dot notation:
print(person1.name) # Output: John
print(person1.age) # Output: 25
Creating Objects from a Class
Once a class is defined, objects can be created from the class using the class name followed by parentheses. This will call the class constructor, which creates an instance of the class.
Here’s an example:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
car1 = Car("Toyota", "Camry", 2021)
car2 = Car("Honda", "Accord", 2022)
print(car1.make, car1.model, car1.year)
print(car2.make, car2.model, car2.year)
In this example, we define a Car
class with three attributes: make
, model
, and year
. The __init__()
method is called when an object is created from the class, and it initializes these attributes with the values passed in as arguments.
We then create two objects, car1
and car2
, using the Car
class. We pass in different arguments for each object, and this sets the attributes for each object accordingly.
Finally, we print out the values of the attributes for each object using dot notation (car1.make
, car2.model
, etc.).
Instance Variables
Instance variables, also known as member variables or attributes, are variables that are associated with individual objects or instances of a class. Each instance of a class has its own set of instance variables.
Instance variables are defined within a class and are accessed using the self
keyword. They are typically initialized in the __init__
method of a class.
Here’s an example:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
In this example, make
, model
, and year
are instance variables of the Car
class. They are initialized using the self
keyword in the __init__
method. When an instance of the Car
class is created, it will have its own make
, model
, and year
attributes.
my_car = Car("Toyota", "Camry", 2022)
print(my_car.make) # Output: Toyota
print(my_car.model) # Output: Camry
print(my_car.year) # Output: 2022
In this example, we create an instance of the Car
class called my_car
. We pass in the arguments "Toyota"
, "Camry"
, and 2022
to initialize the instance variables make
, model
, and year
, respectively. We then print out the values of these instance variables using dot notation.
Class Variables
In Python, class variables are variables that are shared by all instances of a class. They are defined inside the class, but outside any of the class’s methods. Class variables can be accessed by all instances of the class and can be modified by any instance.
Here’s an example of a class with a class variable:
class Car:
car_count = 0
def __init__(self, make, model):
self.make = make
self.model = model
Car.car_count += 1
car1 = Car("Honda", "Civic")
car2 = Car("Toyota", "Corolla")
print(Car.car_count) # Output: 2
In the above example, car_count
is a class variable that is incremented each time an instance of the Car
class is created. We can access the class variable using the class name, as shown in the print
statement.
Note that if we modify the class variable using an instance of the class, it will modify the class variable for all instances of the class:
car1.car_count = 10
print(car1.car_count) # Output: 10
print(car2.car_count) # Output: 2
print(Car.car_count) # Output: 2
In the above example, we modified the car_count
class variable using the car1
instance, but it only modified it for that instance. The car_count
variable for car2
and the Car
class itself remained unchanged.
Methods
Methods in Python are functions that are defined within a class and perform some action on the object created from that class. They are used to define the behavior of an object. There are three types of methods in Python:
- Instance methods: These methods are defined within a class and take an instance of the class (i.e., an object) as the first argument. They can access instance variables and class variables.
- Class methods: These methods are defined within a class and take the class itself as the first argument. They can access class variables but not instance variables.
- Static methods: These methods are also defined within a class, but they do not take the instance or class as the first argument. They are used when we need to perform some operation that is not dependent on the state of the object or class. They can neither access class variables nor instance variables, and can only access the variables passed to them as arguments.
To define a method in Python, we simply define a function within the class. For example, to define an instance method called my_method
that takes no arguments, we can write:
class MyClass:
def my_method(self):
# method body goes here
To define a class method, we use the @classmethod
decorator, and to define a static method, we use the @staticmethod
decorator. For example:
class MyClass:
my_class_variable = 42
def my_instance_method(self):
print("This is an instance method")
@classmethod
def my_class_method(cls):
print("This is a class method")
print("The value of my_class_variable is:", cls.my_class_variable)
@staticmethod
def my_static_method(arg1, arg2):
print("This is a static method")
print("The arguments are:", arg1, arg2)
Here, my_instance_method
is an instance method, my_class_method
is a class method, and my_static_method
is a static method. Note that the first argument to the class method is cls
(i.e., the class itself), while the first argument to the static method is not specified.
The init() Method
The __init__()
method is a special method in Python that is called when an object is created from a class. It is also known as a constructor method because it is used to initialize the attributes of an object.
The __init__()
method takes self
as its first parameter, followed by any additional parameters that you want to pass in. The self
parameter refers to the object that is being created.
Here is an example of a class with an __init__()
method:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
In this example, the Person
class has two instance variables, name
and age
, which are initialized in the __init__()
method.
To create an object from the Person
class and initialize its attributes, you would use code like this:
p = Person("John", 35)
Class Methods
In Python, a class method is a method that is bound to the class and not the instance of the class. It can be called on the class itself, rather than on an instance of the class.
To define a class method in Python, you need to use the @classmethod
decorator before the method definition. The first parameter of a class method is always the class itself, which is conventionally named cls
.
Here is an example of defining a class method in Python:
class MyClass:
x = 0
@classmethod
def set_x(cls, value):
cls.x = value
In this example, the set_x
method is a class method. It takes the class cls
as its first parameter, and sets the class variable x
to the given value. This method can be called on the class itself, rather than on an instance of the class, like this:
MyClass.set_x(42)
After this call, the x
class variable will have the value of 42
. Note that you can also call class methods on an instance of the class, in which case the instance will be passed as the first parameter (cls
), but this is less common.
Static Methods
In Python, a static method is a method that belongs to a class rather than an instance of that class. It can be called on the class itself, rather than on an object of the class.
To define a static method in a class, the @staticmethod
decorator is used before the method definition.
Here’s an example:
class MyClass:
x = 0
def __init__(self, y):
self.y = y
@staticmethod
def my_static_method(a, b):
return a + b
In the example above, my_static_method()
is a static method. It can be called using the class name, like this:
result = MyClass.my_static_method(1, 2)
Here, we are calling the my_static_method()
method on the MyClass
class, passing in two arguments, 1
and 2
. The method returns the sum of the two arguments, which is assigned to the result
variable.
Static methods are often used when a method does not require access to any instance variables or methods, and is only related to the class as a whole.
Overriding Methods
Overriding methods is a concept in Object-Oriented Programming (OOP) where a subclass provides its implementation of a method that is already defined in its superclass. When a method is called on an object of a subclass, the implementation of the method in the subclass is used instead of the one in the superclass.
To override a method in a subclass, you must define a method with the same name and signature (i.e., same parameters) as the method in the superclass. The method in the subclass must also have the same return type or a subtype of the return type of the method in the superclass.
Here’s an example of overriding a method in Python:
class Animal:
def make_sound(self):
print("The animal makes a sound.")
class Dog(Animal):
def make_sound(self):
print("The dog barks.")
animal = Animal()
animal.make_sound() # prints "The animal makes a sound."
dog = Dog()
dog.make_sound() # prints "The dog barks."
In this example, we define a superclass Animal
with a method make_sound()
, which simply prints “The animal makes a sound.” We then define a subclass Dog
that inherits from Animal
and overrides the make_sound()
method with its own implementation that prints “The dog barks.”
When we create an instance of the Animal
class and call its make_sound()
method, it prints “The animal makes a sound.” When we create an instance of the Dog
class and call its make_sound()
method, it prints “The dog barks.” The make_sound()
method is overridden in the Dog
subclass, so its implementation is used instead of the one in the Animal
superclass.
Class Naming Convention
In Python, the naming convention for classes is to use CamelCase, which is a naming convention where each word in a name is capitalized and concatenated together without any underscores. This is done to make the class name more readable and to differentiate it from variable names, which typically use underscores to separate words.
For example, consider a class that represents a car. A suitable name for the class would be Car
in CamelCase notation, while a variable that represents a car object could be named my_car
or the_car
using underscores to separate the words.
It is important to follow naming conventions to ensure that code is easily readable and maintainable.
Object Properties
In Python, object properties are also known as instance variables. These variables are specific to an instance of a class, meaning that each object or instance of a class can have its own set of properties.
Instance variables can be defined within a class by assigning a value to a variable name using the self
keyword. The self
keyword refers to the instance of the class and is used to access its attributes and methods.
Here’s an example of defining instance variables in a class:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
In this example, the Person
class has two instance variables, name
and age
, which are defined in the __init__()
method using the self
keyword. These instance variables can be accessed and modified by creating an object of the Person
class and using dot notation:
person1 = Person("Alice", 25)
print(person1.name) # Output: Alice
person1.age = 26
print(person1.age) # Output: 26
Modify Object Properties
To modify an object’s property in Python, you can simply access the property using the dot notation and assign a new value to it. Here’s an example:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
my_car = Car("Toyota", "Corolla", 2020)
print(my_car.make) # output: Toyota
my_car.make = "Honda"
print(my_car.make) # output: Honda
In this example, we define a Car
class with make
, model
, and year
properties. We create an instance of the Car
class and assign it to the variable my_car
. We then print the value of the make
property using my_car.make
.
We then modify the value of the make
property by assigning a new value to it using the dot notation: my_car.make = "Honda"
. Finally, we print the new value of the make
property using print(my_car.make)
. The output of the program will be Honda
.
Delete object properties
To delete an object property in Python, you can use the del
statement followed by the object name and the property name. Here is an example:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person = Person("John", 25)
print(person.name) # Output: John
del person.age
print(person.age) # Raises AttributeError: 'Person' object has no attribute 'age'
In this example, we define a Person
class with a name
and age
property. We create an instance of the Person
class and assign it to the person
variable. We then print the name
property of the person
object, which outputs “John”.
Next, we use the del
statement to delete the age
property of the person
object. When we try to print the age
property of the person
object again, we get an AttributeError
because the property no longer exists.
Delete Objects
To delete an object in Python, you can use the del
keyword followed by the object name.
Here’s an example:
my_object = MyClass()
# do something with my_object
# delete the object
del my_object
After this, the my_object
variable will no longer exist and the memory allocated to it will be freed up by Python’s garbage collector.
😉Inheritance
inheritance is a fundamental concept in object-oriented programming (OOP) that allows a class to inherit the properties and methods of another class. In Python, inheritance enables us to create a new class (child class) that is a modified or specialized version of an existing class (parent class).
The child class can access all the attributes and methods of the parent class, and it can also override or extend those methods to add new functionality. This makes inheritance a powerful mechanism for code reuse and allows us to build complex applications with ease.
For example, imagine we have a class called Animal
that defines basic attributes and methods for all animals, such as name
, age
, and eat()
. We can create a child class called Dog
that inherits from Animal
and adds its own unique attributes and methods, such as breed
and bark()
.
Inheritance in Python is implemented using the keyword class
, followed by the child class name and the name of the parent class in parentheses. Here’s an example:
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def eat(self):
print(f"{self.name} is eating.")
class Dog(Animal):
def __init__(self, name, age, breed):
super().__init__(name, age)
self.breed = breed
def bark(self):
print("Woof!")
In this example, the Dog
class inherits from the Animal
class and adds a new attribute breed
and a new method bark()
. The super()
function is used to call the constructor of the parent class and initialize its attributes.
Types of Inheritance
In Python, there are several types of inheritance that you can use to create a child class. The most common types of inheritance are:
- Single inheritance: In single inheritance, a child class inherits from a single parent class. This is the simplest and most common type of inheritance.
- Multiple inheritance: In multiple inheritance, a child class inherits from two or more parent classes. This allows you to combine the features of multiple classes into a single class.
- Multi-level inheritance: In multi-level inheritance, a child class inherits from a parent class, which in turn inherits from another parent class. This creates a hierarchical structure of classes.
- Hierarchical inheritance: In hierarchical inheritance, multiple child classes inherit from a single parent class. This allows you to create a set of related classes with a common base class.
- Hybrid inheritance: Hybrid inheritance is a combination of two or more types of inheritance. For example, you can use multiple inheritance along with multi-level inheritance to create a complex class hierarchy.
Each type of inheritance has its own advantages and disadvantages, and the choice of which type to use depends on the specific requirements of your application.
Inheritance Syntax
In Python, inheritance is implemented using the class
keyword, followed by the name of the child class and the name of the parent class in parentheses. Here’s the syntax for creating a child class that inherits from a parent class:
class ParentClass:
# Parent class attributes and methods
class ChildClass(ParentClass):
# Child class attributes and methods
In this syntax, the ParentClass
is the name of the parent class that the ChildClass
is inheriting from. All the attributes and methods of the parent class are automatically inherited by the child class.
To access the parent class attributes and methods from the child class, you can use the super()
function. The super()
function returns a temporary object of the superclass, which allows you to call its methods.
Here’s an example of inheritance in Python:
class Animal:
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name} is eating.")
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
def bark(self):
print("Woof!")
In this example, the Dog
class is inheriting from the Animal
class. The super()
function is used to call the constructor of the parent class and initialize its attributes.
Example : Python Inheritance
Here’s an example of how to use inheritance in Python to create a child class that inherits from a parent class:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
print(f"Hi, my name is {self.name} and I'm {self.age} years old.")
class Student(Person):
def __init__(self, name, age, major):
super().__init__(name, age)
self.major = major
def introduce(self):
super().introduce()
print(f"I'm studying {self.major} at the university.")
In this example, the Person
class defines an __init__()
method that initializes the name
and age
attributes and a introduce()
method that introduces the person.
The Student
class is a child class that inherits from the Person
class. It adds a new attribute major
and overrides the introduce()
method to include information about the major.
The super()
function is used to call the constructor of the parent class and initialize its attributes. It is also used to call the introduce()
method of the parent class from within the introduce()
method of the child class.
Here’s an example of how to use these classes:
person = Person("John", 25)
person.introduce()
student = Student("Jane", 20, "Computer Science")
student.introduce()
Output:
Hi, my name is John and I'm 25 years old.
Hi, my name is Jane and I'm 20 years old.
I'm studying Computer Science at the university.
As you can see, the Student
class inherits the introduce()
method from the Person
class and adds its own functionality to it. This is an example of how inheritance can be used to reuse code and build more complex classes from simpler ones.
Accessing Parent Class from Child Class
In Python, you can access the attributes and methods of the parent class from a child class using the super()
function. The super()
function returns a temporary object of the parent class, which allows you to call its methods and access its attributes.
Here’s an example of how to access the parent class from a child class:
class Parent:
def __init__(self, name):
self.name = name
def greeting(self):
print(f"Hello, my name is {self.name}.")
class Child(Parent):
def __init__(self, name, age):
super().__init__(name)
self.age = age
def greeting(self):
super().greeting()
print(f"I'm {self.age} years old.")
In this example, the Child
class is inheriting from the Parent
class. It overrides the greeting()
method to include the age of the child.
The super()
function is used to call the greeting()
method of the parent class and include its output in the child class’s greeting. This allows the child class to reuse the code of the parent class while adding its own functionality.
Here’s an example of how to use these classes:
parent = Parent("John")
parent.greeting()
child = Child("Jane", 10)
child.greeting()
Output:
Hello, my name is John.
Hello, my name is Jane.
I'm 10 years old.
As you can see, the Child
class is able to access the name
attribute of the parent class and call its greeting()
method using the super()
function. This allows it to reuse the code of the parent class while adding its own functionality.
Overriding Methods in Child Class
In Python, when a child class inherits from a parent class, it can override the parent class’s methods with its own implementation. This is called method overriding.
To override a method in the child class, you simply define a method with the same name in the child class. When the method is called on an instance of the child class, the child class’s implementation of the method will be used instead of the parent class’s implementation.
Here’s an example of method overriding in Python:
class Parent:
def say_hello(self):
print("Hello from Parent class!")
class Child(Parent):
def say_hello(self):
print("Hello from Child class!")
parent = Parent()
parent.say_hello() # Output: Hello from Parent class!
child = Child()
child.say_hello() # Output: Hello from Child class!
In this example, the Child
class is inheriting from the Parent
class and overriding its say_hello()
method with its own implementation. When the say_hello()
method is called on an instance of the Child
class, the child class’s implementation is used instead of the parent class’s implementation.
Note that when you override a method in the child class, the parent class’s implementation is still available by using the super()
function. Here’s an example:
class Parent:
def say_hello(self):
print("Hello from Parent class!")
class Child(Parent):
def say_hello(self):
super().say_hello()
print("Hello from Child class!")
child = Child()
child.say_hello() # Output: Hello from Parent class! Hello from Child class!
In this example, the Child
class is using the super()
function to call the say_hello()
method of the parent class, and then adding its own implementation. This allows the child class to reuse the code of the parent class while adding its own functionality.
Using super() function in Child Class
In Python, the super()
function is used to call a method from the parent class in a child class. It is often used in conjunction with method overriding to reuse code from the parent class while adding additional functionality in the child class.
Here’s an example of using the super()
function in a child class:
class Parent:
def __init__(self, name):
self.name = name
def say_hello(self):
print(f"Hello, my name is {self.name}.")
class Child(Parent):
def __init__(self, name, age):
super().__init__(name)
self.age = age
def say_hello(self):
super().say_hello()
print(f"I'm {self.age} years old.")
child = Child("Alice", 10)
child.say_hello() # Output: Hello, my name is Alice. I'm 10 years old.
In this example, the Child
class is inheriting from the Parent
class and overriding its say_hello()
method with its own implementation. The super()
function is used to call the say_hello()
method of the parent class and then add the child class’s implementation. This allows the child class to reuse the code of the parent class while adding its own functionality.
Note that in the __init__()
method of the Child
class, the super()
function is also used to call the __init__()
method of the parent class. This ensures that the parent class’s __init__()
method is called before the child class’s __init__()
method, and that the name
attribute is properly initialized.
Multiple Inheritance
Multiple inheritance is a feature of object-oriented programming where a subclass can inherit from multiple parent classes. In Python, you can achieve multiple inheritance by specifying multiple parent classes in the class definition separated by commas.
Here’s an example of multiple inheritance in Python:
class Parent1:
def method1(self):
print("Method 1 from Parent 1")
class Parent2:
def method2(self):
print("Method 2 from Parent 2")
class Child(Parent1, Parent2):
def method3(self):
print("Method 3 from Child")
child = Child()
child.method1() # Output: Method 1 from Parent 1
child.method2() # Output: Method 2 from Parent 2
child.method3() # Output: Method 3 from Child
In this example, the Child
class is inheriting from both the Parent1
and Parent2
classes. It can access the methods of both parent classes using dot notation.
When a class inherits from multiple parent classes, it can override methods from both parent classes. If both parent classes have a method with the same name, the method in the first parent class listed in the inheritance statement will be called. If the child class needs to call a specific method from a specific parent class, it can do so using the super()
function along with the parent class’s name.
Here’s an example of using the super()
function to call a method from a specific parent class in a multiple inheritance scenario:
class Parent1:
def method(self):
print("Method from Parent 1")
class Parent2:
def method(self):
print("Method from Parent 2")
class Child(Parent1, Parent2):
def method(self):
super(Parent1, self).method()
child = Child()
child.method() # Output: Method from Parent 1
In this example, the Child
class is inheriting from both the Parent1
and Parent2
classes, both of which have a method named method()
. The super()
function is used to call the method()
of Parent1
, so the output will be “Method from Parent 1”.
Method Resolution Order (MRO)
Method Resolution Order (MRO) is the order in which Python looks for methods in a class hierarchy. When a method is called on an instance of a class, Python first looks for the method in the instance’s class. If the method is not found in the instance’s class, Python looks for the method in the class’s parent classes, following the MRO.
In Python, the MRO is determined using the C3 linearization algorithm, which is a special algorithm designed to create a linear order of inheritance hierarchy.
You can use the mro()
method to get the MRO for a class. The mro()
method returns a tuple that contains the class itself followed by the classes it inherits from, in the order that they will be searched for methods.
Here’s an example of using the mro()
method to get the MRO for a class:
class Parent1:
pass
class Parent2:
pass
class Child(Parent1, Parent2):
pass
print(Child.mro()) # Output: [<class '__main__.Child'>, <class '__main__.Parent1'>, <class '__main__.Parent2'>, <class 'object'>]
In this example, the Child
class is inheriting from both the Parent1
and Parent2
classes. The mro()
method is used to get the MRO for the Child
class, which is a tuple that contains the Child
class itself followed by the Parent1
class, the Parent2
class, and finally the built-in object
class.
Understanding the MRO is important when dealing with multiple inheritance, as it determines the order in which Python will search for methods in the class hierarchy.
Diamond Problem in Multiple Inheritance
The diamond problem is a common issue that arises in multiple inheritance when two parent classes of a child class inherit from the same base class. This results in the diamond shape of the class hierarchy, where the child class is at the bottom of the diamond and there are two paths to the base class.
Consider the following example:
class A:
def method(self):
print("Method from A")
class B(A):
pass
class C(A):
def method(self):
print("Method from C")
class D(B, C):
pass
d = D()
d.method() # Output: Method from C
In this example, the D
class inherits from both B
and C
, which in turn both inherit from the A
class. Both B
and C
have a method named method()
, and when D
calls the method()
method, Python follows the MRO to look for the method in the class hierarchy. The MRO for D
is [D, B, C, A, object]
, which means that Python will first look for the method()
method in the B
class, then the C
class, then the A
class, and finally the object
class.
In this case, Python finds the method()
method in the C
class first, and so it is called. This means that the method()
method of A
is never called, which can cause unexpected behavior if C
is intended to override A
.
To avoid the diamond problem, it’s important to carefully design class hierarchies and avoid multiple inheritance where possible. When multiple inheritance is necessary, it’s important to understand the MRO and the order in which methods will be searched for in the class hierarchy. You can also use the super()
function to explicitly call methods in parent classes and avoid unintended method overrides.
😘Polymorphism
Polymorphism is a core concept in object-oriented programming (OOP) that refers to the ability of different objects to be used interchangeably, even if they have different implementations. In other words, polymorphism allows objects of different classes to be treated as if they were objects of the same class, as long as they share a common interface or parent class.
Polymorphism can be implemented in different ways, such as through inheritance, duck typing, or operator overloading. In inheritance-based polymorphism, subclasses inherit methods and attributes from their parent class, and can override or extend them as needed. In duck typing-based polymorphism, objects are evaluated based on their behavior rather than their class or type. And in operator overloading-based polymorphism, objects can define their own behavior for built-in operators like +, -, *, and /.
Polymorphism is a powerful and flexible feature of OOP that allows for more modular, reusable, and extensible code. It can simplify code by abstracting away implementation details and allowing different objects to be used interchangeably, which can improve maintainability and flexibility.
Types of Polymorphism
There are several types of polymorphism in object-oriented programming, including:
- Inheritance-based Polymorphism: Inheritance allows subclasses to inherit methods and attributes from their parent class, and override or extend them as needed. This allows objects of different classes to be treated as if they were objects of the same class, as long as they share a common interface or parent class.
- Duck Typing-based Polymorphism: Duck typing is a programming concept that evaluates objects based on their behavior rather than their class or type. This means that any object that has the necessary methods or attributes can be used interchangeably, even if it’s not of the same class or type.
- Operator Overloading-based Polymorphism: Operator overloading allows objects to define their own behavior for built-in operators like +, -, *, and /, which allows objects of different classes to be used interchangeably for arithmetic operations.
- Interface-based Polymorphism: Interfaces define a set of methods or attributes that a class must implement, without specifying how they should be implemented. This allows objects of different classes to be used interchangeably as long as they implement the same interface.
- Parametric Polymorphism: Parametric polymorphism allows functions or classes to be written in a generic way, without specifying a specific type. This allows the same code to be used for different types of objects, as long as they meet certain requirements.
Each type of polymorphism has its own strengths and weaknesses, and the choice of which one to use depends on the specific requirements of the program.
Inheritance-based Polymorphism
Inheritance-based polymorphism is a type of polymorphism that allows objects of different classes to be used interchangeably as long as they share a common parent class or interface. Inheritance is a mechanism in object-oriented programming that allows one class to inherit properties and methods from another class, which can then be overridden or extended as needed.
Here’s an example of how inheritance-based polymorphism works in Python
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclass must implement abstract method")
class Dog(Animal):
def speak(self):
return "Woof"
class Cat(Animal):
def speak(self):
return "Meow"
class Bird(Animal):
def speak(self):
return "Tweet"
animals = [Dog("Fido"), Cat("Mittens"), Bird("Tweety")]
for animal in animals:
print(animal.name + " says " + animal.speak())
In this example, we have a parent class called Animal
that defines an abstract method called speak()
, which raises a NotImplementedError
exception. This method must be implemented by any subclasses that inherit from Animal
.
We then define three subclasses of Animal
: Dog
, Cat
, and Bird
, each of which provides its own implementation of speak()
.
Finally, we create a list of Animal
objects, which includes instances of Dog
, Cat
, and Bird
. We can then loop over the list and call the speak()
method on each object, which will call the appropriate implementation of speak()
based on the object’s class.
This example demonstrates how inheritance-based polymorphism allows us to treat objects of different classes as if they were objects of the same class, as long as they share a common parent class or interface. In this case, all of the Animal
subclasses share the speak()
method defined in the Animal
class, which allows us to call speak()
on any Animal
object without knowing its specific subclass.
a. Method Overriding
Method overriding is a form of inheritance-based polymorphism that allows a subclass to provide a different implementation of a method that is already defined in its parent class. When a method is overridden in a subclass, the subclass version of the method is used instead of the parent class version when the method is called on an object of the subclass.
Here’s an example of method overriding in Python:
class Animal:
def make_sound(self):
print("The animal makes a sound")
class Dog(Animal):
def make_sound(self):
print("The dog barks")
class Cat(Animal):
def make_sound(self):
print("The cat meows")
a = Animal()
a.make_sound()
d = Dog()
d.make_sound()
c = Cat()
c.make_sound()
In this example, we have a parent class called Animal
with a method called make_sound()
, which prints a generic message. We then define two subclasses of Animal
: Dog
and Cat
, each of which overrides the make_sound()
method with its own implementation.
When we create an instance of Animal
and call make_sound()
, the parent class implementation of the method is used, which prints “The animal makes a sound”. However, when we create instances of Dog
and Cat
and call make_sound()
, the overridden implementation of the method in the appropriate subclass is used instead.
This example demonstrates how method overriding allows a subclass to provide a different implementation of a method that is already defined in its parent class. When the method is called on an object of the subclass, the overridden version of the method is used instead of the parent class version.
b. Polymorphic Methods
Polymorphic methods are methods that can take objects of different classes and behave differently depending on the type of the object that is passed in. Polymorphic methods are a form of inheritance-based polymorphism and are often used in combination with method overriding.
Here’s an example of a polymorphic method in Python:
class Animal:
def __init__(self, name):
self.name = name
def make_sound(self):
print("The animal makes a sound")
class Dog(Animal):
def make_sound(self):
print("The dog barks")
class Cat(Animal):
def make_sound(self):
print("The cat meows")
def make_animal_sound(animal):
animal.make_sound()
a = Animal("Generic animal")
d = Dog("Fido")
c = Cat("Whiskers")
make_animal_sound(a) # prints "The animal makes a sound"
make_animal_sound(d) # prints "The dog barks"
make_animal_sound(c) # prints "The cat meows"
In this example, we define a polymorphic method called make_animal_sound()
, which takes an object of type Animal
or one of its subclasses and calls the make_sound()
method on it. Because the make_sound()
method is overridden in the Dog
and Cat
subclasses, the appropriate implementation of the method is called depending on the type of the object that is passed in.
When we call make_animal_sound()
with an instance of Animal
, the parent class implementation of make_sound()
is called, which prints “The animal makes a sound”. When we call make_animal_sound()
with an instance of Dog
, the Dog
subclass implementation of make_sound()
is called, which prints “The dog barks”. Similarly, when we call make_animal_sound()
with an instance of Cat
, the Cat
subclass implementation of make_sound()
is called, which prints “The cat meows”.
This example demonstrates how polymorphic methods allow us to write code that can accept objects of different classes and behave differently depending on the type of the object that is passed in. Polymorphic methods are a powerful tool in object-oriented programming that can help make our code more flexible and reusable.
c. Abstract Base Classes
Abstract Base Classes (ABCs) are a form of inheritance-based polymorphism that allow us to define abstract methods in a parent class that must be implemented in its subclasses. ABCs are defined using the abc
module in Python and are useful when we want to enforce certain behaviors or properties in subclasses.
Here’s an example of an ABC in Python:
import abc
class Shape(abc.ABC):
@abc.abstractmethod
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
r = Rectangle(5, 10)
print(r.area()) # prints 50
c = Circle(3)
print(c.area()) # prints 28.26
In this example, we define an abstract class called Shape
using the abc
module. The Shape
class has one abstract method called area()
, which must be implemented in any subclasses of Shape
. We then define two subclasses of Shape
: Rectangle
and Circle
, both of which implement the area()
method with their own calculations.
When we create an instance of Rectangle
or Circle
and call the area()
method, the appropriate implementation of the method in the subclass is called. Because Rectangle
and Circle
both inherit from Shape
, they must implement the area()
method, ensuring that any instances of these subclasses will have a area()
method defined.
This example demonstrates how ABCs allow us to define abstract methods in a parent class that must be implemented in its subclasses. ABCs are useful when we want to enforce certain behaviors or properties in subclasses, ensuring that any instances of the subclasses will have the required methods or attributes.
Duck Typing-based Polymorphism
Duck typing-based polymorphism is a form of polymorphism in Python that is based on the principle of “if it walks like a duck and quacks like a duck, then it must be a duck”. This means that if an object has the necessary attributes or methods that are expected by a function or method, then that object can be used in place of another object that has the same attributes or methods.
Here’s an example of duck typing-based polymorphism in Python:
class Car:
def drive(self):
print("The car is driving")
class Bicycle:
def ride(self):
print("The bicycle is riding")
def take_vehicle(vehicle):
vehicle.drive()
car = Car()
bike = Bicycle()
take_vehicle(car) # prints "The car is driving"
take_vehicle(bike) # raises AttributeError: 'Bicycle' object has no attribute 'drive'
In this example, we define two classes: Car
and Bicycle
. We then define a function called take_vehicle()
that takes a vehicle
object and calls its drive()
method. When we call take_vehicle()
with an instance of Car
, the drive()
method of the Car
object is called and the message “The car is driving” is printed. However, when we call take_vehicle()
with an instance of Bicycle
, an AttributeError
is raised because Bicycle
objects don’t have a drive()
method.
This example demonstrates how duck typing-based polymorphism works in Python. The take_vehicle()
function doesn’t care about the specific type of the vehicle
object that is passed in, as long as it has a drive()
method. This allows us to write code that is more flexible and doesn’t rely on specific class hierarchies or inheritance relationships.
Duck typing-based polymorphism is often used in Python because it allows for more dynamic and flexible code. However, it’s important to be aware of the potential for errors or unexpected behavior when using duck typing, as it relies on objects having the expected attributes and methods, and doesn’t enforce any strict type checking.
Operator Overloading-based Polymorphism
Operator overloading-based polymorphism is a form of polymorphism in Python that allows us to define how operators (such as +
, -
, *
, /
, etc.) behave for objects of our own custom classes. This allows us to write code that uses the same operators for different types of objects, as long as those objects have the necessary methods defined.
Here’s an example of operator overloading-based polymorphism in Python:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __str__(self):
return f"({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v1 + v2
print(v3) # prints "(4, 6)"
v4 = v2 - v1
print(v4) # prints "(2, 2)"
v5 = v1 * 2
print(v5) # prints "(2, 4)"
In this example, we define a class called Vector
that represents a 2D vector. We overload the +
, -
, and *
operators using special methods (__add__()
, __sub__()
, and __mul__()
, respectively) to define how these operators should behave for Vector
objects. We also define a __str__()
method to allow us to print Vector
objects in a more human-readable format.
When we create instances of Vector
and use the overloaded operators, the appropriate implementation of the operator is called based on the types of the operands. This allows us to write code that uses the same operators for different types of objects, as long as those objects have the necessary methods defined.
Operator overloading-based polymorphism is a powerful feature of Python that allows us to write code that is more concise and expressive. However, it’s important to use operator overloading judiciously and to make sure that the behavior of overloaded operators is consistent with what users would expect.
Polymorphism with Multiple Inheritance
Polymorphism with multiple inheritance is a concept in object-oriented programming that allows a class to inherit from multiple parent classes and exhibit polymorphic behavior based on the methods defined in each of its parent classes. This means that the same method name can have different implementations in different parent classes, and the subclass can choose which implementation to use based on the context in which the method is called.
Let’s take an example to illustrate this concept:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
class DogCat(Dog, Cat):
pass
d = DogCat("Fido")
print(d.speak()) # prints "Woof!"
In this example, we define three classes: Animal
, Dog
, and Cat
. Dog
and Cat
are both subclasses of Animal
, and they both define a speak()
method with different implementations. We also define a third class called DogCat
, which inherits from both Dog
and Cat
.
When we create an instance of DogCat
and call its speak()
method, Python looks for the speak()
method in Dog
first, since it is the first parent class listed in the definition of DogCat
. Since Dog
defines a speak()
method, that implementation is used, and the method returns “Woof!”.
If we were to reverse the order of the parent classes in the definition of DogCat
, so that Cat
comes first, then calling speak()
on an instance of DogCat
would return “Meow!” instead of “Woof!”.
Polymorphism with multiple inheritance can be a powerful tool for creating complex object hierarchies and building code that is flexible and extensible. However, it’s important to use multiple inheritance carefully, as it can also make code harder to read and understand if not used judiciously.
Function Polymorphism in Python
Function polymorphism in Python is the ability of a function to accept arguments of different types and behave differently based on the type of argument passed. This is similar to polymorphism in object-oriented programming, but instead of classes and objects, we are working with functions and arguments.
One common example of function polymorphism in Python is the built-in len()
function. The len()
function can accept different types of arguments, including strings, lists, tuples, and dictionaries, and it will return the length of the argument passed based on its type.
Here’s an example of using the len()
function with different types of arguments:
s = "Hello, world!"
l = [1, 2, 3, 4, 5]
t = (6, 7, 8, 9, 10)
d = {"a": 1, "b": 2, "c": 3}
print(len(s)) # prints 13
print(len(l)) # prints 5
print(len(t)) # prints 5
print(len(d)) # prints 3
In this example, we pass different types of arguments to the len()
function, and it behaves differently based on the type of argument passed. When we pass a string, len()
returns the number of characters in the string. When we pass a list or tuple, len()
returns the number of elements in the container. When we pass a dictionary, len()
returns the number of key-value pairs in the dictionary.
We can also create our own functions that exhibit polymorphic behavior based on the types of their arguments. For example, we can define a function called add()
that adds two numbers or concatenates two strings, depending on the types of the arguments passed:
def add(x, y):
if type(x) == type(y):
return x + y
else:
return str(x) + str(y)
print(add(1, 2)) # prints 3
print(add("hello", "world")) # prints "helloworld"
print(add(3, "dogs")) # prints "3dogs"
In this example, the add()
function checks the types of the arguments passed and behaves differently based on whether they are the same type or not. If the arguments are the same type, the function adds them together. If they are different types, the function converts them to strings and concatenates them.
Function polymorphism can make our code more flexible and adaptable to different types of data, and it can also make our code more concise and easier to read.
Class Polymorphism in Python
Class polymorphism in Python is the ability of a class to take on different forms or behaviors depending on its context. This is achieved through inheritance, method overriding, and the use of abstract classes and interfaces.
Here’s an example of class polymorphism in Python using inheritance:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
class Bird(Animal):
def speak(self):
return "Chirp!"
animals = [Dog("Rufus"), Cat("Whiskers"), Bird("Tweety")]
for animal in animals:
print(animal.name + ": " + animal.speak())
In this example, we define a base class called Animal
with an abstract method called speak()
. We then define three subclasses (Dog
, Cat
, and Bird
) that inherit from the Animal
class and implement the speak()
method in their own way. Finally, we create a list of Animal
objects containing instances of each subclass, and we call the speak()
method on each object.
The result of running this code will be:
Rufus: Woof!
Whiskers: Meow!
Tweety: Chirp!
As we can see, each Animal
object behaves differently based on its type. This is an example of class polymorphism.
We can also achieve class polymorphism in Python through method overriding. Method overriding is the process of redefining a method in a subclass that already exists in its parent class. Here’s an example:
class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
shapes = [Rectangle(5, 10), Circle(7)]
for shape in shapes:
print(shape.area())
In this example, we define a base class called Shape
with an abstract method called area()
. We then define two subclasses (Rectangle
and Circle
) that inherit from the Shape
class and override the area()
method to calculate the area of a rectangle or a circle, respectively. Finally, we create a list of Shape
objects containing instances of each subclass, and we call the area()
method on each object.
The result of running this code will be:
50
153.86
As we can see, each Shape
object behaves differently based on its type. This is another example of class polymorphism achieved through method overriding.
Polymorphism and Inheritance
Polymorphism and inheritance are closely related concepts in object-oriented programming. Inheritance allows a subclass to inherit properties and methods from its superclass, while polymorphism allows objects of different classes to be treated as if they were of the same class.
Inheritance is the process by which a class can inherit properties and methods from its parent class. When a class inherits from another class, it automatically gets all the methods and properties of the parent class. This allows us to create new classes that are based on existing classes and to reuse code that has already been written. Inheritance is a key feature of object-oriented programming, and it enables us to create a hierarchy of related classes.
Polymorphism, on the other hand, is the ability of objects of different classes to be treated as if they were of the same class. This means that we can write code that works with objects of a certain type, and that same code will work with objects of other types as long as they have the same interface. Polymorphism allows us to write more generic code that can work with a wider range of objects, and it makes our code more flexible and adaptable.
One of the ways in which polymorphism is achieved in object-oriented programming is through inheritance. By inheriting from a parent class, a subclass can use the methods and properties of the parent class, and it can also add its own methods and properties. This means that an object of the subclass can be treated as if it were an object of the parent class, which enables polymorphic behavior.
For example, consider the following code:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
class Bird(Animal):
def speak(self):
return "Chirp!"
def animal_speak(animal):
print(animal.name + ": " + animal.speak())
dog = Dog("Rufus")
cat = Cat("Whiskers")
bird = Bird("Tweety")
animal_speak(dog)
animal_speak(cat)
animal_speak(bird)
In this example, we define a base class called Animal
with an abstract method called speak()
. We then define three subclasses (Dog
, Cat
, and Bird
) that inherit from the Animal
class and implement the speak()
method in their own way. Finally, we define a function called animal_speak()
that takes an Animal
object as an argument and calls its speak()
method.
When we create objects of the Dog
, Cat
, and Bird
classes, we can pass them to the animal_speak()
function, which treats them as if they were Animal
objects. This is possible because the Dog
, Cat
, and Bird
classes inherit from the Animal
class and implement the speak()
method in a polymorphic way. This enables us to write more generic code that works with a wider range of objects.
😽Encapsulation
Encapsulation is one of the fundamental concepts of object-oriented programming (OOP). It refers to the practice of bundling data and the methods that operate on that data, within a single unit or entity, called a class. Encapsulation allows us to hide the implementation details of a class from the user, by providing a well-defined interface for accessing and modifying its data.
The main idea behind encapsulation is to protect the internal state of an object from being modified by external code. This is achieved by making the internal data members of a class private, so that they can only be accessed or modified through public methods provided by the class. These public methods are known as getters and setters, and they allow the user to access and modify the internal state of the object in a controlled manner.
Encapsulation is important for several reasons:
- Data hiding: Encapsulation allows us to hide the implementation details of a class from the user, which reduces the complexity of the code and makes it easier to maintain and modify.
- Security: By controlling the access to the internal data members of a class, we can ensure that the data is not modified in unexpected ways, which can help prevent security vulnerabilities.
- Reusability: Encapsulation promotes code reusability, as classes can be used as building blocks for other classes and applications.
- Polymorphism: Encapsulation is a key factor in achieving polymorphism, which is the ability of objects to take on different forms or behaviors depending on their context.
Access Modifiers in Python
Python doesn’t have access modifiers like other object-oriented programming languages such as Java or C++. In Python, there are no keywords such as private
, public
, or protected
to restrict access to class attributes and methods.
However, there are conventions that are used to indicate the accessibility of an attribute or method:
_attribute
: A single underscore prefix is used to indicate that an attribute or method is intended for internal use only, and should not be accessed from outside the class. However, there is no actual restriction on accessing these attributes or methods.__attribute
: A double underscore prefix invokes name mangling, which changes the name of the attribute to_classname__attribute
. This is used to avoid naming conflicts between attributes in different classes that have the same name. This also prevents accidental modification of attributes that are intended to be used only within the class.__method
: A double underscore prefix is used to define a private method, which can only be accessed within the class. Like with double underscore attributes, name mangling is used to change the name of the method to_classname__method
.
It’s important to note that these conventions are not enforced by the Python language itself, and can be bypassed if necessary. The conventions are simply a way to indicate the intended use and accessibility of class attributes and methods.
Here are some examples of access modifiers in Python:
- Public Access Modifier: Public access modifier is the default access modifier in Python. In this modifier, all the members of the class are accessible from anywhere within the program. Here is an example:
class MyClass:
def __init__(self, x, y):
self.x = x
self.y = y
myObj = MyClass(5, 10)
print(myObj.x) # Output: 5
- Private Access Modifier: Private access modifier in Python is denoted by double underscores (__). Members of the class with this modifier are only accessible within the class. Here is an example:
class MyClass:
def __init__(self, x, y):
self.__x = x
self.__y = y
def display(self):
print("x:", self.__x)
print("y:", self.__y)
myObj = MyClass(5, 10)
myObj.display() # Output: x: 5, y: 10
print(myObj.__x) # Error: 'MyClass' object has no attribute '__x'
- Protected Access Modifier: Protected access modifier in Python is denoted by single underscore (_). Members of the class with this modifier can be accessed within the class and its subclasses. Here is an example:
class MyClass:
def __init__(self, x, y):
self._x = x
self._y = y
class MySubClass(MyClass):
def display(self):
print("x:", self._x)
print("y:", self._y)
myObj = MySubClass(5, 10)
myObj.display() # Output: x: 5, y: 10
Note that these are just examples, and access modifiers in Python do not work exactly the same way as they do in other programming languages. In Python, there is no true way to enforce private or protected access, and it is up to the programmer to follow naming conventions and best practices.
Public Access Modifier
Public access modifier is the default access modifier in Python. All class members (variables and methods) are public by default, which means they can be accessed from outside the class. Public members can be accessed using the dot (.) operator on an object of the class.
Here’s an example of a class with a public member variable and method:
class MyClass:
# Public member variable
name = "Rocky"
# Public member method
def greet(self):
print(f"Hello, my name is {self.name}.")
# Create an object of the class
obj = MyClass()
# Access the public member variable
print(obj.name)
# Call the public member method
obj.greet()
Output:
Rocky
Hello, my name is Rocky.
Protected Access Modifier
In Python, the protected access modifier is achieved by using a single underscore (_) before the attribute or method name. The attribute or method marked as protected can be accessed from within the class and its subclasses.
Here’s an example:
class Car:
def __init__(self):
self._color = "red"
class SportsCar(Car):
def __init__(self):
super().__init__()
print("Sports car created")
print("Color of sports car:", self._color)
car = Car()
print("Color of car:", car._color)
sports_car = SportsCar()
In the above example, _color
is a protected attribute of the Car
class. The SportsCar
class is a subclass of the Car
class and it can access the _color
attribute using self._color
.
We can also access the _color
attribute outside the class using the instance of the class as shown in the last two lines of the code. However, it’s important to note that accessing a protected attribute outside the class is not recommended as it goes against the principles of encapsulation.
Encapsulation Example
Here is an example of how encapsulation can be used in Python:
class BankAccount:
def __init__(self, account_number, balance):
self.__account_number = account_number
self.__balance = balance
def get_account_number(self):
return self.__account_number
def set_balance(self, balance):
self.__balance = balance
def get_balance(self):
return self.__balance
account = BankAccount("12345", 5000)
print("Account number:", account.get_account_number())
print("Initial balance:", account.get_balance())
account.set_balance(10000)
print("New balance:", account.get_balance())
In this example, we have defined a BankAccount
class with two instance variables __account_number
and __balance
. We have used the double underscore before the variable names to make them private, which means they cannot be accessed from outside the class.
We have also defined two methods get_account_number
and set_balance
to get and set the values of the private variables respectively. The get_account_number
method returns the value of the __account_number
variable, while the set_balance
method allows us to update the value of the __balance
variable.
In the main program, we create an object of the BankAccount
class with an account number of “12345” and an initial balance of 5000. We then call the get_account_number
and get_balance
methods to retrieve the values of the private variables. We also call the set_balance
method to update the value of the __balance
variable to 10000, and then call the get_balance
method again to confirm that the value has been updated.
By encapsulating the __account_number
and __balance
variables and providing methods to access and update them, we can control how the object’s data is accessed and modified from outside the class, which can help to prevent errors and ensure the integrity of the data.
😅Abstraction
Abstraction is one of the fundamental concepts of object-oriented programming (OOP). It is the process of hiding the implementation details of an object and only showing its essential features to the outside world. Abstraction allows you to focus on what an object does rather than how it does it.
In Python, abstraction can be achieved through the use of abstract classes and interfaces. An abstract class is a class that cannot be instantiated and can only be used as a base class for other classes. An interface is a collection of abstract methods that define the behavior of an object, but do not provide any implementation.
The goal of abstraction is to reduce complexity by breaking down a system into smaller, more manageable parts, and to simplify the interface between those parts. By hiding implementation details, abstraction allows you to change the implementation without affecting the rest of the system.
Abstract Classes and Interfaces
In Python, abstraction can be achieved through abstract classes and interfaces.
a. Abstract Classes
An abstract class is a class that cannot be instantiated on its own and is meant to be subclassed by other classes. It serves as a blueprint for the subclasses to follow. An abstract class contains abstract methods that the subclasses must implement.
In Python, an abstract class can be defined using the abc
module. The abc
module provides the ABC
class that can be used as a metaclass for defining abstract classes. An abstract method is defined using the @abstractmethod
decorator.
Here is an example of an abstract class in Python:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
In this example, the Shape
class is an abstract class that defines two abstract methods, area()
and perimeter()
. Any subclass of Shape
must implement these two methods. If a subclass of Shape
fails to implement these methods, a TypeError
will be raised at runtime.
To create a subclass of an abstract class, the subclass must implement all the abstract methods of the abstract class. Here is an example:
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
In this example, Rectangle
is a subclass of Shape
and it implements both area()
and perimeter()
methods.
Creating Abstract Classes in Python
In Python, abstract classes can be created using the abc
(abstract base classes) module. This module provides the ABC
class and the abstractmethod
decorator that can be used to create abstract methods.
Here’s an example of how to create an abstract class in Python:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
In the above code, we define an abstract class Shape
that inherits from the ABC
class. We also define two abstract methods area()
and perimeter()
using the abstractmethod
decorator. These methods do not have any implementation in the abstract class, but they need to be implemented by any concrete class that inherits from the Shape
class.
Any attempt to create an object of an abstract class will result in a TypeError
s = Shape() # This will raise a TypeError
We must create a concrete class that inherits from the abstract class and implements the abstract methods.
b. Interfaces
In Python, there is no built-in support for interfaces. However, we can implement interfaces using abstract classes. An interface can be defined as an abstract class that contains only abstract methods. Any class that implements the interface must provide an implementation for all of the abstract methods.
Here’s an example of an interface defined using an abstract class:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
In this example, the Shape
class is an abstract class that defines an interface for all shapes. It contains two abstract methods: area
and perimeter
. Any class that wants to implement the Shape
interface must provide an implementation for both of these methods.
Here’s an example of a class that implements the Shape
interface:
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
In this example, the Rectangle
class implements the Shape
interface by providing an implementation for both the area
and perimeter
methods.
Creating Interfaces in Python
Python does not have a built-in interface keyword or syntax to create interfaces, but we can use abstract classes to define an interface. To create an interface in Python, we can create an abstract class with only abstract methods.
Here’s an example of creating an interface using an abstract class:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
In this example, we created an abstract class Shape
that defines an interface for geometric shapes. The Shape
class has two abstract methods area()
and perimeter()
, which every concrete shape class must implement.
To use this interface, we can create a concrete class that implements the abstract methods of the Shape
interface:
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
def perimeter(self):
return 2 * (self.length + self.width)
In this example, we created a concrete class Rectangle
that implements the Shape
interface by providing concrete implementations of the area()
and perimeter()
methods. Now, we can use this Rectangle
class to create instances of rectangles and call the area()
and perimeter()
methods, knowing that they will be implemented according to the Shape
interface.
When to Use Abstraction
Abstraction is particularly useful in situations where you want to hide the implementation details of a class or module from its users. By using abstraction, you can provide a simplified and more intuitive interface to your users, which can make it easier for them to work with your code.
For example, imagine you are developing a complex software system with many different modules and classes. If you were to expose the implementation details of each class to the rest of the system, it would be much harder for other developers to understand how your code works and how to use it effectively.
By using abstraction to hide the implementation details of each class, you can simplify the system’s overall architecture and make it easier for other developers to work with. Additionally, abstraction can help you enforce good design principles like modularity and encapsulation, which can make your code more maintainable and easier to modify over time.
Check Python Mastery: From Beginner to Expert Part-1
❤️ If you liked the article, like and subscribe to my channel, “Securnerd”.
👍 If you have any questions or if I would like to discuss the described hacking tools in more detail, then write in the comments. Your opinion is very important to me! at last of post