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

Del*_*gan 8 python import mocking python-3.x python-importlib

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

# my_module.py
print("hello")
Run Code Online (Sandbox Code Playgroud)

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

# my_script.py
import my_module
Run Code Online (Sandbox Code Playgroud)

这将打印"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
Run Code Online (Sandbox Code Playgroud)

不幸的是,我对所有这些抽象类有点迷失,我不知道如何正确执行此操作.

我徒劳地试过:

spec = importlib.util.find_spec("my_module")
spec.loader.get_source = mocked_get_source
module = importlib.util.module_from_spec(spec)
Run Code Online (Sandbox Code Playgroud)

欢迎任何帮助.

Mar*_*gur 13

这是基于这个精彩演讲的内容的解决方案。它允许在导入指定模块之前对源进行任意修改。只要幻灯片没有遗漏任何重要内容,它就应该是合理正确的。这仅适用于 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
Run Code Online (Sandbox Code Playgroud)

所以,使用这个你可以做

my_module = modify_and_import("my_module", None, lambda src: src.replace("hello", "world"))
Run Code Online (Sandbox Code Playgroud)

  • 我已经使用 python 3.6 测试了这个解决方案。`new_source` 包含修改后的模块的代码;但是,返回的模块包含原始代码。关于如何让它发挥作用有什么想法吗? (2认同)

mar*_*eau 5

这不能回答动态修改导入模块的源代码的一般问题,但是可以“覆盖”或“猴子补丁”来使用该print()函数(因为它是 Python 3.x 中的内置函数)。 X)。就是这样:

#!/usr/bin/env python3
# my_script.py

import builtins

_print = builtins.print

def my_print(*args, **kwargs):
    _print('In my_print: ', end='')
    return _print(*args, **kwargs)

builtins.print = my_print

import my_module  # -> In my_print: hello
Run Code Online (Sandbox Code Playgroud)


Del*_*gan 5

我首先需要更好地了解import操作。幸运的是,文档importlib对此进行了很好的解释,并且浏览源代码也有所帮助。

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

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

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

不幸的是,通过查看代码源,我发现基本的加载器没有使用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
Run Code Online (Sandbox Code Playgroud)