Advanced Python Object-Oriented Features and Best Practices
Customizing Instance Behavior with Special Methods
Special methods (often called "dunder" methods) enable intuitive, operator-like interactions with class instances. For example:
class DataContainer:
def __init__(self, label: str, size: int, items: list):
self.label = label
self.size = size
self.items = items
def __len__(self):
return self.size
def __getitem__(self, idx):
return self.items[idx]
def __call__(self):
print(f"Invoked: {self.label}")
container = DataContainer("training_set", 10, ["img_01.jpg", "img_02.jpg", "img_03.jpg"])
print(len(container)) # → 10
print(container[1]) # → "img_02.jpg"
container() # → "Invoked: training_set"
These methods are foundational in building custom sequence-like types—commonly used in PyTorch-style Dataset implementations where __getitem__ supports indexing and __len__ reports dataset length.
Encapsulation via Name Mangling
Python uses double underscores (__) to trigger name mangling, discouraging external access to attributes and methods:
class Vehicle:
def __init__(self, model: str):
self.model = model
self.__mileage = 0
self.__log_startup()
def update_mileage(self, km: float):
if km >= 0:
self.__mileage = km
else:
raise ValueError("Mileage cannot be negative")
def display_mileage(self):
print(f"Current mileage: {self.__mileage} km")
def __log_startup(self):
print(f"{self.model} initialized")
v = Vehicle("Tesla Model S")
v.update_mileage(15720.5)
v.display_mileage() # → "Current mileage: 15720.5 km"
Name mangling doesn’t enforce strict privacy but signals intent and prevents accidental overrides in subclasses. Validation logic inside setter methods adds robustness unavailable with public attributes.
Property Decorators for Controlled Access
The @property decorator allows treating method calls as attribute access—improving readability while preserving encapsulation:
class Temperature:
def __init__(self):
self._celsius = 0.0
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
return (self._celsius * 9/5) + 32
thermo = Temperature()
thermo.celsius = 25.0
print(thermo.fahrenheit) # → 77.0
This pattern unifies inetrface consistency: users read/write celsius like a field, while internal validation and derived computation remain fully controlled.
Class and Static Methods
@classmethod receives the class (cls) as its first argument and operates on class-level state. @staticmethod has no implicit first parameter and behaves like a regular function bundled within a class:
class DatabaseConnection:
active_connections = 0
def __init__(self, host: str):
self.host = host
DatabaseConnection.active_connections += 1
@classmethod
def reset_counter(cls):
cls.active_connections = 0
@staticmethod
def is_valid_host(hostname: str) -> bool:
return isinstance(hostname, str) and "." in hostname
def close(self):
DatabaseConnection.active_connections -= 1
DatabaseConnection.reset_counter()
print(DatabaseConnection.is_valid_host("db.example.com")) # → True
conn = DatabaseConnection("localhost")
print(DatabaseConnection.active_connections) # → 1
Class methods support factory patterns and shared state management; static methods group utility logic logically related to the class but independent of instance or class data.
Runtime Type and Inheritance Inspection
isinstance() and issubclass() provide safe, dynamic type checking:
class Shape:
pass
class Circle(Shape):
pass
class Rectangle(Shape):
pass
shape = Shape()
circle = Circle()
print(issubclass(Circle, Shape)) # → True
print(isinstance(circle, Circle)) # → True
print(isinstance(circle, Shape)) # → True (inherits)
print(isinstance(shape, Rectangle)) # → False
These checks are essential in polymorphic dispatch, plugin systems, and defensive coding where behavior depends on object lineage.
Module and Package Import Mechanics
A directory becomes a Python package when it contains an __init__.py file (evenif empty). Consider this structure:
project/
├── core/
│ ├── __init__.py
│ ├── utils/
│ │ ├── __init__.py
│ │ └── helpers.py
│ └── models/
│ ├── __init__.py
│ └── classifier.py
└── app.py
From app.py, import using absolute paths:
from core.utils.helpers import format_timestamp
from core.models.classifier import NeuralNet
Inside classifier.py, use relative imports to reference sibling modules:
from ..utils.helpers import validate_input # moves up one level, then into utils
Relative imports require execution context to be within a package (e.g., launched via python -m core.models.classifier). Absolute imports are preferred for clarity and maintainability in most projects.
Self-Reference in Instance Methods
Every bound instance method must declare self as its first parameter. Omitting it raises TypeError at call time because Python automatically passes the instance. This rule applies universally—even for methods with no other arguments.