Python Diaries ….. Day 14
Object-Oriented Programming (OOP) in python
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:
- Day 1: Data types, Comment, Type casting and User input.
- Day 2: Numbers, Strings, and Booleans.
- Day 3: Operators
- Day 4: Lists
- Day 5: Tuples and Sets
- Day 6: Dictionaries
- Day 7: If , If Else, If elif else Condition statements
- Day 8: While loops
- Day 9: For loops
- Day 10: Functions
- Day 11: File handling
- Day 12: Error Handling
- Day 13: Regular expressions
- Day 14: OOPS in python (class)
- Day 15: Python GUI libraries
In Day 15, we’ll explore the concept of GUI. Keep up the great work! Happy coding!