【问题标题】:Recursive version of 'reload''reload' 的递归版本
【发布时间】:2013-03-08 13:39:20
【问题描述】:

当我开发 Python 代码时,我通常在解释器中以特别的方式对其进行测试。我先import some_module,测试一下,发现bug,修复bug保存,然后用内置的reload函数到reload(some_module)再测试。

但是,假设在some_module 中我有import some_other_module,并且在测试some_module 时发现some_other_module 中的一个错误并修复它。现在调用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 python-module python-import


【解决方案1】:

我遇到了同样的问题,你启发了我去真正解决这个问题。

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

【讨论】:

  • 其实,现在我正在试验它,我们可能想调用两次reload(module);重新加载子模块之前和之后。我们需要先调用它,以防它引用任何新模块。我们需要在处理from X import Y等语句之后调用它。如果X刚刚被重新加载,我们还需要重新导入Y。我怀疑这可能会更棘手,所以我们需要继续重新加载,直到尘埃落定。
  • 上面的 rreload 实现会导致无限递归(在我的例子中,在 threading 模块内)。考虑一个图 DFS 修改:gist.github.com/shwang/09bd0ffef8ef65af817cf977ce4698b8
  • importlib.reload 将在此合并后工作。 github.com/streamlit/streamlit/pull/537/files/…
  • 你应该把 reload(module) 放在 rreload 所有子模块之后
  • 有时我会保留一个运行我想要的“半脚本”,直到达到某个点,然后使用python -i whatever 运行它。这个,加上breakpoint() 通常可以得到我想要的……
【解决方案2】:

每次修改完模块后,实际编写一些测试用例并运行它们不是更简单吗?

你所做的很酷(你本质上是在使用 TDD(测试驱动开发),但你做错了。

考虑到通过编写单元测试(使用默认的 python unittest 模块,或者更好的是 nose),您可以获得可重用的测试,稳定,与在交互式环境中测试您的模块相比,可帮助您更快更好地检测代码中的不一致性。

【讨论】:

  • +1,听起来这种测试方法已经过时了。它甚至不需要是真实的案例,只需有一个小脚本作为测试代码的便笺簿(如果项目足够小以至于完全测试是多余的)。
  • 讽刺:在测试我正在为另一个应用程序编写的一些单元测试时遇到这个问题后,我被激怒了这个问题。如果我在这里应用您的建议,我最终会无限递归并创建无休止的测试层。 ;)
  • 更严肃地说:正确的测试当然有它们的位置,但有时需要一种更随意的方法(即使它是对正确测试的补充,而不是代替),并且是好的部分Python 是它使这种随意的方法变得容易。也许我已经写了半个函数并且想检查到目前为止它是否输出了我期望的结果,或者我只是将一大堆打印语句放入了一些棘手的代码中并想在一些不正常的参数上运行该函数我指定并查看打印的内容。单元测试不适用于这些场景。
  • 换一种说法:单元测试是确认代码工作的好工具 - 或者用于检测你不希望它们存在的地方的错误 - 只需最少的人力。它们不是用于修改您当前从头开始构建并且已经知道未完成或损坏的代码的好工具,并且在后一种情况下我使用交互式解释器。
【解决方案3】:

我遇到过同样的问题,我已经建立在 @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. 我添加了一个允许重新加载的路径列表(默认为 [''])。有些模块不喜欢以正常方式重新加载,(如图here)。

【讨论】:

    【解决方案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. 我不知道为什么有些 builtin_module_names 前缀带有下划线(例如 collections 被列为 _collections,所以我必须进行双字符串检查。
    2. callable() 为课程返回 True,我想这是意料之中的,但这是我不得不将额外模块列入黑名单的原因之一。

    至少现在我能够在运行时深度重新加载模块,并且从我的测试中,我能够使用 from foo import bar 深入多个级别,并在每次调用 rreload() 时查看结果

    (为冗长而丑陋的深度道歉,但黑色格式的版本在 SO 上看起来不太可读)

    【讨论】:

    • 我遇到了两个问题,我必须创建一个检查来查看属性是否具有 module 属性,并且还要检查 attribute.__module__ 是否在 sys 中.modules。要重新创建它,请安装 websocket-client 并在其上运行 rreload
    【解决方案5】:

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

    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!
    

    【讨论】:

    • 当然,我可以这样做,但是我的实际代码文件会变得丑陋且效率低下。当我在交互式解释器中玩游戏时,我不介意使用愚蠢的 hack,但我不想在我的代码中的每个文件中引入一个愚蠢的 hack,只是为了让我在解释器中变得有点懒惰. :)
    【解决方案6】:

    我发现redsk的答案非常有用。 我提出了一个简化的(针对用户,而不是作为代码)版本,其中自动收集模块的路径,并且递归适用于任意数量的级别。 一切都在一个函数中自包含。 在 Python 3.4 上测试。我猜对于 python 3.3 必须import reload from imp 而不是... from importlib。 它还检查__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__))
    

    【讨论】:

    • 谨慎的-1,因为您没有解释这比redsk的版本有什么优势。你说它是“为用户简化的”,但你的版本和 redsk 都允许调用者只调用rreload(some_module);简化在哪里?也许这里有一些我不欣赏的价值,但如果是这样,你已经很好地隐藏了它。
    • @MarkAmery,从那时起已经过去了很长时间,但我认为这两个“对用户的好处”可以表示为:1.所有需要的导入都嵌入在调用中,并且; 2.如果__init__.py文件丢失会抛出异常。第二,这意味着开发人员被告知并可以解决问题,而不是忽略子模块。当然,将这两种选择合并到一个解决方案中可能是有意义的。
    【解决方案7】:

    对于 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
    

    【讨论】:

    • 如果我没看错,这(按设计)只会重新加载属于m 的子模块的模块。当我问这个问题时,这并不是我想要的。如果m 导入它的兄弟姐妹或表亲模块,我也想导入它们。但也许它会对其他读者有所帮助。
    • 哦,是的,你是对的。我正在使用它来开发搅拌机插件,因此不应重新加载外部依赖项。
    • 对于正确的解决方案,需要在加载的模块之间建立依赖关系树,否则您需要在更新所有内容之前多次重新加载。
    【解决方案8】:

    我找到了清除所有模块然后重新导入您的模块here 的想法,建议这样做:

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

    重新导入您的入口点将重新导入其所有导入,因为模块已被清除并且它是自动递归的。

    【讨论】:

      【解决方案9】:

      下面是我使用的递归重载函数,包括 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)
      

      【讨论】:

        【解决方案10】:

        这是一件棘手的事情 - 我在这个答案中有一个工作示例: how to find list of modules which depend upon a specific module in python

        【讨论】:

        • 尚未阅读并尝试理解代码,但我确实尝试将定义粘贴到解释器中并在我的模块上调用 reload_dependences:Traceback (most recent call last): File "&lt;stdin&gt;", line 1, in &lt;module&gt; File "&lt;stdin&gt;", line 9, in reload_dependences ImportError: Cannot re-init internal module __main__
        • 嗯 - 我认为错误消息是它试图重新加载“main”模块本身 - 在这种情况下,您的解释器会话。尝试将该代码粘贴到您导入 itnerpreter IIRC 的单独文件中,这就是我开发它的方式。
        • 试过了。同样的错误。顺便说一句,抱歉,呃,回复有点慢。
        • 您只是没有在 6 年后提出反对答案,是吗? :-) 顺便说一句,自动重新加载在这个时代的大型 Python 项目中很常见——你应该检查 Flask 和 Django 使用的“调​​试模式”。
        猜你喜欢
        • 2014-07-21
        • 1970-01-01
        • 1970-01-01
        • 2021-11-29
        • 1970-01-01
        • 1970-01-01
        • 2014-09-13
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多