Card 05/ 09
Visitor in the Wild: Python's ast.NodeVisitor
Subclass it, write visit_Call, and it is called for every function call in a parsed program.
Python's standard library uses the Visitor pattern to walk parsed programs. ast.parse turns source code into an abstract syntax tree, a tree of objects in which each node is one part of the program, such as a call or a loop. ast.NodeVisitor visits every node, and calls the subclass's method for that node's class. This card counts the function calls in a short program with it.
import ast
from collections import Counter
SOURCE = """
names = load("users.csv")
for name in sorted(names):
print(name.title())
print(len(names))
"""
class CallCounter(ast.NodeVisitor):
def __init__(self) -> None:
self.calls: Counter[str] = Counter()
def visit_Call(self, node: ast.Call) -> None:
self.calls[ast.unparse(node.func)] += 1
self.generic_visit(node)
counter = CallCounter()
counter.visit(ast.parse(SOURCE))
for name, count in counter.calls.most_common():
print(f"{name}: {count}")print: 2
load: 1
sorted: 1
name.title: 1
len: 1The program calls print twice and four other functions once each, including the method call name.title. The visitor found them all, including the call nested inside another call, because generic_visit carries on into the node's children; without that line, the name.title() inside print would be missed.
What NodeVisitor shares with the usual form of the pattern is one class per operation, with a method per element class, called once per element. The difference is how the method is found. NodeVisitor has no accept on the node classes: its visit method builds the name visit_ plus the node's class name, and calls that method if the subclass defines it, falling back to generic_visit if not. A visitor that defines only visit_Call therefore ignores every other kind of node without writing a method for each.
Tools that inspect Python source, such as linters, walk the same trees, often with one visitor class per check.
← → move through sections and cards