Object-Oriented Programming V2
Object-Oriented Programming bundles data and the functions that work on that data into one unit called an object. Instead of data floating around with separate functions, everything lives together. Organized chaos.
The shift:
- Before (imperative): Write instructions, use functions
- Now (OOP): Create objects that contain both data AND behavior
You've Been Using OOP All Along
Plot twist: every data type in Python is already a class.
# Lists are objects
my_list = [1, 2, 3]
my_list.append(4) # Method call
my_list.reverse() # Another method
# Strings are objects
name = "hello"
name.upper() # Method call
# Even integers are objects
x = 5
x.__add__(3) # Same as x + 3
Use help(list) or help(str) to see all methods of a class.
Level 1: Your First Class
The Syntax
class ClassName:
# stuff goes here
pass
A Simple Counter
Let's build step by step.
Step 1: Empty class
class Counter:
pass
Step 2: The constructor
class Counter:
def __init__(self, value):
self.val = value
Step 3: A method
class Counter:
def __init__(self, value):
self.val = value
def tick(self):
self.val = self.val + 1
Step 4: More methods
class Counter:
def __init__(self, value):
self.val = value
def tick(self):
self.val = self.val + 1
def reset(self):
self.val = 0
def value(self):
return self.val
Step 5: Use it
c1 = Counter(0)
c2 = Counter(3)
c1.tick()
c2.tick()
print(c1.value()) # 1
print(c2.value()) # 4
Level 2: Understanding the Pieces
The Constructor: __init__
The constructor runs automatically when you create an object. It sets up the initial state.
def __init__(self, value):
self.val = value
When you write Counter(5):
- Python creates a new Counter object
- Calls
__init__withvalue = 5 - Returns the object
The self Parameter
self = "this object I'm working on"
def tick(self):
self.val = self.val + 1
self.valmeans "thevalthat belongs to THIS object"- Each object has its own copy of
self.val
c1 = Counter(0)
c2 = Counter(100)
c1.tick() # c1.val becomes 1
print(c2.value()) # Still 100, different object
Every method needs self as the first parameter. But when calling, you don't pass it — Python does that automatically.
# Defining: include self
def tick(self):
...
# Calling: don't include self
c1.tick() # NOT c1.tick(c1)
Instance Variables
Variables attached to self are instance variables — each object gets its own copy.
def __init__(self, value):
self.val = value # Instance variable
self.count = 0 # Another one
Level 3: Special Methods (Magic Methods)
Python has special method names that enable built-in behaviors.
__str__ — For print()
class Counter:
def __init__(self, value):
self.val = value
def __str__(self):
return f"Counter: {self.val}"
c = Counter(5)
print(c) # Counter: 5
Without __str__, you'd get something ugly like <__main__.Counter object at 0x7f...>
__add__ — For the + operator
class Counter:
def __init__(self, value):
self.val = value
def __add__(self, other):
return Counter(self.val + other.val)
c1 = Counter(3)
c2 = Counter(7)
c3 = c1 + c2 # Calls c1.__add__(c2)
print(c3.val) # 10
__len__ — For len()
def __len__(self):
return self.val
c = Counter(5)
print(len(c)) # 5
__getitem__ — For indexing [ ]
def __getitem__(self, index):
return something[index]
__init__ → Constructor
__str__ → print() and str()
__add__ → + operator
__sub__ → - operator
__mul__ → * operator
__eq__ → == operator
__len__ → len()
__getitem__ → obj[index]
Level 4: Encapsulation
The idea that you should access data through methods, not directly. This lets you change the internals without breaking code that uses the class.
Bad (direct access):
c = Counter(5)
c.val = -100 # Directly messing with internal data
Good (through methods):
c = Counter(5)
c.reset() # Using the provided interface
Python doesn't enforce encapsulation — it trusts you. Convention: prefix "private" variables with underscore: self._val
Level 5: Designing Classes
When creating a class, think about:
| Question | Becomes |
|---|---|
| What thing am I modeling? | Class name |
| What data does it have? | Instance variables |
| What can it do? | Methods |
Example: Student
- Class:
Student - Data: name, age, grades
- Behavior: add_grade(), average(), pass_course()
Full Example: Card Deck
Let's see a more complex class.
Step 1: Constructor — Create all cards
class Deck:
def __init__(self):
self.cards = []
for num in range(1, 11):
for suit in ["Clubs", "Spades", "Hearts", "Diamonds"]:
self.cards.append((num, suit))
Step 2: Shuffle method
from random import randint
class Deck:
def __init__(self):
self.cards = []
for num in range(1, 11):
for suit in ["Clubs", "Spades", "Hearts", "Diamonds"]:
self.cards.append((num, suit))
def shuffle(self):
for i in range(200):
x = randint(0, len(self.cards) - 1)
y = randint(0, len(self.cards) - 1)
# Swap
self.cards[x], self.cards[y] = self.cards[y], self.cards[x]
Step 3: Special methods
def __len__(self):
return len(self.cards)
def __getitem__(self, i):
return self.cards[i]
def __str__(self):
return f"I am a {len(self)} card deck"
Step 4: Using it
deck = Deck()
print(deck) # I am a 40 card deck
print(deck[0]) # (1, 'Clubs')
deck.shuffle()
print(deck[0]) # Something random now
Complete Counter Example
Putting it all together:
class Counter:
def __init__(self, value):
self.val = value
def tick(self):
self.val = self.val + 1
def reset(self):
self.val = 0
def value(self):
return self.val
def __str__(self):
return f"Counter: {self.val}"
def __add__(self, other):
return Counter(self.val + other.val)
c1 = Counter(0)
c2 = Counter(3)
c1.tick()
c2.tick()
c3 = c1 + c2
print(c1.value()) # 1
print(c2) # Counter: 4
print(c3) # Counter: 5
Quick Reference
class Name: → Define a class
def __init__(self): → Constructor
self.var = x → Instance variable
obj = Class() → Create object
obj.method() → Call method
__str__ → For print()
__add__ → For +
__eq__ → For ==
__len__ → For len()
__getitem__ → For [ ]
Classes help organize large programs. Also, many frameworks (like PyTorch for machine learning) require you to define your own classes. So yeah, you need this. Sorry.