2

给定一个内联定义的函数,我如何getsource提供输出?- 这是一个测试,这是我正在尝试的那种事情:

from importlib.util import module_from_spec, spec_from_loader

_locals = module_from_spec(
    spec_from_loader("helper", loader=None, origin="str")  # loader=MemoryInspectLoader
)
exec(
    'def f(): return "foo"',
    _locals.__dict__,
)
f = getattr(_locals, "f")
setattr(f, "__loader__", MemoryInspectLoader)

通过我的尝试,因为它看起来像一个linecache问题:

from importlib.abc import Loader

class MemoryInspectLoader(Loader):
    def get_code(self): raise NotImplementedError()

但是永远不会引发错误。从getsource(f),我得到:

In [2]: import inspect
   ...: inspect.getsource(f)
---------------------------------------------------------------------------
OSError                                   Traceback (most recent call last)
<ipython-input-3-1348c7a45f75> in <module>
----> 1 inspect.getsource(f)

/usr/lib/python3.8/inspect.py in getsource(object)
    983     or code object.  The source code is returned as a single string.  An
    984     OSError is raised if the source code cannot be retrieved."""
--> 985     lines, lnum = getsourcelines(object)
    986     return ''.join(lines)
    987 

/usr/lib/python3.8/inspect.py in getsourcelines(object)
    965     raised if the source code cannot be retrieved."""
    966     object = unwrap(object)
--> 967     lines, lnum = findsource(object)
    968 
    969     if istraceback(object):

/usr/lib/python3.8/inspect.py in findsource(object)
    796         lines = linecache.getlines(file)
    797     if not lines:
--> 798         raise OSError('could not get source code')
    799 
    800     if ismodule(object):

OSError: could not get source code

如何getsource在 Python 3.6+ 中使用内联定义的函数?

4

3 回答 3

2

这是我的解决方案:

import os.path
import sys
import tempfile
from importlib.util import module_from_spec, spec_from_loader
from types import ModuleType
from typing import Any, Callable

class ShowSourceLoader:
    def __init__(self, modname: str, source: str) -> None:
        self.modname = modname
        self.source = source

    def get_source(self, modname: str) -> str:
        if modname != self.modname:
            raise ImportError(modname)
        return self.source


def make_function(s: str) -> Callable[..., Any]:
    filename = tempfile.mktemp(suffix='.py')
    modname = os.path.splitext(os.path.basename(filename))[0]
    assert modname not in sys.modules
    # our loader is a dummy one which just spits out our source
    loader = ShowSourceLoader(modname, s)
    spec = spec_from_loader(modname, loader, origin=filename)
    module = module_from_spec(spec)
    # the code must be compiled so the function's code object has a filename
    code = compile(s, mode='exec', filename=filename)
    exec(code, module.__dict__)
    # inspect.getmodule(...) requires it to be in sys.modules
    sys.modules[modname] = module
    return module.f


import inspect
func = make_function('def f(): print("hi")')
print(inspect.getsource(func))

输出:

$ python3 t.py 
def f(): print("hi")

有一些微妙的,不幸的点:

  1. 它需要注入一些东西sys.modulesinspect.getsource总是在那里寻找inspect.getmodule
  2. __loader__我建造的东西是假的,如果你正在做其他需要正常工作的事情,这__loader__可能会因此而中断
  3. 其他奇怪的东西是内联记录的

顺便说一句,您可能最好以其他方式保留原始来源,而不是通过几个全局变量(sys.modules, linecache,__loader__等)

于 2020-11-22T22:49:44.613 回答
0

不完全确定我是否正确回答了问题。

但是,如果您有以下代码:

class MemoryInspectLoader(Loader):
    def get_code(self): raise NotImplementedError()

您可以使用dill提取函数主体。

from dill.source import getsource

print(getsource(MemoryInspectLoader.get_code))

这将输出:

        def get_code(self): raise NotImplementedError()

这也在这个SO 答案中得到了证明。

于 2020-11-28T10:22:17.497 回答
0

猴子补丁 linecache.getlines 以使 inspect.getsource() 与来自 exec() 的代码一起工作。当您查看错误堆栈时,它会在 inspect.py 中的 findsource() 处停止。当你查看findsource() 的代码时,你会看到一个提示:

# Allow filenames in form of "<something>" to pass through.
# `doctest` monkeypatches `linecache` module to enable
# inspection, so let `linecache.getlines` to be called.

然后,如果您查看此测试功能,您将了解它的含义。您可以临时更改核心 Python 函数之一以满足您的目的。

无论如何,这是解决方案:

import linecache
import inspect

def exec_getsource(code):
    getlines = linecache.getlines
    def monkey_patch(filename, module_globals=None):
        if filename == '<string>':
            return code.splitlines(keepends=True)
        else:
            return getlines(filename, module_globals)
    linecache.getlines = monkey_patch
    
    try:
        exec(code)
        #you can now use inspect.getsource() on the result of exec() here
        
    finally:
        linecache.getlines = getlines
于 2021-10-21T21:24:35.107 回答