在Python中有没有办法获取顶级代码的代码对象?

Mar*_*yne 18 python toplevel

是否可以获取模块内顶级代码的代码对象?例如,如果你有一个像这样的 python 文件:

myvar = 1
print('hello from top level')

def myfunction():
    print('hello from function')
Run Code Online (Sandbox Code Playgroud)

并且您想要访问 的代码对象myfunction,那么您可以使用myfunction.__code__. 例如,myfunction.__code__.co_consts将包含字符串'hello from function'等...

有没有办法获取顶级代码的代码对象?也就是说,对于代码:

myvar = 1
print('hello from top level')

def myfunction():
    print('hello from function')
Run Code Online (Sandbox Code Playgroud)

我想要类似的东西__main__.__code__.co_consts包含'hello from top level',但我找不到任何方法来得到这个。这样的事情存在吗?

Mar*_*ski 18

在模块顶层执行的代码不能像函数的代码对象一样作为代码对象直接访问,因为顶层代码在模块导入或运行时立即执行,并且它不像函数那样作为单独的实体存在。

但是当Python运行脚本时,它首先将其编译为字节码并将其存储在代码对象中。顶层代码(__main__模块),有一个代码对象,但它不是直接暴露的,所以需要使用inspect模块来深入挖掘:

import inspect

def get_top_level_code_object():
    frame = inspect.currentframe()

    # Go back to the top-level frame
    while frame.f_back:
        frame = frame.f_back

    # The code object is stored in f_code
    return frame.f_code

if __name__ == "__main__":
    top_level_code_obj = get_top_level_code_object()
    print(top_level_code_obj.co_consts) 
Run Code Online (Sandbox Code Playgroud)

会产生

(0, None, <code object get_top_level_code_object at 0x7f970ad658f0, file "/tmp/test.py", line 3>, '__main__')
Run Code Online (Sandbox Code Playgroud)


wim*_*wim 10

选项1:

您始终可以使用从模块源代码创建代码对象compile。要在模块本身内执行此操作:

myvar = 1
print('hello from top level')

def myfunction():
    print('hello from function')

import inspect, sys
c = compile(inspect.getsource(sys.modules[__name__]), "mymodule", "exec")
print(c)
print(c.co_consts[1].upper())
Run Code Online (Sandbox Code Playgroud)

输出:

hello from top level
<code object <module> at 0xcafef00d, file "mymodule", line 1>
HELLO FROM TOP LEVEL
Run Code Online (Sandbox Code Playgroud)

这可能是您最明智的选择,假设原始源代码仍然存在并且可用于inspect.

选项2:

为了访问模块本身的相同代码对象,而不是重新创建一个新的代码对象,我发现了一种“标签外”方法,即故意在模块内引发异常并对其进行处理:

myvar = 1
print('hello from top level')

def myfunction():
    print('hello from function')

try:
    errorerrorerror
except NameError as e:
    c = e.__traceback__.tb_frame.f_code

print(c)
print(c.co_consts[:2])
Run Code Online (Sandbox Code Playgroud)

输出:

hello from top level
<code object <module> at 0xdeadbeef, file "/path/to/mymodule.py", line 1>
(1, 'hello from top level')
Run Code Online (Sandbox Code Playgroud)

这是一种恶魔般的把戏,如果它在 Python 的其他实现上,甚至在 CPython 的未来版本中出现问题,我不会感到惊讶。但它在某些检查源代码不起作用的其他情况下有效。另请参阅Marcin 的答案,它使用类似的方法inspect.currentframe()

选项 3:

导入系统在创建模块实例时会缓存导入源文件的字节,这样除非必要,否则不需要重新编译。选项 3 涉及从该缓存加载导入模块的字节码。它仅适用于可以导入的模块,即它不适用于脚本(像这样执行的文件__main__python myfile.py,也不适用于缓存的字节码因任何原因不可用的其他几种情况(例如启用了PYTHONDONTWRITEBYTECODE ,Python 运行时使用- B选项,用于放置缓存 .pyc 文件的文件系统是只读的,等等)

在你的文件中:

# mymodule.py
myvar = 1
print('hello from top level')

def myfunction():
    print('hello from function')
Run Code Online (Sandbox Code Playgroud)

从外面”:

>>> import mymodule
hello from top level
>>> import marshal
>>> with open(mymodule.__cached__, 'rb') as f:
...     f.read(16)
...     c = marshal.load(f)
... 
b'\xcb\r\r\n\x00\x00\x00\x00\xe3\xa7|ej\x00\x00\x00'
>>> c.co_consts
(1, 'hello from top level', <code object myfunction at 0xdefeca7e, file "/path/to/mymodule.py", line 5>, None)
Run Code Online (Sandbox Code Playgroud)

该语句import mymodule将编译模块并在 处放置一个字节码缓存/path/to/__pycache__/mymodule.cpython-312.pyc,如果那里尚不存在有效的缓存,或者现有的缓存已过时(即源代码已被修改)。此字节码文件名特定于Python 次要版本和实现,因此如果您使用的不是 CPython 3.12,文件名将会有所不同。如果设置了PYTHONPYCACHEPREFIX,它也可能位于不同的位置。

“丢弃”的 16 个字节f.read(16)是 .pyc 标头:

  • 4字节幻数,表示Python次要版本。
  • 4字节标志,指定字节码失效模式
  • .py如果使用TIMESTAMP失效模式(默认),则为原始文件信息的序列化版本(8 字节 mtime + 8 字节大小),否则为原始文件源代码的 16 字节SipHash 。.py

封送的代码对象位于 .pyc 标头之后。正如演示的那样,它可以轻松地反序列化为代码对象。