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:
- Method Overriding (runtime polymorphism): A child class provides a specific implementation of a method already defined in the parent class.
- 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 |