Python Proxy Pattern

The Proxy Pattern is a structural design pattern that provides a substitute or placeholder for another object to control access to it. In simple terms, a proxy is an intermediary that acts as a go-between between the client and the target object.

Real-Life Proxy Metaphors

Imagine a scenario from real life:

  • Real estate agent: You don't need to communicate directly with the homeowner; you can learn about property listings and schedule viewings through the agent.
  • Celebrity agent: If a company wants to invite a celebrity for endorsement, it needs to negotiate the cooperation details through the agent.
  • Network proxy server: Your network request goes through the proxy server first, and then the proxy server forwards it to the target website.

In these examples, the intermediary/agent is the proxy, and they control access to the real object.


Core Components of the Proxy Pattern

The Proxy Pattern typically involves three key roles:

1. Abstract Subject (Subject)

Defines a common interface for the real subject and the proxy subject, so that the proxy subject can be used wherever the real subject is used.

2. Real Subject

Defines the real object that the proxy represents; it is the object that will ultimately be referenced.

3. Proxy

Holds a reference that allows the proxy to access the entity, provides an interface identical to the real subject, and controls access to the real subject.


Types of Proxy Pattern

The Proxy Pattern has several variants, each with different use cases:

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 the object should have different access permissions.

3. Remote Proxy

Provides a local representative for an object located in a different address space.

4. Smart Reference Proxy

Performs additional operations when accessing the object, such as reference counting, lazy loading, etc.


Implementing the Proxy Pattern in Python

Let's understand the implementation of the Proxy Pattern through specific 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 the common operations of the real subject and the proxy
    """

   
    @abstractmethod
    def request(self) -> Any:
        """Main method to execute a request"""
        pass

Real Subject Implementation

Example

class RealSubject(Subject):
    """
Real subject class, containing the 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 the real request"""
        print(f"RealSubject: handling request - {self.name}")
        return f"Response from {self.name}"

Proxy Class Implementation

Example

class Proxy(Subject):
    """
Proxy class, controlling access to the real subject
Additional features can be added, such as lazy loading, access control, etc.
    """

   
    def __init__(self, subject_name: str):
        self.subject_name = subject_name
        self._real_subject = None  # Lazy initialization
   
    def _lazy_init(self) -> None:
        """Lazily load the real subject object"""
        if self._real_subject is None:
            self._real_subject = RealSubject(self.subject_name)
   
    def request(self) -> str:
        """Proxy's request method, where additional logic can be added"""
        print("Proxy: preprocessing before calling the real object")
       
        # Lazily load the real object
        self._lazy_init()
       
        # Call the real object's method
        result = self._real_subject.request()
       
        print("Proxy: post-processing after calling the real object")
        return f"Enhanced result from proxy: {result}"

Client Code

Example

def client_code(subject: Subject) -> None:
    """
Client code, interacting with the subject through the abstract interface
Does not know or care whether it is using the real subject or the proxy
    """

    print("Client: starting operation")
    result = subject.request()
    print(f"Client: received result - {result}")
    print()

# Usage example
if __name__ == "__main__":
    print("Using the real subject directly:")
    real_subject = RealSubject("Real object A")
    client_code(real_subject)
   
    print("Using the proxy:")
    proxy = Proxy("Proxy object B")
    client_code(proxy)
   
    # Use the same proxy again to observe the lazy loading effect
    print("Using the same proxy again:")
    client_code(proxy)

Running the above code, you will see the following output:

直接使用真实主题:
创建 RealSubject 实例: 真实对象A (这是一个昂贵的操作)
客户端: 开始执行操作
RealSubject: 处理请求 - 真实对象A
客户端: 收到结果 - 来自 真实对象A 的响应

使用代理:
客户端: 开始执行操作
Proxy: 在调用真实对象前进行预处理
创建 RealSubject 实例: 代理对象B (这是一个昂贵的操作)
RealSubject: 处理请求 - 代理对象B
Proxy: 在调用真实对象后进行后处理
客户端: 收到结果 - 代理增强的结果: 来自 代理对象B 的响应

再次使用同一个代理:
客户端: 开始执行操作
Proxy: 在调用真实对象前进行预处理
RealSubject: 处理请求 - 代理对象B
Proxy: 在调用真实对象后进行后处理
客户端: 收到结果 - 代理增强的结果: 来自 代理对象B 的响应

Real-World 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 the expensive operation of loading a large image"""
   
    def __init__(self, filename: str):
        self.filename = filename
        self._load_image()
   
    def _load_image(self) -> None:
        """Simulate the expensive operation of loading a large image"""
        print(f"Loading image: {self.filename} (this may take a few seconds...)")
        time.sleep(2)  # Simulate loading time
        print(f"Image {self.filename} loaded successfully!")
   
    def display(self) -> None:
        """Display the 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 the image, loading it first if necessary"""
        if self._image is None:
            print("Proxy: detected that the image is not loaded, starting lazy loading...")
            self._image = Image(self.filename)
        else:
            print("Proxy: image is already loaded, displaying directly")
       
        self._image.display()

# Usage example
def demo_image_proxy():
    print("Creating image proxy (the image will not be loaded immediately)")
    proxy = ImageProxy("large_photo.jpg")
   
    print("\n"First call to display() - triggers lazy loading:")
    proxy.display()
   
    print("\n"Second call to display() - directly uses the already loaded image:")
    proxy.display()

# Run the example
demo_image_proxy()

Protection Proxy Example: Access Control

Protection proxies are 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 checking"""
        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 yuan")
   
    # A regular user attempts to access
    user_proxy = ProtectionProxy(sensitive_data, "user")
    print("Regular user attempts to access:")
    print(user_proxy.read_data())
   
    # Administrator accesses
    admin_proxy = ProtectionProxy(sensitive_data, "admin")
    print("\nAdministrator access:)
    print(admin_proxy.read_data())

demo_protection_proxy()

Advantages and Disadvantages of the Proxy Pattern

Advantages

  1. Control object access: The proxy can control how and when the client accesses the real object
  2. Lazy loading optimization: Virtual proxies can delay the creation of expensive objects, improving performance
  3. Enhanced security: The protection proxy can add access control logic
  4. Open/Closed Principle: New proxies can be introduced without modifying client code
  5. Separation of concerns: The proxy can handle auxiliary functions unrelated to core business logic

Disadvantages

  1. Increased complexity: Introducing a new abstraction layer makes the code structure more complex
  2. Response latency: The proxy may increase request processing time
  3. Possibility of over-engineering: For simple scenarios, using a proxy may seem unnecessary

Application Scenarios of the Proxy Pattern

The Proxy Pattern is particularly useful in the following scenarios:

1. Lazy Loading

When object creation is expensive but the object may not be used immediately.

2. Access Control

When access rights to certain objects need to be restricted.

3. Local Representative (Remote Representation)

Provide a local interface for remote objects, such as RPC calls.

4. Logging

Add logging before and after method calls.

5. Caching

Provide caching for the results of expensive operations.


Practical Exercises

To reinforce your understanding of the Proxy Pattern, try to complete the following exercises:

Exercise 1: Caching Proxy

Create a cache proxy that adds caching functionality to a function that computes the Fibonacci sequence:

Example

class FibonacciCalculator:
    """Compute the Fibonacci sequence"""
   
    def fibonacci(self, n: int) -> int:
        if n <= 1:
            return n
        return self.fibonacci(n - 1) + self.fibonacci(n - 2)

# Your task: implement the FibonacciCacheProxy class
# Requirement: add caching to Fibonacci calculation to avoid repeated computation

class FibonacciCacheProxy:
    def __init__(self):
        self._calculator = FibonacciCalculator()
        self._cache = {}  # Implement the caching logic here
   
    def fibonacci(self, n: int) -> int:
        # Implement caching logic
        pass

# Test code
def test_cache_proxy():
    proxy = FibonacciCacheProxy()
   
    print("First computation of fib(10):")
    result1 = proxy.fibonacci(10)
    print(f"Result: {result1}")
   
    print("Second computation of fib(10) (should be obtained from cache):")
    result2 = proxy.fibonacci(10)
    print(f"Result: {result2}")
   
    print(f"Both results are the 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 the LoggingProxy class
# Requirement: record the call time, parameters, and result of 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

The Proxy Pattern is a powerful design pattern that controls access to real objects by introducing an intermediate layer. Implementing the Proxy Pattern in Python is relatively simple, mainly thanks to Python's dynamic features.

Key Points

  1. The proxy is a mediator: Acts as an intermediary between the client and the real object
  2. Consistent interface: The proxy and the real object implement the same interface
  3. Access control: The proxy can add extra control logic, such as lazy loading, permission checks, etc.
  4. Flexible application: Choose different types of proxies according to needs
Other extensions