# -*- coding: utf-8 -*-
"""
================================================================================
Comprehensive Python Guide: Object-Oriented Programming (OOP)
================================================================================
This program provides an in-depth demonstration of Object-Oriented Programming
(OOP) concepts in Python. OOP allows us to structure our code by bundling
related properties (attributes) and behaviors (methods) into individual objects.
Sections:
1. Classes and Objects: The basic blueprint and its instances.
2. The Constructor (`__init__`): Initializing object state.
3. Instance Methods: Defining object behavior.
4. Inheritance: Creating a specialized class from a general one.
5. Encapsulation: Protecting data with private attributes.
6. Polymorphism: Using objects of different classes through a common interface.
"""
# A function to print section headers for better readability.
def print_header(title):
"""Prints a formatted header to the console."""
print("\n" + "="*60)
print(f"| {title.center(56)} |")
print("="*60)
# ==============================================================================
# SECTION 1 & 2: CLASSES, OBJECTS, AND THE CONSTRUCTOR
# ==============================================================================
print_header("1. Classes, Objects, and __init__")
class Car:
"""
A class representing a car. This is the blueprint.
Attributes are the properties (e.g., make, model).
Methods are the behaviors (e.g., start_engine).
"""
# The __init__ method is the constructor. It's called when a new object
# is created. 'self' refers to the instance of the class being created.
def __init__(self, make, model, year, color):
print(f"Creating a new Car object: {make} {model}")
self.make = make
self.model = model
self.year = year
self.color = color
self.is_engine_on = False # Default attribute
# Creating objects (instances) of the Car class
my_car = Car("Toyota", "Camry", 2021, "Blue")
your_car = Car("Honda", "Civic", 2022, "Red")
print("\n--- Accessing Object Attributes ---")
print(f"My car's make: {my_car.make}")
print(f"Your car's model: {your_car.model}")
print(f"My car's engine status: {my_car.is_engine_on}")
# ==============================================================================
# SECTION 3: INSTANCE METHODS
# ==============================================================================
print_header("3. Instance Methods")
# We will add methods to our Car class definition.
# For clarity in this demo, we'll redefine the class here. In a real
# application, you'd have all methods in one class block.
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.is_engine_on = False
self.speed = 0
# An instance method that modifies the object's state
def start_engine(self):
if not self.is_engine_on:
self.is_engine_on = True
print(f"The {self.make} {self.model}'s engine is now ON.")
else:
print("The engine is already running.")
# Another instance method
def stop_engine(self):
if self.is_engine_on:
self.is_engine_on = False
self.speed = 0 # Can't move without the engine on
print(f"The {self.make} {self.model}'s engine is now OFF.")
else:
print("The engine is already off.")
# A method that takes an argument
def accelerate(self, amount):
if self.is_engine_on:
self.speed += amount
print(f"The car accelerated to {self.speed} km/h.")
else:
print("Cannot accelerate, the engine is off.")
# Create a new car object from the updated class
my_sedan = Car("Ford", "Fusion", 2020)
my_sedan.start_engine()
my_sedan.accelerate(50)
my_sedan.stop_engine()
my_sedan.accelerate(20) # This will fail
# ==============================================================================
# SECTION 4: INHERITANCE
# ==============================================================================
# Inheritance allows a new class (child) to take on the attributes and methods
# of an existing class (parent).
print_header("4. Inheritance")
# The ElectricCar class inherits from the Car class
class ElectricCar(Car):
"""A specialized class for electric cars."""
# We override the parent's __init__ method
def __init__(self, make, model, year, battery_size):
# super() calls the __init__ method of the parent class (Car)
super().__init__(make, model, year)
# Add a new attribute specific to ElectricCar
self.battery_size = battery_size # in kWh
# We can also override a parent's method
def start_engine(self):
"""Electric cars don't have engines, so we override this method."""
self.is_engine_on = True # We'll reuse this flag for 'motor on'
print(f"The {self.make} {self.model}'s electric motor is now active. Silent and ready!")
# Add a new method specific to the child class
def charge_battery(self):
print(f"Charging the {self.battery_size}-kWh battery.")
my_ev = ElectricCar("Tesla", "Model 3", 2023, 75)
print(f"\nCreated an Electric Car: {my_ev.make} {my_ev.model}")
print(f"It has a battery of {my_ev.battery_size} kWh.")
# Call the overridden method from the child class
my_ev.start_engine()
# Call a method inherited from the parent class
my_ev.accelerate(80)
# Call a method unique to the child class
my_ev.charge_battery()
# ==============================================================================
# SECTION 5: ENCAPSULATION (Private Attributes)
# ==============================================================================
# Encapsulation is the bundling of data (attributes) and the methods that
# operate on that data. It's often used to restrict direct access to attributes.
# In Python, this is done by convention using a leading underscore `_` (protected)
# or double leading underscore `__` (private).
print_header("5. Encapsulation")
class BankAccount:
def __init__(self, owner, starting_balance):
self.owner = owner
# The double underscore "mangles" the name, making it harder to access
# from outside the class. This is now `_BankAccount__balance`.
self.__balance = starting_balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
print(f"Deposited ${amount}. New balance: ${self.__balance}")
else:
print("Deposit amount must be positive.")
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
print(f"Withdrew ${amount}. New balance: ${self.__balance}")
else:
print("Invalid withdrawal amount or insufficient funds.")
# We provide a "getter" method to safely access the balance
def get_balance(self):
return self.__balance
my_account = BankAccount("John Doe", 1000)
print(f"\nAccount owner: {my_account.owner}")
# Direct access to the private attribute is discouraged and won't work easily.
# print(my_account.__balance) # This would raise an AttributeError
print(f"Current balance (via getter method): ${my_account.get_balance()}")
my_account.deposit(500)
my_account.withdraw(2000) # This will fail
my_account.withdraw(300)
# ==============================================================================
# SECTION 6: POLYMORPHISM
# ==============================================================================
# Polymorphism means "many forms". In OOP, it refers to the ability to use
# a common interface for objects of different classes.
print_header("6. Polymorphism")
# We have two classes, Car and ElectricCar, with a common method 'accelerate'
# We also create a simple Boat class with a different implementation.
class Boat:
def accelerate(self, amount):
print(f"The boat chugs forward with {amount} knots of power.")
# A function that can work with any object that has an 'accelerate' method
def make_vehicle_go_fast(vehicle, speed):
print(f"\n--- Testing vehicle: {vehicle.__class__.__name__} ---")
vehicle.accelerate(speed)
# Create a list of different objects
vehicles = [
Car("Ford", "Mustang", 2022),
ElectricCar("Lucid", "Air", 2023, 118),
Boat()
]
# The same function works on different objects, demonstrating polymorphism
for v in vehicles:
# We need to start the engine for cars, but not for the boat
if hasattr(v, 'start_engine'):
v.start_engine()
make_vehicle_go_fast(v, 60)
print("\n" + "="*60)
print("DEMONSTRATION COMPLETE".center(60))
print("="*60)