The ultimate guide to OOP with simple learning scenarios

Gagandeep Kaur
Dev Genius
Published in
10 min readMay 5, 2024

--

Let’s say you’re making a game about cars. Instead of writing separate code for each car, like a sports car, a truck, and a sedan, you create a blueprint called a "Car" class. This blueprint describes what every car should have, like a speed, color, and number of doors. Then, whenever you want to add a new car to the game, you just create a new "object" of the Car class with its own specific details, like the speed and color. So, OOP helps you organize your code by grouping similar things together and making it easier to manage.

OOP stands for Object-Oriented Programming. In Python, it’s a programming paradigm where you structure your code around objects, which are instances of classes. Classes define the attributes and behaviors of objects, allowing for code reusability and organization.

Pictorial Representation of an OOP Implementation (Source: spiceworks)

Class and Object

In object-oriented programming (OOP), a class is a blueprint or template for creating objects. It defines the properties (attributes) and behaviors (methods) that all objects created from it will have.

An object, on the other hand, is a specific instance of a class. It represents a concrete realization of the blueprint defined by the class, with its own unique set of values for the attributes defined in the class. Objects can interact with each other and perform actions according to the behaviors defined in their class.

Imagine you have a big box of LEGO bricks. Each brick does something special, like making a wheel or a window. In object-oriented programming, modularity is like building different parts of your program with these LEGO bricks. Each part (or module) does a specific job, and you can put them together to create something bigger, like a spaceship or a castle. So, modularity helps keep things organized and makes it easier to build and understand your program.

Creating classes and objects in Python

Creating classes: You define a class using the class keyword followed by the class name. Inside the class, you define attributes (data) and methods (functions).

Class attributes are variables that are shared by all instances of the class. They are defined within the class but outside of any methods.

Class methods are methods that are bound to the class rather than its instances. They are defined using the ‘@classmethod’ decorator and take the class itself (cls) as the first argument instead of the instance (‘self’).

class MyClass:
def __init__(self, attribute1, attribute2):
self.attribute1 = attribute1
self.attribute2 = attribute2

def method1(self):
# Method definition
pass

Creating Objects (Instantiation):
You create objects (instances) of a class by calling the class name followed by parentheses, optionally passing arguments to the class constructor (__init__ method).

my_object = MyClass(value1, value2)

Encapsulation in Python

OOP allows you to encapsulate data (attributes) and functions (methods) into objects, which helps in controlling access to data and prevents unintended modifications.

Encapsulation is like putting your toys in a toy box. You can still play with them, but they’re all tucked away inside the box where they’re safe. In programming, encapsulation means putting your code and data together, like a toy box, and only letting certain parts of the code access them. It helps keep things neat and prevents other parts of the program from accidentally messing with them. So, just like how you keep your toys safe in a toy box, encapsulation keeps your code safe and organized.

In Python, there are no strict access modifiers like in some other languages (e.g., Java), but you can achieve encapsulation, which is the principle of restricting access to certain parts of an object, through conventions and mechanisms. Here’s how you can achieve encapsulation in Python:

  1. Public access: By default, all attributes and methods in a class are public and can be accessed from outside the class.
  2. Protected Access:
    Attributes and methods that start with a single underscore (_) are conventionally considered protected. It’s a signal to other developers that they should be treated as non-public parts of the API. However, they can still be accessed from outside the class.
  3. Private Access: Attributes and methods that start with double underscore (__) are considered private. Python performs name mangling on these attributes, which means their names are modified to avoid clashes in subclasses. They can still be accessed from outside the class using name mangling, but it’s discouraged.

Syntax

Getter and Setter methods

Getter and setter methods are used to access and modify the private attributes of a class in a controlled manner. They provide a way to encapsulate the access and modification of class attributes, allowing you to enforce validation rules or perform additional actions when getting or setting the attribute values.

Example

In this example, ‘_value’ is a private attribute, indicated by the single underscore. The get_value() method acts as the getter method, allowing access to the private attribute. The set_value() method acts as the setter method, which validates the input before modifying the attribute.

Using getter and setter methods provides better control over the access and modification of class attributes, promoting encapsulation and maintainability.

Inheritance in Python

Python supports inheritance, allowing you to create new classes based on existing ones, thereby promoting code reuse and enabling hierarchical relationships between classes.

Inheritance in object-oriented programming is like passing down traits in a family. Imagine you have a superhero, and they have special powers like flying and super strength. Now, if that superhero has a sidekick, the sidekick might also have some of the same powers because they inherit them from the superhero. In programming, inheritance works similarly. You can create new "characters" (or classes) that can have the same abilities (or methods and properties) as another class, like the superhero passing down their powers to the sidekick. This helps make your code more organized and lets you reuse code without writing it all over again.

Syntax for Inheritance

class SuperClass:
def __init__(self, attribute):
self.attribute = attribute

def method(self):
pass

class SubClass(SuperClass): # Subclass inherits from Superclass
def __init__(self, attribute, sub_attribute):
super().__init__(attribute) # Call superclass constructor
self.sub_attribute = sub_attribute

def sub_method(self):
pass

In this syntax:

  • SuperClass is the superclass or base class.
  • SubClass is the subclass that inherits from the superclass.
  • Inside the subclass definition, SuperClass is specified in parentheses after the subclass name to indicate that it inherits from the superclass.
  • The subclass can override methods and attributes of the superclass if necessary.
  • super().__init__() is used to call the superclass constructor from the subclass constructor, allowing the subclass to initialize the inherited attributes.

Example of inheritance

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

def sound(self):
pass

class Dog(Animal):
def __init__(self, species, breed):
super().__init__(species)
self.breed = breed

def sound(self):
return "Woof!"

# Creating objects
animal = Animal("Mammal")
dog = Dog("Mammal", "Labrador")

# Accessing attributes and methods
print(animal.species) # Output: Mammal
print(dog.species) # Output: Mammal
print(dog.breed) # Output: Labrador
print(dog.sound()) # Output: Woof!

In this example, ‘Dog’ is a subclass of ‘Animal’, inheriting the species attribute. The ‘sound()’ method is overridden in the ‘Dog’ subclass to provide a specific implementation.

Polymorphism in Python

OOP in Python supports polymorphism, meaning that objects of different classes can be treated as objects of a common superclass, making code more flexible and adaptable to different types of data.

Polymorphism in object-oriented programming is like shape-shifting. Imagine you have a magic potion that can turn you into different animals. Sometimes you’re a lion, sometimes a bird, and sometimes a fish. Each time you look different and act differently, but you’re still you underneath. In programming, polymorphism lets you do something similar. You can have different objects that can do the same action, like making a sound, but each object might make a different sound based on what it is. So, just like how you can be different animals with the magic potion, in programming, polymorphism lets objects behave differently while still using the same method or function.

Method Overloading

Method overloading in OOP allows multiple methods with the same name but different signatures in a class. It’s commonly used to provide flexibility by accommodating varying parameter types or numbers. In Python, although not natively supported, similar functionality is achieved through default arguments or variable-length argument lists.

Imagine you have a toy box. Method overloading is like having a toy box that can play with different toys in different ways. If you put in a ball, it bounces. If you put in a car, it zooms. Same box, different actions based on what you give it.

In Python, method overloading is not directly supported. But we can achieve same functionality using default arguments and variable-length argument lists.

Default Arguments: You can define a method with default arguments, allowing it to be called with different numbers of arguments.

class MyClass:
def my_method(self, arg1, arg2=None):
if arg2 is None:
# Do something with arg1
pass
else:
# Do something with arg1 and arg2
pass

Variable-Length Argument Lists:
You can use ‘*args’ and ‘**kwargs’ to define methods that accept a variable number of positional and keyword arguments, respectively.

class MyClass:
def my_method(self, *args):
# Access arguments using args tuple
pass

def another_method(self, **kwargs):
# Access arguments using kwargs dictionary
pass

Example

In this example, the add() method can be called with one or two arguments. If only one argument is provided, the default value for num2 is used, effectively overloading the method.

Method Overriding

Method overriding in Python occurs when a subclass provides a specific implementation of a method that is already defined in its superclass. This allows the subclass to customize or extend the behavior of the inherited method.

Imagine you have a parent who loves baking cookies, and they have a recipe for making chocolate chip cookies. Now, you, as their child, also love baking, but you want to put your own twist on the chocolate chip cookie recipe.

Method overriding in Python is like you taking the chocolate chip cookie recipe from your parent (the superclass) and adding your own special ingredient or changing a step to make your version of the cookies (the subclass). So, you’re still using the original recipe as a base, but you’re tweaking it to make it your own.

Syntax

class ParentClass:
def method(self):
# Superclass method implementation
pass

class ChildClass(ParentClass):
def method(self):
# Subclass method implementation, overriding the superclass method
pass

In this syntax:

  • ParentClass is the superclass with a method named method().
  • ChildClass is the subclass that inherits from ParentClass.
  • The method() in ChildClass has the same name as the method in ParentClass, effectively overriding it.

Example

class Animal:
def sound(self):
return "Animal sound"

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

# Creating objects
animal = Animal()
dog = Dog()

# Accessing overridden method
print(animal.sound()) # Output: Animal sound
print(dog.sound()) # Output: Woof!

In this example, Dog overrides the sound() method from Animal, providing its own implementation specific to dogs.

Operator Overloading

Operator overloading is a feature in object-oriented programming languages that allows a programmer to redefine the behavior of built-in operators (like `+`, `-`, `*`, `/`, etc.) for user-defined types or classes. It enables objects to behave like built-in types and allows them to interact with operators in a meaningful way.

Imagine you have a magic wand. By default, it can only perform certain spells, like adding numbers. But with operator overloading, you can teach it new tricks. For example, you could make it double as a paintbrush and use it to draw shapes. Operator overloading is like giving your wand new powers, allowing it to do more than just its default spell.

In Python, you can implement operator overloading by defining special methods with names like `__add__`, `__sub__`, `__mul__`, etc., which correspond to the behavior of the `+`, `-`, `*`, etc. operators. These methods enable objects of your class to work with the respective operators in custom ways.

Syntax

In this syntax:

  • __add__(self, other) defines the behavior for the addition operator (+).
  • __sub__(self, other) defines the behavior for the subtraction operator (-).
  • __mul__(self, other) defines the behavior for the multiplication operator (*).
  • Other operators can be overloaded using similar naming conventions (__div__ for division, __eq__ for equality comparison, etc.).

These special methods take self (the instance of the class) and other (the operand) as arguments and return the result of the corresponding operation. With these methods defined, objects of the class can interact with operators as if they were built-in types.

Example

In this example, the Number class overloads the addition (+) and subtraction (-) operators. When + or - is used with Number objects, the corresponding __add__ or __sub__ method is called, allowing for custom behavior.

Abstraction

By hiding the implementation details and exposing only the necessary features, OOP promotes abstraction, making it easier to work with complex systems.

Abstraction in object-oriented programming is like using a TV remote. You don’t need to know how the TV works inside; you just press a button, and it does what you want. Similarly, in programming, abstraction means hiding the complex details of how something works and just giving you a simple way to use it. So, instead of worrying about all the complicated stuff behind the scenes, you can focus on using the code to do what you need, like playing a game or showing a picture.

Syntax

In Python, abstraction in OOP is typically implemented using abstract base classes (ABCs) from the abc module. Here’s the syntax:

from abc import ABC, abstractmethod

class AbstractClass(ABC):
@abstractmethod
def abstract_method(self):
pass

In this syntax:

  • ABC is the base class for defining abstract base classes.
  • abstractmethod is a decorator used to declare abstract methods, which must be overridden by concrete subclasses.
  • AbstractClass is an abstract class that inherits from ABC.
  • abstract_method is a method declared as abstract using the @abstractmethod decorator. It does not provide an implementation and must be overridden by concrete subclasses.

Concrete subclasses of AbstractClass must provide implementations for all abstract methods to be considered concrete and usable.

Example

In this example, ConcreteClass is a concrete subclass of AbstractClass, providing an implementation for the abstract method abstract_method.

--

--

Convincing you to read GOOD books and join me on my Machine Learning journey