Python 中的访问者模式(Visitor Pattern)
访问者模式是一种行为型设计模式,其核心目的是:
将算法(操作)与对象结构分离,让你在不改变对象结构的前提下,为该结构中的元素添加新的操作。
形象比喻:就像一个动物园(对象结构)里有很多动物(元素),来了不同的“访客”(访问者)——摄影师会拍照、饲养员会喂食、兽医会检查健康。动物本身不需要改变,就能支持不同的新操作。
为什么需要访问者模式?
当你有一个稳定的对象结构(例如 AST 抽象语法树、图形元素树、文件系统),但需要频繁添加新操作时:
- 如果用继承:在每个类中添加新方法 → 违反开闭原则
- 如果用条件判断:分散在各个类中 → 难以维护
访问者模式通过双分派(Double Dispatch)解决这个问题:第一次分派决定访问者,第二次分派决定具体元素。
典型应用场景
- 编译器:对 AST(抽象语法树)进行不同操作(类型检查、代码生成、优化、打印)
- 文档转换:将结构化文档转为 HTML、PDF、Markdown
- 图形渲染:对图形元素树执行绘制、计算面积、序列化等操作
- 报表统计:在组织架构树上统计人数、薪资等
- XML/JSON 处理:遍历 DOM 树执行不同操作
Python 实现示例:图形元素访问者
我们实现一个简单的图形编辑器,支持圆形和矩形,访问者包括:绘制、计算面积、导出 XML。
fromabcimportABC,abstractmethodfromtypingimportListimportxml.etree.ElementTreeasET# === 元素接口(Element)===classShape(ABC):@abstractmethoddefaccept(self,visitor):pass# 具体元素1:圆形classCircle(Shape):def__init__(self,x:float,y:float,radius:float):self.x,self.y,self.radius=x,y,radiusdefaccept(self,visitor):returnvisitor.visit_circle(self)# 具体元素2:矩形classRectangle(Shape):def__init__(self,x:float,y:float,width:float,height:float):self.x,self.y,self.width,self.height=x,y,heightdefaccept(self,visitor):returnvisitor.visit_rectangle(self)# === 访问者接口(Visitor)===classShapeVisitor(ABC):@abstractmethoddefvisit_circle(self,circle:Circle):pass@abstractmethoddefvisit_rectangle(self,rectangle:Rectangle):pass# 具体访问者1:绘制访问者classDrawVisitor(ShapeVisitor):defvisit_circle(self,circle:Circle):print(f"绘制圆形:中心({circle.x},{circle.y}), 半径{circle.radius}")defvisit_rectangle(self,rectangle:Rectangle):print(f"绘制矩形:左上角({rectangle.x},{rectangle.y}), "f"宽{rectangle.width}, 高{rectangle.height}")# 具体访问者2:面积计算访问者classAreaVisitor(ShapeVisitor):def__init__(self):self.total_area=0.0defvisit_circle(self,circle:Circle):importmath area=math.pi*circle.radius**2self.total_area+=areaprint(f"圆形面积:{area:.2f}")defvisit_rectangle(self,rectangle:Rectangle):area=rectangle.width*rectangle.height self.total_area+=areaprint(f"矩形面积:{area:.2f}")# 具体访问者3:XML 导出访问者classXMLExportVisitor(ShapeVisitor):def__init__(self):self.root=ET.Element("shapes")defvisit_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))defvisit_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))defget_xml(self)->str:returnET.tostring(self.root,encoding='unicode')# 对象结构:画布(可以包含多个图形)classCanvas:def__init__(self):self.shapes:List[Shape]=[]defadd(self,shape:Shape):self.shapes.append(shape)defaccept(self,visitor:ShapeVisitor):forshapeinself.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 的替代方式:
fromfunctoolsimportsingledispatch@singledispatchdefprocess_shape(shape):raiseNotImplementedError(f"Unsupported shape:{type(shape)}")@process_shape.registerdef_(shape:Circle):print(f"处理圆形: 半径{shape.radius}")@process_shape.registerdef_(shape:Rectangle):print(f"处理矩形: 宽高{shape.width}x{shape.height}")# 使用forshapeincanvas.shapes:process_shape(shape)注意事项
- 访问者模式违反了“依赖倒置原则”(高层依赖抽象),因为访问者需要知道所有具体元素类型
- 添加新元素类型时,需要修改所有访问者(违反开闭原则)
- 适合元素结构稳定、操作频繁变化的场景
- 如果元素经常变化,考虑用组合 + 访问者双向结合
总结
访问者模式是处理稳定数据结构 + 多变操作的经典解决方案,在编译器、解释器、序列化等系统中非常常见。
但在 Python 中,由于语言的动态性,通常优先考虑更简单的方案(如 singledispatch、函数式编程)。
如果你想看更实际的例子(如 AST 遍历、HTML 渲染器、报表统计访问者),或者如何结合组合模式构建复杂结构,欢迎继续问!