BehavioralPythonverifiedVerified
Visitor Pattern in Python
Lets you add new operations to an object structure without modifying the objects themselves, by separating the algorithm from the object structure it operates on.
How to Implement the Visitor Pattern in Python
1Step 1: Define shape classes and use match/case for visitor dispatch
import math
from dataclasses import dataclass
from typing import Protocol
@dataclass
class Circle:
radius: float
@dataclass
class Rectangle:
width: float
height: float
@dataclass
class Triangle:
base: float
height: float
side_a: float
side_b: float
Shape = Circle | Rectangle | Triangle2Step 2: Implement visitors as functions using structural pattern matching
def area(shape: Shape) -> float:
match shape:
case Circle(radius=r):
return math.pi * r ** 2
case Rectangle(width=w, height=h):
return w * h
case Triangle(base=b, height=h):
return 0.5 * b * h
def perimeter(shape: Shape) -> float:
match shape:
case Circle(radius=r):
return 2 * math.pi * r
case Rectangle(width=w, height=h):
return 2 * (w + h)
case Triangle(base=b, side_a=a, side_b=c):
return b + a + c3Step 3: Apply visitors to a collection of shapes
shapes: list[Shape] = [Circle(5), Rectangle(4, 6), Triangle(3, 4, 3, 5)]
print("Areas:", [area(s) for s in shapes])
print("Perimeters:", [perimeter(s) for s in shapes])"""AST Visitor for code analysis using the Visitor pattern."""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Generic, TypeVar
T = TypeVar("T")
# [step] Define AST node types
class ASTNode(ABC):
@abstractmethod
def accept(self, visitor: "ASTVisitor[T]") -> T: ...
@dataclass
class ProgramNode(ASTNode):
body: list[ASTNode]
def accept(self, visitor: "ASTVisitor[T]") -> T:
return visitor.visit_program(self)
@dataclass
class FunctionDeclNode(ASTNode):
name: str
params: list[str]
body: list[ASTNode]
is_async: bool = False
def accept(self, visitor: "ASTVisitor[T]") -> T:
return visitor.visit_function_decl(self)
@dataclass
class VariableDeclNode(ASTNode):
name: str
kind: str # "let" | "const" | "var"
init: ASTNode | None = None
def accept(self, visitor: "ASTVisitor[T]") -> T:
return visitor.visit_variable_decl(self)
@dataclass
class CallExpressionNode(ASTNode):
callee: str
args: list[ASTNode]
def accept(self, visitor: "ASTVisitor[T]") -> T:
return visitor.visit_call_expression(self)
@dataclass
class ReturnStatementNode(ASTNode):
argument: ASTNode | None = None
def accept(self, visitor: "ASTVisitor[T]") -> T:
return visitor.visit_return_statement(self)
# [step] Define the visitor interface
class ASTVisitor(ABC, Generic[T]):
@abstractmethod
def visit_program(self, node: ProgramNode) -> T: ...
@abstractmethod
def visit_function_decl(self, node: FunctionDeclNode) -> T: ...
@abstractmethod
def visit_variable_decl(self, node: VariableDeclNode) -> T: ...
@abstractmethod
def visit_call_expression(self, node: CallExpressionNode) -> T: ...
@abstractmethod
def visit_return_statement(self, node: ReturnStatementNode) -> T: ...
# [step] Implement a MetricsVisitor for code analysis
@dataclass
class CodeMetrics:
function_count: int = 0
async_function_count: int = 0
variable_count: int = 0
var_usage_count: int = 0
call_count: int = 0
return_count: int = 0
class MetricsVisitor(ASTVisitor[None]):
def __init__(self) -> None:
self.metrics = CodeMetrics()
def visit_program(self, node: ProgramNode) -> None:
for child in node.body:
child.accept(self)
def visit_function_decl(self, node: FunctionDeclNode) -> None:
self.metrics.function_count += 1
if node.is_async:
self.metrics.async_function_count += 1
for child in node.body:
child.accept(self)
def visit_variable_decl(self, node: VariableDeclNode) -> None:
self.metrics.variable_count += 1
if node.kind == "var":
self.metrics.var_usage_count += 1
if node.init:
node.init.accept(self)
def visit_call_expression(self, node: CallExpressionNode) -> None:
self.metrics.call_count += 1
for arg in node.args:
arg.accept(self)
def visit_return_statement(self, node: ReturnStatementNode) -> None:
self.metrics.return_count += 1
if node.argument:
node.argument.accept(self)
# [step] Implement a PrettyPrintVisitor for code formatting
class PrettyPrintVisitor(ASTVisitor[str]):
def __init__(self) -> None:
self._indent = 0
def _pad(self) -> str:
return " " * self._indent
def visit_program(self, node: ProgramNode) -> str:
return "\n".join(child.accept(self) for child in node.body)
def visit_function_decl(self, node: FunctionDeclNode) -> str:
prefix = "async def" if node.is_async else "def"
self._indent += 1
body = "\n".join(child.accept(self) for child in node.body)
self._indent -= 1
params = ", ".join(node.params)
return f"{self._pad()}{prefix} {node.name}({params}):\n{body}"
def visit_variable_decl(self, node: VariableDeclNode) -> str:
init = f" = {node.init.accept(self)}" if node.init else ""
return f"{self._pad()}{node.name}{init}"
def visit_call_expression(self, node: CallExpressionNode) -> str:
args = ", ".join(a.accept(self) for a in node.args)
return f"{node.callee}({args})"
def visit_return_statement(self, node: ReturnStatementNode) -> str:
value = f" {node.argument.accept(self)}" if node.argument else ""
return f"{self._pad()}return{value}"Visitor Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Visitor Pattern in the Real World
“Think of a tax auditor visiting different types of businesses—a restaurant, a law firm, a retail shop. The auditor (visitor) knows exactly what to examine at each type of business and applies the appropriate inspection procedure. The businesses (elements) simply let the auditor in; they don't change their own operations to accommodate the audit.”