Паттерн Заместитель в Python
Паттерн Прокси — это структурный паттерн проектирования, который предоставляет замену или заполнитель для другого объекта, чтобы контролировать доступ к нему. Проще говоря, прокси — это посредник, который выступает связующим звеном между клиентом и целевым объектом.
Метафоры прокси из реальной жизни
Представьте сценарий из реальной жизни:
- Агент по недвижимости: вам не нужно общаться напрямую с владельцем дома; вы можете узнать о вариантах недвижимости и запланировать просмотры через агента.
- Агент знаменитости: Если компания хочет пригласить знаменитость для рекламы, ей нужно обсудить детали сотрудничества через агента.
- Сетевой прокси-сервер: Ваш сетевой запрос сначала проходит через прокси-сервер, а затем прокси-сервер перенаправляет его на целевой веб-сайт.
В этих примерах посредник/агент — это прокси, и он контролирует доступ к реальному объекту.
Основные компоненты паттерна «Прокси»
Паттерн «Прокси» обычно включает три ключевые роли:
1. Абстрактный субъект (Subject)
Определяет общий интерфейс для реального субъекта и прокси-субъекта, чтобы прокси-субъект можно было использовать везде, где используется реальный субъект.
2. Реальный субъект
Определяет реальный объект, который представляет прокси; это объект, к которому в конечном итоге будет происходить обращение.
3. Прокси
Хранит ссылку, которая позволяет прокси получать доступ к сущности, предоставляет интерфейс, идентичный интерфейсу реального субъекта, и контролирует доступ к реальному субъекту.

Типы паттерна «Прокси»
Паттерн Прокси (Proxy) имеет несколько вариантов, каждый с разными сценариями использования:
1. Виртуальный прокси
Откладывает создание дорогостоящих объектов до момента, когда они действительно понадобятся.
2. Защитный прокси
Контролирует доступ к исходному объекту; используется, когда объект должен иметь разные права доступа.
3. Удалённый прокси
Предоставляет локального представителя для объекта, находящегося в другом адресном пространстве.
4. Умный ссылочный прокси
Выполняет дополнительные операции при доступе к объекту, такие как подсчёт ссылок, ленивая загрузка и т. д.
Реализация паттерна Прокси в Python
Давайте разберём реализацию паттерна Прокси на конкретных примерах кода.
Определение базового интерфейса
Сначала мы определяем интерфейс абстрактного субъекта:
Пример
from typing import Any
class Subject(ABC):
"""
Интерфейс абстрактного субъекта, определяющий общие операции реального субъекта и прокси
"""
@abstractmethod
def request(self) -> Any:
"""Основной метод для выполнения запроса"""
pass
Реализация реального субъекта
Пример
"""
Класс реального субъекта, содержащий основную бизнес-логику
Обычно дорого создавать и инициализировать
"""
def __init__(self, name: str):
self.name = name
print(f"Создание экземпляра RealSubject: {self.name} (это дорогостоящая операция)")
def request(self) -> str:
"""Выполнить реальный запрос"""
print(f"RealSubject: обработка запроса - {self.name}")
return f"Ответ от {self.name}"
Реализация класса Proxy
Пример
"""
Класс Proxy, контролирующий доступ к реальному субъекту
Можно добавить дополнительные возможности, такие как ленивая загрузка, контроль доступа и т. д.
"""
def __init__(self, subject_name: str):
self.subject_name = subject_name
self._real_subject = None # Ленивая инициализация
def _lazy_init(self) -> None:
"""Ленивая загрузка объекта реального субъекта"""
if self._real_subject is None:
self._real_subject = RealSubject(self.subject_name)
def request(self) -> str:
"""Метод request прокси, куда можно добавить дополнительную логику"""
print("Прокси: предварительная обработка перед вызовом реального объекта")
# Ленивая загрузка реального объекта
self._lazy_init()
# Вызов метода реального объекта
result = self._real_subject.request()
print("Прокси: постобработка после вызова реального объекта")
return f"Улучшенный результат от прокси: {result}"
Клиентский код
Пример
"""
Клиентский код, взаимодействующий с субъектом через абстрактный интерфейс
Не знает и не заботится, использует ли он реальный субъект или прокси
"""
print("Клиент: начало операции")
result = subject.request()
print(f"Клиент: получен результат - {result}")
print()
# Пример использования
if __name__ == "__main__":
print("Использование реального субъекта напрямую:")
real_subject = RealSubject("Реальный объект A")
client_code(real_subject)
print("Использование прокси:")
proxy = Proxy("Прокси-объект B")
client_code(proxy)
# Используйте тот же прокси снова, чтобы наблюдать эффект ленивой загрузки
print("Использование того же прокси ещё раз:")
client_code(proxy)
Запустив приведённый выше код, вы увидите следующий вывод:
直接使用真实主题: 创建 RealSubject 实例: 真实对象A (这是一个昂贵的操作) 客户端: 开始执行操作 RealSubject: 处理请求 - 真实对象A 客户端: 收到结果 - 来自 真实对象A 的响应 使用代理: 客户端: 开始执行操作 Proxy: 在调用真实对象前进行预处理 创建 RealSubject 实例: 代理对象B (这是一个昂贵的操作) RealSubject: 处理请求 - 代理对象B Proxy: 在调用真实对象后进行后处理 客户端: 收到结果 - 代理增强的结果: 来自 代理对象B 的响应 再次使用同一个代理: 客户端: 开始执行操作 Proxy: 在调用真实对象前进行预处理 RealSubject: 处理请求 - 代理对象B Proxy: 在调用真实对象后进行后处理 客户端: 收到结果 - 代理增强的结果: 来自 代理对象B 的响应
Пример из реального мира: прокси загрузки изображений
Давайте рассмотрим более практичный пример — виртуальный прокси для загрузки изображений.
Система загрузки изображений
Пример
import time
class Image:
"""Класс реального изображения, имитирующий затратную операцию загрузки большого изображения"""
def __init__(self, filename: str):
self.filename = filename
self._load_image()
def _load_image(self) -> None:
"""Имитация затратной операции загрузки большого изображения"""
print(f"Загрузка изображения: {self.filename} (это может занять несколько секунд...)")
time.sleep(2) # Имитация времени загрузки
print(f"Изображение {self.filename} успешно загружено!")
def display(self) -> None:
"""Отобразить изображение"""
print(f"Отображение изображения: {self.filename}")
class ImageProxy:
"""Прокси изображения, реализующий ленивую загрузку"""
def __init__(self, filename: str):
self.filename = filename
self._image = None
def display(self) -> None:
"""Отобразить изображение, при необходимости сначала загрузив его"""
if self._image is None:
print("Прокси: обнаружено, что изображение не загружено, запуск ленивой загрузки...")
self._image = Image(self.filename)
else:
print("Прокси: изображение уже загружено, отображается напрямую")
self._image.display()
# Пример использования
def demo_image_proxy():
print("Создание прокси изображения (изображение не будет загружено немедленно)")
proxy = ImageProxy("large_photo.jpg")
print("\n"Первый вызов display() - запускает ленивую загрузку:")
proxy.display()
print("\n"Второй вызов display() - напрямую использует уже загруженное изображение:")
proxy.display()
# Запуск примера
demo_image_proxy()
Пример защитного прокси: контроль доступа
Защитные прокси используются для контроля доступа к конфиденциальным ресурсам.
Пример
"""Класс конфиденциальных данных"""
def __init__(self, data: str):
self.data = data
def read_data(self) -> str:
"""Чтение конфиденциальных данных"""
return f"Конфиденциальные данные: {self.data}"
class ProtectionProxy:
"""Защитный прокси, реализующий контроль доступа"""
def __init__(self, sensitive_data: SensitiveData, user_role: str):
self._sensitive_data = sensitive_data
self.user_role = user_role
def read_data(self) -> str:
"""Чтение данных с проверкой прав доступа"""
if self.user_role != "admin":
return "Ошибка: недостаточно прав, только администраторы могут получить доступ к конфиденциальным данным"
return self._sensitive_data.read_data()
# Пример использования
def demo_protection_proxy():
sensitive_data = SensitiveData("Конфиденциальная информация: бюджет проекта составляет 1 миллион юаней")
# Обычный пользователь пытается получить доступ
user_proxy = ProtectionProxy(sensitive_data, "user")
print("Обычный пользователь пытается получить доступ:")
print(user_proxy.read_data())
# Доступ администратора
admin_proxy = ProtectionProxy(sensitive_data, "admin")
print("\nДоступ администратора:)
print(admin_proxy.read_data())
demo_protection_proxy()
Преимущества и недостатки паттерна «Прокси»
Преимущества
- Контроль доступа к объекту: Прокси может контролировать, как и когда клиент обращается к реальному объекту
- Оптимизация ленивой загрузки: Виртуальные прокси могут отложить создание дорогостоящих объектов, повышая производительность
- Повышенная безопасность: Защитный прокси может добавлять логику контроля доступа
- Принцип открытости/закрытости: Новые прокси можно вводить без изменения клиентского кода
- Разделение ответственности: Прокси может обрабатывать вспомогательные функции, не связанные с основной бизнес-логикой
Недостатки
- Повышенная сложность: Введение нового слоя абстракции делает структуру кода более сложной
- Задержка отклика: Прокси может увеличить время обработки запроса
- Возможность избыточного проектирования: В простых сценариях использование прокси может показаться ненужным
Сценарии применения паттерна «Прокси»
Паттерн «Прокси» особенно полезен в следующих сценариях:
1. Ленивая загрузка
Когда создание объекта дорогостоящее, но объект может не использоваться сразу.
2. Контроль доступа
Когда необходимо ограничить права доступа к определённым объектам.
3. Локальный представитель (удалённое представление)
Предоставить локальный интерфейс для удалённых объектов, например, для вызовов RPC.
4. Логирование
Добавить логирование до и после вызовов методов.
5. Кэширование
Обеспечить кэширование результатов дорогостоящих операций.
Практические упражнения
Чтобы закрепить понимание паттерна «Прокси», попробуйте выполнить следующие упражнения:
Упражнение 1: Кэширующий прокси
Создайте кэширующий прокси, который добавляет кэширование функции, вычисляющей последовательность Фибоначчи:
Пример
"""Вычислить последовательность Фибоначчи"""
def fibonacci(self, n: int) -> int:
if n <= 1:
return n
return self.fibonacci(n - 1) + self.fibonacci(n - 2)
# Ваша задача: реализовать класс FibonacciCacheProxy
# Требование: добавить кэширование к вычислению чисел Фибоначчи, чтобы избежать повторных вычислений
class FibonacciCacheProxy:
def __init__(self):
self._calculator = FibonacciCalculator()
self._cache = {} # Реализуйте здесь логику кэширования
def fibonacci(self, n: int) -> int:
# Реализуйте логику кэширования
pass
# Тестовый код
def test_cache_proxy():
proxy = FibonacciCacheProxy()
print("Первое вычисление fib(10):")
result1 = proxy.fibonacci(10)
print(f"Результат: {result1}")
print("Второе вычисление fib(10) (должно быть получено из кэша):")
result2 = proxy.fibonacci(10)
print(f"Результат: {result2}")
print(f"Оба результата совпадают: {result1 == result2}")
Упражнение 2: Логирующий прокси
Создайте логирующий прокси, который записывает информацию о вызовах методов:
Пример
"""Служба базы данных"""
def query(self, sql: str) -> str:
return f"Выполнение запроса: {sql}"
def update(self, sql: str) -> str:
return f"Выполняется обновление: {sql}"
# Ваша задача: реализовать класс LoggingProxy
# Требование: записывать время вызова, параметры и результат каждого метода
class LoggingProxy:
def __init__(self):
self._service = DatabaseService()
def query(self, sql: str) -> str:
# Добавить логику логирования
pass
def update(self, sql: str) -> str:
# Добавить логику логирования
pass
Итог
Паттерн Proxy — мощный паттерн проектирования, который контролирует доступ к реальным объектам, вводя промежуточный слой. Реализация паттерна Proxy в Python относительно проста, в основном благодаря динамическим особенностям Python.
Ключевые моменты
- Прокси — это посредник: выступает промежуточным звеном между клиентом и реальным объектом
- Единый интерфейс: прокси и реальный объект реализуют один и тот же интерфейс
- Контроль доступа: прокси может добавлять дополнительную логику контроля, например ленивую загрузку, проверку прав и т. д.
- Гибкое применение: выбирайте разные типы прокси в соответствии с потребностями