Docs
oop
09 - Object-Oriented Programming (OOP)
📌 What You'll Learn
- •Classes and objects
- •Constructors (
__init__) - •Instance vs class variables
- •Methods (instance, class, static)
- •Encapsulation (public, protected, private)
- •Inheritance
- •Polymorphism
- •Abstraction
- •Magic/dunder methods
- •Property decorators
🏗️ What is OOP?
Object-Oriented Programming is a programming paradigm that organizes code into objects that contain both data (attributes) and behavior (methods).
Key Concepts
- •Class: A blueprint for creating objects
- •Object: An instance of a class
- •Attribute: Data stored in an object
- •Method: Function defined inside a class
📦 Classes and Objects
Basic Class
class Dog:
# Class attribute (shared by all instances)
species = "Canis familiaris"
# Constructor (initializer)
def __init__(self, name, age):
# Instance attributes (unique to each instance)
self.name = name
self.age = age
# Instance method
def bark(self):
return f"{self.name} says Woof!"
def description(self):
return f"{self.name} is {self.age} years old"
# Create objects (instances)
buddy = Dog("Buddy", 5)
max = Dog("Max", 3)
# Access attributes
print(buddy.name) # Buddy
print(buddy.species) # Canis familiaris
# Call methods
print(buddy.bark()) # Buddy says Woof!
print(buddy.description()) # Buddy is 5 years old
🔧 Constructor and self
class Person:
def __init__(self, name, age):
"""
Constructor - called when creating an object.
'self' refers to the instance being created.
"""
self.name = name
self.age = age
self.email = None # Default value
def greet(self):
"""
Instance method - first parameter is always 'self'.
"""
return f"Hello, I'm {self.name}"
# Create instance
person = Person("Alice", 25)
# self is automatically passed
print(person.greet()) # Same as Person.greet(person)
📊 Instance vs Class Variables
class Employee:
# Class variable - shared by ALL instances
company = "TechCorp"
employee_count = 0
def __init__(self, name, salary):
# Instance variables - unique to each instance
self.name = name
self.salary = salary
# Modify class variable
Employee.employee_count += 1
def get_info(self):
return f"{self.name} works at {Employee.company}"
# Create employees
emp1 = Employee("Alice", 50000)
emp2 = Employee("Bob", 60000)
print(Employee.employee_count) # 2
# Class variable is shared
print(emp1.company) # TechCorp
print(emp2.company) # TechCorp
# Changing class variable affects all
Employee.company = "MegaCorp"
print(emp1.company) # MegaCorp
print(emp2.company) # MegaCorp
🔄 Types of Methods
Instance Methods
class Circle:
def __init__(self, radius):
self.radius = radius
# Instance method - operates on instance data
def area(self):
return 3.14159 * self.radius ** 2
Class Methods
class Circle:
pi = 3.14159
def __init__(self, radius):
self.radius = radius
# Class method - operates on class data
@classmethod
def from_diameter(cls, diameter):
"""Alternative constructor"""
return cls(diameter / 2)
@classmethod
def set_pi(cls, value):
cls.pi = value
# Use class method as factory
circle = Circle.from_diameter(10)
print(circle.radius) # 5.0
Static Methods
class MathUtils:
@staticmethod
def add(a, b):
"""Static method - no access to class or instance"""
return a + b
@staticmethod
def is_positive(n):
return n > 0
# Call without creating instance
print(MathUtils.add(3, 5))
print(MathUtils.is_positive(-5))
🔒 Encapsulation
Control access to attributes and methods.
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # Public
self._account_type = "savings" # Protected (convention)
self.__balance = balance # Private (name mangling)
# Getter
def get_balance(self):
return self.__balance
# Setter with validation
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return True
return False
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
return True
return False
account = BankAccount("Alice", 1000)
# Public - accessible
print(account.owner) # Alice
# Protected - accessible but shouldn't be modified directly
print(account._account_type) # savings
# Private - name mangled
# print(account.__balance) # AttributeError!
print(account.get_balance()) # 1000
# Still accessible via mangled name (don't do this!)
print(account._BankAccount__balance) # 1000
🎭 Property Decorator
Pythonic way to create getters/setters.
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
"""Getter for celsius"""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Setter with validation"""
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
@property
def fahrenheit(self):
"""Read-only computed property"""
return self._celsius * 9/5 + 32
# Use like attributes
temp = Temperature(25)
print(temp.celsius) # 25
print(temp.fahrenheit) # 77.0
temp.celsius = 30
print(temp.celsius) # 30
# temp.fahrenheit = 100 # AttributeError - read-only
👪 Inheritance
Create new classes based on existing ones.
# Parent class (base class)
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def speak(self):
return "Some sound"
def description(self):
return f"{self.name} is {self.age} years old"
# Child class (derived class)
class Dog(Animal):
def __init__(self, name, age, breed):
super().__init__(name, age) # Call parent constructor
self.breed = breed
def speak(self): # Override parent method
return f"{self.name} says Woof!"
def fetch(self): # Add new method
return f"{self.name} is fetching!"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
# Create instances
dog = Dog("Buddy", 5, "Golden Retriever")
cat = Cat("Whiskers", 3)
print(dog.description()) # From parent
print(dog.speak()) # Overridden
print(dog.fetch()) # New method
print(cat.speak()) # Overridden
Multiple Inheritance
class Flyable:
def fly(self):
return "Flying!"
class Swimmable:
def swim(self):
return "Swimming!"
class Duck(Animal, Flyable, Swimmable):
def speak(self):
return "Quack!"
duck = Duck("Donald", 2)
print(duck.fly()) # From Flyable
print(duck.swim()) # From Swimmable
print(duck.speak()) # From Duck
# Method Resolution Order
print(Duck.__mro__)
🎭 Polymorphism
Same interface, different implementations.
class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
# Polymorphism in action
shapes = [Rectangle(4, 5), Circle(3), Rectangle(2, 6)]
for shape in shapes:
print(f"Area: {shape.area()}")
# Each shape calculates area differently!
🎨 Abstract Classes
Define interface that subclasses must implement.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
"""Subclasses MUST implement this"""
pass
@abstractmethod
def perimeter(self):
pass
def description(self):
"""Concrete method - optional to override"""
return "I am a shape"
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)
# shape = Shape() # TypeError - can't instantiate abstract class
rect = Rectangle(4, 5)
print(rect.area())
print(rect.description())
✨ Magic/Dunder Methods
Special methods for operator overloading and object behavior.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
# String representation
def __str__(self):
"""For print() and str()"""
return f"Vector({self.x}, {self.y})"
def __repr__(self):
"""For debugging"""
return f"Vector({self.x!r}, {self.y!r})"
# Arithmetic operators
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
# Comparison
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __lt__(self, other):
return (self.x**2 + self.y**2) < (other.x**2 + other.y**2)
# Length
def __len__(self):
return int((self.x**2 + self.y**2) ** 0.5)
# Boolean
def __bool__(self):
return self.x != 0 or self.y != 0
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1) # Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v1 * 2) # Vector(6, 8)
print(v1 == v2) # False
print(len(v1)) # 5
print(bool(v1)) # True
Common Magic Methods
| Method | Purpose |
|---|---|
__init__ | Constructor |
__str__ | String for users |
__repr__ | String for developers |
__add__ | + operator |
__sub__ | - operator |
__mul__ | * operator |
__eq__ | == operator |
__lt__, __gt__ | < and > operators |
__len__ | len() function |
__getitem__ | [] indexing |
__setitem__ | [] assignment |
__iter__ | Iteration |
__call__ | () calling |
📋 Summary
| Concept | Description |
|---|---|
| Class | Blueprint for objects |
| Object | Instance of a class |
self | Reference to current instance |
__init__ | Constructor method |
| Instance var | Unique to each object |
| Class var | Shared by all objects |
@classmethod | Method with class access |
@staticmethod | Method without class/instance access |
@property | Getter/setter decorator |
| Inheritance | Derive classes from parent |
| Polymorphism | Same interface, different behavior |
| Abstraction | Hide complexity, show essentials |
| Encapsulation | Restrict access to internals |
🎯 Next Steps
After mastering OOP, proceed to 10_advanced_functions to learn about closures, decorators in depth, and generators!