Object Oriented Programming
Object-Oriented Programming (OOP): A Fundamental Concept
Object-Oriented Programming (OOP) is a programming paradigm that organizes software design around data, or objects, rather than functions and logic. Python is an object-oriented language that supports OOP concepts like classes, objects, inheritance, encapsulation, and polymorphism. At its core, OOP revolves around the concept of objects, which are self-contained units consisting of both data (attributes) and behavior (methods).
Understanding Classes and Objects
What is a Class?
A class is a blueprint for creating objects (a particular data structure). Classes encapsulate data for the object and methods to manipulate that data.What is an Object?
An object is an instance of a class. When a class is defined, no memory is allocated until an object of that class is created.Basic Syntax of a Class:
class ClassName:
# Class attributes (optional)
attribute1 = value1
attribute2 = value2
# Constructor method (optional)
def __init__(self, parameter1, parameter2):
self.parameter1 = parameter1
self.parameter2 = parameter2
# Other methods (optional)
def method1(self):
# Method body
pass
def method2(self, parameter):
# Method body
pass
Here class CLasssName
decalres the start of the class definition (where ClassName
represents the name of the class). The 'Class Attribute' are the variables defined with the class scope. They are shared among all instances of the class. The __init__
special method, also known as a Constructor, is used to initialize the class with its attributes. Here self
parameter represents the instance of the class, allowing us to access its attributes and methods.
Example: Creating a Class and Object
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
def bark(self):
return f"{self.name} says Woof!"
and therefore,
# Creating an object of the Dog class
my_dog = Dog("Buddy", "Golden Retriever")
# Accessing object properties and methods
print(my_dog.name) # Output: Buddy
print(my_dog.breed) # Output: Golden Retriever
print(my_dog.bark()) # Output: Buddy says Woof!
Inheritance in Python
Inheritance is a key feature of OOP that allows a new class to inherit the properties and methods of an existing class. The new class is called the child class (or subclass), and the existing class is the parent class (or superclass).Basic Syntax of Inheritance:
class ParentClass:
def __init__(self, param):
self.param = param
def parent_method(self):
return f"Parent method called with {self.param}"
class ChildClass(ParentClass):
def __init__(self, param, child_param):
super().__init__(param) # Call the constructor of the parent class
self.child_param = child_param
def child_method(self):
return f"Child method called with {self.child_param}"
where:
- super(): Used to call a method from the parent class.
- Method Overriding: Child classes can override methods from the parent class by defining methods with the same name.
- Multiple Inheritance: Python supports multiple inheritance, where a class can inherit from multiple parent classes.
Example of single inheritance:
class Animal:
def __init__(self, species):
self.species = species
def make_sound(self):
return "Some generic sound"
class Dog(Animal):
def __init__(self, species, name):
super().__init__(species) # Inherit species from Animal
self.name = name
def bark(self):
return f"{self.name} says Woof!"
# Creating an object of the Dog class
my_dog = Dog("Canine", "Buddy")
# Accessing inherited and new methods
print(my_dog.species) # Output: Canine
print(my_dog.make_sound()) # Output: Some generic sound
print(my_dog.bark()) # Output: Buddy says Woof!
Method Overriding
Method overriding allows a child class to provide a specific implementation of a method that is already defined in its parent class.
class Animal:
def make_sound(self):
return "Some generic sound"
class Dog(Animal):
def make_sound(self):
return "Woof Woof!"
# Creating objects
generic_animal = Animal()
dog = Dog()
print(generic_animal.make_sound()) # Output: Some generic sound
print(dog.make_sound()) # Output: Woof Woof!
Multiple Inheritance
Python supports multiple inheritance, where a class can inherit from more than one parent class.
class Bird:
def fly(self):
return "Flies in the sky"
class Fish:
def swim(self):
return "Swims in the water"
class FlyingFish(Bird, Fish):
pass
# Creating an object of FlyingFish
flying_fish = FlyingFish()
# Accessing methods from both parent classes
print(flying_fish.fly()) # Output: Flies in the sky
print(flying_fish.swim()) # Output: Swims in the water
Encapsulation and Access Modifiers
Encapsulation is the mechanism of restricting access to some of the object's components. Python uses underscores to indicate the intended level of access.Public, Protected, and Private Members:
- Public: Accessible from anywhere.
- Protected: Indicated by a single underscore '
_
''. Meant for internal use within the class and subclasses but not restricted. - Private: Indicated by a double underscore '
__
'. Intended to be inaccessible from outside the class.
Example: Encapsulation
class MyClass:
def __init__(self):
self.public_var = "I am public"
self._protected_var = "I am protected"
self.__private_var = "I am private"
def get_private_var(self):
return self.__private_var
# Creating an object of MyClass
obj = MyClass()
# Accessing public variable
print(obj.public_var) # Output: I am public
# Accessing protected variable (possible but not recommended)
print(obj._protected_var) # Output: I am protected
# Accessing private variable (will raise an AttributeError)
# print(obj.__private_var) # Uncommenting this will cause an error
# Accessing private variable through a method
print(obj.get_private_var()) # Output: I am private
Polymorphism in Python
Polymorphism allows objects of different classes to be treated as objects of a common superclass. It typically refers to the ability to call the same method on different objects and have each of them respond in a way appropriate to their class.Example: Polymorphism with Methods:
class Cat:
def make_sound(self):
return "Meow"
class Dog:
def make_sound(self):
return "Woof"
def animal_sound(animal):
print(animal.make_sound())
# Creating objects
cat = Cat()
dog = Dog()
# Polymorphic behavior
animal_sound(cat) # Output: Meow
animal_sound(dog) # Output: Woof
Classes vs Instances
Classes allow you to create user-defined data structures. Classes define functions called methods, which identify the behaviors and actions that an object created from the class can perform with its data.
While the class is the blueprint, an instance is an object that’s built from a class and contains real data. An instance of the Dog class is not a blueprint anymore. It’s an actual dog with a name, like Miles, who’s four years old.
Example: 1
Here's a simple example of a class definition in Python:
class Car:
# Class attribute
num_wheels = 4
# Constructor method
def __init__(self, color, model):
self.color = color
self.model = model
# Method to display car details
def display_details(self):
print(f"Color: {self.color}, Model: {self.model}, Wheels: {Car.num_wheels}")
Here:
- class attributes: are attribures that have the same value for all class instances. We can define a class attribute by assigning a value to a variable name outside of
.__init__()
. - instance attributes: Attributes created in
.__init__()
are called 'instance attributes'. An instance attribute’s value is specific to a particular instance of the class. self.color=color
: creates an attribute calledcolor
and assigns the value of thecolor
parameter to it.self.model
: creates an attribute calledmodel
and assigns the value of themodel
parameter to it.
> my_car = Car("Red", "Toyota")So Accesing the attrinutes and calling methods of the object:
> print(my_car.color)
Output: Red
> my_car.display_details()
Output: Color: Red, Model: Toyota, Wheels: 4
.__init__()
. Every time you create a new Car object, .__init__()
sets the initial state of the object by assigning the values of the object’s properties. That is, .__init__()
initializes each new instance of the class.
We can give .__init__()
any number of parameters, but the first parameter will always be a variable called self
. When we create a new class instance, then Python automatically passes the instance to the self
parameter in .__init__()
so that Python can define the new attributes on the object.
Example: 2
- (a). Define a new Complex class with 2 attributes:
part_re
which contains the real part of a complex number, andpart_im
which contains the imaginary part of a complex number. - (b). Define in the
Complex
class adisplay
method which prints a Complex in its algebraic form a ± bi. This method should adapt to the sign of the imaginary part (The method should be able to display 4 - 2i,6 + 2i, 5, ...). - (c). Instantiate two Complex objects corresponding to the complex numbers 4+5𝑖, and 3−2𝑖, then print them on the console.
class complex:
def __init__(self, part_re, part_im):
self.part_re = part_re
self.part_im = part_im
# (b) display method
def display(self):
if self.part_im >= 0:
print(f"{self.part_re} + {self.part_im}i")
else:
print(f"{self.part_re} - {abs(self.part_im)}i")
# (c) printing 4+5i, 3-2i
c1 = complex(4, 5)
c1.display() # display the c1
c2 = complex(3, -2)
c2.display() # display the c2
Output: 4 + 5i 3 - 2i
Example: 3 The flexibility of classes in object-oriented programming allows the developer to broaden a class by adding new attributes and methods to it. All instances of this class will then be able to call these methods.
For example, we can define in the Vehicle
class a new add
method which will add an individual to the passenger list:
class Vehicle:
def __init__(self, a, b=[]):
self.seats = a
self.passengers = b
def print_passengers(self):
for i in range(len(self.passengers)):
print(self.passengers[i])
def add(self,name): #New methd
self.passengers.append(name)
car1 = Vehicle(4, ['Charles', 'Paul']) # Instantiation of car1
car1.add('Raphaël') # 'Raphaël' is added to the list of passengers
car1.print_passengers() # Display of the list of passengers
Output: Charles Paul Raphaël
Example: 1 Let's consider the Vehicle
class again:
class Vehicle:
def __init__(self, a, b = []):
self.seats = a
self.passengers = b
def print_passengers(self):
for i in range (len (self.passengers)):
print (self.passengers [i])
def add(self, name):
self.passengers.append (name)
We can define a Motorcycle
class which inherits from theVehicle class as follows:
class Motorcycle(Vehicle):
def__init__(self, b, c):
self.seats = 2
self.passengers = b
self.brand = c
Motorcycle = Motorcycle(['Pierre', 'Dimitri'], 'Yamaha')
By rewriting the __init__ method, any Motorcycle object will automatically have 2 seats and a new brand attribute. So following code gives the list of passengers:
moto1 = Motorcycle(['Pierre','Dimitri'], 'Yamaha')
moto1.add('Yohann')
moto1.print_passengers()
Output: Pierre Dimitri Yohann
We see that even with the number of seats 2, we can add more number of passngers to the moto class. We need to modify the Motorcycle
class.
class Motorcycle(Vehicle):
def__init__(self, b, c):
self.seats = 2
self.passengers = b
self.brand = c
def add(self, name):
if len(self.passengers) < self.seats:
super().add(name)
remaining_seats = self.seats - len(self.passengers)
print(f"{name} added to the list. {remaining_seats} seat(s) remaining.")
else:
print("The vehicle is full.")
# Test the Motorcycle class
moto1 = Motorcycle(['Pierre', 'Dimitri'], 'Yamaha')
moto1.add('Yohann')
moto1.print_passengers()
moto1.add('Sophie')
Output: The vehicle is full. Pierre Dimitri The vehicle is full.
Predefined classes
In Python, many predefined classes such as the list,tuple or str classes are regularly used to facilitate the developer's tasks. Like all other classes, they have their own attributes and methods that are available to the user. One of the great interests of object oriented programming is to be able to create classes and share them with other developers. This is done through packages such as numpy, pandas or scikit-learn. All of these packages are actually classes created by other developers in the Python community to give us tools that will make easier to develop our own algorithms. To know all the available attributes in a given class, we just needdir(className)
. For example, dir(list)
will give all available attributes in the list class.
Built-in methods
All classes defined in Python have methods whose name is already defined. The first example of such a method we have seen is the__init__
method which allows us to initialize an object, but it is not the only one. Built-in methods give the class the ability to interact with predefined Python functions such as print
, len
, help
and basic operators. These methods usually have the affixes __
at the beginning and end of their names, which allows us to easily identify them. The dir(object)
lists all available predefined methods common to all python objects:
Attribute/Method | Description | Example |
---|---|---|
__class__ | Returns the class of the object | df.__class__ |
__delattr__ | Deletes an attribute | del df.attribute_name |
__dir__ | Returns the list of attributes and methods of an object | dir(df) |
__doc__ | Returns the docstring (documentation) of the object | df.__doc__ |
__eq__ | Compares two objects for equality | df1 == df2 |
__format__ | Returns a formatted string representation of the object | formatted_str = df.__format__("csv") |
__ge__ | Compares greater than or equal to | df1 >= df2 |
__getattribute__ | Returns the value of an attribute of an object | value = df.__getattribute__("column_name") |
__gt__ | Compares greater than | df1 > df2 |
__hash__ | Returns a hash value for the object | hash_value = hash(df) |
__init__ | Initializes an object | df = pd.DataFrame(data) |
__init_subclass__ | Called when a subclass is initialized | class CustomDataFrame(pd.DataFrame): def __init_subclass__(cls): print("CustomDataFrame subclass created") |
__le__ | Compares less than or equal to | df1 <= df2 |
__lt__ | Compares less than | df1 < df2 |
__ne__ | Compares not equal to | df1 != df2 |
__new__ | Creates a new instance of a class | df = pd.DataFrame() |
__reduce__ | Used for pickling | reduced_obj = df.__reduce__() |
__reduce_ex__ | Used for pickling | reduced_obj = df.__reduce_ex__(protocol) |
__repr__ | Returns a string representation of the object | repr_str = df.__repr__() |
__setattr__ | Sets the value of an attribute of an object | df.__setattr__("attribute_name", value) |
__sizeof__ | Returns the size of the object in memory | size = df.__sizeof__() |
__str__ | Returns a string representation of the object | str_rep = df.__str__() |
__subclasshook__ | Used to customize subclass checks | is_subclass = DataFrame.__subclasshook__(subclass) |
This table provides a brief description of each attribute and method associated with the DataFrame class in pandas, along with examples demonstrating their usage.
Example: Instantiate a Complex object corresponding to the number 6−3𝑖 then display it on the console using the print function.
class Complex:
def __init__(self, a = 0, b = 0):
self.part_re = a
self.part_im = b
def __str__(self):
if(self.part_im < 0):
return self.part_re.__str__() + self.part_im.__str__() + 'i' # returns 'a' '-b' 'i'
if(self.part_im == 0):
return self.part_re.__str__() # returns 'a'
if(self.part_im > 0):
return self.part_re.__str__() + '+' + self.part_im.__str__() + 'i' # returns 'a' '+' 'b' + 'i'
complex_number = Complex(6, -3)
print(complex_number)
It preints:
6-3 i
Example:
- (a) Define in the Complex class a mod method which returns the modulus of the Complex calling the method. You can use the sqrt function of the numpy package to calculate a square root.
- (b) Define in the Complex class the methods __lt__ and __gt__ (strictly lower and strictly higher). These methods must return a boolean.
- (c) Perform the two comparisons defined above on the complex numbers 3+4𝑖 and 2−5𝑖 .
import numpy as np
class Complex:
def __init__(self, a = 0, b = 0):
self.part_re = a
self.part_im = b
def __str__(self):
if(self.part_im < 0):
return self.part_re.__str__() + self.part_im.__str__() + 'i' # returns 'a' '-b' 'i'
if(self.part_im == 0):
return self.part_re.__str__() # returns 'a'
if(self.part_im > 0):
return self.part_re.__str__() + '+' + self.part_im.__str__() + 'i' # returns 'a' '+' 'b' + 'i'
def mod(self):
return np.sqrt( self.part_re ** 2 + self.part_im ** 2) # returns (sqrt(a² + b²))
def __lt__(self, other):
if(self.mod() < other.mod()): # returns True if |self| < |other|
return True
else:
return False
def __gt__(self, other):
if(self.mod() > other.mod()): # returns True if |self| > |other|
return True
else:
return False
z1 = Complex(3, 4)
z2 = Complex(2, 5)
print(z1 > z2)
print(z1 < z2)
False True
Some other interesting things to know:
- Visit my website on For Data, Big Data, Data-modeling, Datawarehouse, SQL, cloud-compute.
- Visit my website on Data engineering