Summary Notes

Object-Oriented Programming (OOP) in Python

OOP is a programming paradigm that organizes code around objects and classes. Python supports OOP with full features including inheritance, polymorphism, encapsulation, and abstraction.

Key Concepts:

Inheritance: Allows a class to inherit attributes and methods from another class

Polymorphism: Ability of different classes to be treated as instances of the same class through inheritance

Encapsulation: Hiding internal details and protecting data

Special Methods:

Exception Handling

Exceptions are errors that occur during program execution. Python provides robust exception handling to manage runtime errors gracefully.

Exception Hierarchy:

Common Built-in Exceptions:

Exception Handling Structure:

try:
    # Code that might raise exception
except ExceptionType:
    # Handle specific exception
except (ExceptionType1, ExceptionType2):
    # Handle multiple exceptions
else:
    # Execute if no exception
finally:
    # Always execute

Raising Exceptions:

Custom Exceptions: Create by inheriting from Exception class

Algorithms and Data Structures

Algorithms are step-by-step procedures for solving problems. Understanding algorithms is crucial for efficient programming.

Sorting Algorithms:

Searching Algorithms:

Algorithm Analysis:

Common Data Structures:

Data Analysis with NumPy and Pandas

NumPy and Pandas are essential libraries for data manipulation and analysis in Python.

NumPy:

Array Creation:

Pandas:

DataFrame Operations:

Matplotlib: Plotting library for data visualization

Q&A

What is a class in Python?
A class is a blueprint for creating objects. It defines attributes and methods that objects of that class will have.
How to handle exceptions?
Use try-except blocks. Put potentially error-causing code in try block, and error handling in except block.
What is NumPy used for?
NumPy is used for numerical computing in Python. It provides support for large, multi-dimensional arrays and matrices, along with mathematical functions to operate on them.
How to create a Pandas DataFrame?
Use pd.DataFrame(data) where data can be a dictionary, list, or other data structure.
What is inheritance in OOP?
Inheritance allows a class (child) to inherit attributes and methods from another class (parent), promoting code reuse.
What is polymorphism?
Polymorphism allows objects of different classes to be treated as objects of a common superclass, enabling method overriding.
How do you raise an exception?
Use the raise keyword: raise ExceptionType("error message").
What is the difference between __str__ and __repr__?
__str__ is for user-friendly string representation, __repr__ is for unambiguous representation used by developers.
What is Big O notation?
Big O notation describes the upper bound of an algorithm's time or space complexity as input size grows.
How does NumPy broadcasting work?
Broadcasting allows operations on arrays of different shapes by automatically expanding smaller arrays to match larger ones.
What is a Pandas Series?
A Series is a one-dimensional labeled array in Pandas, similar to a column in a spreadsheet.
How to handle missing data in Pandas?
Use methods like dropna() to remove missing values, fillna() to fill them, or isnull() to detect them.
What is method overriding?
Method overriding occurs when a subclass provides a specific implementation of a method already defined in its superclass.
How do you create a custom exception?
Create a class that inherits from Exception: class MyError(Exception): pass
What is the time complexity of binary search?
O(log n) where n is the number of elements in the search space.
How to merge DataFrames in Pandas?
Use pd.merge() or DataFrame.merge() with parameters for join type (inner, outer, left, right).
What is encapsulation in OOP?
Encapsulation is the bundling of data and methods that operate on that data within a single unit (class), hiding internal details.
How does garbage collection work in Python?
Python uses reference counting and cyclic garbage collection to automatically manage memory and clean up unused objects.
What is a lambda function?
A lambda function is an anonymous function defined with the lambda keyword: lambda x: x**2
How to create a NumPy array from a list?
Use np.array(list_name) to convert a Python list to a NumPy array.
What is the difference between loc and iloc in Pandas?
loc uses label-based indexing, iloc uses integer-based (positional) indexing.

Fill in the Blanks

  1. ________ is the method called when an object is created. (__init__)
  2. ________ handles runtime errors in Python. (Exceptions)
  3. ________ sort is a simple sorting algorithm. (Bubble)
  4. ________ is a library for data manipulation. (Pandas)
  5. ________ is used for plotting in Python. (Matplotlib)
  6. ________ allows a class to inherit from another class. (Inheritance)
  7. ________ exception is raised for division by zero. (ZeroDivisionError)
  8. ________ search works on sorted arrays. (Binary)
  9. ________ provides N-dimensional arrays. (NumPy)
  10. ________ represents 2D data in Pandas. (DataFrame)
  11. ________ hides internal class details. (Encapsulation)
  12. ________ allows different classes to be treated uniformly. (Polymorphism)
  13. ________ handles exceptions in Python. (try-except)
  14. ________ complexity measures algorithm efficiency. (Time)
  15. ________ creates anonymous functions. (lambda)
  16. ________ joins DataFrames in Pandas. (merge)
  17. ________ provides vectorized operations. (NumPy)
  18. ________ is used for data visualization. (Matplotlib)
  19. ________ creates custom exceptions. (class)
  20. ________ searches sequentially through data. (Linear)

True/False

  1. Classes can inherit from multiple classes in Python. (True)
  2. Try block must have an except block. (False)
  3. Binary search works on unsorted lists. (False)
  4. NumPy arrays are faster than lists for numerical operations. (True)
  5. Pandas is built on top of NumPy. (True)
  6. __str__ method is called for string representation. (True)
  7. Exception handling prevents program crashes. (True)
  8. Bubble sort has O(n log n) time complexity. (False)
  9. DataFrame is a 2D data structure in Pandas. (True)
  10. Matplotlib is used for data visualization. (True)
  11. Private attributes start with double underscore. (True)
  12. Polymorphism requires inheritance. (True)
  13. Finally block executes only when no exception occurs. (False)
  14. Binary search is faster than linear search. (True)
  15. NumPy supports broadcasting. (True)
  16. Pandas Series is 2D. (False)
  17. Method overriding changes parent class behavior. (True)
  18. Custom exceptions inherit from BaseException. (False)
  19. Time complexity measures space usage. (False)
  20. Lambda functions can have multiple statements. (False)

Multiple Choice Questions

  1. What keyword defines a class?
    a) def
    b) class
    c) object
    d) init
    Answer: b) class
  2. Which exception is raised for division by zero?
    a) ValueError
    b) TypeError
    c) ZeroDivisionError
    d) IndexError
    Answer: c) ZeroDivisionError
  3. What does Pandas DataFrame represent?
    a) A 1D array
    b) A 2D table
    c) A list
    d) A dictionary
    Answer: b) A 2D table
  4. Which is the correct syntax for inheritance?
    a) class Child(Parent):
    b) class Child extends Parent:
    c) class Child inherits Parent:
    d) class Child from Parent:
    Answer: a) class Child(Parent):
  5. What is the time complexity of bubble sort?
    a) O(n)
    b) O(n log n)
    c) O(n²)
    d) O(1)
    Answer: c) O(n²)
  6. Which method is called when an object is created?
    a) __new__
    b) __init__
    c) __create__
    d) __start__
    Answer: b) __init__
  7. What does 'self' refer to in a class?
    a) The class itself
    b) The current instance
    c) The parent class
    d) A global variable
    Answer: b) The current instance
  8. Which exception is raised for invalid dictionary key?
    a) ValueError
    b) TypeError
    c) KeyError
    d) IndexError
    Answer: c) KeyError
  9. What is the purpose of super()?
    a) Create parent object
    b) Call parent class methods
    c) Access class variables
    d) Create child object
    Answer: b) Call parent class methods
  10. Which library provides N-dimensional arrays?
    a) Pandas
    b) Matplotlib
    c) NumPy
    d) SciPy
    Answer: c) NumPy
  11. What does df.head() do in Pandas?
    a) Shows last rows
    b) Shows first 5 rows
    c) Shows column names
    d) Shows data types
    Answer: b) Shows first 5 rows
  12. Which sorting algorithm does Python use internally?
    a) Bubble sort
    b) Quick sort
    c) TimSort
    d) Merge sort
    Answer: c) TimSort
  13. What is encapsulation?
    a) Hiding data
    b) Combining data and methods
    c) Both a and b
    d) Creating objects
    Answer: c) Both a and b
  14. Which method removes missing values in Pandas?
    a) drop()
    b) remove()
    c) dropna()
    d) delete()
    Answer: c) dropna()
  15. What is the time complexity of binary search?
    a) O(n)
    b) O(log n)
    c) O(n log n)
    d) O(n²)
    Answer: b) O(log n)

Application Based Questions

1. Implement a simple class for a Bank Account with deposit and withdraw methods.

Solution:

class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance
    
    def deposit(self, amount):
        if amount > 0:
            self.balance += amount
            return f"Deposited ${amount}. New balance: ${self.balance}"
        return "Invalid deposit amount"
    
    def withdraw(self, amount):
        if amount > 0 and amount <= self.balance:
            self.balance -= amount
            return f"Withdrew ${amount}. New balance: ${self.balance}"
        return "Invalid withdrawal amount or insufficient funds"
    
    def get_balance(self):
        return f"Current balance: ${self.balance}"

# Usage
account = BankAccount(100)
print(account.deposit(50))    # Deposited $50. New balance: $150
print(account.withdraw(30))   # Withdrew $30. New balance: $120
print(account.get_balance())  # Current balance: $120

2. Create a class hierarchy for shapes with inheritance and polymorphism.

Solution:

class Shape:
    def area(self):
        pass
    
    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)

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14159 * self.radius ** 2
    
    def perimeter(self):
        return 2 * 3.14159 * self.radius

# Polymorphism in action
shapes = [Rectangle(5, 3), Circle(4)]
for shape in shapes:
    print(f"Area: {shape.area():.2f}, Perimeter: {shape.perimeter():.2f}")

3. Implement bubble sort algorithm with exception handling.

Solution:

def bubble_sort(arr):
    if not isinstance(arr, list):
        raise TypeError("Input must be a list")
    
    n = len(arr)
    for i in range(n):
        for j in range(0, n-i-1):
            try:
                if arr[j] > arr[j+1]:
                    arr[j], arr[j+1] = arr[j+1], arr[j]
            except TypeError:
                raise TypeError("List elements must be comparable")
    return arr

# Usage
try:
    numbers = [64, 34, 25, 12, 22, 11, 90]
    sorted_numbers = bubble_sort(numbers)
    print("Sorted array:", sorted_numbers)
except Exception as e:
    print(f"Error: {e}")

4. Create a data analysis program using NumPy and Pandas.

Solution:

import numpy as np
import pandas as pd

# Create sample data
data = {
    'Name': ['Alice', 'Bob', 'Charlie', 'Diana'],
    'Age': [25, 30, 35, 28],
    'Score': [85, 92, 78, 96]
}

df = pd.DataFrame(data)

# Basic analysis
print("DataFrame:")
print(df)
print("\nSummary statistics:")
print(df.describe())
print(f"\nAverage age: {df['Age'].mean():.1f}")
print(f"Highest score: {df['Score'].max()}")

# NumPy operations
ages = np.array(df['Age'])
scores = np.array(df['Score'])

print(f"Age standard deviation: {np.std(ages):.2f}")
print(f"Score correlation with age: {np.corrcoef(ages, scores)[0,1]:.3f}")

5. Implement a custom exception and use it in a class.

Solution:

class InsufficientFundsError(Exception):
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"Insufficient funds. Balance: ${balance}, Required: ${amount}")

class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance
    
    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientFundsError(self.balance, amount)
        self.balance -= amount
        return self.balance

# Usage
account = BankAccount(100)
try:
    account.withdraw(150)
except InsufficientFundsError as e:
    print(f"Transaction failed: {e}")
else:
    print(f"Withdrawal successful. New balance: ${account.balance}")

6. Create a binary search function with proper error handling.

Solution:

def binary_search(arr, target):
    if not isinstance(arr, list):
        raise TypeError("Input must be a list")
    
    left, right = 0, len(arr) - 1
    
    while left <= right:
        mid = (left + right) // 2
        try:
            if arr[mid] == target:
                return mid
            elif arr[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
        except TypeError:
            raise TypeError("List elements must be comparable")
    
    return -1  # Not found

# Usage
sorted_list = [1, 3, 5, 7, 9, 11, 13, 15]
target = 7
index = binary_search(sorted_list, target)
if index != -1:
    print(f"Element {target} found at index {index}")
else:
    print(f"Element {target} not found")

7. Create a Pandas program to analyze student grades.

Solution:

import pandas as pd

# Create student data
data = {
    'Student': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'],
    'Math': [85, 92, 78, 96, 88],
    'Science': [90, 85, 92, 88, 95],
    'English': [88, 90, 85, 92, 87]
}

df = pd.DataFrame(data)

# Add total and average columns
df['Total'] = df[['Math', 'Science', 'English']].sum(axis=1)
df['Average'] = df[['Math', 'Science', 'English']].mean(axis=1)

# Grade based on average
def assign_grade(avg):
    if avg >= 90:
        return 'A'
    elif avg >= 80:
        return 'B'
    elif avg >= 70:
        return 'C'
    else:
        return 'F'

df['Grade'] = df['Average'].apply(assign_grade)

print("Student Report:")
print(df)
print(f"\nClass Average: {df['Average'].mean():.2f}")
print(f"Highest Scorer: {df.loc[df['Total'].idxmax(), 'Student']}")

Examples

Object-Oriented Programming - Basic Class

class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
        self.mileage = 0
    
    def drive(self, miles):
        self.mileage += miles
        return f"Drove {miles} miles. Total mileage: {self.mileage}"
    
    def get_info(self):
        return f"{self.year} {self.make} {self.model}, Mileage: {self.mileage}"

# Usage
my_car = Car("Toyota", "Camry", 2020)
print(my_car.get_info())        # 2020 Toyota Camry, Mileage: 0
print(my_car.drive(100))        # Drove 100 miles. Total mileage: 100
print(my_car.get_info())        # 2020 Toyota Camry, Mileage: 100

Inheritance and Polymorphism

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return "Some sound"

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

# Polymorphism
animals = [Dog("Buddy"), Cat("Whiskers"), Animal("Unknown")]
for animal in animals:
    print(animal.speak())

# Method overriding and super()
class Bird(Animal):
    def __init__(self, name, can_fly=True):
        super().__init__(name)
        self.can_fly = can_fly
    
    def speak(self):
        return f"{self.name} says Chirp!"

bird = Bird("Tweety")
print(bird.speak())  # Tweety says Chirp!

Encapsulation and Special Methods

class BankAccount:
    def __init__(self, balance=0):
        self.__balance = balance  # Private attribute
    
    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
    
    def get_balance(self):
        return self.__balance
    
    def __str__(self):
        return f"Account balance: ${self.__balance}"
    
    def __add__(self, other):
        if isinstance(other, BankAccount):
            return BankAccount(self.__balance + other.__balance)
        return BankAccount(self.__balance + other)

account1 = BankAccount(100)
account2 = BankAccount(200)
account1.deposit(50)
print(account1)              # Account balance: $150
combined = account1 + account2
print(combined)              # Account balance: $350

Exception Handling

def divide_numbers(a, b):
    try:
        result = a / b
        return result
    except ZeroDivisionError:
        return "Cannot divide by zero"
    except TypeError:
        return "Invalid input types"
    finally:
        print("Division operation completed")

# Usage
print(divide_numbers(10, 2))    # 5.0
print(divide_numbers(10, 0))    # Cannot divide by zero
print(divide_numbers(10, "2"))  # Invalid input types

# Custom exception
class NegativeNumberError(Exception):
    pass

def sqrt_positive(num):
    if num < 0:
        raise NegativeNumberError("Cannot take square root of negative number")
    return num ** 0.5

try:
    print(sqrt_positive(-4))
except NegativeNumberError as e:
    print(e)

Sorting Algorithms

# Bubble Sort
def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(0, n-i-1):
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
    return arr

# Selection Sort
def selection_sort(arr):
    for i in range(len(arr)):
        min_idx = i
        for j in range(i+1, len(arr)):
            if arr[j] < arr[min_idx]:
                min_idx = j
        arr[i], arr[min_idx] = arr[min_idx], arr[i]
    return arr

# Binary Search
def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1

# Usage
numbers = [64, 34, 25, 12, 22, 11, 90]
print("Original:", numbers)
print("Bubble sorted:", bubble_sort(numbers.copy()))
print("Selection sorted:", selection_sort(numbers.copy()))

sorted_numbers = sorted(numbers)
print("Binary search for 25:", binary_search(sorted_numbers, 25))

NumPy Arrays and Operations

import numpy as np

# Creating arrays
arr1 = np.array([1, 2, 3, 4, 5])
arr2 = np.array([[1, 2], [3, 4]])
zeros = np.zeros((3, 3))
ones = np.ones((2, 4))
range_arr = np.arange(0, 10, 2)
linspace_arr = np.linspace(0, 1, 5)

print("1D array:", arr1)
print("2D array:\n", arr2)
print("Zeros:\n", zeros)

# Array operations
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print("Addition:", a + b)
print("Multiplication:", a * b)
print("Dot product:", np.dot(a, b))

# Mathematical functions
angles = np.array([0, np.pi/2, np.pi])
print("Sine values:", np.sin(angles))
print("Cosine values:", np.cos(angles))

# Statistical operations
data = np.random.randn(100)
print(f"Mean: {np.mean(data):.3f}")
print(f"Standard deviation: {np.std(data):.3f}")
print(f"Min: {np.min(data):.3f}, Max: {np.max(data):.3f}")

# Broadcasting
matrix = np.array([[1, 2, 3], [4, 5, 6]])
vector = np.array([1, 0, -1])
result = matrix + vector  # Broadcasting
print("Broadcasting result:\n", result)

Pandas DataFrames

import pandas as pd

# Creating DataFrame
data = {
    'Name': ['Alice', 'Bob', 'Charlie', 'Diana'],
    'Age': [25, 30, 35, 28],
    'City': ['NYC', 'LA', 'Chicago', 'Houston'],
    'Salary': [50000, 60000, 55000, 65000]
}

df = pd.DataFrame(data)

print("DataFrame:")
print(df)
print("\nDataFrame info:")
print(df.info())
print("\nDescriptive statistics:")
print(df.describe())

# Selecting data
print("\nNames and Ages:")
print(df[['Name', 'Age']])
print("\nPeople from NYC:")
print(df[df['City'] == 'NYC'])

# Adding new column
df['Experience'] = [2, 5, 8, 3]
print("\nWith Experience column:")
print(df)

# Grouping and aggregation
print("\nAverage salary by city:")
print(df.groupby('City')['Salary'].mean())

# Handling missing data
df_with_nan = df.copy()
df_with_nan.loc[0, 'Salary'] = None
print("\nDataFrame with NaN:")
print(df_with_nan)
print("\nAfter dropping NaN:")
print(df_with_nan.dropna())

Data Visualization with Matplotlib

import matplotlib.pyplot as plt
import numpy as np

# Line plot
x = np.linspace(0, 10, 100)
y = np.sin(x)
plt.figure(figsize=(10, 6))
plt.plot(x, y, label='sin(x)', color='blue', linewidth=2)
plt.title('Sine Wave')
plt.xlabel('x')
plt.ylabel('sin(x)')
plt.grid(True)
plt.legend()
plt.show()

# Bar chart
categories = ['A', 'B', 'C', 'D']
values = [23, 45, 56, 78]
plt.figure(figsize=(8, 5))
plt.bar(categories, values, color=['red', 'green', 'blue', 'orange'])
plt.title('Sample Bar Chart')
plt.xlabel('Categories')
plt.ylabel('Values')
plt.show()

# Scatter plot
x = np.random.randn(100)
y = 2*x + np.random.randn(100)
plt.figure(figsize=(8, 6))
plt.scatter(x, y, alpha=0.6, color='purple')
plt.title('Scatter Plot')
plt.xlabel('X values')
plt.ylabel('Y values')
plt.grid(True)
plt.show()

# Histogram
data = np.random.normal(0, 1, 1000)
plt.figure(figsize=(8, 6))
plt.hist(data, bins=30, alpha=0.7, color='green', edgecolor='black')
plt.title('Normal Distribution Histogram')
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.show()

Comprehensive OOP Example

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.__salary = salary  # Private attribute
    
    def get_salary(self):
        return self.__salary
    
    def set_salary(self, salary):
        if salary > 0:
            self.__salary = salary
    
    def __str__(self):
        return f"Employee: {self.name}, Salary: ${self.__salary}"

class Manager(Employee):
    def __init__(self, name, salary, department):
        super().__init__(name, salary)
        self.department = department
        self.employees = []
    
    def add_employee(self, employee):
        if isinstance(employee, Employee):
            self.employees.append(employee)
    
    def get_team_size(self):
        return len(self.employees)
    
    def __str__(self):
        return f"Manager: {self.name}, Department: {self.department}, Team Size: {self.get_team_size()}"

# Usage
emp1 = Employee("Alice", 50000)
emp2 = Employee("Bob", 45000)
manager = Manager("Charlie", 70000, "IT")

manager.add_employee(emp1)
manager.add_employee(emp2)

print(emp1)
print(manager)
print(f"Manager's salary: ${manager.get_salary()}")