05.3 - Encapsulation, Properties & Dunder Methods
Theory 25 min Intermediate
Encapsulation and Name Mangling
Python uses naming conventions for access control (no true private like Java/C++):
| Convention | Example | Meaning |
|---|---|---|
name | self.balance | Public |
_name | self._balance | Protected (convention) — don't access from outside |
__name | self.__secret | Name-mangled → _ClassName__secret |
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # public
self._balance = balance # protected
self.__pin = "1234" # name-mangled
account = BankAccount("Alice", 1000)
account.owner # "Alice" OK
account._balance # 1000 works but discouraged
account.__pin # AttributeError!
account._BankAccount__pin # "1234" — still accessible!
Dunder (Magic) Methods
Dunder methods (double underscore) are Python's way to make objects behave like built-in types.
String Representation: __str__ and __repr__
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
"""Unambiguous — for developers (repr())"""
return f"Vector(x={self.x}, y={self.y})"
def __str__(self):
"""Human-readable — for users (str(), print())"""
return f"({self.x}, {self.y})"
v = Vector(3, 4)
str(v) # "(3, 4)"
repr(v) # "Vector(x=3, y=4)"
print(v) # (3, 4)
Arithmetic Operators
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
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)
def __rmul__(self, scalar):
return self.__mul__(scalar) # handles scalar * vector
def __abs__(self):
import math
return math.sqrt(self.x**2 + self.y**2)
def __neg__(self):
return Vector(-self.x, -self.y)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v1 + v2 # Vector(4, 6)
v2 - v1 # Vector(2, 2)
v1 * 3 # Vector(3, 6)
3 * v1 # Vector(3, 6) — via __rmul__
abs(v2) # 5.0
-v1 # Vector(-1, -2)
Comparison Operators
from functools import total_ordering
@total_ordering # generates <=, >, >= from __eq__ and __lt__
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius == other.celsius
def __lt__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius < other.celsius
def __repr__(self):
return f"Temperature({self.celsius}°C)"
temps = [Temperature(100), Temperature(0), Temperature(37)]
sorted(temps) # [Temperature(0°C), Temperature(37°C), Temperature(100°C)]
min(temps) # Temperature(0°C)
Container Protocol
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
return self._items.pop()
def __len__(self):
return len(self._items)
def __getitem__(self, index):
return self._items[index]
def __contains__(self, item):
return item in self._items
def __iter__(self):
return iter(self._items)
def __repr__(self):
return f"Stack({self._items})"
s = Stack()
s.push(1); s.push(2); s.push(3)
len(s) # 3
s[0] # 1
2 in s # True
for x in s: print(x) # 1, 2, 3 (via __iter__)
Context Manager: __enter__ and __exit__
class DatabaseConnection:
def __init__(self, host, port):
self.host = host
self.port = port
self.connection = None
def __enter__(self):
print(f"Connecting to {self.host}:{self.port}")
self.connection = True # simulate connection
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("Closing connection")
self.connection = None
# Return False to propagate exceptions, True to suppress
return False
def query(self, sql):
if not self.connection:
raise RuntimeError("Not connected")
return f"Results of: {sql}"
with DatabaseConnection("localhost", 5432) as db:
result = db.query("SELECT * FROM users")
print(result)
# Connecting to localhost:5432
# Results of: SELECT * FROM users
# Closing connection
Callable Objects: __call__
class RateLimit:
"""A callable that limits how many times it can be called per second."""
def __init__(self, max_calls, func):
self.max_calls = max_calls
self.func = func
self._call_count = 0
def __call__(self, *args, **kwargs):
self._call_count += 1
if self._call_count > self.max_calls:
raise RuntimeError("Rate limit exceeded!")
return self.func(*args, **kwargs)
def reset(self):
self._call_count = 0
def fetch_api(url):
return f"Response from {url}"
limited_fetch = RateLimit(3, fetch_api)
limited_fetch("api.example.com/1") # OK
limited_fetch("api.example.com/2") # OK
limited_fetch("api.example.com/3") # OK
limited_fetch("api.example.com/4") # RuntimeError!
Key Vocabulary
| Term | Definition |
|---|---|
| Dunder method | Special method with double underscores: __init__, __str__ |
__repr__ | Developer-facing string: repr(obj) |
__str__ | User-facing string: str(obj), print(obj) |
__eq__ | Defines == behavior |
__len__ | Defines len(obj) behavior |
__getitem__ | Defines obj[index] behavior |
__iter__ | Makes object iterable in for loops |
__enter__/__exit__ | Defines with statement behavior |
__call__ | Makes an object callable: obj() |
@total_ordering | Auto-generates comparison operators from __eq__ and __lt__ |
Summary
- Use
_namefor protected,__namefor name-mangled private attributes __repr__is for developers (debug/REPL);__str__is for users (print)- Implement arithmetic dunders (
__add__,__mul__) to support operators @functools.total_orderingreduces comparison boilerplate to__eq__+__lt__- Container dunders make objects behave like lists/dicts
__enter__/__exit__enableswithstatement for resource management__call__makes objects callable like functions
📄️ 05.1 - Classes & Objects
Define Python classes with __init__, instance methods, class variables, static methods, and properties
📄️ 05.2 - Inheritance & Polymorphism
Design class hierarchies using inheritance, method overriding, super(), abstract classes, and polymorphism
📄️ 05.3 - Dunder Methods
Control object behavior with Python's special methods: __repr__, __eq__, __len__, __add__, __iter__, and more
📄️ Lab - Module 05
Build a complete OOP bank account system with inheritance, properties, dunder methods, and abstract classes
📄️ Quiz - Module 05
30 questions on classes, inheritance, polymorphism, and dunder methods