- Understand and implement special methods (__init__, __str__, etc.)
- Create objects that behave like built-in types
- Implement comparison and arithmetic operators
- Use context managers and iterators
Special Methods and Properties
Have you ever wondered how len(my_list) works, or why you can add two custom objects with +? Behind the scenes, Python uses special methods (also called dunder methods because they start and end with double underscores __).
These methods allow your objects to behave like built-in types. Let's unlock the magic.
String Representations
str vs repr
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
"""User-friendly string representation."""
return f"{self.name}, age {self.age}"
def __repr__(self):
"""Developer-friendly representation (unambiguous)."""
return f"Person(name='{self.name}', age={self.age})"
person = Person("Alice", 30)
print(person) # Alice, age 30 (__str__)
print(repr(person)) # Person(name='Alice', age=30) (__repr__)
# In interactive mode, __repr__ is used
person # Person(name='Alice', age=30)
Rule of thumb:
__str__: For end users (readable)__repr__: For developers (unambiguous, could recreate object)
Arithmetic Operators
Make your objects work with +, -, *, etc.:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""Vector addition: self + other"""
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
"""Vector subtraction: self - other"""
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
"""Scalar multiplication: self * scalar"""
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar):
"""Right multiplication: scalar * self"""
return self.__mul__(scalar)
def __str__(self):
return f"Vector({self.x}, {self.y})"
# Usage
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v2 - v1) # Vector(2, 2)
print(v1 * 3) # Vector(3, 6)
print(2 * v1) # Vector(2, 4) (__rmul__)
Comparison Operators
Implement <, >, ==, etc.:
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __eq__(self, other):
"""self == other"""
return self.grade == other.grade
def __lt__(self, other):
"""self < other"""
return self.grade < other.grade
def __le__(self, other):
"""self <= other"""
return self.grade <= other.grade
def __str__(self):
return f"{self.name}: {self.grade}"
# Usage
alice = Student("Alice", 85)
bob = Student("Bob", 92)
charlie = Student("Charlie", 85)
print(alice == charlie) # True (same grade)
print(alice < bob) # True (85 < 92)
print(bob <= alice) # False (92 <= 85)
# Can now sort students
students = [bob, alice, charlie]
students.sort() # Uses __lt__
for student in students:
print(student) # Alice: 85, Charlie: 85, Bob: 92
Container Methods
Make your objects work like lists or dictionaries:
class ShoppingCart:
def __init__(self):
self.items = {}
def __setitem__(self, key, value):
"""cart[key] = value"""
self.items[key] = value
def __getitem__(self, key):
"""cart[key]"""
return self.items[key]
def __delitem__(self, key):
"""del cart[key]"""
del self.items[key]
def __len__(self):
"""len(cart)"""
return len(self.items)
def __iter__(self):
"""for item in cart:"""
return iter(self.items)
def __contains__(self, key):
"""key in cart"""
return key in self.items
# Usage
cart = ShoppingCart()
cart["apple"] = 5
cart["banana"] = 3
print(len(cart)) # 2
print("apple" in cart) # True
print(cart["banana"]) # 3
for item in cart:
print(f"{item}: {cart[item]}")
Context Managers
Make your objects work with with statements:
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
self.connected = False
def __enter__(self):
"""Called when entering 'with' block."""
self.connected = True
print(f"Connected to {self.db_name}")
return self # This becomes the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
"""Called when exiting 'with' block."""
self.connected = False
print(f"Disconnected from {self.db_name}")
# Return False to propagate exceptions, True to suppress
def query(self, sql):
if not self.connected:
raise RuntimeError("Not connected to database")
return f"Results for: {sql}"
# Usage
with DatabaseConnection("mydb") as db:
results = db.query("SELECT * FROM users")
print(results)
# Connection automatically closed here
Callable Objects
Make your objects callable like functions:
class Polynomial:
def __init__(self, coefficients):
self.coefficients = coefficients # [a, b, c] for ax^2 + bx + c
def __call__(self, x):
"""Make object callable: poly(x)"""
result = 0
for power, coeff in enumerate(self.coefficients):
result += coeff * (x ** power)
return result
def __str__(self):
terms = []
for power, coeff in enumerate(self.coefficients):
if coeff == 0:
continue
if power == 0:
terms.append(str(coeff))
elif power == 1:
terms.append(f"{coeff}x")
else:
terms.append(f"{coeff}x^{power}")
return " + ".join(reversed(terms))
# Usage
poly = Polynomial([1, 2, 3]) # x^2 + 2x + 3
print(poly) # 3 + 2x + 1x^2
print(poly(0)) # 3
print(poly(1)) # 6
print(poly(2)) # 15
Custom List
class SortedList:
"""A list that keeps itself sorted."""
def __init__(self, items=None):
self._items = sorted(items or [])
def add(self, item):
"""Add item and keep sorted."""
self._items.append(item)
self._items.sort()
def remove(self, item):
"""Remove item."""
self._items.remove(item)
# Special methods for list-like behavior
def __len__(self):
return len(self._items)
def __getitem__(self, index):
return self._items[index]
def __setitem__(self, index, value):
self._items[index] = value
self._items.sort() # Re-sort after modification
def __delitem__(self, index):
del self._items[index]
def __iter__(self):
return iter(self._items)
def __repr__(self):
return f"SortedList({self._items})"
def __add__(self, other):
"""Concatenate two SortedLists."""
if isinstance(other, SortedList):
return SortedList(self._items + other._items)
return SortedList(self._items + list(other))
def __contains__(self, item):
return item in self._items
# Usage
numbers = SortedList([3, 1, 4, 1, 5])
print(numbers) # SortedList([1, 1, 3, 4, 5])
numbers.add(2)
print(numbers) # SortedList([1, 1, 2, 3, 4, 5])
print(len(numbers)) # 6
print(3 in numbers) # True
print(numbers[2]) # 2
more_numbers = SortedList([6, 0])
combined = numbers + more_numbers
print(combined) # SortedList([0, 1, 1, 2, 3, 4, 5, 6])
Common Special Methods
| Method | Purpose | Example |
|---|---|---|
__init__ |
Constructor | obj = Class() |
__str__ |
String representation | print(obj) |
__repr__ |
Developer representation | repr(obj) |
__len__ |
Length | len(obj) |
__getitem__ |
Indexing | obj[key] |
__setitem__ |
Item assignment | obj[key] = value |
__add__ |
Addition | obj1 + obj2 |
__eq__ |
Equality | obj1 == obj2 |
__lt__ |
Less than | obj1 < obj2 |
__enter__ |
Context entry | with obj: |
__exit__ |
Context exit | End of with |
__call__ |
Callable | obj() |
Key Points to Remember
- Special methods start and end with
__(called "dunder" methods for double underscore) - str for users (readable), repr for developers (unambiguous)
- Operator overloading with
__add__,__sub__,__mul__, etc. - Container behavior with
__len__,__getitem__,__setitem__, etc. - Context managers with
__enter__and__exit__ - Callable objects with
__call__
You've now mastered the fundamentals of Object-Oriented Programming in Python. You understand classes and objects, inheritance, polymorphism, encapsulation, and special methods. OOP is a powerful paradigm that will serve you well in building complex applications. The next intermediate topic will explore Advanced Data Structures and Algorithms - where we'll dive deep into efficient data handling and algorithmic thinking.
