Python 中的访问者模式(Visitor Pattern)
访问者模式是一种行为型设计模式,其核心目的是:
将算法(操作)与对象结构分离,让你在不改变对象结构的前提下,为该结构中的元素添加新的操作。
形象比喻:就像一个动物园(对象结构)里有很多动物(元素),来了不同的“访客”(访问者)——摄影师会拍照、饲养员会喂食、兽医会检查健康。动物本身不需要改变,就能支持不同的新操作。
为什么需要访问者模式?
当你有一个稳定的对象结构(例如 AST 抽象语法树、图形元素树、文件系统),但需要频繁添加新操作时:
- 如果用继承:在每个类中添加新方法 → 违反开闭原则
- 如果用条件判断:分散在各个类中 → 难以维护
访问者模式通过双分派(Double Dispatch) 解决这个问题:第一次分派决定访问者,第二次分派决定具体元素。
典型应用场景
- 编译器:对 AST(抽象语法树)进行不同操作(类型检查、代码生成、优化、打印)
- 文档转换:将结构化文档转为 HTML、PDF、Markdown
- 图形渲染:对图形元素树执行绘制、计算面积、序列化等操作
- 报表统计:在组织架构树上统计人数、薪资等
- XML/JSON 处理:遍历 DOM 树执行不同操作
Python 实现示例:图形元素访问者
我们实现一个简单的图形编辑器,支持圆形和矩形,访问者包括:绘制、计算面积、导出 XML。
from abc import ABC, abstractmethod
from typing import List
import xml.etree.ElementTree as ET
# === 元素接口(Element)===
class Shape(ABC):
@abstractmethod
def accept(self, visitor):
pass
# 具体元素1:圆形
class Circle(Shape):
def __init__(self, x: float, y: float, radius: float):
self.x, self.y, self.radius = x, y, radius
def accept(self, visitor):
return visitor.visit_circle(self)
# 具体元素2:矩形
class Rectangle(Shape):
def __init__(self, x: float, y: float, width: float, height: float):
self.x, self.y, self.width, self.height = x, y, height
def accept(self, visitor):
return visitor.visit_rectangle(self)
# === 访问者接口(Visitor)===
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle: Circle):
pass
@abstractmethod
def visit_rectangle(self, rectangle: Rectangle):
pass
# 具体访问者1:绘制访问者
class DrawVisitor(ShapeVisitor):
def visit_circle(self, circle: Circle):
print(f"绘制圆形:中心({circle.x}, {circle.y}), 半径 {circle.radius}")
def visit_rectangle(self, rectangle: Rectangle):
print(f"绘制矩形:左上角({rectangle.x}, {rectangle.y}), "
f"宽 {rectangle.width}, 高 {rectangle.height}")
# 具体访问者2:面积计算访问者
class AreaVisitor(ShapeVisitor):
def __init__(self):
self.total_area = 0.0
def visit_circle(self, circle: Circle):
import math
area = math.pi * circle.radius ** 2
self.total_area += area
print(f"圆形面积: {area:.2f}")
def visit_rectangle(self, rectangle: Rectangle):
area = rectangle.width * rectangle.height
self.total_area += area
print(f"矩形面积: {area:.2f}")
# 具体访问者3:XML 导出访问者
class XMLExportVisitor(ShapeVisitor):
def __init__(self):
self.root = ET.Element("shapes")
def visit_circle(self, circle: Circle):
elem = ET.SubElement(self.root, "circle")
elem.set("x", str(circle.x))
elem.set("y", str(circle.y))
elem.set("radius", str(circle.radius))
def visit_rectangle(self, rectangle: Rectangle):
elem = ET.SubElement(self.root, "rectangle")
elem.set("x", str(rectangle.x))
elem.set("y", str(rectangle.y))
elem.set("width", str(rectangle.width))
elem.set("height", str(rectangle.height))
def get_xml(self) -> str:
return ET.tostring(self.root, encoding='unicode')
# 对象结构:画布(可以包含多个图形)
class Canvas:
def __init__(self):
self.shapes: List[Shape] = []
def add(self, shape: Shape):
self.shapes.append(shape)
def accept(self, visitor: ShapeVisitor):
for shape in self.shapes:
shape.accept(visitor)
# 客户端使用
if __name__ == "__main__":
canvas = Canvas()
canvas.add(Circle(10, 20, 5))
canvas.add(Rectangle(30, 40, 15, 10))
canvas.add(Circle(50, 50, 8))
print("=== 绘制所有图形 ===")
draw_visitor = DrawVisitor()
canvas.accept(draw_visitor)
print("\n=== 计算总面积 ===")
area_visitor = AreaVisitor()
canvas.accept(area_visitor)
print(f"总面积: {area_visitor.total_area:.2f}")
print("\n=== 导出为 XML ===")
xml_visitor = XMLExportVisitor()
canvas.accept(xml_visitor)
print(xml_visitor.get_xml())
输出:
=== 绘制所有图形 ===
绘制圆形:中心(10, 20), 半径 5
绘制矩形:左上角(30, 40), 宽 15, 高 10
绘制圆形:中心(50, 50), 半径 8
=== 计算总面积 ===
圆形面积: 78.54
矩形面积: 150.00
圆形面积: 201.06
总面积: 429.60
=== 导出为 XML ===
<shapes><circle x="10" y="20" radius="5" /><rectangle x="30" y="40" width="15" height="10" /><circle x="50" y="50" radius="8" /></shapes>
访问者模式结构总结
| 角色 | 说明 |
|---|---|
| Visitor | 抽象访问者接口(ShapeVisitor) |
| ConcreteVisitor | 具体访问者(DrawVisitor、AreaVisitor 等) |
| Element | 元素接口(Shape),定义 accept 方法 |
| ConcreteElement | 具体元素(Circle、Rectangle) |
| Object Structure | 对象结构(Canvas),管理元素集合 |
访问者模式 vs 其他模式对比
| 模式 | 目的 | 扩展方向 | 典型场景 |
|---|---|---|---|
| 访问者 | 在稳定结构上添加新操作 | 添加新操作 | AST 处理、文档转换 |
| 组合 | 构建树形结构 | 添加新元素 | GUI 树、文件系统 |
| 策略 | 替换算法 | 替换行为 | 支付、排序 |
| 命令 | 封装请求 | 添加新命令 | 撤销、宏 |
Python 中的实用建议
- 访问者模式在 Python 中使用较少,因为 Python 是动态语言,很多场景可以用:
- 函数作为访问者(传入不同函数)
getattr(element, operation_name)()动态调用- 多分派库(如
functools.singledispatch或multipledispatch)
更 Pythonic 的替代方式:
from functools import singledispatch
@singledispatch
def process_shape(shape):
raise NotImplementedError(f"Unsupported shape: {type(shape)}")
@process_shape.register
def _(shape: Circle):
print(f"处理圆形: 半径 {shape.radius}")
@process_shape.register
def _(shape: Rectangle):
print(f"处理矩形: 宽高 {shape.width}x{shape.height}")
# 使用
for shape in canvas.shapes:
process_shape(shape)
注意事项
- 访问者模式违反了“依赖倒置原则”(高层依赖抽象),因为访问者需要知道所有具体元素类型
- 添加新元素类型时,需要修改所有访问者(违反开闭原则)
- 适合元素结构稳定、操作频繁变化的场景
- 如果元素经常变化,考虑用组合 + 访问者双向结合
总结
访问者模式是处理稳定数据结构 + 多变操作的经典解决方案,在编译器、解释器、序列化等系统中非常常见。
但在 Python 中,由于语言的动态性,通常优先考虑更简单的方案(如 singledispatch、函数式编程)。
如果你想看更实际的例子(如 AST 遍历、HTML 渲染器、报表统计访问者),或者如何结合组合模式构建复杂结构,欢迎继续问!