Python 3.6中AST解析:isinstance等四种方案该如何选择?
Great question! Let's walk through each of these Python approaches for implementing your AST evaluator, comparing their tradeoffs and giving recommendations based on your use case (a simple expression evaluator with Plus, Negate, and IntConst nodes):
1. Using isinstance() for Type Checking
This is the most direct analog to Haskell's pattern matching, and perfect for small, simple ASTs like yours. You define plain classes for each AST node, then use isinstance() checks in your evaluation function to branch on node type.
class Plus: def __init__(self, left, right): self.left = left self.right = right class Negate: def __init__(self, expr): self.expr = expr class IntConst: def __init__(self, value): self.value = value def ev(node): if isinstance(node, Plus): return ev(node.left) + ev(node.right) elif isinstance(node, Negate): return -ev(node.expr) elif isinstance(node, IntConst): return node.value else: raise ValueError(f"Unknown AST node type: {type(node)}") # Example usage expr = Plus(IntConst(10), Negate(IntConst(4))) print(ev(expr)) # Outputs 6
Pros:
- Simple & familiar: No extra frameworks or dependencies needed, maps directly to your Haskell implementation.
- Low boilerplate: Minimal code to get up and running.
- Easy to debug: Straightforward control flow that's easy to trace.
Cons:
- Scalability issues: As you add more node types, you'll end up with a long chain of
if-elifstatements that's hard to maintain. - No separation of concerns: All evaluation logic lives in one function, which gets messy if you add more operations (like pretty-printing or type checking).
2. Monkey-Patching (or Embedded Methods)
Instead of a standalone ev function, you can embed the evaluation logic directly into each AST node class. Monkey-patching would be used if you couldn't modify the original classes (e.g., they're from an external library), but for your own AST, defining methods directly is cleaner.
class Plus: def __init__(self, left, right): self.left = left self.right = right def ev(self): return self.left.ev() + self.right.ev() class Negate: def __init__(self, expr): self.expr = expr def ev(self): return -self.expr.ev() class IntConst: def __init__(self, value): self.value = value def ev(self): return self.value # Example usage expr = Plus(IntConst(10), Negate(IntConst(4))) print(expr.ev()) # Outputs 6
Pros:
- Encapsulation: Each node type owns its own evaluation logic, which aligns with OOP principles.
- Clean calls: Invoke evaluation directly on the node (
expr.ev()instead ofev(expr)).
Cons:
- Pollutes node classes: If you need multiple operations (e.g.,
ev,pretty_print,type_check), your node classes will become cluttered with unrelated methods. - Inflexible: If you need to add a new operation later, you have to modify every node class.
- Monkey-patching specifically: Modifying external classes at runtime can lead to unexpected behavior and is generally discouraged unless absolutely necessary.
3. Visitor Pattern with visit_NodeType Methods
This is the standard industrial-strength approach for AST processing in Python. You define a visitor class with a visit method that dispatches to type-specific visit_* methods, keeping your AST node classes clean and separating different operations into distinct visitor classes.
class Plus: def __init__(self, left, right): self.left = left self.right = right class Negate: def __init__(self, expr): self.expr = expr class IntConst: def __init__(self, value): self.value = value class EvaluatorVisitor: def visit(self, node): # Dynamically find the correct visit method for the node type method_name = f"visit_{type(node).__name__}" visit_method = getattr(self, method_name, self._visit_unknown) return visit_method(node) def visit_Plus(self, node): return self.visit(node.left) + self.visit(node.right) def visit_Negate(self, node): return -self.visit(node.expr) def visit_IntConst(self, node): return node.value def _visit_unknown(self, node): raise ValueError(f"Unsupported node type: {type(node).__name__}") # Example usage visitor = EvaluatorVisitor() expr = Plus(IntConst(10), Negate(IntConst(4))) print(visitor.visit(expr)) # Outputs 6
Pros:
- Separation of concerns: Each operation (evaluation, printing, type checking) lives in its own visitor class, so your AST nodes stay focused on holding data.
- Extensible: Adding a new operation just requires writing a new visitor class—no changes to existing AST nodes.
- Maintainable: Type-specific logic is grouped into individual methods, making it easy to find and modify.
Cons:
- Slightly more boilerplate: You need to set up the visitor class structure, which is overkill for tiny ASTs but pays off as you scale.
- Indirect dispatch: The dynamic method lookup can be confusing for beginners, though it's a standard pattern once you get used to it.
4. Macros (via Third-Party Libraries)
Python doesn't have native macros, but libraries like macropy let you add pattern-matching syntax that's almost identical to Haskell's. This lets you write evaluation logic in a functional style without isinstance() checks.
from macropy.match import macros, match class Plus: def __init__(self, left, right): self.left = left self.right = right class Negate: def __init__(self, expr): self.expr = expr class IntConst: def __init__(self, value): self.value = value def ev(node): match node: case Plus(a, b): return ev(a) + ev(b) case Negate(x): return -ev(x) case IntConst(i): return i case _: raise ValueError("Unknown node type") # Example usage expr = Plus(IntConst(10), Negate(IntConst(4))) print(ev(expr)) # Outputs 6
Pros:
- Idiomatic functional syntax: If you're coming from Haskell, this feels natural and clean.
- Concise: No messy
if-elifchains or visitor boilerplate.
Cons:
- Third-party dependency: You need to install and configure
macropy, which adds overhead to your project. - Non-standard syntax: Macros modify Python's syntax, which can confuse developers unfamiliar with the library.
- Debugging complexity: Macro-expanded code can be harder to debug than plain Python.
Recommendation for Your Use Case
Given that you're building a small interpreter and currently focusing only on AST evaluation logic:
- Start with
isinstance(): It's simple, maps directly to your Haskell code, and requires no extra dependencies. It's perfect for your current scope. - Switch to the Visitor Pattern if you scale: If you later add more operations (like pretty-printing, optimization, or type checking) or more node types, the Visitor Pattern will keep your codebase clean and maintainable.
- Avoid monkey-patching: It's unnecessary for your own AST classes and leads to messy code as you add more functionality.
- Use macros only if you prioritize functional syntax: Unless you're deeply attached to Haskell-style pattern matching and don't mind adding a dependency, stick to native Python approaches.
内容的提问来源于stack exchange,提问作者Andrey Tyukin

