Improving our Python Volvo Bus!

Expanding the Volvo Bus: Advanced Python OOPPhoto by Adam Cai on UnsplashIntroductionIn the previous article, we explored the basics of Python class inheritance by creating a Vehicle class and extending it with a Bus class for a Volvo bus. Now, let’s g…


This content originally appeared on Level Up Coding - Medium and was authored by CyCoderX

Expanding the Volvo Bus: Advanced Python OOP

Photo by Adam Cai on Unsplash

Introduction

In the previous article, we explored the basics of Python class inheritance by creating a Vehicle class and extending it with a Bus class for a Volvo bus. Now, let’s go even further!

Hi there! I’m CyCoderX, and I’ll be your guide through this article! This time, we will add more features to our Volvo Bus, such as class-level attributes, encapsulation for data hiding, and even advanced method overriding.

In case you missed my first article, where we built a Volvo Bus class with added attributes and methods, here is the link.

Building a Volvo Bus: Python Class Inheritance Guide

By the end of this article, you will have a deeper understanding of object-oriented programming in Python and be able to create more complex, feature-rich class structures that simulate real-world scenarios.

Let’s dive in!

Please consider helping me out by clapping. And if you’re interested in more Python, Data Engineering and Data Science content, please consider following me.😊

What is PEP 8 and Why is it Important in Python?

Adding Class-Level Attributes

In Python, attributes can be defined at the class level, meaning they are shared by all instances of the class. This is useful for data that should be the same for every instance of a class. For example, let’s add a class-level attribute for the number of wheels, which is common for all buses.

class Vehicle:
# Constructor method to initialize the vehicle's attributes
def __init__(self, name, max_speed, mileage) -> None:
self.name = name # Name of the vehicle
self.max_speed = max_speed # Maximum speed of the vehicle
self.mileage = mileage # Mileage of the vehicle

class Bus(Vehicle):
# Class-level attribute indicating the number of wheels for all Bus instances
number_of_wheels = 6

# Constructor method to initialize the bus's attributes, including those inherited from Vehicle
def __init__(self, name, max_speed, mileage, is_electric) -> None:
super().__init__(name, max_speed, mileage) # Initialize attributes from the parent class
self.is_electric = is_electric # Indicates if the bus is electric or not

Explanation:

Vehicle Class:

The Vehicle class has a constructor method (__init__) that initializes three attributes:

  • name: This is the name of the vehicle.
  • max_speed: This represents the maximum speed the vehicle can achieve.
  • mileage: This indicates the mileage of the vehicle.

Bus Class (inherits from Vehicle):

The Bus class has a class-level attribute:

  • number_of_wheels: This is set to 6, indicating that all instances of the Bus class have six wheels.

The Bus class also has a constructor method (__init__) that:

  • Calls the parent class (Vehicle) constructor to initialize the inherited attributes (name, max_speed, mileage).
  • Initializes an additional attribute:
  • is_electric: This indicates whether the bus is electric or not.

Here, number_of_wheels is defined at the class level. This means every bus, regardless of its individual attributes, will have 6 wheels by default.

To access it, you can use:

# Instantiate a Bus object
bus1 = Bus("Electric Bus", 120, 300, True)

# Print the number of wheels for the Bus class
print(f"Number of wheels: {Bus.number_of_wheels}") # Output: Number of wheels: 6

This will output:

Number of wheels: 6

This is a great way to manage data that doesn’t vary between instances but is part of the general characteristics of the class.

Class Definition:

  • Vehicle and Bus classes are defined with their respective attributes and methods.

Instantiation:

  • bus1 is an instance of the Bus class with the name “Electric Bus”, a maximum speed of 120, mileage of 300, and it is electric (True).

Output:

  • print(f"Number of wheels: {Bus.number_of_wheels}") prints the class-level attribute number_of_wheels of the Bus class, which is 6.

Automate Email Response Generation Using the ChatGPT API with Python

Photo by Barbara Zandoval on Unsplash

Using the ChatGPT API in Your Projects

Encapsulation and Data Hiding

Encapsulation allows us to restrict access to certain attributes or methods, hiding the internal workings of an object from the outside. In Python, we use a convention of prefixing attributes with an underscore (_) or double underscore (__) to indicate that they are intended to be private.

Let’s encapsulate the mileage attribute in our Vehicle class and provide a method to update it securely.

class Vehicle:
def __init__(self, name, max_speed, mileage) -> None:
self.name = name # Name of the vehicle
self.max_speed = max_speed # Maximum speed of the vehicle
self.__mileage = mileage # Private attribute to store the mileage of the vehicle

def get_mileage(self):
return self.__mileage # Method to return the current mileage of the vehicle

def update_mileage(self, new_mileage):
if new_mileage >= self.__mileage:
self.__mileage = new_mileage # Update mileage if the new mileage is greater than or equal to the current mileage
else:
print("New mileage cannot be lower than current mileage!") # Print a warning message if the new mileage is lower

Explanation:

  • The Vehicle class has a constructor method (__init__) that initializes three attributes: name, max_speed, and __mileage.
  • __mileage is a private attribute, meaning it cannot be accessed directly from outside the class.
  • The get_mileage method returns the current value of the private attribute __mileage.
  • The update_mileage method updates the value of __mileage if the new mileage is greater than or equal to the current mileage. If the new mileage is lower, it prints a warning message.

In this version, __mileage is a private attribute that can only be accessed through the get_mileage() method. The update_mileage() method provides a controlled way to modify the mileage, ensuring no one can lower the mileage arbitrarily (a common problem in vehicle management systems).

To interact with this, you would do:

school_bus = Bus("Volvo", 180, 12, False)
print("Mileage: ", school_bus.get_mileage()) # Outputs 12
school_bus.update_mileage(15)
print("Updated mileage: ", school_bus.get_mileage()) # Outputs 15

Encapsulation helps maintain the integrity of your data and makes sure that attributes are modified in a controlled way.

Method Overriding and Extending Parent Methods

We’ve previously explored how to override methods in Python. Let’s now extend a parent class method instead of completely overriding it. This is useful when you want to retain the functionality of the parent class method but also add new behavior.

In this example, we’ll modify the __str__() method of the Bus class to include the number of wheels and the electric status, while still utilizing the parent Vehicle class’s __str__() method.

class Vehicle:
# Constructor method to initialize the Vehicle object
def __init__(self, name, max_speed, mileage) -> None:
self.name = name # Name of the vehicle
self.max_speed = max_speed # Maximum speed of the vehicle
self.mileage = mileage # Mileage of the vehicle

# String representation method for the Vehicle object
def __str__(self):
return f"Vehicle(name={self.name}, max_speed={self.max_speed}, mileage={self.mileage})"

class Bus(Vehicle):
number_of_wheels = 6 # Class attribute for the number of wheels

# Constructor method to initialize the Bus object
def __init__(self, name, max_speed, mileage, is_electric) -> None:
super().__init__(name, max_speed, mileage) # Initialize attributes from the Vehicle class
self.is_electric = is_electric # Indicates if the bus is electric

# String representation method for the Bus object
def __str__(self):
electric_status = "Electric" if self.is_electric else "Non-Electric" # Determine electric status
return super().__str__() + f", wheels={self.number_of_wheels}, type={electric_status}"

Explanation:

Class Vehicle:

  • __init__ method: This is the constructor method that initializes the attributes name, max_speed, and mileage for an instance of the Vehicle class.
  • __str__ method: This method returns a string representation of the Vehicle object, which includes its name, max_speed, and mileage.

Class Bus (inherits from Vehicle):

  • Class attribute number_of_wheels: This is a class-level attribute that is set to 6, indicating that all instances of Bus have 6 wheels.
  • __init__ method: This constructor method initializes the attributes name, max_speed, mileage, and is_electric for an instance of the Bus class. It uses the super() function to call the constructor of the parent Vehicle class to initialize name, max_speed, and mileage.
  • __str__ method: This method returns a string representation of the Bus object. It calls the __str__ method of the parent Vehicle class and adds additional information about the number of wheels and whether the bus is electric or not.

Summary:

  • The Vehicle class is defined with three attributes: name, max_speed, and mileage.
  • The __init__ method in Vehicle initializes these attributes.
  • The __str__ method in Vehicle provides a string representation of the object.
  • The Bus class inherits from Vehicle, meaning it has all the attributes and methods of Vehicle.
  • Bus has an additional class attribute number_of_wheels set to 6.
  • The __init__ method in Bus initializes the inherited attributes using super() and adds a new attribute is_electric.
  • The __str__ method in Bus extends the string representation to include the number of wheels and the electric status of the bus.

By using super().__str__(), we retain the basic string output from the Vehicle class but append additional details about the bus’s electric status and wheels.

school_bus = Bus("Volvo", 180, 12, False)
print(school_bus)

The output will be:

Vehicle(name=Volvo, max_speed=180, mileage=12), wheels=6, type=Non-Electric

This technique allows us to build on the functionality of parent methods while still tailoring them to the specific needs of your child class.

Must know Python Built-in Functions!

Adding Class Methods and Static Methods

In Python, in addition to instance methods (which are tied to object instances), you can also define class methods and static methods. These are associated with the class itself rather than any particular object.

Class Methods

A class method can access and modify class-level attributes. For example, we can create a class method in the Bus class that returns information about the bus’s wheels, which are defined at the class level.

class Bus(Vehicle):
number_of_wheels = 6

def __init__(self, name, max_speed, mileage, is_electric) -> None:
super().__init__(name, max_speed, mileage)
self.is_electric = is_electric

@classmethod
def get_wheel_info(cls):
return f"All buses have {cls.number_of_wheels} wheels."

The @classmethod decorator allows the method to access class variables like number_of_wheels.

print(Bus.get_wheel_info())

This will output:

All buses have 6 wheels.

Automating Boring Routine Tasks with Python!

Static Methods

Static methods, on the other hand, are not tied to the class or any instance. They’re used to create utility functions that don’t need access to class or instance data.

class Bus(Vehicle):
@staticmethod
def vehicle_type():
return "This is a bus."

Static methods are called without any reference to class or instance attributes:

print(Bus.vehicle_type())

This will output:

This is a bus.

Class and static methods add flexibility when you need functionality that isn’t tightly coupled to an instance of the class.

10 If-Else Practice Problems in Python

Polymorphism in Action

Polymorphism allows methods in different classes to have the same name but behave differently. With inheritance, polymorphism often means calling a method on a parent class can result in different behaviors depending on the child class.

Let’s demonstrate this by adding a new Car class that also inherits from the Vehicle class, but with a different implementation of the __str__() method.

class Car(Vehicle):
def __str__(self):
return f"Car(name={self.name}, max_speed={self.max_speed}, mileage={self.mileage})"

Now we have both Bus and Car classes, and though both inherit from Vehicle, their __str__() methods behave differently.

school_bus = Bus("Volvo", 180, 12, False)
family_car = Car("Toyota", 220, 15)

vehicles = [school_bus, family_car]

for vehicle in vehicles:
print(vehicle)

The output will be:

Vehicle(name=Volvo, max_speed=180, mileage=12), wheels=6, type=Non-Electric
Car(name=Toyota, max_speed=220, mileage=15)

This demonstrates polymorphism because the same __str__() method is called on both objects, but it results in different outputs depending on the class.

Photo by Hitesh Choudhary on Unsplash

5 Essential Built-in Modules for Every Python Developer

Customizing Error Handling with Exceptions

Now, let’s introduce custom exceptions to handle specific error cases within our classes. For example, let’s say we want to ensure that the mileage of a vehicle can never be negative. We can create a custom exception for this scenario.

Step 1: Define a Custom Exception

class InvalidMileageError(Exception):
pass

Step 2: Use the Exception in the update_mileage() Method

We can modify the update_mileage() method to raise this custom exception if an invalid mileage is provided.

class Vehicle:
def __init__(self, name, max_speed, mileage) -> None:
self.name = name
self.max_speed = max_speed
self.__mileage = mileage

def get_mileage(self):
return self.__mileage

def update_mileage(self, new_mileage):
if new_mileage >= self.__mileage:
self.__mileage = new_mileage
else:
raise InvalidMileageError("Mileage cannot decrease!")

Step 3: Test the Exception

Let’s test this by trying to update the mileage with an invalid value.

try:
school_bus = Bus("Volvo", 180, 12, False)
school_bus.update_mileage(10) # This should raise an exception
except InvalidMileageError as e:
print(e)

The output will be:

Mileage cannot decrease!

By defining custom exceptions, we make error handling more specific to our application’s requirements, allowing for more robust and user-friendly code.

range (Python) vs. arange (NumPy)

Complete Code:

Here’s the complete code that incorporates everything we discussed:

# Custom exception for invalid mileage
class InvalidMileageError(Exception):
pass

# Parent Vehicle class
class Vehicle:
def __init__(self, name, max_speed, mileage) -> None:
self.name = name
self.max_speed = max_speed
self.__mileage = mileage # Private attribute

def get_mileage(self):
return self.__mileage

def update_mileage(self, new_mileage):
if new_mileage >= self.__mileage:
self.__mileage = new_mileage
else:
raise InvalidMileageError("Mileage cannot decrease!")

def __str__(self):
return f"Vehicle(name={self.name}, max_speed={self.max_speed}, mileage={self.__mileage})"


# Child Bus class inheriting from Vehicle
class Bus(Vehicle):
number_of_wheels = 6 # Class-level attribute

def __init__(self, name, max_speed, mileage, is_electric) -> None:
super().__init__(name, max_speed, mileage)
self.is_electric = is_electric

@classmethod
def get_wheel_info(cls):
return f"All buses have {cls.number_of_wheels} wheels."

@staticmethod
def vehicle_type():
return "This is a bus."

def __str__(self):
electric_status = "Electric" if self.is_electric else "Non-Electric"
return super().__str__() + f", wheels={self.number_of_wheels}, type={electric_status}"


# New Car class inheriting from Vehicle to demonstrate polymorphism
class Car(Vehicle):
def __str__(self):
return f"Car(name={self.name}, max_speed={self.max_speed}, mileage={self.get_mileage()})"


# Testing the Bus and Car classes with inheritance, encapsulation, and method overriding
if __name__ == "__main__":
# Create a Bus instance
school_bus = Bus("Volvo", 180, 12, False)

# Access class-level attribute
print(Bus.get_wheel_info()) # Outputs: All buses have 6 wheels.

# Use the static method
print(Bus.vehicle_type()) # Outputs: This is a bus.

# Test encapsulation and mileage update with valid data
print(f"Initial mileage: {school_bus.get_mileage()}")
school_bus.update_mileage(20)
print(f"Updated mileage: {school_bus.get_mileage()}")

# Test invalid mileage (will raise custom exception)
try:
school_bus.update_mileage(10) # Should raise InvalidMileageError
except InvalidMileageError as e:
print(e)

# Test polymorphism with both Bus and Car
family_car = Car("Toyota", 220, 15)
vehicles = [school_bus, family_car]

for vehicle in vehicles:
print(vehicle)
# Outputs:
# Vehicle(name=Volvo, max_speed=180, mileage=20), wheels=6, type=Non-Electric
# Car(name

Key Features:

  1. Custom Exception (InvalidMileageError) for mileage validation.
  2. Encapsulation to protect the mileage attribute using a getter and controlled update method.
  3. Class and Static Methods in the Bus class.
  4. Polymorphism with the Car class having its own __str__() implementation.
  5. Method Overriding to extend parent class behavior using super().

This code demonstrates how to build flexible and scalable Python classes using advanced OOP techniques. Let me know if you need further modifications!

Build a Web Scraper in Python

Conclusion

In this article, we extended our understanding of Python inheritance by further building on our Vehicle and Bus classes with advanced object-oriented programming concepts. We explored the use of class-level attributes, encapsulation for data protection, and method overriding, while also delving into class and static methods for broader functionality. Additionally, we touched on polymorphism and the creation of custom exceptions for better error handling.

By now, you should have a strong grasp of how to create more sophisticated and real-world object hierarchies in Python, enhancing the flexibility, maintainability, and clarity of your code. Whether you’re working on small projects or larger systems, these OOP concepts will help you write cleaner and more scalable applications.

Photo by Rowen Smith on Unsplash

Final Words:

Thank you for taking the time to read my article.

This article was first published on medium by CyCoderX.

Hey There! I’m CyCoderX, a data engineer who loves crafting end-to-end solutions. I write articles about Python, SQL, AI, Data Engineering, lifestyle and more!

If you want to explore similar articles and updates, feel free to explore my Medium profile:

Python Tips By CyCoderX

Join me as we explore the exciting world of tech, data and beyond!

What did you think about this article? Let me know in the comments below … or above, depending on your device! 🙃


Improving our Python Volvo Bus! was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding - Medium and was authored by CyCoderX


Print Share Comment Cite Upload Translate Updates
APA

CyCoderX | Sciencx (2024-09-16T02:21:42+00:00) Improving our Python Volvo Bus!. Retrieved from https://www.scien.cx/2024/09/16/improving-our-python-volvo-bus/

MLA
" » Improving our Python Volvo Bus!." CyCoderX | Sciencx - Monday September 16, 2024, https://www.scien.cx/2024/09/16/improving-our-python-volvo-bus/
HARVARD
CyCoderX | Sciencx Monday September 16, 2024 » Improving our Python Volvo Bus!., viewed ,<https://www.scien.cx/2024/09/16/improving-our-python-volvo-bus/>
VANCOUVER
CyCoderX | Sciencx - » Improving our Python Volvo Bus!. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/09/16/improving-our-python-volvo-bus/
CHICAGO
" » Improving our Python Volvo Bus!." CyCoderX | Sciencx - Accessed . https://www.scien.cx/2024/09/16/improving-our-python-volvo-bus/
IEEE
" » Improving our Python Volvo Bus!." CyCoderX | Sciencx [Online]. Available: https://www.scien.cx/2024/09/16/improving-our-python-volvo-bus/. [Accessed: ]
rf:citation
» Improving our Python Volvo Bus! | CyCoderX | Sciencx | https://www.scien.cx/2024/09/16/improving-our-python-volvo-bus/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.