Advanced Python: Design Patterns, Exception Handling, and Module Management
Object Instantiation Mechanics
In Python, object creation is a two-step process involving __new__ and __init__. The __new__ method is responsible for allocating memory and returning a new instance, while __init__ handles the initialization of that instance's attributes. When overriding __new__, you must explicitly call the parent class's __new__ to ensure proper memory allocation, and it must return the newly created instance. Conversely, __init__ receives the instance as self, modifies its state, and implicitly returns None.
Factory Design Patterns
Directly instantiating concrete classes within application code creates tight coupling. Factory patterns abstract this process to improve maintainability.
Simple Factory
A simple factory centralizes object creation logic. Instead of scattering instantiation code, a dedicated function or class handles it based on input parameters.
class CreditPayment:
def process(self):
return "Processing credit payment"
class PayPalPayment:
def process(self):
return "Processing PayPal payment"
class PaymentFactory:
@staticmethod
def create_payment(method: str):
if method == "credit":
return CreditPayment()
elif method == "paypal":
return PayPalPayment()
raise ValueError("Unknown payment method")
# Usage
processor = PaymentFactory.create_payment("credit")
print(processor.process())
This approach isolates creation logic, making it easier to add new payment types without modifying the client code that requests them.
Factory Method Pattern
When object creation depends on specific contexts or requires polymorphic behavior, the factory method pattern delegates instantiation to subcalsses. A base class defines an abstract creation interface, and concrete subclasses implement it.
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def create_handler(self):
pass
def execute(self):
handler = self.create_handler()
handler.process()
class StripeProcessor(PaymentProcessor):
def create_handler(self):
return CreditPayment()
class DirectProcessor(PaymentProcessor):
def create_handler(self):
return PayPalPayment()
# Usage
gateway = StripeProcessor()
gateway.execute()
This design postpones instantiation decisions to runtime, adhering to the Open/Closed Principle by allowing new processors to be added without altering the base execution logic.
The Singleton Pattern
A singleton restricts a class to a single instance and provides a global access point. This is useful for managing shared resources like configuration managers or database connections.
Implementation via __new__
class ConfigManager:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, db_host, db_port):
if not hasattr(self, 'initialized'):
self.db_host = db_host
self.db_port = db_port
self.initialized = True
The first call allocates memory and stores the reference in _instance. Subsequent calls return the existing reference. To prevent __init__ from resetting state on repeated instantiations, a guard flag (initialized) ensures initialization runs exactly once.
Exception Handling
Python uses exceptions to manage runtime errors. Instead of crashing, programs can intercept and handle these events gracefully.
Basic Structure
The try block contains code that might fail, while except handles specific error types.
try:
with open("data.json", "r") as f:
content = f.read()
except FileNotFoundError as e:
print(f"File missing: {e}")
Multiple Exceptions and Control Flow
You can catch multiple exception types using a tuple. The else clause executes only if no exceptions occur, and finally runs regardless of the outcome, making it ideal for cleanup operations like closing files or releasing locks.
try:
value = int("123")
result = 10 / value
except (ValueError, ZeroDivisionError) as err:
print(f"Calculation failed: {err}")
else:
print("Calculation successful")
finally:
print("Cleanup routine executed")
Custom Exceptions
Applications often require domain-specific error reporting. Custom exceptions inherit from Exception and can carry additional context.
class ValidationError(Exception):
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(self.message)
def validate_input(age):
if age < 0:
raise ValidationError("age", "Age cannot be negative")
try:
validate_input(-5)
except ValidationError as ve:
print(f"Invalid {ve.field}: {ve.message}")
Exception Propagation
If a function does not handle an exception, it propagates up the call stack to the nearest enclosing try block. If it reaches the top level without a handler, the interpreter terminates the program and prints a traceback. Nested try blocks handle inner exceptions first; unhandled ones bubble outward. When an exception is caught and handled, execution continues from the except or finally block rather than resuming at the point of failure.
Modules and Import Mechanics
Python organizes code into modules, which are simply .py files containing definitions and statements.
Import Strategies
Using import module requires prefixing identifiers with the module name, preventing namespace collisions. Alternatively, from module import name injects specific identifiers directly into the local namespace.
import math
print(math.sqrt(16)) # Namespaced access
from os import path
print(path.exists("/tmp")) # Direct access
Aliasing with as simplifies long names or avoids conflicts:
import pandas as pd
from datetime import datetime as dt
Execution Guard and Controlled Exports
Every module has a __name__ attribute. When executed directly, it equals "__main__"; when imported, it equals the module's filename. This allows modules to include test code that only runs during direct execution.
def calculate(a, b):
return a + b
if __name__ == "__main__":
print(calculate(10, 20)) # Only runs when this file is executed
The __all__ list defines the public API of a module. When clients use from module import *, only names in __all__ are imported.
__all__ = ["public_function", "ConstantsClass"]
Packages and Directory Organization
A package is a directory containing multiple modules and an __init__.py file. This file signals Python to treat the directory as a package and controls import behavior.
Package Structure
myapp/
├── __init__.py
├── auth.py
└── utils/
├── __init__.py
└── helpers.py
Submodules are accesed via dot notation: import myapp.utils.helpers. The __init__.py file can remain empty, contain initialization code, or define __all__ to control wildcard imports at the package level. When using from myapp import *, Python imports whatever is listed in myapp/__init__.py's __all__ variable.
Packaging and Distribution
To share code across environments, Python uses distribution packages. The traditional approach relies on a setup.py script.
Setup Configuration
from setuptools import setup, find_packages
setup(
name="data_tools",
version="1.2.0",
description="Utility functions for data processing",
author="Dev Team",
packages=find_packages(),
install_requires=["requests>=2.28.0"]
)
Build and Install Workflow
- Build: Compiles the project structure.
python setup.py build - Source Distribution: Creates a compressed archive for distribution.
python setup.py sdist - Installation: Deploys the package to the Python environment.
python setup.py installTo specify a custom installation directory, use:python setup.py install --prefix=/custom/path
Modern Python packaging often uses pyproject.toml and build backends like build, but the underlying distribution principles remain consistent: define metadata, bundle source files, and deploy via standard installers.