python-3.x 如何动态修改导入的源代码?

ve7v8dk2  于 2023-02-26  发布在  Python
关注(0)|答案(6)|浏览(186)

假设我有一个如下的模块文件:

# 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)

任何帮助都是受欢迎的,谢谢。

fnvucqvd

fnvucqvd1#

这里有一个基于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"))
ecfsfe2w

ecfsfe2w2#

这并没有回答动态修改导入模块的源代码的一般问题,但是要“Override”或“monkey-patch”,可以使用print()函数(因为它是Python 3.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
wqsoz72f

wqsoz72f3#

我首先需要更好地理解import操作。幸运的是,这在the importlib documentation中解释得很好,通过the source code抓挠也有帮助。
这个import进程实际上分为两部分:首先,finder负责解析模块名(包括点分隔的包)并示例化一个适当的加载器。实际上,内置并不作为本地模块导入。然后,加载器根据查找器返回的内容被调用。此加载器从文件或缓存中获取源代码。并且如果该模块先前未被加载则执行该代码。
这非常简单,这就解释了为什么我实际上不需要使用importutil.abc中的抽象类:我不想提供我自己的导入过程。相反,我可以创建一个继承自importuil.machinery的类之一的子类,并从SourceFileLoader覆盖get_source()。然而,这不是一种可行的方法,因为加载器是由finder示例化的,所以我无法控制使用哪个类。我不能指定应该使用我的子类。
因此,最好的解决方案是让finder完成它的工作,然后替换示例化的Loader的get_source()方法。
不幸的是,通过查看代码源代码,我发现基本的加载器没有使用get_source()(只有inspect模块使用)。所以我的整个想法无法工作。
最后,我猜想应该手动调用get_source(),然后修改返回的源代码,最后执行代码,这是Martin Valgur在his answer中详细介绍的。
如果需要与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
eqzww0vc

eqzww0vc4#

如果可以在打补丁之前导入模块,则可能的解决方案是

import inspect

import my_module

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

如果您在寻找一个更通用的解决方案,那么您还可以看看我刚才在another answer中使用的方法。

yhuiod9q

yhuiod9q5#

我的解决方案更新了源文件,它适用于内部导入的情况。内部导入意味着transformers.models.albert从源文件导入modeling_albert。在这种情况下,即使我使用Martin Valgur的solution,它也不会工作。所以我更新了源文件。希望它能帮助与我有同样问题的人。

import inspect
from transformers.models.albert import modeling_albert

# Get source
source = inspect.getsource(modeling_albert)
source_before = "AlbertModel(config, add_pooling_layer=False)"
source_after = "AlbertModel(config, add_pooling_layer=True)"
new_source = source.replace(source_before, source_after)

# Update file
file_path = modeling_albert.__spec__.origin
with open(file_path, 'w') as f:
    f.write(new_source)
zxlwwiss

zxlwwiss6#

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

with open ('my_module.py') as aFile:
    exec (aFile.read () .replace (<something>, <something else>))

相关问题