Object-oriented programming (OOP) is a fundamental programming paradigm in Python that structures code into modular and reusable blocks called objects. Classes provide the blueprint for how objects are defined, enabling developers to model real-world entities with code. This guide will walk through practical exercises for creating custom classes and working with class instances as objects in Python. We will cover key OOP concepts like abstraction, encapsulation, inheritance and polymorphism.
Table of Contents
Open Table of Contents
Introduction
Python fully supports OOP, making it easy to represent objects and their interactions. Objects allow bundling data and behavior together for organized, flexible programming. Classes act as templates for creating objects with shared attributes and methods.
Some key benefits of OOP in Python include:
-
Modularity - Each class encapsulates related properties and actions, separating concerns into single units.
-
Reusability - Common logic can be defined once in a parent class and inherited by child classes.
-
Extensibility - New features can be added by creating new classes without modifying existing ones.
-
Maintainability - Each class is responsible for specific functionality, easing debugging and updates.
We will explore these advantages by incrementally developing classes to model real-world objects. Let’s start with a simple class definition.
Defining and Instantiating Classes
A class definition starts with the class
keyword, followed by the class name and a colon. Indented underneath are the class contents including attributes and methods:
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def description(self):
return f"{self.name} is {self.age} years old"
This Dog
class has two attributes - name
and age
, set during instantiation via the special __init__
method. It also defines a description()
method to describe the dog.
We can create individual dog objects using the Dog()
constructor:
dog1 = Dog("Rex", 4)
print(dog1.description())
# Rex is 4 years old
The dog1
object encapsulates the name
and age
data along with access to the description()
method. Class attributes are accessed via dot notation on the instance.
Let’s try modeling some more types of objects with custom classes.
Modeling Real-World Entities
OOP shines for representing real-world entities like people, places and things in code. We can identify common attributes and functionality to abstract into classes.
Person Class
A Person
class could have properties like name
, age
, job
and methods such as introduce()
and celebrate_birthday()
:
class Person:
def __init__(self, name, age, job):
self.name = name
self.age = age
self.job = job
def introduce(self):
statement = f"Hello, my name is {self.name}. I am {self.age} years old and work as a {self.job}."
print(statement)
def celebrate_birthday(self):
self.age += 1
person1 = Person("Maria", 32, "Data Analyst")
person1.introduce()
# Hello, my name is Maria. I am 32 years old and work as a Data Analyst.
person1.celebrate_birthday()
print(person1.age)
# 33
We can instantiate distinct Person
objects to represent individuals with shared methods like introduce()
customized via their attributes.
Vehicle Class
We can similarly model vehicles with properties like make
, model
, year
and methods like description()
and drive()
:
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def description(self):
desc_str = f"{self.year} {self.make} {self.model}"
print(desc_str)
def drive(self):
print(f"The {self.model} is driving")
car = Vehicle('Toyota', 'Prius', 2020)
car.description()
# 2020 Toyota Prius
car.drive()
# The Prius is driving
The Vehicle
class allows us to describe and manipulate various vehicle objects with the same interface.
Product Class
Similarly, a basic Product
class can represent retail items in an e-commerce app:
class Product:
def __init__(self, name, price, qty):
self.name = name
self.price = price
self.qty = qty
def get_total(self):
total = self.price * self.qty
print(f"Total: ${total:.2f}")
shirt = Product("T-Shirt", 15, 2)
pants = Product("Jeans", 50, 1)
shirt.get_total()
# Total: $30.00
pants.get_total()
# Total: $50.00
Custom classes like these provide building blocks to construct programs modeling complex real-world systems. Next we’ll explore how to leverage inheritance between classes.
Inheriting Attributes and Behavior
A key OOP concept is inheritance - allowing child classes to inherit attributes and methods from parent classes.
For example, we can create a ElectricVehicle
class that inherits from the Vehicle
parent:
class ElectricVehicle(Vehicle):
def __init__(self, make, model, year, battery_size):
super().__init__(make, model, year)
self.battery_size = battery_size
ev = ElectricVehicle('Tesla', 'Model 3', 2020, 75)
ev.description()
# 2020 Tesla Model 3
The child ElectricVehicle
class inherits the __init__
constructor and description()
method from Vehicle
. We expand on the parent by adding a battery_size
attribute in the child constructor.
The super()
function calls the parent __init__
allowing us to leverage inherited behavior. Child classes can override or expand upon parent class functionality.
Let’s override the drive()
method to customize it:
#...Rest of ElectricVehicle class
def drive(self):
print("The electric vehicle is driving silently")
ev.drive()
# The electric vehicle is driving silently
We’ve tailored the drive()
logic for electric vehicles while reusing the Vehicle
parent class. This demonstrates polymorphism - child classes can implement behavior specific to their type.
Creating Subclasses for Specialization
We can further specialize classes through sub-classing. For example, an ElectricCar
class:
class ElectricCar(ElectricVehicle):
def __init__(self, make, model, year):
super().__init__(make, model, year, battery_size=75)
def charge(self):
print(f"Charging {self.make} {self.model}")
ecar = ElectricCar('Tesla', 'Model S', 2018)
ecar.charge()
# Charging Tesla Model S
ElectricCar
inherits from ElectricVehicle
allowing us to further specialize the subclass. We customize the constructor and add a charge()
method specific to electric cars vs. a generic electric vehicle.
Subclassing enables extensive reuse of logic between related classes. This powerful hierarchy of inheritance is a key advantage of OOP.
Encapsulating Class Details
A key principle in OOP is encapsulation - bundling data and methods into a single class and limiting outside access. This gives control over the internal representation.
For example, we can make the battery_size
attribute private:
class ElectricVehicle:
def __init__(self, make, model, year):
#...
self.__battery_size = 75 # private attribute
def get_range(self):
range = self.__battery_size * 5
print(f"Range is {range} miles")
ev = ElectricVehicle('Tesla','Model 3', 2018)
ev.get_range()
# Range is 375 miles
ev.__battery_size # Raises AttributeError
The private __battery_size
attribute can only be accessed via the get_range()
method. This prevents external code from inadvertently breaking internal object details.
Getters and setters can expand encapsulation by implementing access logic:
class ElectricVehicle:
# ...
def get_battery_size(self):
print(f"Battery size: {self.__battery_size} kWh")
def set_battery_size(self, new_size):
if new_size < 75:
raise ValueError("Battery too small")
else
self.__battery_size = new_size
ev = ElectricVehicle('Tesla','Model 3', 2018)
ev.get_battery_size()
# Battery size: 75 kWh
ev.set_battery_size(100)
ev.get_battery_size()
# Battery size: 100 kWh
This control over access restrictions and data validation enhances encapsulation in OOP.
Leveraging Magic Methods
Python classes have special __method__()
names called magic methods that act as hooks or shortcuts for built-in behavior.
For example, the __str__()
method returns a string representation of the object:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"{self.name} is {self.age} years old"
user = Person('Eric', 25)
print(user)
# Eric is 25 years old
This allows customizing string conversion instead of the default <__main__.Person object>
. Other useful built-ins include __add__()
for +
operator overloading and __len__()
to override len()
.
Magic methods give extensive control over class behavior and integration with Python features.
Conclusion
In this guide, we explored practical OOP techniques in Python such as:
- Defining classes and instantiating objects
- Modeling real-world entities with classes
- Inheriting behavior between parent and child classes
- Specializing functionality through subclassing
- Encapsulating implementation details
- Leveraging magic methods for built-in behavior
Object-oriented programming facilitates code reuse, maintainability and scalability using abstraction, encapsulation, inheritance and polymorphism. Mastering classes unlocks Python’s capabilities for elegantly modeling complex systems across various domains.
The class mechanics provide the building blocks - the key is creativity in analyzing problems to effectively apply OOP principles. There are infinite possibilities for leveraging classes to produce concise, expressive and extensible code.