8.21 实现访问者模式

问题

你要处理由大量不同类型的对象组成的复杂数据结构,每一个对象都需要需要进行不同的处理。比如,遍历一个树形结构,然后根据每个节点的相应状态执行不同的操作。

解决方案

这里遇到的问题在编程领域中是很普遍的,有时候会构建一个由大量不同对象组成的数据结构。假设你要写一个表示数学表达式的程序,那么你可能需要定义如下的类:

  1. class Node:
  2. pass
  3.  
  4. class UnaryOperator(Node):
  5. def __init__(self, operand):
  6. self.operand = operand
  7.  
  8. class BinaryOperator(Node):
  9. def __init__(self, left, right):
  10. self.left = left
  11. self.right = right
  12.  
  13. class Add(BinaryOperator):
  14. pass
  15.  
  16. class Sub(BinaryOperator):
  17. pass
  18.  
  19. class Mul(BinaryOperator):
  20. pass
  21.  
  22. class Div(BinaryOperator):
  23. pass
  24.  
  25. class Negate(UnaryOperator):
  26. pass
  27.  
  28. class Number(Node):
  29. def __init__(self, value):
  30. self.value = value

然后利用这些类构建嵌套数据结构,如下所示:

  1. # Representation of 1 + 2 * (3 - 4) / 5
  2. t1 = Sub(Number(3), Number(4))
  3. t2 = Mul(Number(2), t1)
  4. t3 = Div(t2, Number(5))
  5. t4 = Add(Number(1), t3)

这样做的问题是对于每个表达式,每次都要重新定义一遍,有没有一种更通用的方式让它支持所有的数字和操作符呢。这里我们使用访问者模式可以达到这样的目的:

  1. class NodeVisitor:
  2. def visit(self, node):
  3. methname = 'visit_' + type(node).__name__
  4. meth = getattr(self, methname, None)
  5. if meth is None:
  6. meth = self.generic_visit
  7. return meth(node)
  8.  
  9. def generic_visit(self, node):
  10. raise RuntimeError('No {} method'.format('visit_' + type(node).__name__))

为了使用这个类,可以定义一个类继承它并且实现各种 visit_Name() 方法,其中Name是node类型。例如,如果你想求表达式的值,可以这样写:

  1. class Evaluator(NodeVisitor):
  2. def visit_Number(self, node):
  3. return node.value
  4.  
  5. def visit_Add(self, node):
  6. return self.visit(node.left) + self.visit(node.right)
  7.  
  8. def visit_Sub(self, node):
  9. return self.visit(node.left) - self.visit(node.right)
  10.  
  11. def visit_Mul(self, node):
  12. return self.visit(node.left) * self.visit(node.right)
  13.  
  14. def visit_Div(self, node):
  15. return self.visit(node.left) / self.visit(node.right)
  16.  
  17. def visit_Negate(self, node):
  18. return -node.operand

使用示例:

  1. >>> e = Evaluator()
  2. >>> e.visit(t4)
  3. 0.6
  4. >>>

作为一个不同的例子,下面定义一个类在一个栈上面将一个表达式转换成多个操作序列:

  1. class StackCode(NodeVisitor):
  2. def generate_code(self, node):
  3. self.instructions = []
  4. self.visit(node)
  5. return self.instructions
  6.  
  7. def visit_Number(self, node):
  8. self.instructions.append(('PUSH', node.value))
  9.  
  10. def binop(self, node, instruction):
  11. self.visit(node.left)
  12. self.visit(node.right)
  13. self.instructions.append((instruction,))
  14.  
  15. def visit_Add(self, node):
  16. self.binop(node, 'ADD')
  17.  
  18. def visit_Sub(self, node):
  19. self.binop(node, 'SUB')
  20.  
  21. def visit_Mul(self, node):
  22. self.binop(node, 'MUL')
  23.  
  24. def visit_Div(self, node):
  25. self.binop(node, 'DIV')
  26.  
  27. def unaryop(self, node, instruction):
  28. self.visit(node.operand)
  29. self.instructions.append((instruction,))
  30.  
  31. def visit_Negate(self, node):
  32. self.unaryop(node, 'NEG')

使用示例:

  1. >>> s = StackCode()
  2. >>> s.generate_code(t4)
  3. [('PUSH', 1), ('PUSH', 2), ('PUSH', 3), ('PUSH', 4), ('SUB',),
  4. ('MUL',), ('PUSH', 5), ('DIV',), ('ADD',)]
  5. >>>

讨论

刚开始的时候你可能会写大量的if/else语句来实现,这里访问者模式的好处就是通过 getattr() 来获取相应的方法,并利用递归来遍历所有的节点:

  1. def binop(self, node, instruction):
  2. self.visit(node.left)
  3. self.visit(node.right)
  4. self.instructions.append((instruction,))

还有一点需要指出的是,这种技术也是实现其他语言中switch或case语句的方式。比如,如果你正在写一个HTTP框架,你可能会写这样一个请求分发的控制器:

  1. class HTTPHandler:
  2. def handle(self, request):
  3. methname = 'do_' + request.request_method
  4. getattr(self, methname)(request)
  5. def do_GET(self, request):
  6. pass
  7. def do_POST(self, request):
  8. pass
  9. def do_HEAD(self, request):
  10. pass

访问者模式一个缺点就是它严重依赖递归,如果数据结构嵌套层次太深可能会有问题,有时候会超过Python的递归深度限制(参考 sys.getrecursionlimit() )。

可以参照8.22小节,利用生成器或迭代器来实现非递归遍历算法。

在跟解析和编译相关的编程中使用访问者模式是非常常见的。Python本身的 ast 模块值的关注下,可以去看看源码。9.24小节演示了一个利用 ast 模块来处理Python源代码的例子。

原文:

http://python3-cookbook.readthedocs.io/zh_CN/latest/c08/p21_implementing_visitor_pattern.html