Python Proxy
Proxy pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. Simply put, a proxy is an intermediary that acts as a mediator between the client and the target object.
### Real-life Proxy Analogies
Imagine scenarios in real life:
* **Real estate agent**: You don't need to communicate directly with the homeowner; through an agent, you can learn about property listings and arrange viewings
* **Celebrity agent**: Manufacturers wanting to invite a celebrity for endorsement need to go through an agent to negotiate cooperation details
* **Web proxy server**: Your network requests first go through a proxy server, which then forwards them to the target website
In these examples, the agent/broker acts as the proxy, controlling access to the real object.
* * *
## Core Components of Proxy Pattern
Proxy pattern typically consists of three key roles:
### 1. Subject
Defines the common interface for both Real Subject and Proxy, so that the proxy can be used anywhere the real subject is used.
### 2. Real Subject
Defines the real object that the proxy represents, which is the object ultimately referenced.
### 3. Proxy
Maintains a reference so the proxy can access the entity, provides an interface identical to the real subject, and controls access to the real subject.
!(#)
* * *
## Types of Proxy Patterns
There are several variants of proxy pattern, each with different application scenarios:
### 1. Virtual Proxy
Delays the creation of expensive objects until they are actually needed.
### 2. Protection Proxy
Controls access to the original object, used when objects should have different access permissions.
### 3. Remote Proxy
Provides a local representative for an object that resides in a different address space.
### 4. Smart Reference Proxy
Performs additional operations when accessing an object, such as reference counting, lazy loading, etc.
* * *
## Python Implementation of Proxy Pattern
Let's understand the implementation of proxy pattern through concrete code examples.
### Basic Interface Definition
First, we define the abstract subject interface:
## Example
from abc import ABC, abstractmethod
from typing import Any
class Subject(ABC):
"""
Abstract subject interface, defining common operations for real subject and proxy
"""
@abstractmethod
def request(self) -> Any:
"""Main method to execute requests"""
pass
### Real Subject Implementation
## Example
class RealSubject(Subject):
"""
Real subject class, containing core business logic
Usually expensive to create and initialize
"""
def __init__ (self, name: str):
self.name= name
print(f"Creating RealSubject instance: {self.name} (This is an expensive operation)")
def request(self) ->str:
"""Execute real request"""
print(f"RealSubject: Processing request - {self.name}")
return f"Response from {self.name}"
### Proxy Class Implementation
## Example
class Proxy(Subject):
"""
Proxy class, controlling access to real subject
Can add additional features such as lazy loading, access control, etc.
"""
def __init__ (self, subject_name: str):
self.subject_name= subject_name
self._real_subject =None# Delayed initialization
def _lazy_init(self) ->None:
"""Lazy loading of real subject object"""
if self._real_subject is None:
self._real_subject = RealSubject(self.subject_name)
def request(self) ->str:
"""Proxy request method, can add extra logic"""
print("Proxy: Pre-processing before calling real object")
# Lazy load real object
self._lazy_init()
# Call real object's method
result =self._real_subject.request()
print("Proxy: Post-processing after calling real object")
return f"Proxy enhanced result: {result}"
### Client Code
## Example
def client_code(subject: Subject) ->None:
"""
Client code, interacting with subject through abstract interface
Doesn't know or care whether real subject or proxy is being used
"""
print("Client: Starting operation")
result = subject.request()
print(f"Client: Received result - {result}")
print()
# Usage example
if __name__ =="__main__":
print("Using real subject directly:")
real_subject = RealSubject("Real Object A")
client_code(real_subject)
print("Using proxy:")
proxy = Proxy("Proxy Object B")
client_code(proxy)
# Use the same proxy again, observe lazy loading effect
print("Using the same proxy again:")
client_code(proxy)
Running the above code, you will see the following output:
Using real subject directly:Creating RealSubject instance: Real Object A (This is an expensive operation)Client: Starting operationRealSubject: Processing request - Real Object A Client: Received result - Response from Real Object AUsing proxy:Client: Starting operationProxy: Pre-processing before calling real objectCreating RealSubject instance: Proxy Object B (This is an expensive operation)RealSubject: Processing request - Proxy Object B Proxy: Post-processing after calling real objectClient: Received result - Proxy enhanced result: Response from Proxy Object BUsing the same proxy again:Client: Starting operationProxy: Pre-processing before calling real objectRealSubject: Processing request - Proxy Object B Proxy: Post-processing after calling real objectClient: Received result - Proxy enhanced result: Response from Proxy Object B
* * *
## Practical Application Example: Image Loading Proxy
Let's look at a more practical exampleβa virtual proxy for image loading.
### Image Loading System
## Example
from pathlib import Path
import time
class Image:
"""Real image class, simulating expensive operation of loading large images"""
def __init__ (self, filename: str):
self.filename= filename
self._load_image()
def _load_image(self) ->None:
"""Simulate expensive operation of loading large images"""
print(f"Loading image: {self.filename} (This may take a few seconds...)")
time.sleep(2)# Simulate loading time
print(f"Image {self.filename} loaded!")
def display(self) ->None:
"""Display image"""
print(f"Displaying image: {self.filename}")
class ImageProxy:
"""Image proxy, implementing lazy loading"""
def __init__ (self, filename: str):
self.filename= filename
self._image =None
def display(self) ->None:
"""Display image, load if necessary"""
if self._image is None:
print("Proxy: Detected image not loaded, starting lazy loading...")
self._image = Image(self.filename)
else:
print("Proxy: Image already loaded, displaying directly")
self._image.display()
# Usage example
def demo_image_proxy():
print("Creating image proxy (will not load image immediately)")
proxy = ImageProxy("large_photo.jpg")
print("n First call to display() - triggers lazy loading:")
proxy.display()
print("n Second call to display() - uses already loaded image directly:")
proxy.display()
# Run example
demo_image_proxy()
* * *
## Protection Proxy Example: Access Control
Protection proxy is used to control access to sensitive resources.
## Example
class SensitiveData:
"""Sensitive data class"""
def __init__ (self, data: str):
self.data= data
def read_data(self) ->str:
"""Read sensitive data"""
return f"Sensitive data: {self.data}"
class ProtectionProxy:
"""Protection proxy, implementing access control"""
def __init__ (self, sensitive_data: SensitiveData, user_role: str):
self._sensitive_data = sensitive_data
self.user_role= user_role
def read_data(self) ->str:
"""Read data, but perform permission check"""
if self.user_role!="admin":
return"Error: Insufficient permissions, only administrators can access sensitive data"
return self._sensitive_data.read_data()
# Usage example
def demo_protection_proxy():
sensitive_data = SensitiveData("Confidential information: Project budget is 1 million")
# Regular user attempts to access
user_proxy = ProtectionProxy(sensitive_data,"user")
print("Regular user attempting access:")
print(user_proxy.read_data())
# Administrator access
admin_proxy = ProtectionProxy(sensitive_data,"admin")
print("n Administrator access:")
print(admin_proxy.read_data())
demo_protection_proxy()
* * *
## Pros and Cons of Proxy Pattern
### Advantages
1. **Control object access**: Proxy can control how and when clients access the real object
2. **Lazy loading optimization**: Virtual proxy can delay creation of expensive objects, improving performance
3. **Enhanced security**: Protection proxy can add access control logic
4. **Open/closed principle**: New proxies can be introduced without modifying client code
5. **Separation of concerns**: Proxy can handle auxiliary functions unrelated to core business logic
### Disadvantages
1. **Increased complexity**: Introduces new abstraction layer, making code structure more complex
2. **Response delay**: Proxy may increase request processing time
3. **Potential over-engineering**: Using proxy for simple scenarios may be unnecessary
* * *
## Application Scenarios of Proxy Pattern
Proxy pattern is particularly useful in the following scenarios:
### 1. Lazy Loading
When object creation is costly but may not be immediately used.
### 2. Access Control
When access to certain objects needs to be restricted.
### 3. Local Representation
Providing local interfaces for remote objects, such as RPC calls.
### 4. Logging
Adding logging before and after method calls.
### 5. Caching
Providing caching for results of expensive operations.
* * *
## Practice Exercises
To consolidate understanding of proxy pattern, try completing the following exercises:
### Exercise 1: Cache Proxy
Create a cache proxy that adds caching functionality for a Fibonacci sequence calculation function:
## Example
class FibonacciCalculator:
"""Calculate Fibonacci sequence"""
def fibonacci(self, n: int) ->int:
if n int:
# Implement caching logic
pass
# Test code
def test_cache_proxy():
proxy = FibonacciCacheProxy()
print("First calculation of fib(10):")
result1 = proxy.fibonacci(10)
print(f"Result: {result1}")
print("Second calculation of fib(10) (should be retrieved from cache):")
result2 = proxy.fibonacci(10)
print(f"Result: {result2}")
print(f"Both results are same: {result1 == result2}")
### Exercise 2: Logging Proxy
Create a logging proxy that records method call information:
## Example
class DatabaseService:
"""Database service"""
def query(self, sql: str) ->str:
return f"Executing query: {sql}"
def update(self, sql: str) ->str:
return f"Executing update: {sql}"
# Your task: Implement LoggingProxy class
# Requirement: Record call time, parameters, and results for each method
class LoggingProxy:
def __init__ (self):
self._service = DatabaseService()
def query(self, sql: str) ->str:
# Add logging logic
pass
def update(self, sql: str) ->str:
# Add logging logic
pass
* * *
## Summary
Proxy pattern is a powerful design pattern that controls access to real objects by introducing an intermediary layer. Implementing proxy pattern in Python is relatively simple, thanks mainly to Python's dynamic features.
### Key Points
1. **Proxy is intermediary**: Acts as a middleman between client and real object
2. **Consistent interface**: Proxy and real object implement the same interface
3. **Controlled access**: Proxy can add additional control logic such as lazy loading, permission checks, etc.
4. **Flexible application**: Choose different types of proxies based on requirements
YouTip