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.singledispatchmultipledispatch

更 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 渲染器、报表统计访问者),或者如何结合组合模式构建复杂结构,欢迎继续问!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐