OOP in Python

Object-oriented programming is a vital concept in modern software development. OOP offers a structured approach to designing and managing code, promoting reusability, scalability, and maintainability. At its core, OOP emphasizes the organization of code into objects, each encapsulating data and behavior. Python, with its clear syntax and dynamic nature, is an ideal platform for practicing OOP principles.

Understanding objects and classes

Objects are instances of classes representing specific data with associated behaviors. Classes are blueprints for creating objects and encapsulating data and methods within a single entity.

Let's consider this scenario:

A class of cars with three objects
A class of cars with three objects

We start by defining a class named Car that serves as a blueprint for creating individual car objects. The Car class defines the structure and behavior of car objects. It encapsulates data (attributes) and behaviors (methods) related to cars. The attributes in this example are characteristics that define an object, such as the Color, Model, Make, and Year. The methods are functions defined within a class that perform actions or operations on the object’s data.

Let's define our class Car in Python:

class Car:
def __init__(self, color, model, make, year):
self.Color = color # Attribute
self.Model = model # Attribute
self.Make = make # Attribute
self.Year = year # Attribute
def update_color(self, color): # Method
self.Color = color
def print_attributes(self): # Method
print("Color:", self.Color, "\nModel:", self.Model, "\nMake:", self.Make, "\nYear:", self.Year)
car1 = Car("Red", "Civic", "Honda", 2015) # Instance
car1.print_attributes()
car1.update_color("Yellow")
car1.print_attributes()

In the code, we’ve defined a class called Car with an __init__() method serving as its constructor, initializing attributes with parameters like color, model, make, and year. The self parameter allows accessing instance attributes and methods within the class. Two additional methods, update_color(self, color) and print_attributes(self), updates the color attribute and print the attributes, respectively. Furthermore, we are creating an instance named car1 with specific arguments that initialize its attributes. We then print its attributes using print_attributes(). Later, we update the color attribute of car1 from "Red" to "Yellow" using update_color() and reprint the attributes.

Note: Even if there are no parameters for a method, the parameter self still has to be defined.

Encapsulation

Encapsulation involves bundling attributes and methods that operate on that data within a single class. It hides the internal state of objects from the outside world and only exposes the necessary functionality through well-defined interfaces. Encapsulation prevents direct access to an object's data outside the class, enforcing data integrity and promoting code maintainability.

Let's continue with the Car example; however, let's make some changes to the attributes and methods:

class Car:
def __init__(self, color, model, make, year):
self.Color = color # Public attribute
self.Model = model # Public attribute
self._Make = make # Protected attribute
self.__Year = year # Private attribute
def update_year(self, year): # Method
self.__Year = year
def print_attributes(self): # Method
print("Color:", self.Color, "\nModel:", self.Model, "\nMake:", self._Make, "\nYear:", self.__Year)
car1 = Car("Red", "Civic", "Honda", 2015) # Instance
car1.print_attributes()
car1.update_year(2019)
car1.print_attributes()

We have now introduced the public, protected, and private attributes in our class. Protected attributes can be accessed outside of the class; however, the underscore means that they should only be used within the class or its subclasses. Private attributes are entirely inaccessible from outside the class. Due to the private and protected attributes, encapsulation hides the internal state of objects from the outside world. Moreover, it also provides the users with a controlled way to modify the private or protected attributes.

Abstraction

Abstraction in programming simplifies interaction by hiding complex implementation details, allowing developers to focus on essential features. Users can interact intuitively without needing to understand the underlying complexity.

Again, continuing with our car example, let's perform abstraction in our class:

class Car:
def __init__(self, color, model, make, year):
self._Color = color # Protected attribute
self._Model = model # Protected attribute
self._Make = make # Protected attribute
self._Year = year # Protected attribute
self._isEngineOn = False # Protected attribute
def print_attributes(self): # Method
print("Color:", self._Color, "\nModel:", self._Model, "\nMake:", self._Make, "\nYear:", self._Year)
def turn_engine_on(self): # Method
if self._isEngineOn == False:
self._isEngineOn = True
else:
print("Engine already on!")
car1 = Car("Red", "Civic", "Honda", 2015) # Instance
car1.print_attributes()
car1.turn_engine_on()
car1.turn_engine_on()

Abstraction within the Car class is achieved through encapsulation, where the internal details are shielded while a simplified interface is provided. Attributes are designated as protected, such as _isEngineOn, initialized as False by default, ensuring controlled access and state management. The print_attributes() method offers structured access to car information, promoting data integrity and code organization by encapsulating retrieval functionality. Similarly, turn_engine_on() abstracts engine activation logic, hiding the implementation details from users and facilitating simplified interaction. Through encapsulation and method abstraction, the Car class presents users with a streamlined interface, enhancing code maintainability and usability by concealing internal complexities and offering structured access points.

Inheritance

Inheritance allows a subclass to inherit properties and behavior from a superclass, enhancing code reuse and modularity. Subclasses can specialize superclass behavior, add new attributes and methods, override existing ones, or extend the functionality of the inherited methods. Single inheritance involves one superclass, while multiple inheritance permits inheritance from multiple superclasses, though it can introduce complexity and ambiguity.

Let's continue with our car example, but now the Cars is a superclass and ECar will be a subclass of Cars.

Heirarchy of classes
Heirarchy of classes

Let's define the subclass in our existing code:

class Car:
def __init__(self, color, make, model, year):
self.Color = color
self.Make = make
self.Model = model
self.Year = year
def print_attributes(self): # Method
print("Color:", self.Color, "\nModel:", self.Model, "\nMake:", self.Make, "\nYear:", self.Year)
class ElectricCar(Car): # ElectricCar inherits from Car
def __init__(self, color, make, model, year, battery_capacity):
super().__init__(color, make, model, year) # Calling the constructor of the superclass
self.battery_capacity = battery_capacity
def print_attributes(self):
super().print_attributes() # Calling the display_info method of the superclass
print(f"Battery Capacity: {self.battery_capacity} kWh")
# Creating instances of Car and ElectricCar
car1 = Car("Red", "Toyota", "Camry", 2022)
print("Attribute of superclass: ")
car1.print_attributes()
electric_car1 = ElectricCar("Blue", "Tesla", "Model S", 2023, 100)
print("Attribute of subclass:")
electric_car1.print_attributes()

In the provided code, single class inheritance is demonstrated, with the ElectricCar class inheriting attributes and methods from the superclass, Cars. The Car class serves as the generic superclass, defining attributes like color, make, model, and year, while ElectricCar is a specialized subclass introducing an additional attribute battery_capacity specific to electric cars. Within ElectricCar’s __init__() method, the super().__init___() line invokes the superclass constructor to initialize shared attributes and the print_attributes method overrides the superclass method to incorporate details about battery capacity.

By promoting modularity and scalability, inheritance enables the creation of specialized classes that build upon existing ones, enhancing code organization and efficiency.

Conclusion

In conclusion, we've explored key concepts in object-oriented programming: objects represent instances of classes, encapsulating data and methods. Abstraction hides complexities, offering simplified interfaces. Inheritance fosters code reuse and hierarchy. Understanding these fundamentals empowers developers to create modular, maintainable, and scalable software solutions.

Free Resources

Copyright ©2024 Educative, Inc. All rights reserved