Object-Oriented Programming (OOP)

Classes and objects

What Are Classes and Objects?

A class is a blueprint for creating objects — a way to bundle data and functionality together.

An object is an instance of a class, representing a specific implementation of that blueprint.

Defining a Class

class Dog:
def __init__(self, name, breed):
self.name = name # attribute
self.breed = breed # attribute

def bark(self): # method
print(f”{self.name} says woof!”)

Key Parts:

  • __init__: a constructor that runs when an object is created.
  • self: refers to the current object instance.
  • name and breed: attributes of the object.
  • bark: a method, a function that belongs to the class.

Creating Objects (Instances)

dog1 = Dog(“Buddy”, “Labrador”)
dog2 = Dog(“Coco”, “Poodle”)

dog1.bark() # Buddy says woof!
dog2.bark() # Coco says woof!

Each object has its own state (data) and can use the class’s methods.

Accessing and Modifying Attributes

print(dog1.name) # Buddy
dog1.name = “Max”
print(dog1.name) # Max

Why Use Classes?

  • Encapsulation: bundle data + behavior.
  • Reusability: define once, create many objects.
  • Organization: keeps code clean and modular.
  • Extensibility: easy to expand with new methods or attributes.

Example with Behavior

class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance

def deposit(self, amount):
self.balance += amount

def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
else:
print(“Insufficient funds”)

account = BankAccount(“Alice”, 100)
account.deposit(50)
account.withdraw(30)
print(account.balance) # 120

Summary:

Concept Explanation
Class A template that defines the structure and behavior of objects (instances)
Object A concrete instance created from a class, with its own data and behavior
Attributes Variables that store object state (defined in __init__ as self.attribute)
Methods Functions defined within a class that operate on object data (always take self parameter)
__init__() Constructor method that initializes new objects (automatically called when creating instances)

Constructors (__init__)

What Is a Constructor?

A constructor is a special method in a class that’s automatically called when a new object (instance) is created.

In Python, this constructor method is named __init__().

Purpose of __init__()

  • To initialize attributes (variables) of a new object.
  • To ensure each object starts in a well-defined state.
  • To optionally accept arguments during object creation.

Basic Syntax

class Person:
def __init__(self, name, age): # Constructor
self.name = name
self.age = age

  • self: Refers to the current instance.
  • name, age: Parameters used to set object-specific data.

Creating an Object

p1 = Person(“Alice”, 30)
print(p1.name) # Alice
print(p1.age) # 30

When Person(“Alice”, 30) is called:

  • Python creates a new Person object.
  • Calls __init__() with name=”Alice” and age=30.
  • Sets self.name and self.age.

Default Values

You can set default values for parameters:

class Person:
def __init__(self, name=”Unknown”, age=0):
self.name = name
self.age = age

Now you can call Person() with no arguments:

p2 = Person()
print(p2.name, p2.age) # Unknown 0

Points to Remember

Feature Description
Special method __init__ is automatically called when creating a new object instance
Self parameter Refers to the current object instance (must be first parameter in method definition)
Initialization Primary purpose is to initialize the object’s attributes and state
Default values Parameters can have default values to make them optional during instantiation
Not mandatory Can be omitted if no initialization logic is required (Python provides default)

Without __init__

class Empty:
pass

e = Empty()

This still works — but the object won’t have any initialized attributes unless added manually.

Summary:

  • __init__ is Python’s constructor method, used to initialize object attributes.
  • It runs automatically when an object is created.
  • It makes your classes more flexible, dynamic, and useful.

Instance and class variables

What Are Instance and Class Variables?

Instance Variables are variables that are specific to each instance (object) of a class.

Class Variables are variables that are shared across all instances of the class. These are stored at the class level and have the same value for every object of the class.

1. Instance Variables

What Are They?

  • Instance variables are unique to each object. Every time a new object is created, it can have its own values for instance variables.
  • They are typically initialized inside the __init__() constructor.

Example

class Dog:
def __init__(self, name, age):
self.name = name # instance variable
self.age = age # instance variable

dog1 = Dog(“Buddy”, 3)
dog2 = Dog(“Bella”, 5)

print(dog1.name) # Buddy
print(dog2.name) # Bella

In this case, name and age are instance variables because each dog object can have different values for these attributes.

2. Class Variables

What Are They?

  • Class variables are shared across all instances of the class.
  • They are typically defined inside the class but outside of any methods.
  • Class variables are often used for properties that should be common to all objects, such as a constant or counter.

Example

class Dog:
species = “Canine” # class variable

def __init__(self, name, age):
self.name = name # instance variable
self.age = age # instance variable

dog1 = Dog(“Buddy”, 3)
dog2 = Dog(“Bella”, 5)

print(dog1.species) # Canine
print(dog2.species) # Canine

# Changing class variable for the class
Dog.species = “Dog”

print(dog1.species) # Dog
print(dog2.species) # Dog

  • species is a class variable because it is the same for all instances of Dog. If you change it at the class level, it changes for all instances automatically.

Differences Between Instance and Class Variables

Feature Instance Variables Class Variables
Scope Unique to each object instance Shared among all instances of the class
Initialization Defined in __init__() method using self Defined directly in class body (outside methods)
Access Accessed via self.<variable_name> Accessed via ClassName.<variable_name> or self.<variable_name>
Usage Stores object-specific data (unique state) Stores class-level data (shared state)

3. Modifying Class Variables

Class variables can be accessed and modified using the class name or instance:

class Dog:
species = “Canine” # class variable

dog1 = Dog(“Buddy”, 3)
dog2 = Dog(“Bella”, 5)

print(dog1.species) # Canine

# Changing class variable using the class name
Dog.species = “Dog”

print(dog1.species) # Dog
print(dog2.species) # Dog

Warning:

Modifying a class variable using an instance (e.g., dog1.species = “New species”) will create an instance variable with that name, effectively shadowing the class variable.

Summary:

  • Instance Variables: Unique to each object; defined inside the __init__() method using self.
  • Class Variables: Shared by all objects of the class; defined directly in the class body.

Inheritance and polymorphism

What Is Inheritance?

Inheritance is a way to allow a new class (child class) to inherit attributes and methods from an existing class (parent class). This promotes code reuse and allows for creating more specialized classes based on general ones.

Key Concepts:

  • Parent Class (or Base Class): The class being inherited from.
  • Child Class (or Derived Class): The class that inherits from the parent class.

Example of Inheritance

# Parent Class
class Animal:
def __init__(self, name):
self.name = name

def speak(self):
print(f”{self.name} makes a sound”)

# Child Class
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # Calls the __init__ of Animal
self.breed = breed

def speak(self): # Method overriding
print(f”{self.name} barks”)

class Cat(Animal):
def __init__(self, name):
super().__init__(name) # Calls the __init__ of Animal

def speak(self): # Method overriding
print(f”{self.name} meows”)

# Using the classes
dog = Dog(“Buddy”, “Golden Retriever”)
cat = Cat(“Whiskers”)

dog.speak() # Buddy barks
cat.speak() # Whiskers meows

Key Points:

  • The child class Dog and Cat inherit from the parent class Animal.
  • The Dog and Cat classes override the speak() method to provide behavior specific to dogs and cats.
  • super().__init__(name): Calls the constructor (__init__()) of the parent class to initialize the name attribute.

What Is Polymorphism?

Polymorphism means “many forms”. In Python, it allows different classes to be used interchangeably if they implement the same method signature (e.g., method name and parameters), even if their internal implementation differs.

Types of Polymorphism in Python:

  1. Method Overriding (runtime polymorphism): A child class provides a specific implementation of a method already defined in the parent class.
  2. Method Overloading (not directly supported in Python, but can be mimicked using default arguments).

Example of Polymorphism (Method Overriding)

class Bird(Animal):
def __init__(self, name, color):
super().__init__(name)
self.color = color

def speak(self): # Overriding the speak method
print(f”{self.name} chirps”)

# Using Polymorphism: Treating objects of different types as the same type
def make_sound(animal):
animal.speak()

dog = Dog(“Buddy”, “Golden Retriever”)
cat = Cat(“Whiskers”)
bird = Bird(“Tweety”, “Yellow”)

# All three classes have a `speak()` method, so we can pass them to the same function
make_sound(dog) # Buddy barks
make_sound(cat) # Whiskers meows
make_sound(bird) # Tweety chirps

Key Points:

  • The function make_sound() treats all animals the same by calling their speak() method, regardless of their specific type.
  • This demonstrates polymorphism: different animal types can respond differently to the same method call.

Combining Inheritance and Polymorphism

Inheritance and polymorphism often go hand in hand. With inheritance, you can define common functionality in a base class, and with polymorphism, you can override it to provide specific behavior in subclasses.

Example: Combining Both

class Animal:
def __init__(self, name):
self.name = name

def make_sound(self):
raise NotImplementedError(“Subclasses should implement this method”)

class Dog(Animal):
def make_sound(self):
return “Woof!”

class Cat(Animal):
def make_sound(self):
return “Meow!”

# Using Polymorphism
animals = [Dog(“Buddy”), Cat(“Whiskers”)]

for animal in animals:
print(f”{animal.name}: {animal.make_sound()}”)

Output:

Buddy: Woof!
Whiskers: Meow!

In this example:

  • Inheritance allows Dog and Cat to share the make_sound() method.
  • Polymorphism allows each class to have its own specific implementation of make_sound().

Summary:

Concept Description
Inheritance Enables a new class to inherit attributes and methods from an existing class
Polymorphism Allows objects of different classes to be treated as objects of a common parent class, with different behavior based on class type
Method Overriding Child classes can provide their own implementation of a method defined in the parent class
super() Used to call methods from a parent class in the child class, typically for initialization

Special methods (__str__, __repr__, etc.)

What Are Special Methods?

Special methods in Python are those that are defined by convention to interact with built-in Python functions or operators. They always begin and end with double underscores (__method__), which is why they are called “dunder” methods (short for “double underscore”).

These methods allow you to define custom behavior for operations like string representation, arithmetic, comparison, and more.

Commonly Used Special Methods

1. __str__() – String Representation (for End-User)

  • Purpose: The __str__() method is used to define a human-readable string representation of an object. This is what will be returned when you call str() on an object or print the object.
  • It’s meant to provide a readable description of the object for the user.

Example:

class Dog:
def __init__(self, name, age):
self.name = name
self.age = age

def __str__(self):
return f”{self.name} is {self.age} years old”

dog = Dog(“Buddy”, 3)
print(dog) # Buddy is 3 years old

Here, when we print dog, Python calls the __str__() method to return a string representation of the object that is easy for the user to understand.

2. __repr__() – Official String Representation (for Developers)

  • Purpose: The __repr__() method is used to define the official string representation of an object. This string should ideally be a valid Python expression that, when passed to eval(), would recreate the object (though not always possible).
  • It’s intended for developers and is used in the Python interpreter and for debugging.

Example:

class Dog:
def __init__(self, name, age):
self.name = name
self.age = age

def __repr__(self):
return f”Dog(‘{self.name}’, {self.age})”

dog = Dog(“Buddy”, 3)
print(repr(dog)) # Dog(‘Buddy’, 3)

  • __repr__() is typically used for the developer’s view of the object, providing a more detailed, unambiguous representation that could potentially recreate the object using eval().

3. __len__() – Length of Object (for Built-In Functions)

  • Purpose: The __len__() method defines how len() behaves for instances of your class.
  • It should return an integer representing the “length” of the object.

Example:

class Box:
def __init__(self, items):
self.items = items

def __len__(self):
return len(self.items)

box = Box([1, 2, 3, 4])
print(len(box)) # 4

  • Here, __len__() allows us to use the len() function on an instance of Box.

4. __eq__() – Equality Comparison (for ==)

  • Purpose: The __eq__() method allows you to define how == (equality comparison) works for instances of your class.
  • It should return True or False based on whether the two objects are considered equal.

Example:

class Dog:
def __init__(self, name, age):
self.name = name
self.age = age

def __eq__(self, other):
return self.name == other.name and self.age == other.age

dog1 = Dog(“Buddy”, 3)
dog2 = Dog(“Buddy”, 3)
print(dog1 == dog2) # True

Here, we define the == operator to compare two Dog objects based on their name and age attributes.

5. __add__() – Addition (for + Operator)

  • Purpose: The __add__() method allows you to define how the + operator behaves for instances of your class.
  • This can be useful for classes that represent numeric data or collections.

Example:

class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)

point1 = Point(1, 2)
point2 = Point(3, 4)
result = point1 + point2
print(result.x, result.y) # 4 6

Here, we define the + operator to add two Point objects by adding their respective x and y coordinates.

6. __getitem__() – Indexing (for [] Operator)

  • Purpose: The __getitem__() method allows you to define how the indexing operator [] works for your objects.
  • It’s often used in classes that represent sequences or mappings (like lists, tuples, or dictionaries).

Example:

class Basket:
def __init__(self, items):
self.items = items

def __getitem__(self, index):
return self.items[index]

basket = Basket([1, 2, 3, 4])
print(basket[2]) # 3

Here, __getitem__() lets us use the indexing operator [] to retrieve an item from the Basket.

Other Useful Special Methods

  • __setitem__(self, key, value): Used for setting values using the [] operator.
  • __delitem__(self, key): Used for deleting items using the del statement.
  • __iter__(): Returns an iterator for the object (needed for iteration).
  • __next__(): Defines behavior for getting the next item in an iteration.
  • __call__(): Allows an instance of a class to be called like a function.

Summary of Common Special Methods

Method Purpose Example Use Case
__str__() Provides a human-readable string representation of an object Used when printing or converting to string (str())
__repr__() Provides an official string representation of an object for developers Used for debugging and interpreter representations
__len__() Defines the behavior of the len() function for an object Used for classes representing collections
__eq__() Defines the behavior of the == comparison between objects Used for comparing objects
__add__() Defines the behavior of the + operator for objects Used for adding objects (like Point objects)
__getitem__() Defines the behavior of the indexing operator [] Used for accessing elements in a custom collection

 

Leave a Comment