'reload'的递归版本

39
当我开发Python代码时,通常会在解释器中以临时方式进行测试。我会import some_module,测试它,发现错误,修复错误并保存,然后使用内置的reload函数来reload(some_module)并再次测试。
然而,假设在some_module中我有import 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导入的模块(以及每个这些模块导入的模块等等),以便我可以确保没有使用任何旧版本的some_module依赖其他模块。
我认为Python没有内置类似于我在这里描述的recursive_reload函数,但是否有一种简单的方法可以将它们组合在一起?

可能是重复的问题:如何在Python中找到依赖于特定模块的模块列表 - jsbueno
11个回答

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


1
实际上,现在我正在尝试使用它,我们可能需要调用reload(module)两次;在重新加载子模块之前和之后都需要这样做。在重新加载新模块的情况下,我们需要先调用它。我们需要在之后调用它以处理诸如from X import Y之类的语句。如果X刚刚被重新加载,我们仍然需要重新导入Y。我怀疑它可能会更加棘手,因此我们需要不断地重新加载直到事情稳定下来。 - Sergey Orshanskiy
1
上述的rreload实现可能会导致无限递归(在我的情况下,是在“threading”模块中)。考虑使用图形DFS修改:https://gist.github.com/shwang/09bd0ffef8ef65af817cf977ce4698b8 - twink_ml
importlib.reload将在此合并后起作用。https://github.com/streamlit/streamlit/pull/537/files/88d316b8a8eab24fd78a9d32564d799875b39bfe#diff-081b31a3d0f9c7948c3f7c7c973b3f09 - SetupX
1
在重新加载所有子模块后,应该将reload(module)放置在rreload之后。 - kkkobelief24
有时候我会保留一个“半脚本”,运行我想要的内容,直到达到某个特定点,然后使用 python -i whatever 运行它。这样做再加上 breakpoint() 通常可以让我得到想要的结果... - Nathan Chappell

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允许它们,所以这个reload函数也可以处理它们。
  3. 我添加了一个路径列表(默认为['']),从中可以重新加载模块。有些模块不喜欢被正常方式重新加载,(如这里所示)。

对我没用。它不像Matthew的另一个答案一样以无限循环结束,但实际上没有重新加载子模块。 - Jan Pisl

4
代码在只是导入依赖模块时,像这样import another_module运行良好,但当模块使用from another_module import some_func导入函数时,则失败了。我根据@redsk的答案进行了扩展,以尝试聪明地处理这些函数。我还添加了一个黑名单,因为不幸的是,typingimportlib不出现在sys.builtin_module_names中(也许还有其他)。此外,我希望防止重新加载一些我已知的依赖项。我还跟踪重新加载的模块名称并返回它们。已在Python 3.7.4 Windows上测试。
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上看起来并不那么可读)


我在这方面遇到了两个问题,我不得不创建一个检查来查看属性是否具有 module 属性,还要另外一个检查来查看 attribute.module 是否在 sys.modules 中。 要重新创建这个,需要安装 websocket-client 并在其上运行 rreload。 - Miojo_Esperto

3
你是否觉得,写一些测试用例并在修改模块后每次运行它们会更简单呢?
你所做的很酷(本质上是使用TDD(测试驱动开发)),但你做错了。
考虑到编写单元测试(使用默认的Python unittest 模块,或者更好的 nose)可以让你拥有可重用、稳定的测试,并且比在交互式环境中测试模块更快更好地帮助你检测代码不一致性。

+1,听起来这种测试方法已经过时了。甚至不需要是真实的案例,只需有一个小脚本作为测试代码的草稿本(如果项目足够小,完整的测试就过度了)。 - Gareth Latty
5
讽刺的是,正当我在为另一个应用编写单元测试时,遇到了这个问题,因此被激发提出了这个问题。如果我在这里应用你的建议,最终会无限递归并创建无尽的测试层级。 ;) - Mark Amery
4
更加严谨的测试当然有它们的用处,但有时需要采用更加随意的方法(即使这是作为适当测试的补充而不是替代),而Python之所以好用的部分原因就在于它让这种随意的方法变得容易。也许我只编写了一半的函数,想要检查它是否产生我期望的输出,或者我只是将一堆打印语句插入到一些棘手的代码中,并想要对某些特定参数运行该函数并查看打印出的结果。在这些情况下,单元测试并不适用。 - Mark Amery
12
换句话说,单元测试是用最小的人力投入来确认代码是否有效或检测意料之外的错误的好工具。它们不适用于调试你当前正在从头构建且已知未完成或有缺陷的代码,我会在后者的情况下使用交互式解释器。 - Mark Amery

3

我发现一个想法,即清除所有模块,然后重新导入你的模块(点击此处)。这个想法建议只需执行以下操作:

import sys
sys.modules.clear()

如果你只想重载自己的模块,那么这样做会扰乱加载的其他模块。我的建议是只清除包含你自己文件夹的模块。就像这样:

import sys
import importlib

def reload_all():
    delete_folders = ["yourfolder", "yourotherfolder"]

    for module in list(sys.modules.keys()):
        if any(folder in module for folder in delete_folders):
            del sys.modules[module]

    # And then you can reimport the file that you are running.
    importlib.import_module("yourfolder.entrypoint")


重新导入入口点将重新导入其所有导入项,因为模块已被清除,这是自动递归的过程。

2

对于 Python 3.6+,您可以使用:

from types import ModuleType
import sys
import importlib

def deep_reload(m: ModuleType):
    name = m.__name__  # get the name that is used in sys.modules
    name_ext = name + '.'  # support finding sub modules or packages

    def compare(loaded: str):
        return (loaded == name) or loaded.startswith(name_ext)

    all_mods = tuple(sys.modules)  # prevent changing iterable while iterating over it
    sub_mods = filter(compare, all_mods)
    for pkg in sorted(sub_mods, key=lambda item: item.count('.'), reverse=True):
        importlib.reload(sys.modules[pkg])  # reload packages, beginning with the most deeply nested

1
如果我理解正确的话,这个(按设计)只会重新加载m的子模块。那不完全是我在提问时所寻找的;如果m导入了它的同级或堂兄弟模块,我也想要导入它们。但或许对其他读者有帮助。 - Mark Amery
哦,是的,你说得对。我正在使用它来开发一个Blender插件,所以外部依赖项不应该被重新加载。 - machinekoder
为了得到正确的解决方案,需要在加载的模块之间建立依赖树,否则你需要多次重新加载才能更新所有内容。 - machinekoder

1
我发现redsk的答案非常有用。我提出了一个简化版本(对用户而言,不是代码),其中模块路径会自动收集,并且递归可以在任意级别上运行。一切都包含在一个单独的函数中。在Python 3.4上进行了测试。我猜想对于Python 3.3,必须使用from imp import reload而不是from importlib import reload。它还检查__file__文件是否存在,如果编码器忘记在子模块中定义__init__.py文件,则会引发异常。
def rreload(module):
    """
    Recursive reload of the specified module and (recursively) the used ones.
    Mandatory! Every submodule must have an __init__.py file
    Usage:
        import mymodule
        rreload(mymodule)

    :param module: the module to load (the module itself, not a string)
    :return: nothing
    """

    import os.path
    import sys

    def rreload_deep_scan(module, rootpath, mdict=None):
        from types import ModuleType
        from importlib import reload

        if mdict is None:
            mdict = {}

        if module not in mdict:
            # modules reloaded from this module
            mdict[module] = []
        # print("RReloading " + str(module))
        reload(module)
        for attribute_name in dir(module):
            attribute = getattr(module, attribute_name)
            # print ("for attr "+attribute_name)
            if type(attribute) is ModuleType:
                # print ("typeok")
                if attribute not in mdict[module]:
                    # print ("not int mdict")
                    if attribute.__name__ not in sys.builtin_module_names:
                        # print ("not a builtin")
                        # If the submodule is a python file, it will have a __file__ attribute
                        if not hasattr(attribute, '__file__'):
                            raise BaseException("Could not find attribute __file__ for module '"+str(attribute)+"'. Maybe a missing __init__.py file?")

                        attribute_path = os.path.dirname(attribute.__file__)

                        if attribute_path.startswith(rootpath):
                            # print ("in path")
                            mdict[module].append(attribute)
                            rreload_deep_scan(attribute, rootpath, mdict)

    rreload_deep_scan(module, rootpath=os.path.dirname(module.__file__))

谨慎地给你打个负一分,因为你没有解释这个版本相对于redsk的版本有什么优势。你说它是“简化了用户操作”,但是你的版本和redsk的版本都允许调用者只需调用rreload(some_module);哪里简化了呢?也许我没有欣赏到其中的价值,但如果有的话,你隐藏得很好。 - Mark Amery
@MarkAmery,时光荏苒,但我认为这两个“用户优势”可以表述为:
  1. 所有必需的导入都嵌入到调用中;
  2. 如果缺少__init__.py文件,则会抛出异常。对于第二个优势,这意味着开发人员会得到通知并可以解决问题,而不是忽略子模块。
当然,将这两种选择合并为一个解决方案可能是有意义的。
- fnunnari

1

从技术上讲,在每个文件中,您可以放置一个重新加载命令,以确保每次导入时都会重新加载。

a.py:

def testa():
    print 'hi!'

b.py:

import a
reload(a)
def testb():
    a.testa()

现在,交互式地进行:
import b
b.testb()
#hi!

#<modify a.py>

reload(b)
b.testb()
#hello again!

3
好的,我可以做这个,但我的实际代码文件会变得丑陋而低效。当我在交互式解释器中玩耍时,我不介意使用一些愚蠢的技巧,但我不想为了让自己在解释器中更懒惰而在每个代码文件中引入一个愚蠢的技巧。 :) - Mark Amery

0
以下是我使用的递归重新加载函数,包括用于ipython/jupyter的magic函数。
它通过所有子模块进行深度优先搜索,并按依赖关系的正确顺序重新加载它们。
import logging
from importlib import reload, import_module
from types import ModuleType
from IPython.core.magic import register_line_magic

logger = logging.getLogger(__name__)


def reload_recursive(module, reload_external_modules=False):
    """
    Recursively reload a module (in order of dependence).

    Parameters
    ----------
    module : ModuleType or str
        The module to reload.

    reload_external_modules : bool, optional

        Whether to reload all referenced modules, including external ones which
        aren't submodules of ``module``.

    """
    _reload(module, reload_external_modules, set())


@register_line_magic('reload')
def reload_magic(module):
    """
    Reload module on demand.

    Examples
    --------
    >>> %reload my_module
    reloading module: my_module

    """
    reload_recursive(module)


def _reload(module, reload_all, reloaded):
    if isinstance(module, ModuleType):
        module_name = module.__name__
    elif isinstance(module, str):
        module_name, module = module, import_module(module)
    else:
        raise TypeError(
            "'module' must be either a module or str; "
            f"got: {module.__class__.__name__}")

    for attr_name in dir(module):
        attr = getattr(module, attr_name)
        check = (
            # is it a module?
            isinstance(attr, ModuleType)

            # has it already been reloaded?
            and attr.__name__ not in reloaded

            # is it a proper submodule? (or just reload all)
            and (reload_all or attr.__name__.startswith(module_name))
        )
        if check:
            _reload(attr, reload_all, reloaded)

    logger.debug(f"reloading module: {module.__name__}")
    reload(module)
    reloaded.add(module_name)

0
在 @machinekoder 上面的答案基础上,我会将重新加载的模块设置为 sys,并最终从 sys 返回重新加载的模块 m,代码如下:
def deep_reload(m: ModuleType):
    name = m.__name__  # get the name that is used in sys.modules
    name_ext = name.split('.')[0]  # support finding submodules or packages

    def compare(loaded: str):
        return (loaded == name) or loaded.startswith(name_ext)

    all_mods = tuple(sys.modules)  # prevent changing iterable while iterating over it
    sub_mods = filter(compare, all_mods)
    for pkg in sorted(sub_mods, key=lambda item: item.count('.'), reverse=True):
        sys.modules[pkg] = importlib.reload(sys.modules[pkg])
    return sys.modules[name]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接