Python Diaries ….. Day 14

Object-Oriented Programming (OOP) in python

Nishitha Kalathil
15 min readOct 17, 2023

--

Object-Oriented Programming (OOP) is like a way of organizing and building things in a computer program. Imagine you’re building a virtual world. In OOP, you think about things in the world as objects. Objects can be anything — like people, cars, or even the sun.

It is a programming paradigm that focuses on organizing code around objects that represent real-world entities or concepts. These objects have attributes (also known as properties or member variables) and behaviors (also known as methods or member functions). OOP is widely used in software development because it helps in creating modular, reusable, and maintainable code.

Advantages of OOP:

  • Modularity: OOP promotes modular code design, allowing developers to break down complex systems into smaller, manageable components (objects).
  • Reusability: Objects can be reused across different parts of an application or even in different projects, saving time and effort in development.
  • Encapsulation: This is a key principle of OOP that involves bundling data (attributes) and methods (functions) that operate on the data within a single unit (object). This protects the data from external interference and misuse.
  • Inheritance: It allows one class (the subclass) to inherit attributes and behaviors from another class (the superclass), enabling code reuse and promoting a hierarchical structure.
  • Polymorphism: This allows objects to take on different forms or have multiple behaviors. It enables flexibility and extensibility in code design.
  • Abstraction: Abstraction involves hiding the implementation details of an object and exposing only the necessary information or functionalities. This helps in simplifying complex systems.

OOP Concepts

  • Classes: A class is a blueprint or template for creating objects. It defines the attributes and behaviors that the objects of the class will have. For example, if “Car” is a class, it would define attributes like “color,” “make,” and behaviors like “start_engine,” “accelerate,” etc.
  • Objects: An object is an instance of a class. It represents a specific entity or concept in the program. For example, if “Car” is a class, an object of this class could be a specific car instance with its unique characteristics.
  • Inheritance: Inheritance is a mechanism that allows one class (the subclass) to inherit attributes and behaviors from another class (the superclass). This promotes code reuse and establishes a hierarchical relationship between classes.
  • Polymorphism: Polymorphism allows objects to take on different forms or have multiple behaviors. This means that different objects can respond to the same message or method call in different ways. For example, a “Shape” class can have different subclasses like “Circle” and “Rectangle,” each implementing their own version of a “calculate_area” method.
  • Encapsulation: Encapsulation involves bundling data (attributes) and methods (functions) that operate on the data within a single unit (object). This protects the data from external interference and ensures that it can only be accessed through the defined interfaces.
  • Abstraction: Abstraction involves hiding the implementation details of an object and exposing only the necessary information or functionalities. This helps in simplifying complex systems and focusing on the essential aspects.

Class

Defining a Class:

Think of a class as a blueprint or a template for creating things. It’s like a recipe for making cookies. The recipe tells you what ingredients you need and how to put them together.

Example:

Let’s say we’re making a class for a “Dog”. It might have attributes like name and breed, and actions it can do like bark and sit.

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

def bark(self):
print(f"{self.name} says 'Woof!'")

def sit(self):
print(f"{self.name} sits down")

Creating Objects (Instances):

Now that we have our blueprint, we can use it to create actual dogs. Each actual dog is called an “object” or an “instance”. It’s like making specific cookies from the cookie recipe.

my_dog = Dog("Rex", "Labrador")
your_dog = Dog("Max", "Poodle")

Here, my_dog and your_dog are two different dogs created from the same class. my_dog has the name "Rex" and is of the breed "Labrador", while your_dog has the name "Max" and is of the breed "Poodle".

Class Variables vs Instance Variables:

Now, let’s talk about variables inside a class. There are two types: class variables and instance variables.

Class Variables: These are like shared attributes among all objects created from the class. Using our Dog example, a class variable could be something like species = "Canis familiaris" because all dogs belong to the same species.

class Dog:
species = "Canis familiaris"
# ... rest of the class definition ...

Instance Variables: These are unique to each object. In our Dog example, name and breed are instance variables because each dog has its own name and breed.

class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
# ... rest of the class definition ...

Example:

print(my_dog.name)  # Output: "Rex"
print(your_dog.name) # Output: "Max"

print(my_dog.species) # Output: "Canis familiaris"
print(your_dog.species) # Output: "Canis familiaris"

In this example, species is the same for all dogs (class variable), but name and breed are unique to each dog (instance variables).

So, classes help us make objects (like dogs), and each object can have its own unique attributes and actions.

Attributes and Methods

Attributes are like characteristics or properties of an object. For example, a car has attributes like color, make, and speed.

Methods are like actions that an object can perform. For example, a car can have methods like start_engine() and stop().

# Define a Car class
class Car:
def __init__(self, color, size, speed):
self.color = color
self.size = size
self.speed = speed

def drive(self):
print(f"The {self.color} car is driving at {self.speed} mph.")

def stop(self):
print(f"The {self.color} car has stopped.")

def honk(self):
print(f"The {self.color} car is honking.")


# Create car objects
red_car = Car("red", "medium", 60)
blue_car = Car("blue", "small", 40)
green_car = Car("green", "large", 80)

# Access attributes of car objects
print(f"The {red_car.color} car is {red_car.size} in size and has a speed of {red_car.speed} mph.")
print(f"The {blue_car.color} car is {blue_car.size} in size and has a speed of {blue_car.speed} mph.")
print(f"The {green_car.color} car is {green_car.size} in size and has a speed of {green_car.speed} mph.")

# Call methods of car objects
red_car.drive()
blue_car.stop()
green_car.honk()

Class Attributes vs Instance Attributes:

Class Attributes are like traits that are shared by all objects created from a class. They’re like a common feature for all things made using the same blueprint.

class Dog:
species = "Canis familiaris" # Class Attribute

def __init__(self, name, breed):
self.name = name # Instance Attribute
self.breed = breed # Instance Attribute

Here, species is a class attribute because it's the same for all dogs. name and breed are instance attributes because they vary from dog to dog.

Class Methods vs Instance Methods:

Class Methods are like actions that involve the class itself, rather than specific instances. They can access and modify class-level variables.

class Dog:
total_dogs = 0 # Class Attribute

@classmethod
def count_dogs(cls):
return cls.total_dogs

Here, count_dogs() is a class method. It can be used to get the total number of dogs created.

Instance Methods are actions that an object can perform. They work on specific instances and can access and modify instance-level variables.

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

def bark(self):
return f"{self.name} says 'Woof!'"

In this example, bark() is an instance method because it works on a specific dog.

Access Modifiers: Public, Private, Protected:

Public: These attributes and methods can be accessed from outside the class. They’re like the front door of a house.

Private: These attributes and methods are meant to be kept hidden from outside the class. They’re like a secret room in a house.

class Dog:
def __init__(self, name, breed):
self.name = name # Public Attribute
self._breed = breed # Protected Attribute
self.__age = 3 # Private Attribute

def bark(self):
return f"{self.name} says 'Woof!'"

def _sleep(self):
return f"{self.name} is sleeping" # Protected Method

def __eat(self):
return f"{self.name} is eating" # Private Method

Here, name is public, _breed is protected, and __age is private. Similarly, bark() is public, _sleep() is protected, and __eat() is private.

In simple terms, public attributes and methods are accessible by anyone, protected ones are like a “use with caution” sign, and private ones are meant to be kept really secret!

Constructor Method (__init__)

Purpose of __init__:

Think of __init__ as a special instruction manual that tells the computer what to do when a new object is created from a class. It's like the first thing that happens when you make a new thing using a blueprint.

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

In this example, the Dog class has a constructor __init__ method. It takes two parameters, name and breed, which are used to initialize the self.name and self.breed attributes. self refers to the instance of the class, so self.name and self.breed represent the attributes of the specific instance being created.

Here, when you create a new Dog object, like my_dog = Dog("Rex", "Labrador"), the __init__ method runs automatically and sets the name and breed for that specific dog.

Initializing Instance Variables:

Instance variables are like the personal details of each object. The __init__ method helps set these details when an object is created.

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

When you create my_dog = Dog("Rex", "Labrador"), it sets name to "Rex" and breed to "Labrador" for that specific dog.

Default Arguments in __init__:

You can also give default values to the arguments in __init__. This means that if you don't provide a value when creating an object, it will use the default value.

class Dog:
def __init__(self, name="Unknown", breed="Mutt"):
self.name = name
self.breed = breed

Now, if you create my_dog = Dog(), it will assume the name is "Unknown" and the breed is "Mutt" unless you specify otherwise.

my_dog = Dog()  # name="Unknown", breed="Mutt"
your_dog = Dog("Max", "Poodle") # name="Max", breed="Poodle"

So, the __init__ method is like a welcome party for a new object. It gets called automatically when you create a new thing from a class and sets up its personal details. If you want, you can give default values to these details too!

Python primarily uses the __init__ method as the constructor for classes. However, it also has a few other special methods related to object creation and destruction. Some of these include:

__new__:

This method is called before __init__ when a new instance of a class is created. It's responsible for creating and returning the new instance. It's less commonly used compared to __init__ and is typically used when you need more control over the instance creation process.

def __new__(cls, name, breed):
return super(Dog, cls).__new__(cls)

In our Dog class, __new__ isn't explicitly defined because Python's default behavior handles instance creation.

__del__:

This method is called when an instance of a class is about to be destroyed (garbage collected). It's used for cleanup tasks before an object is removed from memory. However, it's generally recommended to avoid using __del__ as it can have unpredictable behavior due to the nature of garbage collection.

def __del__(self):
print(f"{self.name} has been deleted.")

If you create a dog, and then delete it (e.g., del my_dog), it would print a message like "Fido has been deleted."

__str__ and __repr__:

These methods control how an object is represented as a string. __str__ is used for the str() function and print() function, while __repr__ is used for the repr() function and in the interactive interpreter.

def __str__(self):
return f"I'm {self.name}, a {self.breed}."

def __repr__(self):
return f"Dog({self.name}, {self.breed})"

str(my_dog) would now give "I'm Fido, a Labrador.", and repr(my_dog) might give "Dog('Fido', 'Labrador')".

__getattr__, __setattr__, and __delattr__:

These methods allow you to customize attribute access on instances of a class. They are used when you want to intercept getting, setting, or deleting attributes.

def __getattr__(self, attr):
return f"{self.name} doesn't have a {attr} attribute."

def __setattr__(self, attr, value):
self.__dict__[attr] = value

def __delattr__(self, attr):
del self.__dict__[attr]

With these, you can customize how attributes are accessed, set, and deleted.

__call__:

This method allows an instance of a class to be called as if it were a function. This can be useful for creating objects that behave like functions.

def __call__(self, action):
return f"{self.name} is {action}!"

Now, you can do something like my_dog("barking"), which will return "Fido is barking!".

__len__:

This method allows you to define the behavior of the len() function when called on an instance of your class. It should return the length of the object.

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

len(my_dog) would now return the length of the dog's name.

Remember, these methods provide additional customization and behavior for your objects. They are used when you need specific control over how instances of your class are created, represented, or interacted with.

Inheritance

Inheritance in Python is a fundamental concept of object-oriented programming (OOP). It allows a class (known as the child class or subclass) to inherit attributes and behaviors (methods) from another class (known as the parent class or superclass). This means that the child class can reuse and extend the functionality defined in the parent class.

  • A superclass (or parent class) is the class that is being inherited from.
  • A subclass (or child class) is the class that inherits from another class.

Creating Subclasses:

Inheritance is like passing down traits from parents to children. In programming, it means we can create a new class (child class) based on an existing class (parent class) and add more specific features to it.

Let’s say we have a class Animal:

class Animal:
def speak(self):
print("I can speak")

Now, we want to create a more specific class Dog based on Animal:

class Dog(Animal):
def bark(self):
print("Woof!")

Here, Dog is a subclass of Animal. It automatically gets the speak method from Animal and adds its own bark method.

Polymorphism

The word “polymorphism” means “many forms”, and in programming it refers to methods/functions/operators with the same name that can be executed on many objects or classes.

It allows objects of different classes to be treated as objects of a common superclass.

There are two types of polymorphism in Python:

> Compile-time Polymorphism (Method Overloading)
> Runtime Polymorphism (Method Overriding)

Compile-time Polymorphism (Method Overloading):

Method overloading allows a class to have multiple methods with the same name but different parameters. However, Python does not support true method overloading like some other languages (Java or C++). Still, you can achieve a similar effect using default arguments.

Runtime Polymorphism: Overriding Methods:

What if the same method is present in both the superclass and subclass?

In this case, the method in the subclass overrides the method in the superclass. This concept is known as method overriding in Python.

Sometimes, the child class wants to do things differently than its parent. This is called method overriding.

Let’s continue with our Animal and Dog classes. We'll override the speak method in Dog:

class Dog(Animal):
def speak(self):
print("I can bark") # This overrides the method from Animal
def bark(self):
print("Woof!")

Now, when a Dog speaks, it says "I can bark" instead of the generic "I can speak".

animal = Animal()
animal.speak() # Output: "I can speak"

dog = Dog()
dog.speak() # Output: "I can bark" (overridden method)
dog.bark() # Output: "Woof!" (unique method to Dog)

Here, animal is an instance of the Animal class and can call the speak() method. dog is an instance of the Dog class and can call both the overridden speak() method and the unique bark() method.

Accessing Parent Class Methods:

Sometimes, the child class wants to use a method from its parent. It can do this by calling the method using super().

Continuing with our Dog class, let's add a method that uses the speak method from Animal:

class Dog(Animal):
def speak(self):
print("I can bark")
def introduce(self):
super().speak() # Calls the speak method from Animal
print("I am a dog")

Now, when you call dog.introduce(), it will say "I can speak" (from Animal) and "I am a dog".

Multiple Inheritance

In multiple inheritance, A class can be derived from more than one superclass in Python.

class Animal:
def speak(self):
print("I can speak")

class Canine:
def bark(self):
print("Woof!")

class Dog(Animal, Canine):
def play(self):
print("I can play fetch")

Here, Dog inherits from both Animal and Canine. It inherits the speak method from Animal and the bark method from Canine.

my_dog = Dog()

# Call methods from Animal class
my_dog.speak() # output: I can speak

# Call methods from Canine class
my_dog.bark() # output: Woof!

# Call method from Dog class
my_dog.play() # output: I can play fetch

Multilevel inheritance

Multilevel inheritance involves inheriting from a base class, then inheriting from the derived class to create another class.

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

def speak(self):
pass

class Mammal(Animal):
def give_birth(self):
pass

class Dog(Mammal):
def speak(self):
return "Woof!"

class Cat(Mammal):
def speak(self):
return "Meow!"

class Bird(Animal):
def fly(self):
pass

class Eagle(Bird):
def speak(self):
return "Screech!"

In this example, Animal is the grandparent class, which provides a basic structure for all animals. Mammal inherits from Animal and adds a method give_birth. Dog and Cat inherit from Mammal and override the speak method to provide specific sounds for each animal. Bird inherits from Animal and adds a fly method. Eagle inherits from Bird and overrides the speak method.

When we create instances of these classes and call their speak method, the appropriate speak method for each type of animal will be used.

# Creating instances
dog = Dog("Fido")
cat = Cat("Whiskers")
eagle = Eagle("Baldy")

# Accessing methods
print(f"{dog.name} says: {dog.speak()}") # output: Fido says: Woof!
print(f"{cat.name} says: {cat.speak()}") # output: Whiskers says: Meow!
print(f"{eagle.name} says: {eagle.speak()}") # output:Baldy says: Screech!

Since the Bird class doesn’t have a speak() method, it will use the one from the Animal class.

Hierarchical Inheritance

Hierarchical inheritance occurs when multiple classes inherit from the same parent class.

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

def speak(self):
pass

class Dog(Animal):
def speak(self):
return "Woof!"

class Cat(Animal):
def speak(self):
return "Meow!"

class Bird(Animal):
def speak(self):
return "Chirp!"

# Creating instances
dog = Dog("Fido")
cat = Cat("Whiskers")
bird = Bird("Tweety")

# Accessing methods
print(f"{dog.name} says: {dog.speak()}")
print(f"{cat.name} says: {cat.speak()}")
print(f"{bird.name} says: {bird.speak()}")

Here, Dog, cat, and Bird classes inherited from Animal Class.

Data Encapsulation

Imagine you have a box with a lock on it. The box (class) contains some valuable items (data) and a set of instructions (methods) on how to use those items. The lock (encapsulation) keeps the contents safe and private, allowing only specific actions (methods) to interact with the items inside.

A class is an example of encapsulation as it encapsulates all the data that is member functions, variables, etc. The goal of information hiding is to ensure that an object’s state is always valid by controlling access to attributes that are hidden from the outside world.

Data abstraction

Data abstraction in Python is a way of organizing and managing our code so that we can focus on what something does rather than how it does it.

It’s like using a TV remote — you don’t need to know how the remote works internally, you just need to know how to use the buttons to change channels or adjust the volume.

In Python, we can create something called a “class” to represent a concept or an object. This class can have functions (called methods) and variables (called attributes) that define what it can do and what it knows about itself.

For example, let’s say we want to represent a car. We can create a class called Car with attributes like color, make, and model, and methods like start_engine() or drive().

class Car:
def __init__(self, color, make):
self.color = color
self.make = make
self.engine_status = "off"

def start_engine(self):
if self.engine_status == "off":
print(f"The {self.color} {self.make} car's engine is now on.")
self.engine_status = "on"
else:
print("The engine is already on.")

def drive(self):
if self.engine_status == "on":
print(f"The {self.color} {self.make} car is now driving.")
else:
print("The engine is off. You need to start it first.")

Now, when we want to work with a car in our code, we don’t need to worry about how all the internal parts work. We just need to know that we can start it, drive it, and check things like its color or make.

# Create a red Honda car
my_car = Car("red", "Honda")

# Check car details
print(f"My car is a {my_car.color} {my_car.make}.")
# Output: "My car is a red Honda."

# Try to drive (engine is off)
my_car.drive() # Output: "The engine is off. You need to start it first."

# Start the engine
my_car.start_engine() # Output: "The red Honda car's engine is now on."

# Try to start the engine again
my_car.start_engine() # Output: "The engine is already on."

# Drive the car
my_car.drive() # Output: "The red Honda car is now driving."

This helps us to manage complexity in our code. We can use the car object without getting bogged down in all the details about how the car’s engine, transmission, and other parts actually function.

So, data abstraction allows us to focus on the things that are important for our program and hide the details that we don’t need to worry about at a given moment.

Abstract Base classes(ABC)

By default, Python does not provide abstract classes. Python comes with a module that provides the base for defining Abstract Base classes(ABC) and that module name is ABC. This module provides the ABC class and the abstractmethod decorator.

from abc import ABC, abstractmethod

class Vehicle(ABC):
@abstractmethod
def start_engine(self):
pass

@abstractmethod
def drive(self):
pass

class Car(Vehicle):
def start_engine(self):
print("Car engine is now on.")

def drive(self):
print("Car is now driving.")

class Motorcycle(Vehicle):
def start_engine(self):
print("Motorcycle engine is now on.")

def drive(self):
print("Motorcycle is now driving.")

In this example, we’ve created an abstract base class Vehicle with two abstract methods start_engine and drive. Any class that inherits from Vehicle must implement these two methods.

We then have two classes Car and Motorcycle that inherit from Vehicle. Both of them must provide their own implementations of start_engine and drive to be considered valid subclasses of Vehicle.

Using abstract base classes helps to define a common interface that multiple classes should adhere to. This can be particularly useful when you have a group of related classes that need to share some common behavior.

# The code for the classes goes here

# Create a Car instance
my_car = Car()
my_car.start_engine()
my_car.drive()

# Create a Motorcycle instance
my_motorcycle = Motorcycle()
my_motorcycle.start_engine()
my_motorcycle.drive()

What are Concrete Class?

A class that inherits from an abstract class and provides implementations for all the abstract methods. It can be instantiated. If a class contains only concrete methods ( normal methods) we can say it is a concrete class.

Example: Car in the previous example.

Object-Oriented Programming is a powerful paradigm for organizing code and modeling real-world entities. It provides a way to create modular, reusable, and maintainable code. By understanding the key concepts of classes, objects, inheritance, and encapsulation, you can take full advantage of OOP in Python.

You can access the other topics in this tutorial series right here:

In Day 15, we’ll explore the concept of GUI. Keep up the great work! Happy coding!

--

--