Object-Oriented Programming in Python: Classes and Objects

Object-Oriented Programming in Python: Classes and Objects

In Object-Oriented Programming (OOP) as the name suggests the programming is organised around objects, which are instances of classes. Python, being a versatile language, supports OOP, allowing developers to write clean, modular, and reusable code. Here in this blog we  will discuss  the concepts of classes and objects in Python, illustrating their usage with practical examples, including an advanced and unique example.

1. Introduction to Object-Oriented Programming

OOP is centered around the idea of organising software design around data, or objects, rather than functions and logic. An object is an instance of a class, and a class defines the blueprint for objects.

Key concepts

  • Class
  • Objects
  • Data Abstraction
  • Encapsulation
  • Inheritance
  • Polymorphism
  • Dynamic Binding
  • Message Passing

.

2. Understanding Classes and Objects in Python

A class in Python can be thought of as a blueprint for creating objects. An object is an instance of a class. Each object can have attributes (data) and methods (functions) that define its behaviour.

2.1. Defining a Class

A class in Python is defined using the `class` keyword, followed by the class name and a colon. Inside the class, methods are defined using the `def` keyword.

Here’s a simple Python Class Example

class Book:
   
Constructor
   
def __init__(self, title, author, publication_year):
       
self.title = title
       
self.author = author
       
self.publication_year = publication_year

 

   

Method to display book details
   
def display_info(self):
       print(f
'"{self.title}" by {self.author}, published in {self.publication_year}')

In this example:

- `__init__`: A special method called a constructor. It is automatically called when an object is created from the class. It initialises the object’s attributes.

- `self`: A reference to the current instance of the class. It’s used to access variables that belong to the class.

2.2 Creating Objects

Create a Book object
book = Book(
"To Kill a Mockingbird", "Harper Lee", 1960)

Display book details
book.display_info()

Once the class is defined, you can create objects from it.

3. Advanced Class Features

3.1. Class Variables vs. Instance Variables

In Python, variables can be defined at the class level or instance level. Class variables are shared across all instances of the class, while instance variables are unique to each object.

class Employee:
   Class variable
   raise_percentage =
1.05

   
def __init__(self, name, salary):
       
self.name = name
       
self.salary = salary

   
def apply_raise(self):
       
self.salary = int(self.salary * Employee.raise_percentage)

Creating two Employee objects

emp1 = Employee("Alice", 50000)
emp2 = Employee(
"Bob", 60000)

Applying raise

emp1.apply_raise()
emp2.apply_raise()

print(emp1.salary)  # Output: 52500
print(emp2.salary)  # Output: 63000

Here, `raise_percentage` is a class variable, meaning it’s shared by all instances of `Employee`. When you call `apply_raise`, it uses this shared class variable to update the salary.

3.2. Class Methods and Static Methods

Python provides two additional types of methods besides instance methods: class methods and static methods.

- Class methods are methods that take the class as the first argument. They are defined using the `@classmethod` decorator.

- Static methods are methods that do not take any specific object or class reference as the first argument. They are defined using the `@staticmethod` decorator.

class Employee:
   raise_percentage =
1.05

   
def __init__(self, name, salary):
       
self.name = name
       
self.salary = salary

   @classmethod
   
def set_raise_percentage(cls, amount):
       cls.raise_percentage = amount

   @staticmethod
   
def is_workday(day):
       
return day.weekday() < 5

Class method

Employee.set_raise_percentage(1.10)

Static method

import datetime
my_date = datetime.date(
2024, 9, 1)  # A Sunday
print(Employee.is_workday(my_date))  # Output: False

In this example, `set_raise_percentage` is a class method that modifies the class variable `raise_percentage`. `is_workday` is a static method that checks if a given day is a weekday.

4. Inheritance: Reusing Code

Inheritance allows one class to inherit the attributes and methods of another class. This promotes code reuse and can lead to a hierarchical classification.

This is an example

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

   
def speak(self):
       raise NotImplementedError(
"Subclasses must implement this method")

class Dog(Animal):
   
def speak(self):
       
return f"{self.name} says Woof!"

class Cat(Animal):
   
def speak(self):
       
return f"{self.name} says Meow!"

 Creating objects of Dog and Cat

dog = Dog("Buddy")
cat = Cat(
"Whiskers")

print(dog.speak())  
# Output: Buddy says Woof!
print(cat.speak())  
# Output: Whiskers says Meow!

Here, `Dog` and `Cat` inherit from the `Animal` class. Each subclass implements the `speak` method, which is unique to each animal type.

Chart showing multiple levels of inheritance

5. Polymorphism: The Power of Flexibility

Polymorphism is one of the core concepts of object-oriented programming languages where poly means many and morphism means transforming one form into another. Polymorphism means the same function with different signatures is called many times.

Polymorphism allows objects of different classes to be treated as objects of a common superclass. This is particularly useful in cases where you want to execute the same operation in different ways depending on the object type.

def animal_sound(animal):
   print(animal.speak())

dog = Dog(
"Buddy")
cat = Cat(
"Whiskers")

animal_sound(dog)  
# Output: Buddy says Woof!
animal_sound(cat)  
# Output: Whiskers says Meow!

The `animal_sound` function can accept any object that has a `speak` method, demonstrating polymorphism in action.

6. Encapsulation and Abstraction: Keeping It Simple

Encapsulation is a core concept in object-oriented programming that involves bundling data and the methods that operate on that data into a single unit, or class. It serves as a protective barrier that prevents the data from being accessed by code outside this shield.

It restricts access to certain components of an object, which is essential for hiding the internal implementation details. Abstraction is the concept of exposing only the necessary parts of an object.

class Account:
   
def __init__(self, owner, balance=0):
       
self.owner = owner
       
self.__balance = balance  # Private variable

   
def deposit(self, amount):
       
self.__balance += amount
       print(f
"Added {amount} to the balance")

   
def __str__(self):
       
return f"Account owner: {self.owner}\nAccount balance: {self.__balance}"

   

Getter method for balance

   

def get_balance(self):
       
return self.__balance

Creating an Account object

acc = Account("John", 100)

Accessing the private variable through a method

print(acc.get_balance())  # Output: 100

Trying to access the private variable directly will raise an AttributeError

print(acc.__balance)  # Uncommenting this line will cause an error

In this example, `__balance` is a private variable, meaning it’s not accessible directly outside the class. Instead, the `get_balance` method is used to access it.

7. An Advanced Example: Custom Iterators Using Classes

Now, let’s look at a more advanced and unique example of using classes in Python. We’ll create a custom iterator class that allows iterating over a range of numbers but with a twist—skipping numbers based on a specific condition.

class SkipIterator:
   
def __init__(self, start, end, skip_condition):
       
self.current = start
       
self.end = end
       
self.skip_condition = skip_condition

 

  def __iter__(self):
       
return self

   
def __next__(self):
       
while self.current < self.end:
           current =
self.current
           
self.current += 1
           
if not self.skip_condition(current):
               
return current
       raise StopIteration

Custom skip condition: Skip even numbers

def skip_even(num):
   
return num %

2 == 0

Using the SkipIterator to iterate over numbers from 0 to 10, skipping even numbers

skip_iter = SkipIterator(0, 10, skip_even)
for num in skip_iter:
   
print(num)  # Output: 1, 3, 5, 7, 9

In this advanced example, the `SkipIterator` class allows you to iterate over a range of numbers, skipping those that meet a specific condition. The `__iter__` and `__next__` methods are used to define the iterator behaviour.

The `skip_condition` function is passed as a parameter, making the iterator highly customizable. In this case, we used a condition to skip even numbers, but you could modify the condition to skip numbers based on any other logic.

This example demonstrates the power and flexibility of Python’s object-oriented programming capabilities, allowing you to create highly specialised and reusable components.

Dynamic binding in Python, also known as late binding, is a feature that allows method or attribute resolution to occur at runtime rather than at compile time. This is closely related to polymorphism in object-oriented programming.

Key Concepts:

  1. Polymorphism: Different objects can respond to the same method call in different ways. This is achieved through method overriding, where a subclass provides its own implementation of a method defined in its superclass.
  2. Runtime Resolution: The specific method or attribute to be called is determined based on the actual type of the object at runtime1.       

Here’s a simple example to illustrate dynamic binding in Python:

class Shape:
   
def draw(self):
       print(
"Drawing a shape")

class Circle(Shape):
   
def draw(self):
       print(
"Drawing a circle")

class Rectangle(Shape):
   
def draw(self):
       print(
"Drawing a rectangle")

shapes = [Circle(), Rectangle()]
for shape in shapes:
   shape.draw()

In this example, the draw method is dynamically bound to the appropriate implementation based on the object’s type. When the loop runs, it calls the draw method of Circle and Rectangle objects, respectively1.

Duck Typing:

Another concept related to dynamic binding is duck typing. In Python, an object’s suitability for a particular use is determined by the presence of certain methods or attributes, rather than its type. This allows for greater flexibility and code reuse.

class Circle:
   
def draw(self):
       print(
"Drawing a circle")

class Rectangle:
   
def draw(self):
       print(
"Drawing a rectangle")

def draw_shape(shape):
   shape.draw()

shapes = [Circle(), Rectangle()]
for shape in shapes:
   draw_shape(shape)

In this example, the draw_shape function doesn’t care about the specific types of objects it receives, as long as they have a draw method.

8. Conclusion

Object-Oriented Programming in Python is a powerful way to structure your code. By understanding and utilising classes and objects, you can create modular, reusable, and maintainable code. The concepts of inheritance, polymorphism, encapsulation, and abstraction further enrich your ability to design complex software systems.

Whether you are defining simple objects or creating advanced custom iterators, Python’s OOP features provide the tools you need to write effective and efficient code. By mastering these concepts, you can take your Python programming skills to the next level, enabling you to tackle more complex projects with confidence.

Keep experimenting with these concepts and applying them to real-world problems. As you grow more comfortable with OOP in Python, you'll discover new ways to write cleaner, more organised code that can scale and adapt to your needs.

No comments:

Post a Comment