reload' 的递归版本

39 投票
11 回答
12993 浏览
提问于 2025-04-17 19:39

在我写Python代码的时候,通常会在解释器里随便测试一下。我会先用import some_module导入模块,然后测试一下,发现bug后修复它并保存,然后用内置的reload函数来reload(some_module)再测试一次。

不过,假设在some_module里面我还导入了some_other_module,当我在测试some_module的时候发现some_other_module里也有bug并修复了它。现在调用reload(some_module)并不会自动重新导入some_other_module。我必须手动重新导入这个依赖,比如用reload(some_module.some_other_module),或者import some_other_module; reload(some_other_module)。如果我改了很多依赖,搞不清楚哪些需要重新加载,那我就得重启整个解释器。

更方便的做法是如果有一个recursive_reload函数,我只需要调用recursive_reload(some_module),这样Python不仅会重新加载some_module,还会递归地重新加载some_module所导入的每一个模块(以及那些模块所导入的每一个模块,依此类推),这样我就能确保不会使用到任何旧版本的依赖模块。

我觉得Python里没有内置的功能能像我描述的recursive_reload那样工作,但有没有简单的方法可以实现这样的功能呢?

11 个回答

4

这段代码在以 import another_module 方式导入依赖模块时运行得很好,但当以 from another_module import some_func 导入模块中的函数时就失败了。

我在 @redsk 的回答基础上进行了扩展,试图更聪明地处理这些函数。同时,我还添加了一个黑名单,因为不幸的是,typingimportlib 并没有出现在 sys.builtin_module_names 中(可能还有其他模块也没有)。我还想防止一些我已知的依赖被重新加载。

我还跟踪了重新加载的模块名称,并将它们返回。

在 Windows 上使用 Python 3.7.4 测试过:

def rreload(module, paths=None, mdict=None, base_module=None, blacklist=None, reloaded_modules=None):
    """Recursively reload modules."""
    if paths is None:
        paths = [""]
    if mdict is None:
        mdict = {}
    if module not in mdict:
        # modules reloaded from this module
        mdict[module] = []
    if base_module is None:
        base_module = module
    if blacklist is None:
        blacklist = ["importlib", "typing"]
    if reloaded_modules is None:
        reloaded_modules = []
    reload(module)
    reloaded_modules.append(module.__name__)
    for attribute_name in dir(module):
        attribute = getattr(module, attribute_name)
        if type(attribute) is ModuleType and attribute.__name__ not in blacklist:
            if attribute not in mdict[module]:
                if attribute.__name__ not in sys.builtin_module_names:
                    if os.path.dirname(attribute.__file__) in paths:
                        mdict[module].append(attribute)
                        reloaded_modules = rreload(attribute, paths, mdict, base_module, blacklist, reloaded_modules)
        elif callable(attribute) and attribute.__module__ not in blacklist:
            if attribute.__module__ not in sys.builtin_module_names and f"_{attribute.__module__}" not in sys.builtin_module_names:
                if sys.modules[attribute.__module__] != base_module:
                    if sys.modules[attribute.__module__] not in mdict:
                        mdict[sys.modules[attribute.__module__]] = [attribute]
                        reloaded_modules = rreload(sys.modules[attribute.__module__], paths, mdict, base_module, blacklist, reloaded_modules)
    reload(module)
    return reloaded_modules

一些备注:

  1. 我不知道为什么有些内置模块名称前面会有一个下划线(比如 collections 被列为 _collections,所以我必须进行双重字符串检查。)
  2. callable() 对于类返回 True,我想这是正常的,但这也是我需要将额外模块加入黑名单的原因之一。

至少现在我能够在运行时深度重新加载一个模块,并且通过我的测试,我可以使用 from foo import bar 多次深入,并在每次调用 rreload() 时看到结果。

(抱歉内容有点长且不太美观,但黑色格式的版本在 SO 上看起来不太易读。)

7

我遇到了同样的问题,并在@Mattew和@osa的回答基础上做了一些改进。

from types import ModuleType
import os, sys
def rreload(module, paths=None, mdict=None):
    """Recursively reload modules."""
    if paths is None:
        paths = ['']
    if mdict is None:
        mdict = {}
    if module not in mdict:
        # modules reloaded from this module
        mdict[module] = [] 
    reload(module)
    for attribute_name in dir(module):
        attribute = getattr(module, attribute_name)
        if type(attribute) is ModuleType:
            if attribute not in mdict[module]:
                if attribute.__name__ not in sys.builtin_module_names:
                    if os.path.dirname(attribute.__file__) in paths:
                        mdict[module].append(attribute)
                        rreload(attribute, paths, mdict)
    reload(module)
    #return mdict

这里有三个不同之处:

  1. 一般情况下,reload(module) 也需要在函数的最后调用,正如 @osa 指出的那样。
  2. 如果有循环导入的情况,之前的代码会一直循环下去,所以我添加了一个字典来记录其他模块加载的模块集合。虽然循环依赖不是个好事,但Python是允许的,所以这个重载函数也能处理这种情况。
  3. 我添加了一些路径的列表(默认是 ['']),这些路径是允许重载的。有些模块不喜欢以正常方式被重载(具体情况可以在这里查看)。
40

我也遇到过同样的问题,你让我有了动力去解决它。

from types import ModuleType

try:
    from importlib import reload  # Python 3.4+
except ImportError:
    # Needed for Python 3.0-3.3; harmless in Python 2.7 where imp.reload is just an
    # alias for the builtin reload.
    from imp import reload

def rreload(module):
    """Recursively reload modules."""
    reload(module)
    for attribute_name in dir(module):
        attribute = getattr(module, attribute_name)
        if type(attribute) is ModuleType:
            rreload(attribute)

或者,如果你在使用IPython的话,可以直接用 dreload,或者在启动时加上 --deep-reload

撰写回答