Python Visitor Pattern

The Visitor Pattern is a behavioral design pattern that allows you to add new operations to existing classes without modifying their structure. In simple terms, the Visitor Pattern separates algorithms from the object structure they operate on.

Core Idea

Imagine you are at the hospital for a physical examination: you (the visited) stay still, while different doctors (visitors) come to you to perform checks. Each doctor focuses on their own specialty, but they all examine the same you.

In programming, the Visitor Pattern works in a similar way:

  • The visited elementsremain stable and unchanged
  • Visitorscan flexibly add new operations
  • Elements accept visits from visitors, allowing visitors to perform operations on them.

Why Do We Need the Visitor Pattern?

Limitations of the Traditional Approach

Suppose we have a graphics system containing multiple shapes:

Example

class Circle:
    def __init__(self, radius):
        self.radius = radius
   
    def area(self):
        return 3.14 * self.radius * self.radius
   
    def perimeter(self):
        return 2 * 3.14 * self.radius

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
   
    def area(self):
        return self.width * self.height
   
    def perimeter(self):
        return 2 * (self.width + self.height)

Here's the problem: If we want to add new features, such as calculating the centroid of shapes or exporting to SVG format, we need to modify every shape class. This violates the Open/Closed Principle (open for extension, closed for modification).

Advantages of the Visitor Pattern

The Visitor Pattern solves this problem in the following ways:

  • Easy to add new operations: Simply create a new visitor class without modifying existing element classes
  • Related operations are centrally managed: Organize related operations in the same visitor
  • Element structure remains stable: Element classes do not need frequent modification

Implementing the Visitor Pattern

Basic Structure

Let's understand the implementation of the Visitor Pattern through a concrete example:

Example

from abc import ABC, abstractmethod

# 1. Define the element interface
class ShapeElement(ABC):
    @abstractmethod
    def accept(self, visitor):
        pass

# 2. Define the visitor interface
class ShapeVisitor(ABC):
    @abstractmethod
    def visit_circle(self, circle):
        pass
   
    @abstractmethod
    def visit_rectangle(self, rectangle):
        pass

# 3. Concrete element classes
class Circle(ShapeElement):
    def __init__(self, radius):
        self.radius = radius
   
    def accept(self, visitor):
        visitor.visit_circle(self)

class Rectangle(ShapeElement):
    def __init__(self, width, height):
        self.width = width
        self.height = height
   
    def accept(self, visitor):
        visitor.visit_rectangle(self)

# 4. Concrete visitor classes
class AreaCalculator(ShapeVisitor):
    def visit_circle(self, circle):
        area = 3.14 * circle.radius * circle.radius
        print(f"Circle area: {area:.2f}")
        return area
   
    def visit_rectangle(self, rectangle):
        area = rectangle.width * rectangle.height
        print(f"Rectangle area: {area:.2f}")
        return area

class PerimeterCalculator(ShapeVisitor):
    def visit_circle(self, circle):
        perimeter = 2 * 3.14 * circle.radius
        print(f"Circle perimeter: {perimeter:.2f}")
        return perimeter
   
    def visit_rectangle(self, rectangle):
        perimeter = 2 * (rectangle.width + rectangle.height)
        print(f"Rectangle perimeter: {perimeter:.2f}")
        return perimeter

Usage Example

Example

# Create shape objects
shapes = [
    Circle(5),
    Rectangle(4, 6),
    Circle(3)
]

# Create visitors
area_calculator = AreaCalculator()
perimeter_calculator = PerimeterCalculator()

print("=== Calculating Area ===")
for shape in shapes:
    shape.accept(area_calculator)

print("\n=== Calculating Perimeter ===")
for shape in shapes:
    shape.accept(perimeter_calculator)

Output result:

=== 计算面积 ===
圆的面积: 78.50
矩形的面积: 24.00
圆的面积: 28.26

=== 计算周长 ===
圆的周长: 31.40
矩形的周长: 20.00
圆的周长: 18.84

Core Components of the Visitor Pattern

1. Visitor (Visitor Interface)

Defines visit methods for each concrete element class.

Example

class ShapeVisitor(ABC):
    def visit_circle(self, circle): pass
    def visit_rectangle(self, rectangle): pass
    def visit_triangle(self, triangle): pass  # Can be easily extended

2. ConcreteVisitor (Concrete Visitor)

Implements the operations defined in the Visitor interface.

Example

class ExportVisitor(ShapeVisitor):
    def visit_circle(self, circle):
        return f'<circle r="{circle.radius}" />'
   
    def visit_rectangle(self, rectangle):
        return f'<rect width="{rectangle.width}" height="{rectangle.height}" />'

3. Element (Element Interface)

Defines a method to accept visitors.

Example

class ShapeElement(ABC):
    @abstractmethod
    def accept(self, visitor):
        pass

4. ConcreteElement (Concrete Element)

Implements the Element interface and calls the corresponding visitor method in the accept method.

Example

class Circle(ShapeElement):
    def accept(self, visitor):
        visitor.visit_circle(self)  # This is where double dispatch occurs

Advanced Application Example

Complex Document Processing System

Let's look at a more practical example: processing different types of document elements.

Example

from abc import ABC, abstractmethod

# Document element interface
class DocumentElement(ABC):
    @abstractmethod
    def accept(self, visitor):
        pass

# Concrete document elements
class Paragraph(DocumentElement):
    def __init__(self, text):
        self.text = text
   
    def accept(self, visitor):
        return visitor.visit_paragraph(self)

class Heading(DocumentElement):
    def __init__(self, text, level):
        self.text = text
        self.level = level
   
    def accept(self, visitor):
        return visitor.visit_heading(self)

class List(DocumentElement):
    def __init__(self, items):
        self.items = items
   
    def accept(self, visitor):
        return visitor.visit_list(self)

# Visitor interface
class DocumentVisitor(ABC):
    @abstractmethod
    def visit_paragraph(self, paragraph): pass
   
    @abstractmethod
    def visit_heading(self, heading): pass
   
    @abstractmethod
    def visit_list(self, list_element): pass

# Concrete visitor: HTML export
class HTMLExportVisitor(DocumentVisitor):
    def visit_paragraph(self, paragraph):
        return f"<p>{paragraph.text}</p>"
   
    def visit_heading(self, heading):
        return f"<h{heading.level}>{heading.text}</h{heading.level}>"
   
    def visit_list(self, list_element):
        items = "".join(f"<li>{item}</li>" for item in list_element.items)
        return f"<ul>{items}</ul>"

# Concrete visitor: Word count
class WordCountVisitor(DocumentVisitor):
    def __init__(self):
        self.total_words = 0
   
    def visit_paragraph(self, paragraph):
        words = len(paragraph.text.split())
        self.total_words += words
        return words
   
    def visit_heading(self, heading):
        words = len(heading.text.split())
        self.total_words += words
        return words
   
    def visit_list(self, list_element):
        words = sum(len(item.split()) for item in list_element.items)
        self.total_words += words
        return words

# Usage example
document = [
    Heading("Python Tutorial", 1),
    Paragraph("Python is a powerful programming language."),
    List(["List item 1", "List item 2", "List item 3"]),
    Paragraph("Learning Python is very interesting.")
]

# HTML export
html_visitor = HTMLExportVisitor()
print("=== HTML Export ===")
for element in document:
    print(element.accept(html_visitor))

# Word count
word_visitor = WordCountVisitor()
print("\n=== Word Count ===")
for element in document:
    words = element.accept(word_visitor)
    print(f"{element.__class__.__name__}: {words} words")

print(f"Total words: {word_visitor.total_words}")

Pros and Cons of the Visitor Pattern

Advantages

  1. Open/Closed Principle: Easy to add new operations without modifying existing classes
  2. Single Responsibility Principle: Centralize related behaviors in one visitor class
  3. Flexibility: Different visitors can be selected at runtime
  4. Data Separation: Algorithms are separated from data structures

Disadvantages

  1. Difficult to change the Element interface: Adding a new element class requires modifying all visitors
  2. May break encapsulation: Visitors may need to access private members of elements
  3. Complexity: May be overly complex for simple data structures

Applicable Scenarios

Scenarios Suitable for the Visitor Pattern

  1. Stable object structure: Need to define multiple operations on a relatively stable object structure
  2. Operations change frequently: Need to frequently add new operations or algorithms
  3. Related operations centralized: Want to organize related operations together
  4. Complex object structure: Object structure contains many different types of objects

Real-world Application Cases

  • Compilers: Various analyses on syntax trees (type checking, code optimization, etc.)
  • Document processing: Perform different operations on document elements (export, statistics, formatting, etc.)
  • GUI systems: Perform different operations on UI components (rendering, event handling, etc.)
  • Game development: Perform different operations on game objects (collision detection, AI, etc.)

Best Practices and Considerations

1. Using Double Dispatch

The core of the Visitor Pattern is double dispatch:

  • First dispatch: the element accepts the visitor
  • Second dispatch: the visitor visits the concrete element

Example

# In the element
def accept(self, visitor):
    visitor.visit_circle(self)  # This determines the specific visit method

2. Managing Visitor State

If the visitor needs to maintain state:

Example

class StatisticsVisitor(ShapeVisitor):
    def __init__(self):
        self.total_area = 0
        self.shape_count = 0
   
    def visit_circle(self, circle):
        area = 3.14 * circle.radius * circle.radius
        self.total_area += area
        self.shape_count += 1
   
    def visit_rectangle(self, rectangle):
        area = rectangle.width * rectangle.height
        self.total_area += area
        self.shape_count += 1
   
    def get_statistics(self):
        return {
            'total_area': self.total_area,
            'shape_count': self.shape_count,
            'average_area': self.total_area / self.shape_count if self.shape_count > 0 else 0
        }

3. Handling Visit Exceptions

Example

class SafeVisitor(ShapeVisitor):
    def visit_circle(self, circle):
        try:
            # Perform operation
            pass
        except Exception as e:
            print(f"Error processing circle: {e}")
   
    def visit_rectangle(self, rectangle):
        try:
            # Perform operation
            pass
        except Exception as e:
            print(f"Error processing rectangle: {e}")

Summary

The Visitor Pattern is a powerful design pattern that provides great extensibility by separating algorithms from data structures. Although it has some complexity, it can significantly improve code maintainability and extensibility in appropriate scenarios.

Key points:

  • The Visitor pattern is suitable for scenarios where the object structure is stable but operations change frequently.
  • Achieves polymorphic behavior through a double dispatch mechanism.
  • Easy to add new operations, but difficult to add new element types.
  • In real projects, weigh its complexity against the benefits it brings.
Other extensions