如何即时修改导入的源代码?
How to modify imported source code on-the-fly?
假设我有这样一个模块文件:
# my_module.py
print("hello")
那我有一个简单的脚本:
# my_script.py
import my_module
这将打印 "hello"
。
假设我想 "override" print()
函数,所以它 returns "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)
欢迎任何帮助。
不优雅,但适合我(可能需要添加路径):
with open ('my_module.py') as aFile:
exec (aFile.read () .replace (<something>, <something else>))
如果在打补丁之前导入模块没问题,那么可能的解决方案是
import inspect
import my_module
source = inspect.getsource(my_module)
new_source = source.replace('"hello"', '"world"')
exec(new_source, my_module.__dict__)
如果您寻求更通用的解决方案,那么您也可以看看我刚才在 中使用的方法。
这里有一个基于this great talk内容的解决方案。它允许在导入指定模块之前对源代码进行任意修改。只要幻灯片没有遗漏任何重要内容,它就应该是相当正确的。这仅适用于 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"))
这并没有回答动态修改导入模块源代码的一般问题,但是"Override"或"monkey-patch"可以使用print()
函数来完成(因为它是 Python3.x 中的 built-in 函数)。方法如下:
#!/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
操作。幸运的是,这在 the importlib
documentation and scratching through the source code helped too.
中得到了很好的解释。
这个import
过程实际上分为两部分。首先,一个finder is in charge of parsing the module name (including dot-separated packages) and instantiating an appropriate loader。实际上,例如 built-in 不会作为本地模块导入。然后,根据查找器返回的内容调用加载程序。此加载器从文件或缓存中获取源代码,如果模块之前未加载过,则执行代码。
这个很简单。这解释了为什么我实际上不需要使用来自 importutil.abc
的抽象 classes:我不想提供我自己的导入过程。相反,我可以创建一个 subclass 继承自 importuil.machinery
的 classes 之一,并覆盖 SourceFileLoader
的 get_source()
。但是,这不是要走的路,因为加载器是由查找器实例化的,所以我没有使用 class 的手。我无法指定应该使用我的 subclass。
因此,最好的解决方案是让查找器完成它的工作,然后替换任何已实例化的 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
假设我有这样一个模块文件:
# my_module.py
print("hello")
那我有一个简单的脚本:
# my_script.py
import my_module
这将打印 "hello"
。
假设我想 "override" print()
函数,所以它 returns "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)
欢迎任何帮助。
不优雅,但适合我(可能需要添加路径):
with open ('my_module.py') as aFile:
exec (aFile.read () .replace (<something>, <something else>))
如果在打补丁之前导入模块没问题,那么可能的解决方案是
import inspect
import my_module
source = inspect.getsource(my_module)
new_source = source.replace('"hello"', '"world"')
exec(new_source, my_module.__dict__)
如果您寻求更通用的解决方案,那么您也可以看看我刚才在
这里有一个基于this great talk内容的解决方案。它允许在导入指定模块之前对源代码进行任意修改。只要幻灯片没有遗漏任何重要内容,它就应该是相当正确的。这仅适用于 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"))
这并没有回答动态修改导入模块源代码的一般问题,但是"Override"或"monkey-patch"可以使用print()
函数来完成(因为它是 Python3.x 中的 built-in 函数)。方法如下:
#!/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
操作。幸运的是,这在 the importlib
documentation and scratching through the source code helped too.
这个import
过程实际上分为两部分。首先,一个finder is in charge of parsing the module name (including dot-separated packages) and instantiating an appropriate loader。实际上,例如 built-in 不会作为本地模块导入。然后,根据查找器返回的内容调用加载程序。此加载器从文件或缓存中获取源代码,如果模块之前未加载过,则执行代码。
这个很简单。这解释了为什么我实际上不需要使用来自 importutil.abc
的抽象 classes:我不想提供我自己的导入过程。相反,我可以创建一个 subclass 继承自 importuil.machinery
的 classes 之一,并覆盖 SourceFileLoader
的 get_source()
。但是,这不是要走的路,因为加载器是由查找器实例化的,所以我没有使用 class 的手。我无法指定应该使用我的 subclass。
因此,最好的解决方案是让查找器完成它的工作,然后替换任何已实例化的 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