Chapter 7: Object-Oriented Programming In Python

Object-oriented programming in Python
Object-oriented programming in Python

Introduction to OOP concepts

Object-Oriented Programming (OOP) is a programming paradigm that is based on the concept of “objects”. In Python, almost everything is an object, with its properties and methods. A class is like an object constructor or a “blueprint” for creating objects.

Classes and objects

Classes and objects are fundamental concepts in Object-Oriented Programming (OOP). Python, being an object-oriented programming language, uses classes and objects for structuring the code.

Let’s dive into an example of a simple automotive system using classes and objects. In this scenario, let’s assume we’re creating a system to manage a fleet of cars for a rental service. We’ll have a Car class and a Fleet class.

Please note that this is a very simplified example and does not cover all the complexities that you’d find in a real-world automotive software system.

class Car:
    def __init__(self, brand, model, year, mileage):
        self.brand = brand
        self.model = model
        self.year = year
        self.mileage = mileage
        self.available = True

    def rent(self):
        if self.available:
            self.available = False
            return True
        else:
            return False

    def return_car(self):
        if not self.available:
            self.available = True
            return True
        else:
            return False

    def add_mileage(self, miles):
        self.mileage += miles

    def display_info(self):
        return f'{self.brand} {self.model} ({self.year}): {self.mileage} miles'


class Fleet:
    def __init__(self):
        self.cars = []

    def add_car(self, car):
        if isinstance(car, Car):
            self.cars.append(car)

    def remove_car(self, car):
        if car in self.cars:
            self.cars.remove(car)

    def get_available_cars(self):
        return [car for car in self.cars if car.available]

    def rent_car(self, brand, model):
        for car in self.get_available_cars():
            if car.brand == brand and car.model == model:
                if car.rent():
                    return car
        return None

    def return_car(self, car):
        if car in self.cars and not car.available:
            car.return_car()
            return car
        return None

    def add_mileage(self, car, miles):
        if car in self.cars:
            car.add_mileage(miles)

    def display_fleet(self):
        for car in self.cars:
            print(car.display_info())


# Usage
fleet = Fleet()

car1 = Car('Tesla', 'Model S', 2023, 0)
car2 = Car('Ford', 'Mustang', 2023, 0)

fleet.add_car(car1)
fleet.add_car(car2)

rented_car = fleet.rent_car('Tesla', 'Model S')
if rented_car:
    print(f'Rented: {rented_car.display_info()}')
else:
    print('Requested car not available')

fleet.display_fleet()

returned_car = fleet.return_car(rented_car)
if returned_car:
    print(f'Returned: {returned_car.display_info()}')
else:
    print('This car does not belong to the fleet or it was not rented')

fleet.add_mileage(returned_car, 100)
fleet.display_fleet()

In this code:

The Car class represents a car with attributes such as brand, model, year of manufacture, and mileage. It also has a status indicating whether it is available for rent or not.

The Fleet class represents a fleet of cars. It allows adding and removing cars from the fleet, renting a car, returning a rented car, and adding mileage to a car.

Please note that this code is quite basic and a real-world application would need much more features and error-checking mechanisms. This code is only to illustrate the basic use of classes and objects in Python.

Inheritance

Inheritance is a key concept in Object-Oriented Programming (OOP) that allows one class to inherit the properties and methods of another class. Python also supports inheritance.

Building on the previous Car and Fleet classes, let’s imagine a scenario where we have different types of cars: ElectricCar and GasCar, which have specific attributes and behaviors on top of what a base Car has. For instance, the ElectricCar class might have a battery capacity attribute, while the GasCar class might have a fuel capacity attribute.

Here’s an example of how you might use inheritance in this situation:

class Car:
    def __init__(self, brand, model, year):
        self.brand = brand
        self.model = model
        self.year = year
        self.available = True

    def rent(self):
        if self.available:
            self.available = False
            return True
        else:
            return False

    def return_car(self):
        if not self.available:
            self.available = True
            return True
        else:
            return False

    def display_info(self):
        return f'{self.brand} {self.model} ({self.year})'


class ElectricCar(Car):
    def __init__(self, brand, model, year, battery_capacity):
        super().__init__(brand, model, year)
        self.battery_capacity = battery_capacity  # in kWh

    def charge(self):
        print(f'{self.brand} {self.model} is charging...')

    def display_info(self):
        return super().display_info() + f', Battery Capacity: {self.battery_capacity} kWh'


class GasCar(Car):
    def __init__(self, brand, model, year, fuel_capacity):
        super().__init__(brand, model, year)
        self.fuel_capacity = fuel_capacity  # in liters

    def refuel(self):
        print(f'{self.brand} {self.model} is refueling...')

    def display_info(self):
        return super().display_info() + f', Fuel Capacity: {self.fuel_capacity} liters'


class Fleet:
    def __init__(self):
        self.cars = []

    def add_car(self, car):
        if isinstance(car, Car):
            self.cars.append(car)

    def display_fleet(self):
        for car in self.cars:
            print(car.display_info())


# Usage
fleet = Fleet()

car1 = ElectricCar('Tesla', 'Model S', 2023, 100)
car2 = GasCar('Ford', 'Mustang', 2023, 60)

fleet.add_car(car1)
fleet.add_car(car2)

fleet.display_fleet()

In this code, both ElectricCar and GasCar are subclasses (or derived classes) of the Car class, which is the superclass (or base class). We’re using the super() function to call a method from the superclass.

Again, note that this is a simplistic example. In a real-world application, you’d probably want to add more classes, attributes, methods, and error checking.

Polymorphism

Polymorphism is another crucial concept in object-oriented programming. It allows us to use a single interface with different underlying forms. In Python, polymorphism can be achieved in several ways, including through inheritance and abstract classes.

In the context of our automotive software, let’s extend the previous example with a Maintenance class that can perform maintenance on both ElectricCar and GasCar objects, but the process differs between them.

from abc import ABC, abstractmethod

class Car(ABC):
    def __init__(self, brand, model, year):
        self.brand = brand
        self.model = model
        self.year = year

    @abstractmethod
    def maintenance(self):
        pass


class ElectricCar(Car):
    def __init__(self, brand, model, year, battery_capacity):
        super().__init__(brand, model, year)
        self.battery_capacity = battery_capacity

    def maintenance(self):
        print(f'Maintaining {self.brand} {self.model}: Checking battery health...')


class GasCar(Car):
    def __init__(self, brand, model, year, fuel_capacity):
        super().__init__(brand, model, year)
        self.fuel_capacity = fuel_capacity

    def maintenance(self):
        print(f'Maintaining {self.brand} {self.model}: Changing oil...')


class Maintenance:
    @staticmethod
    def perform_maintenance(car):
        if isinstance(car, Car):
            car.maintenance()


# Usage
car1 = ElectricCar('Tesla', 'Model S', 2023, 100)
car2 = GasCar('Ford', 'Mustang', 2023, 60)

maintenance = Maintenance()
maintenance.perform_maintenance(car1)  # Maintaining Tesla Model S: Checking battery health...
maintenance.perform_maintenance(car2)  # Maintaining Ford Mustang: Changing oil...

In this code:

The Car class has become an abstract base class (ABC) with an abstract method maintenance(). This means that any class inheriting from Car is expected to implement this method.

Both ElectricCar and GasCar implement the maintenance() method in their own way. This demonstrates polymorphism; the Maintenance class can call maintenance() on any Car object, but the actual behavior depends on the type of the car.

This code demonstrates a very basic form of polymorphism. In a real-world application, you would likely have much more complex behavior and more types of cars.

Encapsulation and data hiding

Encapsulation and data hiding are two fundamental concepts in object-oriented programming. Encapsulation involves bundling the data and the methods that operate on that data within one unit – the class. Data hiding is about keeping the class’s internal data protected from external manipulation, which is achieved through the use of private attributes and methods.

In Python, there is no strict concept of private and public as in languages like Java or C++. However, we can achieve a similar effect using name mangling: by prefixing an attribute or method name with double underscores, Python will “hide” this element.

Let’s extend the automotive software example to illustrate these concepts:

class Car:
    def __init__(self, brand, model, year):
        self._brand = brand  # protected attribute
        self._model = model  # protected attribute
        self._year = year  # protected attribute
        self.__mileage = 0  # private attribute

    def get_info(self):
        return f'{self._brand} {self._model} ({self._year}), Mileage: {self.__mileage}'

    def drive(self, miles):
        if miles > 0:
            self.__mileage += miles
        else:
            print("Miles should be positive")

    def _check_mileage(self):  # protected method
        return self.__mileage > 100000

    def needs_maintenance(self):
        if self._check_mileage():
            return f'{self._brand} {self._model} needs maintenance.'
        else:
            return f'{self._brand} {self._model} does not need maintenance.'


# Usage
car1 = Car('Tesla', 'Model S', 2023)

print(car1.get_info())  # Tesla Model S (2023), Mileage: 0

car1.drive(150000)

print(car1.get_info())  # Tesla Model S (2023), Mileage: 150000
print(car1.needs_maintenance())  # Tesla Model S needs maintenance.

In this code:

  • _brand, _model, and _year are “protected” attributes. The single underscore is a convention indicating that these attributes should not be accessed directly, although Python does not enforce this.
  • __mileage is a “private” attribute. Python will mangle the name of this attribute to make it harder to access directly, although it’s still technically possible.
  • get_info(), drive(miles), and needs_maintenance() are public methods, intended to be used by objects of this class.
  • _check_mileage() is a “protected” method, and its usage should be limited to this class and its subclasses.

Again, keep in mind that Python does not enforce strict access control as some other languages do. The single and double underscores are conventions and name mangling can be bypassed if you know how to do it. But in well-designed code, you should respect these conventions and use the provided methods to interact with an object’s data.

Class methods and instance methods

In Python, there are three types of methods:

  • Instance methods: These are the most common type. They can access and modify instance state as well as class state. They have access to the instance (via self).
  • Class methods: These are methods that belong to the class rather than the instance of the class. They can’t access or modify instance state, but they can access and modify class state. They have access to the class.
  • Static methods: These are methods that don’t have access to class or instance state. They work like regular functions but belong to the class’s namespace.

Let’s extend our automotive example by adding a CarFactory class, which will create cars and keep track of how many cars it has made.

class Car:
    def __init__(self, brand, model, year):
        self.brand = brand
        self.model = model
        self.year = year

    def display_info(self):
        return f'{self.brand} {self.model} ({self.year})'


class CarFactory:
    total_cars_made = 0  # Class variable

    @classmethod
    def make_car(cls, brand, model, year):
        cls.total_cars_made += 1
        return Car(brand, model, year)

    @classmethod
    def get_total_cars_made(cls):
        return cls.total_cars_made

    @staticmethod
    def validate_car(brand, model, year):
        # This could be a much more complex validation
        return isinstance(brand, str) and isinstance(model, str) and isinstance(year, int)


# Usage
car1 = CarFactory.make_car('Tesla', 'Model S', 2023)
print(car1.display_info())  # Tesla Model S (2023)

car2 = CarFactory.make_car('Ford', 'Mustang', 2023)
print(car2.display_info())  # Ford Mustang (2023)

print(CarFactory.get_total_cars_made())  # 2

print(CarFactory.validate_car('Tesla', 'Model 3', 2024))  # True
print(CarFactory.validate_car('Tesla', 3, '2024'))  # False

In this code:

  • make_car is a class method that creates a new Car instance and updates the total_cars_made class variable.
  • get_total_cars_made is a class method that returns the number of cars that have been made.
  • validate_car is a static method that validates the parameters for creating a new Car instance. It doesn’t need to access or modify any class or instance state, so it can be a static method.

Conclusion

In conclusion, Object Oriented Programming (OOP) in Python is a crucial approach that offers robust mechanisms to structure and organize code. OOP’s main strength lies in its ability to facilitate reusable, maintainable, and scalable code by allowing related properties and behavior to be bundled into individual objects. This design principle is evident in Python, with its focus on simplicity, readability, and ease of use, while also providing powerful tools to implement advanced OOP features like inheritance, polymorphism, and encapsulation.

Python’s native support for OOP empowers developers to create complex, real-world applications, ranging from web services to scientific computation, with efficient and easy-to-understand code. The Python language’s ‘everything is an object’ design philosophy promotes a deep understanding of OOP concepts, leading to more effective problem-solving strategies.

However, it’s also important to recognize that OOP isn’t always the best fit for every project or problem. Python’s flexibility as a multi-paradigm language means developers can blend functional, procedural, and object-oriented styles to fit the specific needs and context of their work.

Ultimately, the key to successful programming in Python—and any language, for that matter—depends on thoughtful application of principles like OOP. As with any tool, its power lies not just in understanding how it works, but knowing when and where to use it most effectively. By mastering Object Oriented Programming in Python, developers can harness the full potential of this elegant and powerful language, opening the door to create sophisticated, high-quality software.

Chapter 7: Object-Oriented Programming In Python
Scroll to top
error: Content is protected !!