Table of Contents
-
- 1.1 Definition and Intent
- 1.2 Key Participants
- 1.3 Class Structure
-
- 2.1 Virtual Proxy
- 2.2 Protection Proxy
- 2.3 Remote Proxy
- 2.4 Smart Reference Proxy
-
Implementing Proxy Patterns in Python
- 4.1 Virtual Proxy: Lazy Initialization
- 4.2 Protection Proxy: Access Control
- 4.3 Remote Proxy: Interacting with Remote Services
- 4.4 Smart Reference Proxy: Logging and Counting
-
- 5.1 Caching Proxies
- 5.2 Proxies in Python Frameworks
- 5.3 Dynamic Proxy Generation
1. What is the Proxy Pattern?
1.1 Definition and Intent
The Proxy Pattern is a structural design pattern that acts as a “surrogate” or placeholder for another object, controlling access to it. Its primary goal is to interpose additional behavior (e.g., access control, lazy initialization, logging) or simplify interactions (e.g., with remote or resource-heavy objects) without modifying the original object’s code.
In essence, a proxy mimics the interface of the target object (called the “real subject”), allowing clients to interact with it transparently—unaware they’re using a proxy instead of the real thing.
1.2 Key Participants
The Proxy Pattern involves three core components:
| Participant | Role |
|---|---|
| Subject | An interface (or abstract class) defining the common methods that both the RealSubject and Proxy must implement. Ensures the proxy is interchangeable with the real subject. |
| RealSubject | The actual object the proxy represents. It contains the core business logic or resource (e.g., a large image, remote database, or sensitive service). |
| Proxy | The surrogate object that wraps the RealSubject. It implements the Subject interface and delegates calls to the RealSubject, adding its own logic (e.g., access checks, lazy loading) before or after delegation. |
1.3 Class Structure
Conceptually, the Proxy Pattern follows this structure:
Client → Proxy → RealSubject
↑ ↑
└───────┴─── (implements Subject interface)
The client interacts with the Proxy as if it were the RealSubject, thanks to the shared Subject interface.
2. Types of Proxy Patterns
Proxies are versatile and adapt to different needs. Below are the most common types:
2.1 Virtual Proxy
Intent: Delay the creation or initialization of a resource-intensive object until it’s actually needed (lazy initialization). Useful for optimizing performance when dealing with large files, databases, or network resources.
Example: A image viewer that loads high-resolution images from disk only when the user scrolls to view them, rather than loading all images upfront.
2.2 Protection Proxy
Intent: Control access to the RealSubject based on permissions (e.g., user roles, authentication). Ensures only authorized clients can invoke sensitive operations.
Example: A database proxy that allows read access to all users but restricts write operations to admins.
2.3 Remote Proxy
Intent: Act as a local representative for a RealSubject located on a remote server or process. Handles network communication, serialization, and error handling, making remote interactions appear local.
Example: A client-side proxy for a REST API that abstracts HTTP requests, retries, and JSON parsing.
2.4 Smart Reference Proxy
Intent: Add “smart” behavior when accessing the RealSubject, such as logging method calls, counting references, or managing resource lifecycle (e.g., auto-closing connections).
Example: A proxy that logs every time a database connection is opened or closed, or counts how many clients are using a shared resource.
3. When to Use the Proxy Pattern
Use the Proxy Pattern when:
- You need to lazy-load a resource-heavy object (Virtual Proxy).
- You need to restrict access to a sensitive object (Protection Proxy).
- You need to interact with a remote object (e.g., over a network) transparently (Remote Proxy).
- You need to add behavior to an object without modifying its code (e.g., logging, caching) (Smart Reference Proxy).
4. Implementing Proxy Patterns in Python
Python’s dynamic typing and support for abstract base classes (via abc) make it easy to implement proxies. Below are practical examples for each proxy type.
4.1 Virtual Proxy: Lazy Initialization
Let’s implement a Virtual Proxy for an image viewer. The RealSubject (HighResImage) loads a large image from disk, but we’ll delay loading until the user requests to display it.
Step 1: Define the Subject Interface
from abc import ABC, abstractmethod
class Image(ABC):
@abstractmethod
def display(self) -> None:
pass
Step 2: Implement the RealSubject
class HighResImage(Image):
def __init__(self, filename: str) -> None:
# Simulate loading a large image (e.g., from disk/network)
print(f"Loading high-res image from {filename}... (this may take time)")
self.filename = filename
def display(self) -> None:
print(f"Displaying high-res image: {self.filename}")
Step 3: Implement the Virtual Proxy
class LazyImageProxy(Image):
def __init__(self, filename: str) -> None:
self.filename = filename
self._real_image: HighResImage | None = None # Defer initialization
def display(self) -> None:
# Load the real image only when display() is called
if self._real_image is None:
self._real_image = HighResImage(self.filename)
self._real_image.display()
Usage Example
# Client code
if __name__ == "__main__":
# Image is not loaded yet
proxy = LazyImageProxy("vacation.jpg")
print("Proxy created, but image not loaded...")
# Image loads only when display() is called
proxy.display() # Output: "Loading high-res image from vacation.jpg... (this may take time)" followed by "Displaying high-res image: vacation.jpg"
proxy.display() # No reload; uses cached real image
2.2 Protection Proxy
Let’s build a proxy to control access to a database, allowing reads for all users but writes only for admins.
Step 1: Subject Interface
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def read(self, query: str) -> str:
pass
@abstractmethod
def write(self, query: str) -> None:
pass
Step 2: RealSubject (Database)
class RealDatabase(Database):
def read(self, query: str) -> str:
return f"Executing read: {query} → Result: [data]"
def write(self, query: str) -> None:
print(f"Executing write: {query} → Data written successfully")
Step 3: Protection Proxy
class DatabaseProtectionProxy(Database):
def __init__(self, real_db: RealDatabase, user_role: str) -> None:
self.real_db = real_db
self.user_role = user_role # e.g., "admin" or "user"
def read(self, query: str) -> str:
# All users can read
return self.real_db.read(query)
def write(self, query: str) -> None:
# Only admins can write
if self.user_role != "admin":
raise PermissionError("Write access denied: User is not an admin")
self.real_db.write(query)
Usage Example
if __name__ == "__main__":
real_db = RealDatabase()
# Admin user: can read and write
admin_proxy = DatabaseProtectionProxy(real_db, "admin")
print(admin_proxy.read("SELECT * FROM users")) # Works
admin_proxy.write("INSERT INTO users VALUES ('alice')") # Works
# Regular user: can read but not write
user_proxy = DatabaseProtectionProxy(real_db, "user")
print(user_proxy.read("SELECT * FROM users")) # Works
try:
user_proxy.write("DELETE FROM users") # Fails
except PermissionError as e:
print(e) # Output: "Write access denied: User is not an admin"
2.3 Remote Proxy
Let’s simulate a remote weather service where the RealSubject runs on a “remote server,” and the proxy handles network communication.
Step 1: Subject Interface
from abc import ABC, abstractmethod
import time # For simulating network delay
class WeatherService(ABC):
@abstractmethod
def get_temperature(self, city: str) -> float:
pass
Step 2: RealSubject (Remote Server)
class RemoteWeatherServer(WeatherService):
def get_temperature(self, city: str) -> float:
# Simulate a remote server fetching data
print(f"[Remote Server] Fetching temperature for {city}...")
time.sleep(1) # Simulate network delay
# Mock data
temps = {"London": 18.5, "New York": 22.3, "Tokyo": 25.1}
return temps.get(city, 0.0)
Step 3: Remote Proxy (Client)
class WeatherServiceProxy(WeatherService):
def __init__(self, server: RemoteWeatherServer) -> None:
self.server = server # In reality, this would be a network client
def get_temperature(self, city: str) -> float:
# Add proxy-specific logic: error handling, retries, caching, etc.
try:
print(f"[Proxy] Requesting temperature for {city}...")
temp = self.server.get_temperature(city) # Delegate to remote server
print(f"[Proxy] Received temperature: {temp}°C")
return temp
except Exception as e:
print(f"[Proxy] Error: {e}")
return -999.0 # Fallback
Usage Example
if __name__ == "__main__":
remote_server = RemoteWeatherServer()
proxy = WeatherServiceProxy(remote_server)
temp = proxy.get_temperature("London") # Output:
# [Proxy] Requesting temperature for London...
# [Remote Server] Fetching temperature for London...
# [Proxy] Received temperature: 18.5°C
print(f"Temperature in London: {temp}°C") # 18.5
2.4 Smart Reference Proxy
Let’s create a proxy that logs method calls and counts how many times the RealSubject’s methods are invoked.
Step 1: Subject Interface
from abc import ABC, abstractmethod
class CounterService(ABC):
@abstractmethod
def increment(self) -> int:
pass
@abstractmethod
def reset(self) -> None:
pass
Step 2: RealSubject
class RealCounter(CounterService):
def __init__(self) -> None:
self.count = 0
def increment(self) -> int:
self.count += 1
return self.count
def reset(self) -> None:
self.count = 0
Step 3: Smart Reference Proxy
class LoggingCounterProxy(CounterService):
def __init__(self, real_counter: RealCounter) -> None:
self.real_counter = real_counter
self.access_count = 0 # Track total method calls
def increment(self) -> int:
self.access_count += 1
print(f"[Proxy] Calling increment() (total calls: {self.access_count})")
return self.real_counter.increment()
def reset(self) -> None:
self.access_count += 1
print(f"[Proxy] Calling reset() (total calls: {self.access_count})")
self.real_counter.reset()
Usage Example
if __name__ == "__main__":
counter = RealCounter()
proxy = LoggingCounterProxy(counter)
proxy.increment() # Output: "[Proxy] Calling increment() (total calls: 1)" → returns 1
proxy.increment() # Output: "[Proxy] Calling increment() (total calls: 2)" → returns 2
proxy.reset() # Output: "[Proxy] Calling reset() (total calls: 3)"
proxy.increment() # Output: "[Proxy] Calling increment() (total calls: 4)" → returns 1
5. Advanced Use Cases
5.1 Caching Proxy
A caching proxy stores results of expensive operations (e.g., database queries, API calls) to avoid redundant computations. Here’s a simple example for a factorial calculator:
from abc import ABC, abstractmethod
class MathService(ABC):
@abstractmethod
def factorial(self, n: int) -> int:
pass
class RealMathService(MathService):
def factorial(self, n: int) -> int:
print(f"Calculating factorial of {n} (expensive operation)...")
result = 1
for i in range(1, n+1):
result *= i
return result
class CachingMathProxy(MathService):
def __init__(self, real_service: RealMathService) -> None:
self.real_service = real_service
self.cache = {} # Cache: {n: result}
def factorial(self, n: int) -> int:
if n in self.cache:
print(f"[Cache Hit] Returning cached factorial of {n}")
return self.cache[n]
# Cache miss: compute and store
result = self.real_service.factorial(n)
self.cache[n] = result
return result
# Usage
if __name__ == "__main__":
math_service = RealMathService()
proxy = CachingMathProxy(math_service)
print(proxy.factorial(5)) # Computes and caches
print(proxy.factorial(5)) # Uses cache
print(proxy.factorial(10)) # Computes and caches
5.2 Proxies in Python Frameworks
Many Python frameworks use proxies under the hood:
- Django:
ForeignKeyuses a virtual proxy to lazy-load related objects (e.g.,user.profileloads the profile only when accessed). - SQLAlchemy: ORM sessions act as proxies, deferring database commits until explicitly requested.
- Requests-HTML: The
HTMLSessionobject proxies HTTP requests, handling cookies and sessions transparently.
5.3 Dynamic Proxy Generation
For advanced use cases, you can generate proxies dynamically using Python’s type() function or metaclasses. For example, a generic proxy that wraps any object and logs all method calls:
class LoggingProxy:
def __init__(self, target):
self.target = target
def __getattr__(self, name):
# Dynamically forward attribute access to the target
attr = getattr(self.target, name)
if callable(attr):
# Wrap methods to log calls
def wrapper(*args, **kwargs):
print(f"[Proxy] Calling {name}{args}, {kwargs}")
return attr(*args, **kwargs)
return wrapper
return attr
# Usage
class MyClass:
def greet(self, name):
return f"Hello, {name}!"
obj = MyClass()
proxy = LoggingProxy(obj)
print(proxy.greet("Alice")) # Output: "[Proxy] Calling greet('Alice'), {}" → "Hello, Alice!"
6. Pitfalls and Best Practices
Pitfalls
- Over-Engineering: Avoid using a proxy if the RealSubject is simple or the added complexity isn’t justified.
- Performance Overhead: Proxies add indirection; excessive logic (e.g., logging, caching) can slow down calls.
- Interface Mismatch: Failing to implement the full Subject interface in the proxy can break client code expecting the RealSubject’s behavior.
Best Practices
- Transparency: Ensure the proxy implements the exact same interface as the RealSubject. Clients should not need to distinguish between them.
- Single Responsibility: Keep proxies focused on one task (e.g., logging OR caching, not both). Use multiple proxies if needed (e.g., a logging proxy wrapping a caching proxy).
- Testability: Test the proxy and RealSubject independently. Mock the RealSubject when testing the proxy’s logic (e.g., access control, caching).
7. Conclusion
The Proxy Pattern is a versatile tool in Python for adding layers of control, optimization, and flexibility to object interactions. By acting as a surrogate, proxies enable lazy loading, access control, remote communication, and dynamic behavior enhancement—all while keeping the RealSubject’s code clean and focused on its core responsibility.
Whether you’re building a desktop app with lazy-loaded images, a web service with protected APIs, or a distributed system with remote services, the Proxy Pattern empowers you to write modular, maintainable, and efficient code.
8. References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Python Software Foundation. (n.d.). Abstract Base Classes (abc). https://docs.python.org/3/library/abc.html
- Beck, K. (2008). Implementation Patterns. Addison-Wesley.
- Django Documentation. (n.d.). Database Access Optimization. https://docs.djangoproject.com/en/stable/topics/db/optimization/
- SQLAlchemy Documentation. (n.d.). Session Basics. https://docs.sqlalchemy.org/en/20/orm/session_basics.html