Python 设计模式——代理模式(Proxy Pattern)详解
本文面向零基础新手,从概念到多种代理类型(虚拟、保护、缓存、日志),从简单示例到完整用法,全方位讲解代理模式。
一、什么是代理模式?
1.1 通俗理解
代理模式是一种结构型设计模式,核心思想是:
不直接访问“真实对象”,而是通过一个“代理对象”去访问;代理和真实对象实现同一接口,客户只和代理打交道,代理在转交给真实对象前后可以加一层逻辑(如权限检查、日志、缓存、延迟加载),从而在不改真实对象的前提下控制访问、增强或简化使用。
可以这样类比:
- 现实中的代理:你要找明星签字,不会直接冲进他家,而是通过“经纪人”(代理):经纪人先判断你能不能见、记一笔“谁在什么时候请求”,再决定是否转达给明星。明星(真实对象)没变,但访问被经纪人控制了。
- 代码里:你要访问“大图片”,不直接 new 大图片(可能很慢、占内存),而是先拿一个“图片代理”:代理在第一次真正显示时才去加载大图(延迟加载);或代理先查缓存,有则直接返回,没有再调真实对象。客户只和代理交互,代理再决定何时、如何访问真实对象。
所以,代理模式解决的是:在“访问真实对象”前后加一层控制或逻辑(权限、日志、缓存、延迟加载),而不修改真实对象本身。
1.2 为什么需要代理?
场景一:延迟加载(虚拟代理)
大对象创建或加载很慢(如大图、大文件),希望“用到时才加载”。客户拿到的是代理,代理在第一次被调用时才创建/加载真实对象,之后转调真实对象。
场景二:访问控制(保护代理)
只有满足条件(如已登录、有权限)才能调用真实对象。客户调的是代理,代理先检查权限,通过再转给真实对象,否则拒绝。
场景三:增强与记录(日志代理、缓存代理)
在调用前后打日志、计时、统计;或先查缓存,命中则直接返回,未命中再调真实对象并写入缓存。真实对象不关心这些,由代理完成。
场景四:远程代理
真实对象在另一台机器上,代理在本地,客户调代理时,代理通过网络把请求转给远程对象,把结果返回。客户无感知“本地还是远程”。
1.3 适用场景(什么时候用?)
| 场景 | 说明 |
|---|---|
| 延迟加载 | 大对象用到时才创建/加载,用代理占位。 |
| 访问控制 | 权限、登录检查,由代理统一做。 |
| 日志、统计、缓存 | 在调用前后加逻辑,不改真实对象。 |
| 远程访问 | 本地代理代表远程对象,隐藏网络细节。 |
简单记忆:要在不改真实对象的前提下控制或增强对它的访问时,在中间加一个代理。
二、代理模式的结构(三个角色)
| 角色 | 说明 |
|---|---|
| 主题(Subject) | 接口,定义客户和真实对象都要遵守的约定(如 request、load)。 |
| 真实主题(RealSubject) | 真正干活的类,实现主题接口。 |
| 代理(Proxy) | 实现主题接口,内部持有一个真实主题(或能创建/获取真实主题);客户调用代理时,代理先/后做自己的事(检查、日志、缓存、延迟创建等),再转调真实主题。 |
关系可以理解为:
- 客户只依赖 Subject 接口,拿到的是 Proxy 实例(或 RealSubject,但通常通过代理访问)。
- Proxy 实现 Subject,在方法里:可选地先做逻辑(权限、缓存查找)→ 若需要则创建或获取 RealSubject → 调用 RealSubject 的同一方法 → 可选地后做逻辑(日志、写缓存)→ 返回结果。
三、示例一:延迟加载(虚拟代理)——大图
需求:大图加载很慢,希望“显示时才加载”。客户拿到的先是一个“图片代理”,第一次调用 display() 时代理才去加载真实大图,再显示。
3.1 主题接口 + 真实主题
from abc import ABC, abstractmethod
class Image(ABC):
@abstractmethod
def display(self):
pass
class RealImage(Image):
def __init__(self, path: str):
self._path = path
print(f" [RealImage] 从磁盘加载大图: {path}")
def display(self):
print(f" [RealImage] 显示: {self._path}")
3.2 代理:第一次 display 时才加载
class ImageProxy(Image):
def __init__(self, path: str):
self._path = path
self._real = None # 先不创建真实对象
def display(self):
if self._real is None:
self._real = RealImage(self._path) # 第一次调用时才加载
self._real.display()
3.3 使用
proxy = ImageProxy("/big/photo.jpg")
print("代理已创建,尚未加载图片")
proxy.display() # 这时才加载并显示
proxy.display() # 直接显示,不再加载
要点:创建 ImageProxy 很快,不占大内存;真正用到时才加载 RealImage,这就是虚拟代理(延迟加载)。
四、示例二:访问控制(保护代理)
需求:敏感操作只有“已登录用户”才能执行。客户调的是代理,代理先检查是否登录,通过再转给真实对象。
4.1 主题 + 真实主题
class Database(ABC):
@abstractmethod
def query(self, sql: str) -> list:
pass
class RealDatabase(Database):
def query(self, sql: str) -> list:
print(f" [RealDatabase] 执行: {sql}")
return [{"id": 1, "name": "test"}]
4.2 代理:先检查再转发
class DatabaseProxy(Database):
def __init__(self, real: Database):
self._real = real
self._user = None
def login(self, user: str):
self._user = user
print(f" 用户 {user} 已登录")
def query(self, sql: str) -> list:
if self._user is None:
raise PermissionError("请先登录")
return self._real.query(sql)
4.3 使用
real = RealDatabase()
proxy = DatabaseProxy(real)
# proxy.query("SELECT * FROM users") # 会抛 PermissionError
proxy.login("admin")
proxy.query("SELECT * FROM users") # 正常执行
要点:真实对象不关心“谁在调”;权限逻辑都在代理里,这就是保护代理。
五、示例三:日志代理
需求:在每次调用真实对象前后打日志,不改真实对象代码。
class LoggingProxy(Database):
def __init__(self, real: Database):
self._real = real
def query(self, sql: str) -> list:
print("[Log] 请求 query:", sql)
result = self._real.query(sql)
print("[Log] 返回行数:", len(result))
return result
客户使用 LoggingProxy(real),所有对 query 的调用都会先打日志、再调真实对象、再打日志。真实对象无感知。
六、示例四:缓存代理
需求:某操作耗时(如复杂查询),希望相同参数只算一次,结果缓存起来;下次相同参数直接返回缓存。
class CachingProxy(Database):
def __init__(self, real: Database):
self._real = real
self._cache = {}
def query(self, sql: str) -> list:
if sql in self._cache:
print(" [Cache] 命中")
return self._cache[sql]
result = self._real.query(sql)
self._cache[sql] = result
return result
客户用 CachingProxy(real),第一次 query(“SELECT …”) 走真实对象并缓存;第二次相同 SQL 直接返回缓存。这就是缓存代理。
七、代理与装饰器的区别
| 对比 | 代理 | 装饰器 |
|---|---|---|
| 目的 | 控制/管理对对象的访问(延迟、权限、缓存、远程) | 动态给对象增加职责(加料、加日志) |
| 关注点 | “谁能在什么时候访问”“何时创建”“是否走缓存” | “在原有行为前后加什么逻辑” |
| 典型 | 虚拟代理、保护代理、缓存代理 | 咖啡加奶、带日志的发送器 |
简单记忆:代理侧重访问控制(能不能用、何时加载、用不用缓存);装饰器侧重行为增强(同一接口上多加一层逻辑)。两者在实现上很像(都实现同一接口、都持有一个对象并转发),但意图不同。
八、常见问题与注意点
8.1 代理和真实对象接口一致
客户只依赖主题接口,所以代理必须实现同一接口,才能“无缝替换”;否则客户就要区分代理和真实对象,失去代理的意义。
8.2 谁创建真实对象?
可以是代理在需要时自己 new(如虚拟代理);也可以由外部传入(如保护代理、日志代理通常接收一个已存在的真实对象)。按场景选择。
8.3 不要滥用
若没有“延迟加载、访问控制、缓存、日志、远程”等需求,直接使用真实对象即可;代理会多一层调用,只在有明确需求时使用。
九、小结
- 代理模式:通过代理对象代替对真实对象的直接访问;代理与真实对象实现同一接口,在转调前后可加访问控制、延迟加载、缓存、日志等逻辑,而不修改真实对象。
- 三个角色:主题(接口)、真实主题、代理(实现主题并持有/创建真实主题,在方法里加逻辑再转发)。
- 常见类型:虚拟代理(延迟加载)、保护代理(权限)、缓存代理、日志代理、远程代理。
- 实现要点:代理实现主题接口;内部持有或能创建真实主题;在调用真实主题前后执行自己的逻辑。
建议先写“大图延迟加载”和“数据库保护代理”,再试“缓存代理”或“日志代理”,这样对代理模式会掌握得比较扎实。