Python 访问者模式

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

文章已创建 3511

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部