【问题标题】:How to modify imported source code on-the-fly?如何即时修改导入的源代码?
【发布时间】:2017-06-11 00:29:18
【问题描述】:

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

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

欢迎任何帮助。

【问题讨论】:

  • my_module 没有定义print(),这是 Python 3.x 中的内置函数。
  • @martineau 我不明白你的意思。我使用 Python 3,所以使用 print() 没有定义它没有问题。
  • 你说你想覆盖print()函数,我只是指出它没有在你导入的模块中定义。
  • @martineau 我明白了,谢谢,确实我无法正确“覆盖”打印功能,我宁愿说我想对其进行猴子补丁。
  • 另请注意,为print() 执行此操作可能与普通函数不同,因为它是内置函数。

标签: python python-3.x import mocking python-importlib


【解决方案1】:

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

【讨论】:

  • 感谢您抽出宝贵时间帮助我!您的解决方案可能是最好的方法。
  • 对于 Python 3(我认为 Python 2 也是如此),您需要使用 package=None=None 部分。否则,你会得到SyntaxError: non-default argument follows default argument
  • YouTube 上还有一段 David Beazley 的 Modules and Packages 演示视频。
  • 我已经使用 python 3.6 测试了这个解决方案。 new_source 包含修改模块的代码;但是,返回的模块包含原始代码。关于如何使其工作的任何想法?
【解决方案2】:

这并不能回答动态修改导入模块的源代码的一般问题,但可以“覆盖”或“猴子补丁”使用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

【讨论】:

    【解决方案3】:

    我首先需要更好地理解import 操作。幸运的是,这在 the importlib documentation 中得到了很好的解释,并且通过 the source code 抓挠也有帮助。

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

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

    因此,最好的解决方案是让查找器完成其工作,然后替换任何已实例化的 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
    

    【讨论】:

      【解决方案4】:

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

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

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

      【讨论】:

      • 这对我有什么用?您将如何使用您的解决方法更改打印值?
      • 抱歉,我假设您想要一个通用方法来猴子修补模块的任何部分。再次阅读您的问题,您似乎希望避免先导入模块,在这种情况下,我同意,我的解决方案与此处无关。
      • 我完全重写了我的答案。这对你有用吗?如果没有,我会删除它。
      • 谢谢。这对我没有用,但这可以帮助其他人(在导入之前不会遇到模拟问题),所以请不要删除您的答案。 ;)
      【解决方案5】:

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

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

      【讨论】:

      • 我说我想避免指定模块路径。而且你说exec()一点都不优雅,应该有更好的解决方案。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-07-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-29
      相关资源
      最近更新 更多