Python For Beginners

Introduction to OOP Python

#Object-Oriented Programming (OOP) It is a programming paradigm that uses "objects" to model data and methods that can manipulate that data.
Enroll

#Object-Oriented Programming (OOP) It is a programming paradigm that uses "objects" to model data and methods that can manipulate that data. This approach aims to incorporate the principles of real-world objects into programming, making it more intuitive and aligned with how humans perceive the world. Unlike procedural programming, which focuses on functions or procedures to perform operations, OOP focuses on creating reusable and modular code through the use of classes and objects.

Brief History of OOP

The concept of OOP has its roots in the 1960s, but it became more widely recognized and utilized in the 1980s with the popularity of languages such as Smalltalk, which was one of the first programming languages to fully embrace object-oriented principles. Other languages, including C++ and Java, followed, each adding their own take on OOP principles and contributing to the widespread adoption of OOP in software development.

Classes and objects

They are the core building blocks of Object-Oriented Programming (OOP), providing the structure and mechanisms to model real-world entities and behaviors in software applications.

Definition of a Class and an Object

  • Class: A class is a blueprint for creating objects. It defines a set of attributes and methods that characterize any object of the class. In essence, a class outlines the properties (variables) and functionalities (methods) that the objects created from it will have.
  • Object: An object is an instance of a class. It is the actual entity that is created based on the structure defined by the class. Each object has its own set of attributes and can perform the methods defined in its class.

How to Define a Class and Create an Instance (Object) in Python

In Python, a class is defined using the class keyword, followed by the class name and a colon. Objects (instances of a class) are created by calling the class as if it were a function. Here's a basic example:

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

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

# Creating an instance (object) of the Dog class
my_dog = Dog(name="Rex", age=5

Class Attributes and Methods

  • Class Attributes: Attributes are variables that hold data about the class and its objects. They represent the state or properties of an object.
  • Methods: Methods are functions defined inside a class that operate on the objects of the class. They define the behaviors of the objects.

The init Method and self

  • init Method: This special method in Python is called automatically when a new object of a class is created. It is typically used to initialize the attributes of the object. The name __init__ is a convention in Python for an initializer method.
  • self: self represents the instance of the class and allows access to its attributes and methods. In Python, self is passed explicitly to each instance method within a class, including __init__.

Creating Multiple Objects from the Same Class

You can create multiple objects from the same class, each with its own unique state. For example:

dog1 = Dog(name="Buddy", age=3)
dog2 = Dog(name="Lucy", age=7)

# Each object can use the class methods to perform actions based on its own attributes
print(dog1.bark())  # Output: Buddy says woof!
print(dog2.bark())  # Output: Lucy says woof!

In this example, dog1 and dog2 are separate objects of the Dog class, each with its own name and age attributes. Despite being instances of the same class, they maintain distinct states and can interact with class methods independently. This demonstrates the power of OOP in creating modular and reusable code structures.

Let's break down the concepts of class and object with intuitive examples.

Class:

A class is like a blueprint or template for creating objects. It defines the attributes (data) and methods (functions) that all objects of that class will have. Think of a class as a category or type of thing.

Example: Car Class

class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    def display_info(self):
        print(f"Make: {self.make}, Model: {self.model}, Year: {self.year}")

In this example:

  • We define a Car class with attributes like make, model, and year.
  • The __init__ method is a special method used to initialize the object with initial values for its attributes.
  • The display_info method prints out information about the car.

Object:

An object is an instance of a class. It represents a specific realization of the class blueprint, with its own unique data. Objects have access to the methods defined in their class.

Example: Car Objects

# Creating car objects
car1 = Car("Toyota", "Corolla", 2020)
car2 = Car("Honda", "Civic", 2019)

# Accessing object attributes
print(car1.make)  # Output: Toyota
print(car2.model)  # Output: Civic

# Calling object methods
car1.display_info()  # Output: Make: Toyota, Model: Corolla, Year: 2020
car2.display_info()  # Output: Make: Honda, Model: Civic, Year: 2019

In this example:

  • We create two Car objects, car1 and car2, each with its own make, model, and year.
  • We access the object attributes using dot notation (object.attribute).
  • We call the display_info method on each object to print information about the cars.

In summary, a class defines the blueprint for creating objects, while an object is a specific instance of that class with its own unique data and behavior. Think of a class as a cookie cutter and an object as a cookie cut out from the dough using that cutter. Each cookie may have the same basic shape (class), but they can have different decorations or flavors (objects).

Encapsulation

is a fundamental concept in Object-Oriented Programming (OOP) that involves bundling the data (attributes) and methods (functions) that operate on the data into a single unit, or class. It serves as a mechanism to restrict direct access to some of an object's components, which is crucial for safeguarding against unauthorized access and unintended modification of data.

Definition and Importance of Encapsulation

Encapsulation ensures that an object's internal state is hidden from the outside, except through its own methods. This approach to data protection and abstraction allows for a clear modular structure for programs, making maintenance, modification, and debugging easier. By controlling access to the internal state of objects, encapsulation helps in:

  • Maintaining integrity of the data by preventing outside interference and misuse.
  • Decoupling the components of a program, thereby enhancing modularity and reusability.
  • Implementing an interface for object interaction that is independent of the implementation details.

Private vs Public Attributes and Methods

In the context of encapsulation, attributes and methods can be classified as either public or private, indicating their level of visibility and accessibility from outside the class:

  • Public attributes and methods are accessible from outside the class and allow direct interaction with an object’s data and behavior.
  • Private attributes and methods are intended to be inaccessible from outside the class. They are used internally by the class and are not meant to be visible to external code.

In Python, private attributes and methods are conventionally denoted by a prefix of one or two underscores (_ or __), with double underscores being used to name-mangle attributes to avoid naming conflicts in subclasses.

Using Getters and Setters in Python

Getters and setters are methods used to safely access and modify the private attributes of a class. Python provides property decorators that make it easier to implement getters and setters in a clean and readable way.

Here’s how to use getters and setters in Python:

class Person:
    def __init__(self, name, age):
        self._name = name  # A convention to indicate a protected attribute
        self._age = age

    # Getter for name
    @property
    def name(self):
        return self._name

    # Setter for name
    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise ValueError("Name must be a string")
        self._name = value

    # Getter for age
    @property
    def age(self):
        return self._age

    # Setter for age
    @age.setter
    def age(self, value):
        if not isinstance(value, int) or value < 0:
            raise ValueError("Age must be a positive integer")
        self._age = value

In this example, name and age are private attributes of the Person class, accessible and modifiable via the name and age properties. This setup ensures that the internal data can only be modified in ways that are permitted by the class methods, maintaining the integrity of the object's state.

Encapsulation, through the use of private attributes and the implementation of getters and setters, thus plays a crucial role in creating robust and flexible OOP designs.

Abstraction in Object-Oriented Programming

Objective

The goal of this section is to explain the concept of abstraction in Object-Oriented Programming (OOP). Abstraction is a fundamental OOP principle that focuses on hiding the complex reality while exposing only the necessary parts. It is about creating a simple model that represents more complex underlying code.

Definition and Importance of Abstraction

Abstraction is the process of hiding the complex implementation details of a system and exposing only the necessary features of the object to the outside world. In programming, this means that a user interacts with only what is necessary, without needing to understand the internal workings.

Abstraction helps in:

  • Reducing complexity by hiding unnecessary details.
  • Increasing reusability of code.
  • Making the code more maintainable and flexible.
  • Encouraging the decoupling of components.

Abstract Classes and Methods in Python

In Python, abstraction can be achieved by using abstract classes and methods. Abstract classes are classes that cannot be instantiated on their own and are designed to be subclassed. They often contain one or more abstract methods.

An abstract method is a method that is declared in the abstract class but must be implemented by the subclass. This ensures that a certain class structure is followed by the deriving classes.

Python provides the abc module to define abstract base classes (ABCs) and abstract methods. Here’s how to use them:

  1. Import the abc module.
  2. Define an abstract class by inheriting from abc.ABC.
  3. Use the @abstractmethod decorator to denote abstract methods.
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.__width = width
        self.__height = height

    def area(self):
        return self.__width * self.__height

    def perimeter(self):
        return 2 * (self.__width + self.__height)

In the above example, Shape is an abstract class that defines a general blueprint for shapes with methods area and perimeter. The Rectangle class implements these methods, providing the specific calculations for a rectangle.

When to Use Abstraction

Abstraction should be used when:

  • You have a set of methods that should be implemented by all subclasses, but the implementation may differ.
  • You want to hide complex logic from the user, providing a simpler interface.
  • You’re working on a large codebase with many developers, and you want to ensure a consistent API.

Abstraction, by hiding implementation details, allows developers to work independently on different parts of a system without affecting others. It’s a powerful tool for managing complexity and promoting scalability and maintainability in software development.

Definition and Benefits of Inheritance:

Inheritance is a fundamental concept in object-oriented programming (OOP) where a class (subclass or derived class) can inherit attributes and methods from another class (superclass or base class). This means that a subclass can reuse code from its superclass, leading to cleaner and more efficient code organization.

Benefits of Inheritance:

  1. Code Reusability: Inheritance allows subclasses to inherit attributes and methods from their superclass, reducing code duplication.
  2. Modularity: Classes can be organized in a hierarchical structure, making it easier to manage and understand the relationships between different objects.
  3. Extensibility: Subclasses can add new features or modify existing behavior without modifying the superclass, thus promoting flexibility and scalability.
  4. Polymorphism: Inheritance enables polymorphic behavior, where objects of different classes can be treated uniformly through a common interface provided by the superclass.

Creating Subclasses in Python:

In Python, subclasses can be created by specifying the superclass in parentheses after the subclass name in the class definition.

class Animal:
    def sound(self):
        print("Some generic sound")

class Dog(Animal):  # Dog class inherits from Animal
    def sound(self):  # Method overriding
        print("Woof")

class Cat(Animal):  # Cat class inherits from Animal
    def sound(self):  # Method overriding
        print("Meow")

# Creating instances
dog = Dog()
cat = Cat()

# Calling methods
dog.sound()  # Output: Woof
cat.sound()  # Output: Meow

The super() Function:

The super() function is used to call methods from the superclass within the subclass. It returns a proxy object that delegates method calls to the superclass.

class Animal:
    def sound(self):
        print("Some generic sound")

class Dog(Animal):
    def sound(self):
        super().sound()  # Calling superclass method
        print("Woof")

# Creating instance
dog = Dog()

# Calling method
dog.sound()  
# Output:
# Some generic sound
# Woof

Method Overriding:

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

class Animal:
    def sound(self):
        print("Some generic sound")

class Dog(Animal):
    def sound(self):  # Overriding method
        print("Woof")

# Creating instance
dog = Dog()

# Calling method
dog.sound()  # Output: Woof

In the example above, the sound() method in the Dog class overrides the sound() method in the Animal class.

Sure, let's consider a practical example to explain inheritance.

Example: Shape Hierarchy

Suppose we're modeling various shapes in a simple 2D geometry application. We can start with a base class Shape and then create subclasses for specific shapes like Circle, Rectangle, and Triangle. Each subclass will inherit common properties and methods from the Shape class.

class Shape:
    def __init__(self, color):
        self.color = color

    def area(self):
        pass

    def perimeter(self):
        pass

class Circle(Shape):
    def __init__(self, color, radius):
        super().__init__(color)
        self.radius = radius

    def area(self):
        return 3.14 * self.radius**2

    def perimeter(self):
        return 2 * 3.14 * self.radius

class Rectangle(Shape):
    def __init__(self, color, width, height):
        super().__init__(color)
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

class Triangle(Shape):
    def __init__(self, color, a, b, c):
        super().__init__(color)
        self.a = a
        self.b = b
        self.c = c

    def area(self):
        s = (self.a + self.b + self.c) / 2
        return (s * (s - self.a) * (s - self.b) * (s - self.c)) ** 0.5

    def perimeter(self):
        return self.a + self.b + self.c

In this example:

  • We define a base class Shape with attributes like color and methods like area() and perimeter().
  • Subclasses like Circle, Rectangle, and Triangle inherit from Shape.
  • Each subclass implements its own version of area() and perimeter() according to the specific formula for that shape.

Let's use this hierarchy:

circle = Circle("red", 5)
print("Circle Area:", circle.area())
print("Circle Perimeter:", circle.perimeter())

rectangle = Rectangle("blue", 4, 6)
print("Rectangle Area:", rectangle.area())
print("Rectangle Perimeter:", rectangle.perimeter())

triangle = Triangle("green", 3, 4, 5)
print("Triangle Area:", triangle.area())
print("Triangle Perimeter:", triangle.perimeter())


Output:

Circle Area: 78.5Circle 
Perimeter: 31.400000000000002
Rectangle Area: 24
Rectangle Perimeter: 20
Triangle Area: 6.0
Triangle Perimeter: 12


In this example, inheritance helps us reuse code and maintain a clear, organized structure. All shapes share common properties and methods defined in the Shape class, while each shape type has its own specialized behavior defined in its subclass. This approach makes our code modular, extensible, and easier to manage.

Polymorphism:

Polymorphism is a fundamental concept in object-oriented programming (OOP) that allows objects to take on multiple forms. In essence, it enables objects of different classes to be treated as objects of a common superclass. This facilitates code reuse, flexibility, and extensibility in software development.

Importance of Polymorphism:

  1. Flexibility: Polymorphism allows for more flexible code as it enables objects to behave differently based on their specific implementations.
  2. Code Reusability: Polymorphism promotes code reuse by allowing methods to accept objects of different classes that share a common interface.
  3. Extensibility: It facilitates the addition of new functionality by extending existing classes without modifying their interfaces, thus promoting scalability and maintainability.
  4. Reduced Coupling: Polymorphism reduces coupling between components of a system by promoting interaction through interfaces rather than concrete implementations.

Method Overloading and Overriding:

  • Method Overloading: Method overloading involves defining multiple methods with the same name but with different parameters or signatures within the same class. In Python, method overloading is achieved through default parameter values or variable-length argument lists.
class MathOperations:
    def add(self, x, y):
        return x + y

    def add(self, x, y, z):
        return x + y + z

# Overloading in Python is not supported like other languages, the latest definition will override the previous ones

  • Method Overriding: Method overriding occurs when a subclass provides a specific implementation of a method that is already defined in its superclass. This allows subclasses to modify or extend the behavior of inherited methods.
class Animal:
    def sound(self):
        print("Some generic sound")

class Dog(Animal):
    def sound(self):  # Overriding method
        print("Woof")

# Creating instance
dog = Dog()

# Calling method
dog.sound()  # Output: Woof

Operator Overloading:

Operator overloading enables operators such as +, -, *, /, etc., to be used with user-defined classes. In Python, this is achieved by defining special methods with double underscores (e.g., __add__, __sub__, __mul__, __div__, etc.).

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

# Creating instances
v1 = Vector(2, 3)
v2 = Vector(4, 5)

# Adding two vectors
result = v1 + v2
print("Resultant Vector:", result.x, result.y)  # Output: Resultant Vector: 6 8

Polymorphism with Inheritance:

Polymorphism in inheritance allows objects of different subclasses to be treated as objects of their superclass. This enables the use of a common interface to manipulate objects of various subclasses.

class Animal:
    def sound(self):
        print("Some generic sound")

class Dog(Animal):
    def sound(self):  # Overriding method
        print("Woof")

class Cat(Animal):
    def sound(self):  # Overriding method
        print("Meow")

# Polymorphic behavior
def make_sound(animal):
    animal.sound()

# Creating instances
dog = Dog()
cat = Cat()

# Polymorphic function call
make_sound(dog)  # Output: Woof
make_sound(cat)  # Output: Meow

In this example, both Dog and Cat are subclasses of Animal. The make_sound() function accepts objects of the Animal class or its subclasses. It demonstrates polymorphic behavior by invoking the sound() method, which behaves differently based on the actual type of the object passed to it.

#Best Practices in OOP:

  1. Naming Conventions for Classes and Methods:
    • Use descriptive and meaningful names for classes and methods that accurately represent their purpose and functionality.
    • Follow naming conventions such as using CamelCase for class names and lowercase with underscores for method names (PEP 8 style guide for Python).
  2. DRY (Don't Repeat Yourself):
    • Avoid code duplication by extracting common functionality into reusable components such as functions, methods, or classes.
    • Use inheritance, composition, or modules to encapsulate and share code across different parts of the program.
  3. KISS (Keep It Simple, Stupid):
    • Strive for simplicity and clarity in your code. Write code that is easy to understand, maintain, and debug.
    • Avoid unnecessary complexity or over-engineering. Prefer straightforward solutions over convoluted ones.
  4. Organizing Classes in Modules and Packages:
    • Organize related classes into modules (Python files) based on functionality or domain.
    • Use packages (directories containing modules) to group related modules together.
    • Follow a logical hierarchy and naming convention for modules and packages to make it easier to navigate and understand the codebase.

Example:

Suppose we are designing a simple library management system.

# File: library.py (Module)
class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def display_info(self):
        print(f"Title: {self.title}, Author: {self.author}")

class Library:
    def __init__(self, name):
        self.name = name
        self.books = []

    def add_book(self, book):
        self.books.append(book)

    def display_books(self):
        print(f"Books in {self.name}:")
        for book in self.books:
            book.display_info()

# File: main.py
from library import Book, Library

# Creating instances
book1 = Book("The Great Gatsby", "F. Scott Fitzgerald")
book2 = Book("To Kill a Mockingbird", "Harper Lee")

library = Library("My Library")

# Adding books to library
library.add_book(book1)
library.add_book(book2)

# Displaying books
library.display_books()

In this example:

  • Classes Book and Library are defined in the library.py module.
  • Classes are named using CamelCase and methods using lowercase_with_underscores.
  • The Library class encapsulates a collection of books and provides methods to add books and display them.
  • The main.py file imports classes from the library module and demonstrates how to use them.

By following these best practices, the code becomes more readable, maintainable, and reusable, making it easier to collaborate with other developers and extend the system in the future.

Step-by-Step Walkthrough: Designing a Simple Inventory System

Let's design a basic inventory system for a small retail store. We'll have classes for Product, Inventory, and Store.

1. Define the Product Class:

class Product:
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity

    def display_info(self):
        print(f"Name: {self.name}, Price: ${self.price}, Quantity: {self.quantity}")

2. Define the Inventory Class:

class Inventory:
    def __init__(self):
        self.products = []

    def add_product(self, product):
        self.products.append(product)

    def display_inventory(self):
        print("Inventory:")
        for product in self.products:
            product.display_info()

3. Define the Store Class:

class Store:
    def __init__(self, name):
        self.name = name
        self.inventory = Inventory()

    def add_product(self, product):
        self.inventory.add_product(product)

    def display_store_inventory(self):
        print(f"Store: {self.name}")
        self.inventory.display_inventory()

Example Usage:

# Create products
product1 = Product("Laptop", 1000, 10)
product2 = Product("Phone", 500, 20)

# Create store
store = Store("Electronics World")

# Add products to store inventory
store.add_product(product1)
store.add_product(product2)

# Display store inventory
store.display_store_inventory()

Exercises:

  1. Modify the Product class to include a method total_value() that calculates and returns the total value of a product (price * quantity).
  2. Implement a method in the Inventory class to find and return a product by its name.
  3. Extend the Store class with methods to remove a product from the inventory and update its quantity.

Challenge Projects:

  1. Enhance the inventory system to include features like adding discounts, tracking sales, and generating reports.
  2. Develop a more comprehensive retail management system that supports multiple stores, customer transactions, and employee management.
  3. Create a game using OOP principles, such as a text-based adventure game or a simple simulation game.

Lesson Assignment
Challenge yourself with our lab assignment and put your skills to test.
# Python Program to find the area of triangle

a = 5
b = 6
c = 7

# Uncomment below to take inputs from the user
# a = float(input('Enter first side: '))
# b = float(input('Enter second side: '))
# c = float(input('Enter third side: '))

# calculate the semi-perimeter
s = (a + b + c) / 2

# calculate the area
area = (s*(s-a)*(s-b)*(s-c)) ** 0.5
print('The area of the triangle is %0.2f' %area)
Sign up to get access to our code lab and run this code.
Sign up

Create Your Account Now!

Join our platform today and unlock access to expert- led courses, hands- on exercises, and a supportive learning community.
Sign Up