替换 Python 的解析器功能?

Replacing Python's parser functionality?

首先,我想说的是,我知道这是一个糟糕的想法,不应该这样做。我的目的主要是好奇和学习 Python 的内部结构,以及如何 'hack' 它们。

我想知道是否有可能改变我们使用 [] 创建列表时发生的事情。有没有办法修改解析器的行为方式,例如,使 ["hello world"] 调用 print("hello world") 而不是创建包含一个元素的列表?

我试图找到关于此的任何文档或帖子,但没有找到。

下面是替换内置字典以使用自定义字典的示例 class:

from __future__ import annotations
from typing import List, Any
import builtins


class Dict(dict):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__dict__ = self

    def subset(self, keys: List[Any]) -> Dict:
        return Dict({key: self[key] for key in keys})


builtins.dict = Dict

导入此模块时,它会将内置的 dict 替换为 Dict class。然而,这仅在我们直接调用 dict() 时有效。如果我们尝试使用 {} 它将回退到基础 dict 内置实现:

import new_dict

a = dict({'a': 5, 'b': 8})
b = {'a': 5, 'b': 8}

print(type(a))
print(type(b))

产量:

<class 'py_extensions.new_dict.Dict'>
<class 'dict'>

[]{} 被编译为特定的操作码,分别是 return 一个 list 或一个 dict。另一方面,list()dict() 编译为字节码,在全局变量中搜索 listdict,然后将它们作为函数调用:

import dis

dis.dis(lambda:[])
dis.dis(lambda:{})
dis.dis(lambda:list())
dis.dis(lambda:dict())

returns(为清楚起见,有一些额外的换行符):

  3           0 BUILD_LIST               0
              2 RETURN_VALUE

  5           0 BUILD_MAP                0
              2 RETURN_VALUE

  7           0 LOAD_GLOBAL              0 (list)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE

  9           0 LOAD_GLOBAL              0 (dict)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE

因此,您可以通过覆盖全局 dict 来覆盖 dict() return 的内容,但不能覆盖 {} return 的内容.

这些操作码已记录 here. If the BUILD_MAP opcode runs, you get a dict, no way around it. As an example, here is the implementation of BUILD_MAP in CPython, which calls the function _PyDict_FromItems。它不查看任何类型的 user-defined classes,它专门创建一个 C 结构来表示 python dict.

至少在某些情况下可以在运行时操纵 python 字节码。如果您真的想要将{}return设为自定义class,我想您可以编写一些代码来搜索BUILD_MAP操作码并将其替换为适当的操作码。尽管这些操作码的大小不同,因此您可能还需要进行一些额外的更改。

ast 模块是 Python 的抽象语法树的接口,它是在解析 Python 代码后构建的。
通过修改 Python 代码的抽象语法树,可以用 dict 调用替换文字 dict ({})。

import ast
import new_dict

a = dict({"a": 5, "b": 8})
b = {"a": 5, "b": 8}

print(type(a))
print(type(b))
print(type({"a": 5, "b": 8}))

src = """

a = dict({"a": 5, "b": 8})
b = {"a": 5, "b": 8}

print(type(a))
print(type(b))
print(type({"a": 5, "b": 8}))

"""

class RewriteDict(ast.NodeTransformer):
    def visit_Dict(self, node):
        # don't replace `dict({"a": 1})`
        if isinstance(node.parent, ast.Call) and node.parent.func.id == "dict":
            return node
        # replace `{"a": 1} with `dict({"a": 1})
        new_node = ast.Call(
            func=ast.Name(id="dict", ctx=ast.Load()),
            args=[node],
            keywords=[],
            type_comment=None,
        )
        return ast.fix_missing_locations(new_node)


tree = ast.parse(src)

# set parent to every node
for node in ast.walk(tree):
    for child in ast.iter_child_nodes(node):
        child.parent = node

RewriteDict().visit(tree)
exec(compile(tree, "ast", "exec"))

输出;

<class 'new_dict.Dict'>
<class 'dict'>
<class 'dict'>
<class 'new_dict.Dict'>
<class 'new_dict.Dict'>
<class 'new_dict.Dict'>