🏗️ Unit-V: Object-Oriented Programming in Python

Lecture 1: OOP Concepts, Classes, and Objects

1. Introduction to Object-Oriented Programming

Object-Oriented Programming (OOP) is a programming paradigm that uses objects and classes to design applications and computer programs. It provides a clear structure for the programs and makes code more maintainable and reusable.

Class

A blueprint for creating objects. It defines a set of attributes and methods that the created objects will have.

Object

An instance of a class. It's a self-contained component that contains properties and methods needed to make a certain type of data useful.

Method

A function that is defined inside a class and is associated with an object. It defines the behavior of the object.

Attribute

A variable that is defined inside a class and is associated with an object. It represents the state of the object.

Four Pillars of OOP

Encapsulation
Inheritance
Polymorphism
Abstraction
Encapsulation

Binding of data and functions that manipulate that data into a single unit called a class. It restricts direct access to some of an object's components, which is a way of preventing accidental modification of data.

class BankAccount: def __init__(self, initial_balance=0): self._balance = initial_balance # Protected member def deposit(self, amount): if amount > 0: self._balance += amount return f"Deposited ${amount}" return "Invalid amount" def get_balance(self): return f"Current balance: ${self._balance}" # Usage account = BankAccount(100) print(account.deposit(50)) # Deposited $50 print(account.get_balance()) # Current balance: $150
Inheritance

Allows a class to inherit attributes and methods from another class. The class that inherits is called a child class, and the class being inherited from is called a parent class.

class Animal: def __init__(self, name): self.name = name def speak(self): raise NotImplementedError("Subclass must implement this method") class Dog(Animal): def speak(self): return f"{self.name} says Woof!" class Cat(Animal): def speak(self): return f"{self.name} says Meow!" # Usage dog = Dog("Buddy") cat = Cat("Whiskers") print(dog.speak()) # Buddy says Woof! print(cat.speak()) # Whiskers says Meow!
Polymorphism

Allows methods to do different things based on the object it is acting upon. The same method name can be used for different types.

class Bird: def intro(self): return "I am a bird" def flight(self): return "Most birds can fly but some cannot" class Sparrow(Bird): def flight(self): return "Sparrows can fly" class Ostrich(Bird): def flight(self): return "Ostriches cannot fly" # Polymorphic function def bird_info(bird): print(bird.intro()) print(bird.flight()) # Usage bird = Bird() sparrow = Sparrow() ostrich = Ostrich() bird_info(bird) # I am a bird\nMost birds can fly but some cannot bird_info(sparrow) # I am a bird\nSparrows can fly bird_info(ostrich) # I am a bird\nOstriches cannot fly
Abstraction

Hiding the implementation details and showing only the necessary features of an object. In Python, we use abstract base classes (ABC) to achieve abstraction.

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) # Usage rect = Rectangle(5, 4) print(f"Area: {rect.area()}") # Area: 20 print(f"Perimeter: {rect.perimeter()}") # Perimeter: 18 # This will raise an error because Shape is abstract # shape = Shape() # TypeError: Can't instantiate abstract class Shape
2. Classes in Python

In Python, a class is created using the class keyword. The class name should follow the CapWords convention.

Creating a Simple Class

class Dog: # Class attribute (shared by all instances) species = "Canis familiaris" # Initializer / Instance attributes def __init__(self, name, age): self.name = name self.age = age # Instance method def description(self): return f"{self.name} is {self.age} years old" # Another instance method def speak(self, sound): return f"{self.name} says {sound}" # Create instances of the Dog class buddy = Dog("Buddy", 9) miles = Dog("Miles", 4) # Access the instance attributes print(buddy.name) # Buddy print(buddy.age) # 9 # Call instance methods print(buddy.description()) # Buddy is 9 years old print(miles.speak("Woof Woof")) # Miles says Woof Woof # Access class attribute print(buddy.species) # Canis familiaris print(miles.species) # Canis familiaris # Check instance type print(type(buddy)) # print(isinstance(buddy, Dog)) # True

Class and Instance Variables

class Employee: # Class variable (shared by all instances) raise_amount = 1.04 # 4% raise num_of_emps = 0 def __init__(self, first, last, pay): # Instance variables (unique to each instance) self.first = first self.last = last self.pay = pay self.email = f"{first.lower()}.{last.lower()}@company.com" # Increment the number of employees Employee.num_of_emps += 1 def fullname(self): return f"{self.first} {self.last}" def apply_raise(self): self.pay = int(self.pay * self.raise_amount) # Create employee instances emp_1 = Employee('John', 'Doe', 50000) emp_2 = Employee('Jane', 'Smith', 60000) # Access class variable through instance print(emp_1.raise_amount) # 1.04 print(emp_2.raise_amount) # 1.04 # Access class variable through class print(Employee.raise_amount) # 1.04 # Change class variable through class (affects all instances) Employee.raise_amount = 1.05 print(emp_1.raise_amount) # 1.05 # Change class variable through instance (creates instance variable) emp_1.raise_amount = 1.06 print(emp_1.raise_amount) # 1.06 (instance variable) print(emp_2.raise_amount) # 1.05 (class variable) # Number of employees print(Employee.num_of_emps) # 2
Note: When you access an attribute on an instance, Python first checks if the instance has that attribute. If not, it checks the class. This is why you can change class variables on an instance, but it will create an instance variable that shadows the class variable.
3. Methods in Classes

Methods are functions defined inside a class that describe the behaviors of the objects. There are three main types of methods in Python:

Instance Methods
Class Methods
Static Methods
Instance Methods

Methods that take the instance (self) as the first parameter. They can access and modify instance attributes and other methods.

class Book: def __init__(self, title, author, pages): self.title = title self.author = author self.pages = pages self.current_page = 1 def turn_page(self, page): if 1 <= page <= self.pages: self.current_page = page return f"Turned to page {self.current_page}" return "Invalid page number" def next_page(self): return self.turn_page(self.current_page + 1) def prev_page(self): return self.turn_page(self.current_page - 1) def get_reading_progress(self): return f"Page {self.current_page} of {self.pages} ({(self.current_page/self.pages)*100:.1f}%)" # Usage book = Book("Python 101", "John Doe", 200) print(book.turn_page(50)) # Turned to page 50 print(book.next_page()) # Turned to page 51 print(book.get_reading_progress()) # Page 51 of 200 (25.5%)
Class Methods

Methods that take the class (cls) as the first parameter. They can't modify instance state but can modify class state. Defined using the @classmethod decorator.

from datetime import date class Person: def __init__(self, name, age): self.name = name self.age = age # Class method as an alternative constructor @classmethod def from_birth_year(cls, name, birth_year): current_year = date.today().year age = current_year - birth_year return cls(name, age) # Class method to modify class state @classmethod def set_min_age(cls, age): cls.MIN_AGE = age # Create instance using the default constructor person1 = Person("Alice", 25) # Create instance using class method (alternative constructor) person2 = Person.from_birth_year("Bob", 1995) print(person1.name, person1.age) # Alice 25 print(person2.name, person2.age) # Bob (current_year - 1995) # Using class method to modify class state Person.set_min_age(18) print(Person.MIN_AGE) # 18
Static Methods

Methods that don't take self or cls as the first parameter. They can't modify class or instance state. Defined using the @staticmethod decorator.

class MathOperations: # Regular instance method def add(self, a, b): return a + b # Static method (doesn't depend on instance or class state) @staticmethod def multiply(a, b): return a * b # Another static method example @staticmethod def is_even(num): return num % 2 == 0 # Create an instance math = MathOperations() # Call instance method print(math.add(5, 3)) # 8 # Call static method through instance print(math.multiply(5, 3)) # 15 # Call static method through class (no instance needed) print(MathOperations.is_even(4)) # True print(MathOperations.is_even(7)) # False
When to use each method type:
  • Instance methods: When you need to access or modify instance attributes.
  • Class methods: When you need to create alternative constructors or modify class state.
  • Static methods: When the method is a utility function that doesn't need to access instance or class state.
4. Practice Exercise

Bank Account System

Create a BankAccount class with the following requirements:

  1. Each account should have the following attributes:
    • account_number (string)
    • owner (string)
    • balance (float, default to 0.0)
    • account_type (string, e.g., 'savings', 'checking')
  2. Class should have the following methods:
    • deposit(amount) - Add amount to balance
    • withdraw(amount) - Subtract amount from balance (check for sufficient funds)
    • get_balance() - Return current balance
    • add_interest(rate) - Add interest to the balance (rate is a percentage)
  3. Add a class variable account_count that keeps track of the total number of accounts created
  4. Add a class method get_account_count() that returns the total number of accounts
  5. Add a static method validate_amount(amount) that checks if the amount is positive
  6. Add proper validation for all methods

Example Usage:

# Create accounts acc1 = BankAccount("A1001", "John Doe", 1000.0, "savings") acc2 = BankAccount("A1002", "Jane Smith", 500.0, "checking") # Test methods acc1.deposit(500) acc1.withdraw(200) print(acc1.get_balance()) # 1300.0 # Test class method print(BankAccount.get_account_count()) # 2 # Test static method print(BankAccount.validate_amount(-100)) # False
Challenge:
  • Add transaction history that records all deposits and withdrawals with timestamps
  • Add a method to generate a statement showing all transactions
  • Implement transfer method to transfer money between accounts