如何在Python AST中的literal_eval之前用dict替换OrderedDict?

6

我有一个包含Python代码的字符串,如果其中的OrderedDict实例替换为{},我可以使用literal_eval将其作为Python进行评估。

我试图使用ast.parseast.NodeTransformer进行替换,但是当我捕获nodetype == 'Name' and node.id == 'OrderedDict'的节点时,我无法找到节点对象中作为参数的列表,以便我可以用Dict节点替换它。

这是正确的方法吗?

一些代码:

from ast import NodeTransformer, parse

py_str = "[OrderedDict([('a', 1)])]"

class Transformer(NodeTransformer):
    def generic_visit(self, node):
        nodetype = type(node).__name__

        if nodetype == 'Name' and node.id == 'OrderedDict':
            pass # ???

        return NodeTransformer.generic_visit(self, node)

t = Transformer()

tree = parse(py_str)

t.visit(tree)

从那个页面上看,“当询问一个由你的代码引起的问题时”。这不是关于我的代码引起的问题的问题。我认为我的代码不是正确的方法,想要知道在高层次上什么是正确的方法。 - Jim Hunziker
1
并不是我写了“你尝试过什么?”。我认为展示一个 [mcve] 会帮助其他人通过修复你现有的(小)代码来回答。 - Jean-François Fabre
1
我编辑了上面的内容。没有代码。我不知道该写什么代码来解决这个问题!这就是为什么我在问的原因。 - Jim Hunziker
(而且dict()构造函数也不能与literal_eval一起使用) - Jim Hunziker
说实话,这些字符串是我自己创建并保存在某个地方的,但我不是非常有信心未来不会有人不知道 eval 风险而放入一些不安全的内容。 - Jim Hunziker
显示剩余7条评论
3个回答

2

这个想法是要替换所有OrderedDict节点,这些节点表示为具有特定属性的ast.Call(可以从下面的ordered_dict_conditions中看到),用ast.Dict节点来代替,其key/value参数是从ast.Call参数中提取的。

import ast


class Transformer(ast.NodeTransformer):
    def generic_visit(self, node):
        # Need to call super() in any case to visit child nodes of the current one.
        super().generic_visit(node)
        ordered_dict_conditions = (
            isinstance(node, ast.Call)
            and isinstance(node.func, ast.Name)
            and node.func.id == 'OrderedDict'
            and len(node.args) == 1
            and isinstance(node.args[0], ast.List)
        )
        if ordered_dict_conditions:
            return ast.Dict(
                [x.elts[0] for x in node.args[0].elts],
                [x.elts[1] for x in node.args[0].elts]
            )
        return node


def transform_eval(py_str):
    return ast.literal_eval(Transformer().visit(ast.parse(py_str, mode='eval')).body)


print(transform_eval("[OrderedDict([('a', 1)]), {'k': 'v'}]"))  # [{'a': 1}, {'k': 'v'}]
print(transform_eval("OrderedDict([('a', OrderedDict([('b', 1)]))])"))  # {'a': {'b': 1}}

注释

因为我们想要先替换最内层的节点,所以在函数开头调用 super()

每当遇到一个 OrderedDict 节点时,使用以下内容:

  • node.args 是包含 OrderedDict(...) 调用参数的列表。
  • 此调用有一个单一参数,即包含键值对元组的列表,可以通过 node.args[0] (ast.List) 访问,node.args[0].elts 是包装在 list 中的元组。
  • 因此,node.args[0].elts[i] 是不同的 ast.Tuplefor i in range(len(node.args[0].elts))),其元素再次通过 .elts 属性访问。
  • 最后,node.args[0].elts[i].elts[0] 是键,node.args[0].elts[i].elts[1] 是值,这些将在 OrderedDict 调用中使用。

然后使用后面的键和值创建新的 ast.Dict 实例,该实例将用于替换当前节点(即 ast.Call)。


@a_guest:你是对的,我在自己的笔记中也这样说过。这只是第一个可行的方法。我没有深入研究它,但我认为作为dict / OrderedDict参数的元组数组在传递给ast.Dict()之前需要进行转换-后者需要两个列表:键和值。肯定是可能的-另一方面,现在我想起来,做部分评估可能是将输入转换为实际的OrderedDict并保持安全性的一种方式,以及扩展到其他构造函数调用。 - Leo K
是的,你说得对,对于多个键值对,转换需要正确完成,例如ast.Dict([x.elts[0] for x in node.args[0].elts], [x.elts[1] for x in node.args[0].elts])。但是由于子节点已经被解析到树中了(即node.args[0].elts[0].elts[0]已经是一个ast.Str对象),所以没有必要进行任何转换。我没有真正理解你关于部分评估的最后一条评论。在遍历树时,我认为您不应该评估任何内容,而只需替换适当的节点(即保持在ast内;每个eval都会将您带回Python)。 - a_guest
似乎有一些解决方案可以处理我发布的“最小化”测试用例(我不太愿意提供!)。我原以为这会比现在更容易解决!到目前为止,这个案例对任何解决方案都不起作用:“OrderedDict([('a', OrderedDict([('a', 1)]))])”。 - Jim Hunziker
@JimHunziker:我在发布这篇文章后考虑了嵌套情况。它可以很容易地更新以允许这种情况,一旦我测试过它,我会这样做。 - Leo K
2
更新以允许嵌套结构体。这可能成为处理“增强”文字安全解析的可能方式,变得越来越有趣。呼吁@a_guest和其他经验丰富的人编辑或发布改进建议,并将其转化为维基主题。 - Leo K
显示剩余2条评论

0

你可以使用正则表达式来解析和转换表达式,而不是使用 ast。例如:

>>> re.sub(
...     r"OrderedDict\(\[((\(('[a-z]+'), (\d+)\)),?\s*)+\]\)",
...     r'{\3: \4}',
...     "[OrderedDict([('a', 1)])]"
... )
"[{'a': 1}]"

上述表达式基于 OP 的示例字符串,并将单引号字符串视为键,正整数视为值,但当然可以扩展到更复杂的情况。


2
文本级转换绝对是一种替代方案(可能比AST树更好),但是正则表达式无法处理常规情况-它们是有限状态自动机,而FSA无法处理嵌套-需要一个栈自动机。如果我没记错的话,这里有一个库可以做到这一点(text_balanced或类似的东西)。作为一种替代方案可能值得一看。 - Leo K

0
你可以使用ast.NodeVisitor类来观察OrderedDict树,以便从遇到的节点手动构建{}树,使用空字典中解析的节点作为基础。
import ast
from collections import deque


class Builder(ast.NodeVisitor):
    def __init__(self):
        super().__init__()
        self._tree = ast.parse('[{}]')
        self._list_node = self._tree.body[0].value
        self._dict_node = self._list_node.elts[0]
        self._new_item = False

    def visit_Tuple(self, node):
        self._new_item = True
        self.generic_visit(node)

    def visit_Str(self, node):
        if self._new_item:
            self._dict_node.keys.append(node)
        self.generic_visit(node)

    def visit_Num(self, node):
        if self._new_item:
            self._dict_node.values.append(node)
            self._new_item = False
        self.generic_visit(node)

    def literal_eval(self):
        return ast.literal_eval(self._list_node)


builder = Builder()
builder.visit(ast.parse("[OrderedDict([('a', 1)])]"))
print(builder.literal_eval())

请注意,这仅适用于您示例中使用str作为键和int作为值的简单结构。但是,类似的方式可以扩展到更复杂的结构。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接