【问题标题】:depth first traversal of a tree using generators in python在python中使用生成器深度优先遍历树
【发布时间】:2017-08-20 13:02:53
【问题描述】:

目前,我正在为 Python 中的一小部分 Python 编写编译器。我已经设法构建了一个语法树,但是在编写树遍历时遇到了一些问题(这对于生成代码至关重要)。因此,我将首先向您展示我的数据结构:

class AbstractSyntaxTree(object):
    def __init__(self, startSymbol):
        self.root = Node(startSymbol, val=None)
        self.nodes = [self.root]

    def addNode(self, name, val, parentId):
        parent = self.nodes[parentId]
        self.nodes.append(Node(name=name, val=val, parent=parent))
        return len(self.nodes)-1

    def getLastId(self):
        return len(self.nodes)-1

    def __iter__(self):
        for node in self.root:
            yield node

这是我的节点定义:

class Node:
    def __init__(self, name, val, parent=None):
        self.name = name
        self.val = val
        self.parent = parent
        self.children = []

        if parent:
            parent.children.append(self)

    def __iter__(self):
        yield self
        for child in self.children:
            for node in child:
                yield node

我的解析器是一个递归下降解析器,其中每个语法符号都是一个调用其他语法符号的函数。 program 是我的开始符号。

def program(self, indentLvl=0):
    parent = self.synTree.getLastId()
    if self.smellAndConsume(TOK.EOF, parentId=parent): return
    self.smellAndConsume(TOK.NEWL, parentId=parent)
    self.synTree.addNode(name=VAR.statement, val=None, parentId=parent)
    self.statement()
    while self.accept and not self.isConsumable(TOK.EOF):
        self.consume(TOK.NEWL, parentId=parent)
        self.synTree.addNode(name=VAR.statement, val=None, parentId=parent)
        self.statement()
    self.consume(TOK.EOF, parentId=parent)

现在很好奇,如果在成功解析后,我能够通过首先使用在NodeAbstractSyntaxTree 中定义的__iter__ 生成器迭代深度来打印语法树中的所有节点。但是

def test_tree_traversal():
    for node in miniPyGrammar.synTree:
        print(node)

不打印所有节点!当我调试我的代码时,我意识到我的根节点在其子节点列表中没有任何子节点,尽管我使用根节点 ID 调用 addNode。有谁知道这里发生了什么?

如果您需要更多信息或更多代码 sn-ps,请随时询问。

编辑:我刚刚找到了解决方案(尽管我仍然觉得这里发生的事情很奇怪。)此代码现在按预期运行:

def test_tree_traversal(code):
    grammar = Grammar()
    grammar.parse(code)
    for node in grammar.synTree:
        print(node)

def execute_tests():
    for name, code in programs.items():
        parse_test(name, code)
        test_tree_traversal(code)

在我有一个全局语法对象之前,execute_tests 会对该语法调用 parse,之后我运行 test_tree_traversal,它访问语法对象 synTree。奇怪的是,在调用之间,垃圾收集删除了 AST 中的一些节点。为什么我认为这是垃圾收集?因为行为是不确定的。

编辑:这是容易出错的代码: 注意唯一的区别是我在执行测试之前实例化了一个新的语法对象。 Grammar 有一个 'parse' 方法,如果程序在语法上正确,则返回 true,并构造一个可以通过 Grammar.synTree 访问的 AST。

miniPyGrammar = Grammar()

def parse_test(
    programName: str,
    programCode: str):
    success = miniPyGrammar.parse(programCode)
    if success:
        print('{} is a valid miniPyProgram :)'.format(programName))
    else:
        print('{} is not a valid miniPyProgram'.format(programName))
    print(miniPyGrammar.synTree)

def tree_traversal(code):
    for node in miniPyGrammar.synTree:
        print(node)

def execute_tests():
    for name, code in programs.items():
        parse_test(name, code)
        tree_traversal(code)

if __name__ == '__main__':
    execute_tests()

【问题讨论】:

  • 我不认为你会得到关于你的代码的非工作版本的答案,因为你没有充分描述它。我们无法对看不到的代码进行故障排除!在for node in miniPyGrammar.synTree: 中,miniPyGrammar 是什么,您为什么希望它保存您的语法树?您确定您的解析器的任何方法中都没有任何破坏树的错误吗?你只向我们展示了一种方法,我不明白它在做什么,因为它正在调用你没有展示的方法。尝试创建一个 minimal reproducible example 来证明您的问题。
  • 是的,这是真的,但是因为我不知道错误来自哪里,所以我不知道要显示我的代码的哪些部分。我认为问题可能来自错误的迭代器代码。我将创建另一个编辑以指出问题出在哪里。

标签: python compiler-construction generator abstract-syntax-tree depth-first-search


【解决方案1】:

与其尝试迭代你的树,我建议使用Visitor pattern。这种方法可以让您轻松模块化抽象语法树遍历。

请注意,使用这种方法时,您必须为树中的每个节点类型创建特定的类。例如,您可以为操作员节点设置Operator 类,为函数调用节点设置FunctionCall 类等。

这是一个非常简单的 AST 访问者模式示例,可以帮助您入门。 AST 由Operator 节点(用于操作员)和Number 节点(用于数字)组成:

class Node:
    pass


class Operator(Node):
    def __init__(self, op, left, right):
        self.op = op
        self.left = left
        self.right = right


class Number(Node):
    def __init__(self, value):
        self.value = value


class AstWalker:
    def visit(self, node):
        name = 'visit_' + node.__class__.__name__
        vistor = getattr(self, name, self.missing)
        vistor(node)

    def missing(self, node):
        name = node.__class__.__name__
        raise Exception('No visitor method for node type: ' + name)

    def visit_Operator(self, node):
        print('operator:', node.op)
        self.visit(node.left)
        self.visit(node.right)

    def visit_Number(self, node):
        print('number:', node.value)


# The ast represents the expression:
#
# (1 * 5) - (3 / 5)
#
ast = Operator(op='-',
        left=Operator(op='*', 
                   left=Number(1), 
                   right=Number(5)
        ),

        right=Operator(op='/', 
                   left=Number(3), 
                   right=Number(5)
        )
)

walker = AstWalker()
walker.visit(ast)

以上代码的输出为:

operator: -
operator: *
number: 1
number: 5
operator: /
number: 3
number: 5

上述代码中有趣的部分是AstWalker 类。正是在这里,我们实现了模式。这里有一个简要说明。

visit 方法是上述代码的核心。这就是魔法发生的地方。长话短说,visit 接受一个论点node。这将是Operator 节点或Number。然后它使用node.__class__.__name__ 获取节点类的名称。正如你所看到的,我在名字后面加上了visit,因为访问者方法对于树中的每个节点 - visit_Operatorvisit_Number 都有访问权限。

最后在self.visit 中,我使用getattr 从类中获取正确的访问者方法。如果节点是 Number getattr 将返回 visit_Number 方法。这同样适用于Operator。然后调用访问者方法并传入node

如果我们发现传入的node 对象不知何故没有访问者方法,我们返回self.missing 并调用它。 self.missing simple 报告我们遇到的哪个节点对象没有访问者方法。

如上所述,每个访问者方法都有一个参数node。当前节点正在访问。在上面的示例中,我只是打印了每个node 的属性。但是可以很容易地对其进行修改以生成字节码。

【讨论】:

  • 感谢四位您的替代建议!但我仍然很好奇为什么我的代码不打印我的所有节点。您对此有什么建议吗?
  • 恐怕不是,@drssdinblck。您确实应该提供的代码没有提供足够的上下文来了解问题所在。但是从您的问题中查看代码示例,您似乎对解析器的确切结果应该是什么感到有些困惑。我推荐阅读this 文章。它有一些很好的例子来展示 AST 的构造。也许会有所帮助。
  • 嗯,我怀疑。我想python垃圾收集可能有一个错误,因为当我调试我的代码时,突然从node.children中删除的节点甚至没有改变任何东西。另外,我的代码似乎是不确定的。有时,我的 AST 打印结果正确,有时却没有。
猜你喜欢
  • 2018-10-22
  • 2019-01-08
  • 2019-05-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多