Of course! Since "Python Programming 4" isn't a standard title, I'll interpret it as the fourth core topic in a typical Python learning journey.

After mastering the basics (variables, data types, control flow), functions, and data structures (lists, dictionaries), the next major pillar is Object-Oriented Programming (OOP).
Let's dive into Python Programming 4: Object-Oriented Programming (OOP).
Python Programming 4: Object-Oriented Programming (OOP)
Object-Oriented Programming is a programming paradigm that uses "objects" as the fundamental building blocks of your programs. Instead of writing code as a long sequence of instructions, you organize your code into logical, reusable, and self-contained units called classes and objects.
Think of it like building with LEGOs. You have a blueprint for a specific LEGO piece (a class), and then you can create many individual pieces from that blueprint (the objects).

Why Use OOP?
- Reusability: Write a class once and create many objects from it.
- Organization: Code is organized into logical, self-contained units.
- Maintainability: It's easier to update and debug code when it's well-structured.
- Abstraction: You can hide complex implementation details and show only the necessary features (like using a TV remote without knowing the circuitry inside).
Core Concepts of OOP in Python
We'll cover the four main pillars:
- Class
- Object (or Instance)
- Encapsulation
- Inheritance
Class: The Blueprint
A class is a blueprint for creating objects. It defines a set of attributes (data) and methods (functions) that the created objects will have.
Syntax:
class ClassName:
# Class attributes (shared by all instances)
class_attribute = "I am a class attribute"
# The initializer method (runs when a new object is created)
def __init__(self, param1, param2):
# Instance attributes (unique to each object)
self.attribute1 = param1
self.attribute2 = param2
# Instance method (operates on an object)
def method1(self):
print(f"Method 1 is called for {self.attribute1}")
# Another instance method
def method2(self, value):
print(f"Method 2 received value: {value}")
Key Points:

class ClassName:defines the class.__init__is a special method called the constructor or initializer. It's automatically called when you create a new object from the class.selfis a special parameter that represents the instance of the class itself. It's how the object refers to its own attributes and methods. You must include it as the first parameter of any instance method, but you don't provide it when you call the method.
Object (Instance): The Thing Built from the Blueprint
An object is an instance of a class. You create objects by calling the class name as if it were a function.
Example: Let's model a Dog
# 1. Define the Class (the blueprint)
class Dog:
# Class attribute
species = "Canis familiaris"
# The __init__ method to initialize a new Dog object
def __init__(self, name, age, breed):
print(f"Creating a new dog named {name}...")
# Instance attributes
self.name = name
self.age = age
self.breed = breed
# Instance method
def description(self):
return f"{self.name} is a {self.age} year old {self.breed}."
# Another instance method
def speak(self, sound):
return f"{self.name} says {sound}"
# 2. Create Objects (instances) from the Class
dog1 = Dog("Buddy", 3, "Golden Retriever")
dog2 = Dog("Lucy", 5, "Dachshund")
# 3. Access attributes and methods
print(f"Dog 1's name: {dog1.name}")
print(f"Dog 2's breed: {dog2.breed}")
print("\n--- Dog Descriptions ---")
print(dog1.description())
print(dog2.description())
print("\n--- Dog Sounds ---")
print(dog1.speak("Woof! Woof!"))
print(dog2.speak("Yip! Yip!"))
# Accessing a class attribute
print(f"\nBoth dogs are part of the species: {Dog.species}")
print(f"Dog 1's species: {dog1.species}") # You can also access it from an instance
Output:
Creating a new dog named Buddy...
Creating a new dog named Lucy...
Dog 1's name: Buddy
Dog 2's breed: Dachshund
--- Dog Descriptions ---
Buddy is a 3 year old Golden Retriever.
Lucy is a 5 year old Dachshund.
--- Dog Sounds ---
Buddy says Woof! Woof!
Lucy says Yip! Yip!
Both dogs are part of the species: Canis familiaris
Dog 1's species: Canis familiaris
Encapsulation: Bundling Data and Methods
Encapsulation is the idea of bundling the data (attributes) and the methods that operate on that data into a single unit (the class). It also involves restricting direct access to some of an object's components, which is known as data hiding.
In Python, there's no strict "private" keyword like in other languages, but a convention is used:
- Public: No underscore (e.g.,
self.name). Accessible from anywhere. - Protected: One leading underscore (e.g.,
_internal_var). Conventionally, it should not be accessed outside the class, but it's still technically possible. - Private: Two leading underscores (e.g.,
__private_var). Name mangling occurs, making it very difficult to access from outside the class.
Example: A BankAccount with encapsulation
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # Private attribute
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.")
def get_balance(self):
# This is a "getter" method, a controlled way to access the private data
return self.__balance
# Create an account
my_account = BankAccount("Alice", 1000)
# You can access public attributes
print(f"Account owner: {my_account.owner}")
# You CANNOT directly access the private __balance attribute
# This will cause an error:
# print(my_account.__balance) # AttributeError: 'BankAccount' object has no attribute '__balance'
# You must use the public methods to interact with the private data
my_account.deposit(500)
my_account.withdraw(200)
# Use the getter method to see the balance
print(f"Current balance: ${my_account.get_balance()}")
Inheritance: Creating a "Is-A" Relationship
Inheritance allows a new class (the child or subclass) to inherit attributes and methods from an existing class (the parent or superclass). This promotes code reuse and establishes a logical "is-a" relationship.
Example: A Cat class that inherits from Animal
# Parent Class
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclass must implement this abstract method")
# Child Class
class Cat(Animal):
def __init__(self, name, breed):
# Call the parent class's __init__ method
super().__init__(name)
self.breed = breed
# Override the parent's speak method
def speak(self):
return "Meow"
# Another Child Class
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
# Override the parent's speak method
def speak(self):
return "Woof"
# Create objects
whiskers = Cat("Whiskers", "Siamese")
buddy = Dog("Buddy", "Labrador")
print(f"{whiskers.name} says: {whiskers.speak()}")
print(f"{buddy.name} says: {buddy.speak()}")
Key Points:
class Cat(Animal):meansCatinherits fromAnimal.super().__init__(name)calls the constructor of the parent class, ensuring the parent's part of the object is initialized
