Python 如何动态修改导入的源代码?

Python 如何动态修改导入的源代码?,python,python-3.x,import,mocking,python-importlib,Python,Python 3.x,Import,Mocking,Python Importlib,假设我有这样一个模块文件: # my_module.py print("hello") 然后我有一个简单的脚本: # my_script.py import my_module 这将打印“hello” 假设我想“覆盖”函数print(),使其返回“world”。如何以编程方式(无需手动修改my_module.py)执行此操作 我的想法是,在导入模块之前或导入时,我需要以某种方式修改模块的源代码。很明显,我在导入它之后无法执行此操作,因此不可能使用unittest.mock解决方案 我还认为

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

# my_module.py
print("hello")
然后我有一个简单的脚本:

# my_script.py
import my_module
这将打印
“hello”

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


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

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

我认为,好的解决办法是利用

我阅读了文档,发现了一个非常交叉的方法:
get\u 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)

欢迎提供任何帮助。

不优雅,但适合我(可能需要添加路径):

以open('my_module.py')作为文件的
:
exec(aFile.read().replace(,))

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

import inspect

import my_module

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

如果您想要一个更通用的解决方案,那么您也可以看看我刚才使用的方法。

这里有一个基于内容的解决方案。它允许在导入指定模块之前对源代码进行任意修改。只要幻灯片没有遗漏任何重要内容,它应该是合理正确的。这只适用于Python3.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"))

这并不能回答动态修改导入模块的源代码的一般问题,但要“覆盖”或“猴子补丁”,可以使用
print()
函数(因为它是Python3.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

我首先需要更好地理解
import
操作。幸运的是,这一点在中得到了很好的解释,并且也得到了帮助

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

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

因此,最好的解决方案是让finder完成它的工作,然后替换已实例化的任何加载程序的
get\u 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

my_模块
没有定义Python 3.x中的内置函数
print()
。@martineau我不明白你的观点。我使用Python 3,因此在不定义它的情况下使用
print()
是没有问题的。你说你想覆盖
print()
函数,我只是指出它没有在你导入的模块中定义。@martineau我明白了,谢谢,我确实无法正确地“覆盖”print函数,我更愿意说我想对它进行修补。还要注意的是,为
print()
进行修补可能不同于仅仅是一个普通函数,因为它是一个内置函数。我希望避免指定模块路径。此外,正如您所说的
exec()
一点也不优雅,它应该存在一个更好的解决方案。这对我有什么用处?您将如何使用变通方法更改打印值?抱歉,我假设您需要一个通用方法来修补模块的任何部分。再次阅读您的问题,您似乎希望避免先导入模块,在这种情况下,我同意,我的解决方案与此无关。我完全重写了我的答案。这对你有用吗?如果没有,我会删除它。谢谢。这对我没有用处,但可以帮助其他人(在导入之前不会有模仿的问题),因此请不要删除您的答案。;)谢谢你抽出时间来帮助我!您的解决方案可能是最好的方法。对于Python 3(我也认为是Python 2),您需要了解
包=None
=None
部分。否则,您将得到
SyntaxError:non-default argument跟在default argument后面
,youtube上还有一段David Beazley的演示视频。
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