Check out the previous part: Core Python: Part 4
We have procedural and object-oriented as two categories in structured programming. The functions used in programs are procedural in nature that do not hold their own data or remember their state but are defined once and never modified. Object-oriented programming is an alternative to this that has an object representing a concept or a physical entity. An object will have its own data (attributes) and functions to manipulate the data.
Inheritance
A base class (or parent class) is a class from which other classes inherit properties and behaviors. It defines the common attributes and methods that are shared by its derived classes. A derived class (or subclass) is a class that inherits attributes and methods from a base class. An abstract class is a class that cannot be instantiated on its own and is meant to be subclassed by other classes. Abstract classes are useful for creating a consistent interface across multiple subclasses while enforcing certain behaviors.
# Base class Animal class Animal: def speak(self): pass # Placeholder for subclasses to implement # Derived class Dog that extends class Animal that overrides method speak class Dog(Animal): def speak(self): return "Woof!" # Derived class Dog that extends class Animal that overrides method speak class Cat(Animal): def speak(self): return "Meow!" # abstract class using the abc module from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): pass
An instance of a class - object
class Animal: def __init__(self, species): self.species = species # Derived class from Animal class Dog(Animal): def __init__(self, species, breed): # __init__ is called dunder method super().__init__(species) # Calling parent class constructor self.breed = breed # Creating instances of the Dog class dog = Dog("Canine", "Labrador") print(dog.species) # Output: Canine print(dog.breed) # Output: Labrador
Encapsulation
The primary goal of encapsulation is to control access to the internal state of an object, ensuring that the data is not directly manipulated from outside the object. The __age attribute is encapsulated using the double underscore (__) prefix. Direct access to it from outside the class is prevented. Instead, getter and setter methods (get_age() and set_age()) are provided to access and modify the attribute, respectively.
class Student: def __init__(self, name, age): self.name = name self.__age = age # Encapsulated attribute def get_age(self): return self.__age def set_age(self, age): if age > 0: self.__age = age student = Student("Alice", 20) print(student.name) # Accessing public attribute directly print(student.get_age()) # Accessing encapsulated attribute using method student.set_age(22) # Modifying encapsulated attribute using method print(student.get_age()) # Output: 22
Polymorphism
It enables methods to be implemented in different ways in various subclasses while adhering to a common interface. Hence objects of different classes behave as objects of a common base class.
Functions that work with several types are called polymorphic. Polymorphism can facilitate code reuse. For example, the built-in function sum, which adds the elements of a sequence, works as long as the elements of the sequence support addition
class Shape: def area(self): pass class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return 3.14 * self.radius * self.radius class Rectangle(Shape): def __init__(self, length, width): self.length = length self.width = width def area(self): return self.length * self.width def print_area(shape): print("Area:", shape.area()) circle = Circle(5) rectangle = Rectangle(4, 6) print_area(circle) # Output: Area: 78.5 print_area(rectangle) # Output: Area: 24
Abstraction
Only Shape will be visible for users who can see that area method exists but cannot see the details of the area method.
from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): pass class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return 3.14 * self.radius * self.radius class Rectangle(Shape): def __init__(self, length, width): self.length = length self.width = width def area(self): return self.length * self.width circle = Circle(5) rectangle = Rectangle(4, 6) print(circle.area()) # Output: 78.5 print(rectangle.area()) # Output: 24
Overloading
class MathOperations: def add(self, a, b=None): if b is None: return a return a + b math_ops = MathOperations() print(math_ops.add(5)) # Output: 5 print(math_ops.add(2, 3)) # Output: 5 2 * 2 = 4 '2' * 2 = '22' # * behaves differentely when datatypes are different
__str__() and __repr__()
They are special methods that can be defined in a class to control how instances of that class are represented as strings.
class Point: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f"Point({self.x}, {self.y})" p = Point(3, 5) print(repr(p)) # Output: Point(3, 5) print(str(p)) # Output: (3, 5)