class Car:
pass
16 Object-Oriented Programming in Python
16.1 Introduction to Object-Oriented Programming
Object-Oriented Programming (OOP) is a programming paradigm that models real-world entities using objects and classes. While procedural programming focuses on functions and the sequence of operations, OOP centers around organizing code into objects, which encapsulate both data and behavior. This approach makes it easier to manage, extend, and modify large and complex programs, while also allowing for better reuse of code.
To understand OOP at a conceptual level, it can be helpful to look at examples from everyday life. These examples will illustrate the key concepts of OOP: classes, objects, attributes, and methods. Later in this chapter, we will see how these ideas map onto programming.
16.1.1 Objects and Classes in Everyday Life
Imagine you’re organizing a car dealership. Every car on the lot can be seen as an object—a specific instance of a general concept, such as a car. The general concept is what we refer to as a class in OOP. A class acts like a blueprint or template for creating objects. Each car on the lot is unique in some way, but they all share certain common traits and behaviors, just like objects created from a class.
Let’s break this down:
- Class: A car can be described by a blueprint, which details its general characteristics (such as make, model, engine type, etc.) and behaviors (like starting the engine, honking the horn). In OOP, this blueprint is the class.
- Object: Each specific car you have on the lot—a 2021 Toyota Camry, a 2020 Honda Civic, etc.—is an object created from the class blueprint. These cars have specific values for their characteristics (for example, one car is blue, another has a sunroof).
This distinction between class and object is fundamental to OOP. A class provides the general description, while an object is a specific instance created from that description.
16.1.2 Attributes and Methods
Objects have attributes and methods. Attributes are the data that describe the state of an object, while methods are the actions or behaviors an object can perform.
Continuing with our car dealership analogy:
- Attributes: Each car has specific properties like color, make, model, year, and mileage. These properties are the attributes of the car object.
- Methods: The behaviors of a car, such as starting the engine, honking the horn, or accelerating, are the methods. In the class blueprint, methods describe what a car can do, and in OOP terms, they are the actions or functions that can be performed on or by an object.
Just as a car’s attributes are set when the car is manufactured, an object’s attributes are defined when the object is created from the class.
Example: OOP in a Library System
Consider a library system, which can also be modeled using OOP principles. In this case, books are objects, and LibraryBook might be the class that defines their shared characteristics and behaviors.
- Class (LibraryBook): The class defines the general properties of a book, such as its title, author, publication date, and ISBN. It also defines behaviors, such as checking the book out or returning it.
- Objects: Each specific book in the library—like “To Kill a Mockingbird” or “Pride and Prejudice”—is an object created from the LibraryBook class. Each object has specific values for its attributes (e.g., title, author) but shares the same behaviors (e.g., can be checked out or returned).
When you borrow a book from the library, the system doesn’t just perform random tasks; it interacts with the specific book object, checking if it is available or already checked out. This interaction between the object and its methods is one of the strengths of OOP—it provides a structured and organized way to manage complex systems.
16.1.3 Encapsulation: Keeping Attributes and Methods Together
One of the key advantages of OOP is encapsulation—the concept of bundling data (attributes) and behaviors (methods) together in one entity, the object. This allows for better organization and modularity.
Let’s return to our car example: You can think of each car as a self-contained object. If you want to interact with the car, you don’t need to know every intricate detail about its inner workings; you just need to use the car’s methods. For instance, to start the car, you don’t have to understand how the engine works—you just turn the key or press a button, and the car performs the appropriate behavior.
In programming, encapsulation works in a similar way. When a class is designed, its internal workings (the code and data) are hidden from other parts of the program. Instead, the class provides a clear interface (methods) that can be used to interact with its objects. This hiding of internal details makes the system easier to maintain and modify, because changes to the internal structure of a class won’t affect the rest of the program as long as the interface remains the same.
Real-World Example: A Bank Account System
Consider a BankAccount class in an online banking system. This class defines the shared characteristics of all bank accounts (e.g., account number, balance) and the behaviors that all accounts can perform (e.g., deposit money, withdraw money, check balance).
- Class (BankAccount): This acts as the blueprint for creating specific bank accounts. It defines that each account will have an account number, a balance, and methods to deposit or withdraw funds.
- Objects: Each customer’s bank account is an object created from the BankAccount class. For example, one object might represent Alice’s bank account, while another represents Bob’s bank account.
The concept of encapsulation is crucial here. You, as the user of the bank account, don’t need to know how the bank stores your balance or how it processes withdrawals internally. All you need to know is that when you call the deposit()
method, money is added to your account, and when you call the withdraw()
method, money is subtracted. The internal details (data storage, security checks) are hidden from you.
This separation of concerns allows bank developers to modify the internal workings of the bank system without affecting the customers’ interactions. For example, the bank might switch to a more efficient algorithm for handling transactions, but as long as the deposit and withdraw methods work the same way, the change is invisible to the user.
16.1.4 Why Use OOP?
Now that we’ve seen how OOP relates to real-world examples, let’s summarize why this approach is so beneficial in programming:
Modularity and Reusability: By encapsulating data and behavior within classes, OOP promotes modular code that can be reused across different programs. For instance, a class written to represent a “student” in a school system can be reused in other systems (e.g., a library system).
Ease of Maintenance: Because objects are self-contained and interact with the rest of the system through defined methods, changes to one part of the system are less likely to break the entire program. This makes OOP programs easier to maintain and scale.
Flexibility Through Inheritance: OOP allows classes to be extended and specialized through inheritance, a powerful feature that we will discuss in detail later. This allows for the creation of complex systems that can be easily modified and extended.
Real-World Modeling: OOP allows programmers to model real-world systems in a way that’s intuitive and easy to understand. Objects in OOP can directly correspond to real-world entities, making the design and development process more natural.
In the next section, we will explore how to define a class in Python, bringing these concepts into the programming world and showing how to create and work with objects.
16.2 Defining a Class
Now that we’ve explored the key concepts behind Object-Oriented Programming (OOP) and how they relate to everyday examples, it’s time to move into the practical side: defining a class in Python. A class is the fundamental building block of OOP, serving as the blueprint for creating objects. Each object created from a class shares the structure and behavior defined by that class, but with unique values for its attributes.
In this section, we will dive into the syntax of class definition in Python, demonstrate how to create classes, and explain how attributes and methods are defined within a class. As you go through this section, keep in mind the real-world analogies from the previous section to understand how Python classes mimic real-world entities.
16.2.1 Basic Class Syntax
In Python, defining a class is straightforward. The keyword class
is used, followed by the class name and a colon. The body of the class contains the attributes (data) and methods (functions) that define the behavior of objects created from the class.
Here’s a simple example:
This is the simplest possible class definition: a class named Car
with no attributes or methods. The keyword pass
is used as a placeholder, indicating that there is no functionality yet. We will build on this example to make it more meaningful.
16.2.2 The __init__
Method: Class Constructor
In most cases, you’ll want your class to initialize some values when an object is created. This is where the __init__
method comes into play. The __init__
method is known as the constructor, and it is automatically called when an object is instantiated from the class. It is used to set up initial values for the object’s attributes.
Let’s expand on our Car
class:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
Here’s what’s happening:
- The
__init__
method takes several arguments:self
,make
,model
, andyear
. self
refers to the specific object being created. It allows you to assign the values to the object’s attributes.- Inside the method,
self.make
,self.model
, andself.year
refer to the attributes of the object. We assign the values passed in (make
,model
, andyear
) to those attributes.
This way, each object (or instance) of the Car
class will have its own make
, model
, and year
attributes, which are set when the object is created.
Example: Creating Objects from a Class
Let’s create some Car
objects using this class:
= Car("Toyota", "Camry", 2021)
car1 = Car("Honda", "Civic", 2020)
car2
print(car1.make)
print(car2.year)
Toyota
2020
Here, car1
and car2
are objects created from the Car
class. Each object has its own set of attributes: car1
is a Toyota Camry from 2021, and car2
is a Honda Civic from 2020. We can access these attributes using dot notation (car1.make
, car2.year
).
This structure mirrors real-world examples: just as each car in a dealership has specific properties, each object in OOP has specific values for its attributes, even though they are all created from the same class blueprint.
16.3 Methods and Attributes in a Class
In addition to attributes, a class can define methods—functions that describe the behaviors or actions the objects can perform. Methods in a class always take self
as the first argument, which refers to the instance on which the method is being called.
Let’s add a method to the Car
class that allows the car to display its full description:
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def description(self):
return f"{self.year} {self.make} {self.model}"
In this example, the method description
returns a formatted string with the car’s year, make, and model.
= Car("Toyota", "Camry", 2021)
car1 print(car1.description())
2021 Toyota Camry
Here, we define the method description
inside the class. When we call car1.description()
, the car provides its full description. This method operates on the instance of the class (e.g., car1
), and it has access to the attributes via self
.
16.3.1 Instance Attributes vs. Class Attributes
In Python, attributes can be classified into two types: instance attributes and class attributes.
Instance Attributes: These are specific to each object and are defined inside the
__init__
method. For example, in theCar
class,make
,model
, andyear
are instance attributes. Each car object has its own unique values for these attributes.Class Attributes: These are shared across all instances of a class. They are defined directly within the class but outside of any method. All objects of the class will share the same value for a class attribute, unless it is explicitly overridden in an instance.
Let’s modify the Car
class to include a class attribute:
class Car:
= 4 # Class attribute
wheels
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def description(self):
return f"{self.year} {self.make} {self.model}"
In this case, wheels
is a class attribute, meaning that all cars created from the Car
class will have 4 wheels. This attribute is shared among all instances of the class:
= Car("Toyota", "Camry", 2021)
car1 = Car("Honda", "Civic", 2020)
car2
print(car1.wheels)
print(car2.wheels)
4
4
Class attributes can be useful for defining properties that are the same across all instances, such as the number of wheels for cars.
16.3.2 Modifying Attributes
It is possible to modify both instance and class attributes after an object has been created. Let’s see how this works.
Modifying Instance Attributes
= Car("Toyota", "Camry", 2021)
car1 print(car1.description())
# Modifying the year
= 2022
car1.year print(car1.description())
2021 Toyota Camry
2022 Toyota Camry
Here, we update the year
attribute for car1
, and the object reflects the new value when we call the description
method again.
Modifying Class Attributes
Class attributes can be modified directly using the class name. Any change to a class attribute will be reflected across all instances of the class:
= 3
Car.wheels print(car1.wheels)
3
By modifying the wheels
class attribute, we’ve updated the number of wheels for all Car
objects.
16.4 Inheritance
Inheritance is one of the most powerful features of Object-Oriented Programming (OOP). It allows a new class to inherit the properties and behaviors (attributes and methods) of an existing class. The new class, known as the subclass or child class, can also have additional attributes and methods or override existing ones from the parent class. This promotes code reuse and makes it easier to build complex systems by extending functionality rather than starting from scratch.
To understand inheritance conceptually, let’s once again consider some real-world examples.
16.4.1 Inheritance in Everyday Life
Imagine a university system where you have students and graduate students. Both graduate and undergraduate students share many characteristics: they have a name, student ID, and GPA. However, graduate students also have some unique attributes, such as a thesis title and an advisor. It would be redundant to create separate classes for students and graduate students from scratch because many of their attributes overlap.
Instead, we can create a general Student class and a more specialized GraduateStudent class. The GraduateStudent class can inherit the common attributes and behaviors from the Student class, while adding its own unique features. This hierarchical structure is a natural fit for OOP and is made possible through inheritance.
16.4.2 Defining Inheritance in Python
In Python, inheritance is defined by specifying the parent class in parentheses when defining the child class. The child class inherits all the attributes and methods from the parent class.
Let’s define a basic example:
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def display_info(self):
return f"Student: {self.name}, ID: {self.student_id}, GPA: {self.gpa}"
Here, the Student
class defines three attributes (name
, student_id
, and gpa
) and one method (display_info
). Now, we will define a subclass, GraduateStudent
, which will inherit from Student
.
class GraduateStudent(Student):
def __init__(self, name, student_id, gpa, thesis_title):
super().__init__(name, student_id, gpa)
self.thesis_title = thesis_title
def display_thesis(self):
return f"Thesis: {self.thesis_title}"
In this example:
- The
GraduateStudent
class inherits fromStudent
. We can see this by the syntaxclass GraduateStudent(Student)
. - The
__init__
method of theGraduateStudent
class usessuper().__init__()
to call the parent class’s constructor, inheriting the initialization ofname
,student_id
, andgpa
. - A new attribute,
thesis_title
, is added, along with a methoddisplay_thesis()
to show the thesis title.
Example: Creating Instances of Subclasses
Now let’s create instances of the Student
and GraduateStudent
classes to see how inheritance works in practice:
# Creating a Student object
= Student("Alice", "S12345", 3.9)
student1 print(student1.display_info())
# Creating a GraduateStudent object
= GraduateStudent("Bob", "G54321", 4.0, "Quantum Computing")
grad_student1 print(grad_student1.display_info())
print(grad_student1.display_thesis())
Student: Alice, ID: S12345, GPA: 3.9
Student: Bob, ID: G54321, GPA: 4.0
Thesis: Quantum Computing
As shown, GraduateStudent
inherits the display_info()
method from the Student
class, and we can also use the display_thesis()
method that is specific to GraduateStudent
.
16.4.3 Overriding Methods
One of the most useful aspects of inheritance is the ability to override methods from the parent class in the child class. This allows the child class to modify or extend the behavior of a method without affecting the parent class.
Let’s say we want the GraduateStudent
class to display more detailed information about the student, including the thesis title, when calling the display_info()
method. We can override this method in the GraduateStudent
class:
class GraduateStudent(Student):
def __init__(self, name, student_id, gpa, thesis_title):
super().__init__(name, student_id, gpa)
self.thesis_title = thesis_title
# Overriding the display_info method
def display_info(self):
return f"Graduate Student: {self.name}, ID: {self.student_id}, GPA: {self.gpa}, Thesis: {self.thesis_title}"
Now, when display_info()
is called on a GraduateStudent
object, it uses the overridden version of the method instead of the parent class’s version:
= GraduateStudent("Bob", "G54321", 4.0, "Quantum Computing")
grad_student1 print(grad_student1.display_info())
Graduate Student: Bob, ID: G54321, GPA: 4.0, Thesis: Quantum Computing
In this case, we have enhanced the method from the parent class to include additional information about the student’s thesis.
16.4.4 The super()
Function
The super()
function is crucial when working with inheritance. It allows you to call methods from the parent class, which is especially useful when overriding methods. As we saw in the example above, super()
was used to call the parent class’s __init__()
method, so we didn’t have to duplicate the code that initializes the common attributes.
Here’s a breakdown of when to use super()
:
- Constructor Chaining: When defining a child class, you often need to initialize the attributes of the parent class. Using
super()
ensures that the parent class’s__init__()
method is called automatically, making it easier to maintain your code. - Overriding Methods: If a method in the child class overrides a method in the parent class but you still need access to the parent class’s version of the method,
super()
allows you to call it.
class GraduateStudent(Student):
def __init__(self, name, student_id, gpa, thesis_title):
super().__init__(name, student_id, gpa)
self.thesis_title = thesis_title
def display_info(self):
return super().display_info() + f", Thesis: {self.thesis_title}"
In this case, we use super().display_info()
to call the parent class’s method and then add additional information about the thesis. This keeps the code for displaying the basic student information centralized in the Student
class, avoiding code duplication.
16.4.5 Multiple Inheritance
Python supports multiple inheritance, which allows a class to inherit from more than one parent class. While this can be a powerful tool, it should be used with caution because it can lead to more complex and harder-to-maintain code. Let’s look at an example:
class Athlete:
def __init__(self, sport):
self.sport = sport
def show_sport(self):
return f"Sport: {self.sport}"
class StudentAthlete(Student, Athlete):
def __init__(self, name, student_id, gpa, sport):
__init__(self, name, student_id, gpa)
Student.__init__(self, sport)
Athlete.
def display_info(self):
return f"{self.name}, ID: {self.student_id}, GPA: {self.gpa}, Sport: {self.sport}"
In this example, the StudentAthlete
class inherits from both Student
and Athlete
. This allows the student-athlete to have both academic information (name, ID, GPA) and athletic information (sport). However, notice that we have to explicitly call both Student.__init__()
and Athlete.__init__()
because both parent classes need to be initialized.
= StudentAthlete("Charlie", "S98765", 3.8, "Basketball")
student_athlete1 print(student_athlete1.display_info())
Charlie, ID: S98765, GPA: 3.8, Sport: Basketball
In Python, multiple inheritance is resolved using a method resolution order (MRO), which determines the order in which classes are checked when calling a method. Python uses a depth-first, left-to-right search through the inheritance chain, ensuring that the proper methods are called.
16.5 Encapsulation and Data Hiding
Encapsulation is a core concept in Object-Oriented Programming (OOP) that refers to bundling data (attributes) and methods (functions) that operate on that data into a single unit called an object. This not only helps with organization but also protects the internal state of an object from unintended interference or misuse. Encapsulation allows objects to expose only necessary information to the outside world while hiding their internal workings.
This idea is similar to how most real-world objects work: you interact with them in simple ways, without needing to know all the underlying mechanics. Let’s explore this further with examples, and then move on to the specific programming techniques used to implement encapsulation in Python.
16.5.1 Data Hiding: The Foundation of Encapsulation
In programming, data hiding is the practice of restricting access to certain attributes and methods within an object. Data hiding is a key aspect of encapsulation, as it ensures that an object’s internal state cannot be altered in unintended or unpredictable ways by external code. In Python, we achieve data hiding by using certain naming conventions.
- Public attributes: These attributes and methods are accessible from outside the class and can be used or modified freely. By default, all attributes in Python are public unless specified otherwise.
- Private attributes: Private attributes are intended to be hidden from outside the class. In Python, we make an attribute private by prefixing its name with a double underscore (
__
).
Let’s see how this works in practice with a simple example.
Example: Bank Account with Private Balance
class BankAccount:
def __init__(self, account_number, initial_balance):
self.account_number = account_number # Public attribute
self.__balance = initial_balance # Private attribute
def deposit(self, amount):
if amount > 0:
self.__balance += amount
else:
raise ValueError("Deposit amount must be positive")
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
else:
raise ValueError("Insufficient funds or invalid amount")
def get_balance(self):
return self.__balance
In this example: - account_number
is a public attribute, meaning anyone can access and modify it. - __balance
is a private attribute, meaning it cannot be accessed or modified directly from outside the class. - The class provides public methods like deposit()
, withdraw()
, and get_balance()
to interact with the private balance in a controlled way.
Let’s see how this works in action:
# Creating a bank account
= BankAccount("12345", 1000)
account
# Accessing public attribute
print(account.account_number)
12345
# Accessing private attribute directly (raises AttributeError)
print(account.__balance) # Raises: AttributeError: 'BankAccount' object has no attribute '__balance'
# Accessing balance through the public method
print(account.get_balance())
# Depositing money
500)
account.deposit(print(account.get_balance())
# Withdrawing money
200)
account.withdraw(print(account.get_balance())
1000
1500
1300
As you can see, attempting to directly access __balance
raises an error, demonstrating that the attribute is hidden from outside access. Instead, users of the BankAccount
class must interact with the balance through the deposit()
, withdraw()
, and get_balance()
methods. This ensures that the balance is only modified in valid ways, preserving the integrity of the object.
16.5.2 Getter and Setter Methods
Encapsulation does not mean that the data inside an object is completely inaccessible. Often, we need a way to safely access or modify private attributes. In such cases, classes can provide getter and setter methods to expose specific attributes to the outside world, while still maintaining control over how they are accessed or modified.
Let’s modify our BankAccount
class to include a setter for the balance attribute:
class BankAccount:
def __init__(self, account_number, initial_balance):
self.account_number = account_number
self.__balance = initial_balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
else:
raise ValueError("Deposit amount must be positive")
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
else:
raise ValueError("Insufficient funds or invalid amount")
# Getter for balance
def get_balance(self):
return self.__balance
# Setter for balance (with validation)
def set_balance(self, amount):
if amount >= 0:
self.__balance = amount
else:
raise ValueError("Balance cannot be negative")
Now we have added a set_balance()
method to allow controlled modification of the private __balance
attribute. The setter ensures that the balance cannot be set to a negative value, preserving the consistency of the account’s data.
# Setting the balance directly using the setter method
2000) account.set_balance(
# Trying to set a negative balance (raises ValueError)
-500) # Raises: ValueError: Balance cannot be negative account.set_balance(
By using getter and setter methods, we provide a controlled interface for accessing and modifying private data. This helps prevent improper use of an object’s data, enforcing rules and validation where necessary.
16.6 Polymorphism
Polymorphism is another fundamental concept in Object-Oriented Programming (OOP). The term “polymorphism” comes from the Greek words poly (meaning “many”) and morph (meaning “forms”). In programming, polymorphism allows objects of different classes to be treated as objects of a common parent class, while still preserving their individual behaviors. The primary benefit of polymorphism is that it enables the same operation to behave differently on different types of objects.
Polymorphism is essential in creating flexible and reusable code. By allowing methods to take many forms, polymorphism makes it possible to design systems where the exact behavior of an object can vary depending on the object’s class, yet all objects share a common interface. This allows for a more generalized and powerful approach to programming.
16.6.1 Real-World Example: Payment Systems
Imagine a payment processing system for an online store. The store accepts multiple types of payment: credit cards, PayPal, and gift cards. Even though each payment method is different, they all share the same core behavior: they allow customers to make payments. In this scenario, you can create a common interface (or parent class) called Payment
, and then create specific subclasses like CreditCardPayment
, PayPalPayment
, and GiftCardPayment
.
Each subclass implements its own version of a process_payment()
method, but all can be treated as instances of the Payment
class. This is polymorphism in action: the same method name (process_payment()
) works on different types of payment objects, each with its own implementation of the behavior.
16.6.2 Polymorphism Through Method Overriding
Polymorphism in OOP is often achieved through method overriding. When a subclass provides a specific implementation of a method that is already defined in its parent class, this is known as overriding. The method in the subclass replaces the method in the parent class, while still sharing the same interface.
Let’s build an example based on our payment processing system.
class Payment:
def process_payment(self, amount):
raise NotImplementedError("Subclasses must implement this method")
class CreditCardPayment(Payment):
def process_payment(self, amount):
return f"Processing credit card payment of {amount}"
class PayPalPayment(Payment):
def process_payment(self, amount):
return f"Processing PayPal payment of {amount}"
class GiftCardPayment(Payment):
def process_payment(self, amount):
return f"Processing gift card payment of {amount}"
In this example: - Payment
is the parent class, which defines the method process_payment()
but leaves its implementation to the subclasses. - The subclasses (CreditCardPayment
, PayPalPayment
, and GiftCardPayment
) each override the process_payment()
method with their specific implementations.
Now, let’s see polymorphism in action by creating different payment objects and processing payments through the same interface:
= [CreditCardPayment(), PayPalPayment(), GiftCardPayment()]
payments
for payment in payments:
print(payment.process_payment(100))
Processing credit card payment of 100
Processing PayPal payment of 100
Processing gift card payment of 100
Even though we have three different types of payment objects, they all share the same interface (process_payment()
), and we can treat them as instances of the Payment
class. This makes the code more flexible and easier to extend.
16.6.3 Polymorphism Through Method Overloading (Not Native to Python)
Some programming languages allow method overloading, where multiple methods in the same class share the same name but have different parameter lists. While Python doesn’t support method overloading natively, polymorphism can still be achieved through other means, such as using default parameters or the *args
and **kwargs
syntax to handle variable numbers of arguments.
Here’s an example using default parameters to simulate method overloading in Python:
class Calculator:
def add(self, a, b=0, c=0):
return a + b + c
In this example, the add()
method can be called with one, two, or three arguments, providing flexibility in how the method is used.
= Calculator()
calc print(calc.add(5))
print(calc.add(5, 10))
print(calc.add(5, 10, 20))
5
15
35
Although Python doesn’t support method overloading in the traditional sense, this technique allows you to achieve similar functionality.
16.6.4 Polymorphism in Python with Duck Typing
Python’s approach to polymorphism relies heavily on a concept called duck typing. Duck typing is based on the principle that “if it looks like a duck and quacks like a duck, it’s probably a duck.” In Python, an object’s suitability for a given operation is determined by whether it has the necessary attributes and methods, rather than its class hierarchy.
Let’s see how duck typing works with an example:
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
def make_animal_speak(animal):
print(animal.speak())
# Using different objects with the same method
= Dog()
dog = Cat()
cat
make_animal_speak(dog) make_animal_speak(cat)
Woof!
Meow!
In this example, both Dog
and Cat
have a speak()
method, and the function make_animal_speak()
works with both objects without caring about their specific types. This is a form of polymorphism enabled by duck typing—Python doesn’t require a strict type hierarchy as long as the necessary method (speak()
) is implemented.
16.6.5 Advantages of Polymorphism
Polymorphism offers several key benefits that make it a cornerstone of OOP:
Code Reusability: Polymorphism allows you to write code that works with objects of different types but share a common interface. This reduces duplication and promotes reusability.
Extensibility: When new subclasses are added, they automatically work with the existing polymorphic methods without requiring changes to the code that uses the parent class.
Flexibility: Since polymorphism allows methods to adapt to different object types, you can write more general and flexible programs. This makes it easier to build and extend complex systems.
Readability and Maintainability: Polymorphism allows you to design cleaner and more understandable code by abstracting common behaviors into a shared interface.
16.7 Exercises
Exercise 1: Create a Book Class
Define a Book
class with the following attributes: - title
(str) - author
(str) - pages
(int) - year_published
(int)
Implement a method description()
that returns a string with the book’s title, author, and the number of pages. Create a few Book
objects and print their descriptions.
Exercise 2: Car Dealership Simulation
Using the Car
class example from the chapter, create an inventory system for a car dealership. Define a list of cars with different makes, models, and years. Write a method that searches the inventory and returns all cars from a particular year.
Exercise 3: Create a Vehicle Class with Subclasses
Define a parent class Vehicle
with attributes make
and model
. Then, define two subclasses Truck
and Motorcycle
, each with additional attributes (Truck
has a payload_capacity
, and Motorcycle
has cc
for engine capacity). Both subclasses should override a method vehicle_type()
that prints the specific type of vehicle. Create objects for both subclasses and call their methods.
Exercise 4: University System
Expand on the university system example in the chapter by adding a Professor
class that inherits from Person
. In addition to the Person
attributes (e.g., name, age), the Professor
class should include a department
and courses_taught
. Implement a method that prints the professor’s name and department, and a method to add a course to the professor’s list of courses.
Exercise 5: Bank Account with Private Attributes
Create a BankAccount
class where the balance is a private attribute. Write getter and setter methods to access and modify the balance, ensuring that the balance can never be negative. Write a method withdraw()
that subtracts a specified amount from the balance and a method deposit()
to add to the balance. Test these methods to verify the data is encapsulated properly.
Exercise 6: Employee Management System
Create a class Employee
that has private attributes name
and salary
. Implement getter and setter methods for the salary that ensure a minimum salary threshold of $30,000. Write a method to display the employee’s name and salary, and test the setter to ensure no invalid salaries are set.
Exercise 7: Animal Sound System
Create a parent class Animal
with a method speak()
. Then, create subclasses Dog
, Cat
, and Bird
, each overriding the speak()
method to return a sound appropriate to the animal. Write a function that accepts any Animal
object and prints the result of calling its speak()
method.
Exercise 8: Shape Class with Polymorphism
Define a parent class Shape
with a method area()
. Create two subclasses, Circle
and Rectangle
, each overriding the area()
method to calculate the respective areas. Write a function that takes a list of shapes and prints their areas using polymorphism.