如何即时修改导入的源代码?

新手上路,请多包涵

假设我有一个这样的模块文件:

 # my_module.py
print("hello")

然后我有一个简单的脚本:

 # my_script.py
import my_module

这将打印 "hello"

假设我想“覆盖” print() 函数,因此它返回 "world" 。我如何以编程方式执行此操作(无需手动修改 my_module.py )?


我的想法是,我需要以某种方式在导入之前或导入时修改 my_module 的源代码。显然,导入后我无法执行此操作,因此使用 unittest.mock 的解决方案是不可能的。

我还以为我可以读取文件 my_module.py ,进行修改,然后加载它。但这很丑陋,因为如果模块位于其他地方,它将无法工作。

我认为好的解决方案是利用 importlib

我阅读了文档并发现了一个非常交叉的方法: get_source(fullname) 。我以为我可以覆盖它:

 def get_source(fullname):
    source = super().get_source(fullname)
    source = source.replace("hello", "world")
    return source

不幸的是,我对所有这些抽象类有点迷茫,我不知道如何正确地执行它。

我徒劳地尝试:

 spec = importlib.util.find_spec("my_module")
spec.loader.get_source = mocked_get_source
module = importlib.util.module_from_spec(spec)

欢迎任何帮助。

原文由 Delgan 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 646
2 个回答

这是基于 这篇精彩演讲 内容的解决方案。它允许在导入指定模块之前对源代码进行任意修改。只要幻灯片没有遗漏任何重要内容,它就应该是相当正确的。这仅适用于 Python 3.5+。

 import importlib
import sys

def modify_and_import(module_name, package, modification_func):
    spec = importlib.util.find_spec(module_name, package)
    source = spec.loader.get_source(module_name)
    new_source = modification_func(source)
    module = importlib.util.module_from_spec(spec)
    codeobj = compile(new_source, module.__spec__.origin, 'exec')
    exec(codeobj, module.__dict__)
    sys.modules[module_name] = module
    return module

所以,使用这个你可以做

my_module = modify_and_import("my_module", None, lambda src: src.replace("hello", "world"))

原文由 Martin Valgur 发布,翻译遵循 CC BY-SA 4.0 许可协议

我首先需要更好地理解 import 操作。幸运的是,这在 importlib 文档 中得到了很好的解释,并且翻阅 源代码 也有帮助。

这个 import 过程实际上分为两部分。首先,一个 查找器 负责解析模块名称(包括点分隔的包)并实例化一个合适的 加载器。实际上,例如,内置模块不会作为本地模块导入。然后,根据查找器返回的内容调用加载程序。此加载器从文件或缓存中获取源代码,如果模块之前未加载过,则执行代码。

这很简单。这解释了为什么我实际上不需要使用来自 importutil.abc 的抽象类:我不想提供我自己的导入过程。相反,我可以创建一个继承自 importuil.machinery 的类之一的子类,并覆盖 get_source() 来自 SourceFileLoader 例如但是,这不是要走的路,因为加载器是由查找器实例化的,所以我不知道使用哪个类。我不能指定应该使用我的子类。

因此,最好的解决方案是让查找器完成它的工作,然后替换已实例化的任何 Loader 的 get_source() 方法。

不幸的是,通过查看代码源,我发现基本加载器没有使用 get_source() (仅由 inspect 模块使用)。所以我的整个想法都行不通。

最后我猜应该是手动调用 get_source() ,然后修改返回源,最后执行代码。这是 Martin Valgur 在 他的回答 中详述的内容。

如果需要与 Python 2 兼容,我认为除了阅读源文件之外别无他法:

 import imp
import sys
import types

module_name = "my_module"

file, pathname, description = imp.find_module(module_name)

with open(pathname) as f:
    source = f.read()

source = source.replace('hello', 'world')

module = types.ModuleType(module_name)
exec(source, module.__dict__)

sys.modules[module_name] = module

原文由 Delgan 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题