17

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

# my_module.py
print("hello")

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

# my_script.py
import my_module

这将打印"hello".

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


我的想法是我需要以某种方式my_module在导入之前或同时修改源代码。Obvisouly,导入它后我无法执行此操作,因此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)

请提供任何帮助。

4

5 回答 5

14

这是基于这个精彩演讲内容的解决方案。它允许在导入指定模块之前对源进行任意修改。只要幻灯片没有遗漏任何重要内容,它就应该是相当正确的。这仅适用于 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"))
于 2017-01-25T23:43:25.657 回答
5

这并不能回答动态修改导入模块的源代码的一般问题,但是可以完成“覆盖”或“猴子补丁”对print()函数的使用(因为它是 Python 3.0 中的内置函数。 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
于 2017-01-26T00:32:45.590 回答
4

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

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

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

所以,最好的解决办法是让查找器完成它的工作,然后替换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
于 2017-01-26T10:06:19.117 回答
3

如果在修补之前导入模块是可以的,那么可能的解决方案是

import inspect

import my_module

source = inspect.getsource(my_module)
new_source = source.replace('"hello"', '"world"')
exec(new_source, my_module.__dict__)

如果您正在寻求更通用的解决方案,那么您还可以查看我不久前在另一个答案中使用的方法。

于 2017-01-25T17:57:42.093 回答
-2

不优雅,但对我有用(可能需要添加路径):

with open ('my_module.py') as aFile:
    exec (aFile.read () .replace (<something>, <something else>))
于 2017-01-25T17:48:16.867 回答