如何递归获取 python 包中的所有子模块?

Posted

技术标签:

【中文标题】如何递归获取 python 包中的所有子模块?【英文标题】:How do you recursively get all submodules in a python package? 【发布时间】:2018-07-30 11:11:48 【问题描述】:

问题

我有一个这样的文件夹结构:

- modules
    - root
        - abc
            hello.py
            __init__.py
        - xyz
            hi.py
            __init__.py
          blah.py
          __init__.py
      foo.py
      bar.py
      __init_.py

这里是同样的字符串格式:

"modules",
"modues/__init__.py",
"modules/foo.py",
"modules/bar.py",
"modules/root",
"modules/root/__init__.py",
"modules/root/blah,py",
"modules/root/abc",
"modules/root/abc/__init__.py",
"modules/root/abc/hello.py",
"modules/root/xyz",
"modules/root/xyz/__init__.py",
"modules/root/xyz/hi.py"

我正在尝试以 python 导入样式格式打印出所有模块。 示例输出如下所示:

modules.foo
modules.bar
modules.root.blah
modules.root.abc.hello
modules.root.xyz.hi

我怎样才能在python中轻松做到这一点(如果可能没有第三方库)?

我尝试了什么

示例代码

import pkgutil

import modules

absolute_modules = []


def find_modules(module_path):
    for package in pkgutil.walk_packages(module_path):
        print(package)
        if package.ispkg:
            find_modules([package.name])
        else:
            absolute_modules.append(package.name)


if __name__ == "__main__":
    find_modules(modules.__path__)
    for module in absolute_modules:
        print(module)

但是,此代码只会打印出 'foo' 和 'bar'。但不是“根”,它是子包。我也很难弄清楚如何将其转换为保留它的绝对导入风格。当前代码仅获取包/模块名称,而不是实际的绝对导入。

【问题讨论】:

为什么要问“没有任何第三方库”?您正在重新发明***(请原谅双关语),这已经由 pkg_resourcessetuptools 分发的一部分)实现。 嗯,我想学习如何做到这一点,以便我可以自定义它 好的,但我仍然不明白为什么排除第三方库。 嗯,原因是 IRC 上有人建议使用收集库,该库将@decorator 引入所有想要收集的子模块。这是收集模块名称的糟糕方法。只要模块实际上在标准库中,就应该没问题。如果代码是积极维护的第三方库(在大多数情况下不是),也应该没问题。 【参考方案1】:

这使用setuptools.find_packages(用于包)和pkgutil.iter_modules 用于它们的子模块。也支持 Python2。不需要递归,这两个函数一起使用就可以了。

import sys
from setuptools import find_packages
from pkgutil import iter_modules

def find_modules(path):
    modules = set()
    for pkg in find_packages(path):
        modules.add(pkg)
        pkgpath = path + '/' + pkg.replace('.', '/')
        if sys.version_info.major == 2 or (sys.version_info.major == 3 and sys.version_info.minor < 6):
            for _, name, ispkg in iter_modules([pkgpath]):
                if not ispkg:
                    modules.add(pkg + '.' + name)
        else:
            for info in iter_modules([pkgpath]):
                if not info.ispkg:
                    modules.add(pkg + '.' + info.name)
    return modules

【讨论】:

我没有时间验证这个作品。所以我没有将我的答案标记为正确的版本。但请注意:len(find_abs_modules(xml)) == len(list(find_modules(xml.__path__[0]))) 返回 False 并且还显示 _private 模块。 iter_modules 至少对我来说不像文档中那样起作用,并且 iter_modules 不能递归工作【参考方案2】:

所以我终于想出了如何干净利落地做到这一点,并让 pkgutil 为你处理所有的边缘情况。此代码基于 python 的 help() 函数,该函数仅显示***模块和包。

import importlib
import pkgutil

import sys

import modules


def find_abs_modules(module):
    path_list = []
    spec_list = []
    for importer, modname, ispkg in pkgutil.walk_packages(module.__path__):
        import_path = f"module.__name__.modname"
        if ispkg:
            spec = pkgutil._get_spec(importer, modname)
            importlib._bootstrap._load(spec)
            spec_list.append(spec)
        else:
            path_list.append(import_path)
    for spec in spec_list:
        del sys.modules[spec.name]
    return path_list


if __name__ == "__main__":
    print(sys.modules)
    print(find_abs_modules(modules))
    print(sys.modules)

这甚至适用于内置包。

【讨论】:

嗯......它没有在我的用例中递归扫描,但这帮助我想出了自己的解决方案:我使用pkg=importlib.import_module(import_path),然后递归调用find_abs_modules(pkg)。跨度> @TheDiveO 你能告诉我它不适合什么情况吗?我目前正在开发环境中使用它,并希望修补任何边缘情况。 在应用程序(包)foobar 内部我从包foobar.plugins(在扫描之前导入)开始扫描,并且扫描功能没有找到foobar.plugins.footest 子包(我没有导入然而)。 footest 有一个 __init__.py,但从未找到它,即使在将它导入到 foobar.plugins.__init__ 中之后也是如此。所以我只好依靠importlib.import_module(),因为无论如何我都需要导入找到的插件。 它在 Python 3.5.3 (Deb 9) 上 @TheDiveO 感谢您提供详细信息。我为 3.6.5 以上编写了此代码。所以它可以解释为什么它不适合你的情况(调用私有方法通常是一个坏主意)。我将很快使用相同功能的公共版本进行更新,以便每个人都可以受益。感谢您的帮助!【参考方案3】:

以下代码将为您提供代码当前工作目录中的相关包模块。

import os
import re

for root,dirname,filename in os.walk(os.getcwd()):
    pth_build=""
    if os.path.isfile(root+"/__init__.py"):
        for i in filename:
            if i <> "__init__.py" and i <> "__init__.pyc":
                if i.split('.')[1] == "py":
                    slot = list(set(root.split('\\')) -set(os.getcwd().split('\\')))
                    pth_build = slot[0]
                    del slot[0]
                    for j in slot:
                        pth_build = pth_build+"."+j
                    print pth_build +"."+ i.split('.')[0]

此代码将显示:

modules.foo
modules.bar
modules.root.blah
modules.root.abc.hello
modules.root.xyz.hi

如果你在模块文件夹之外运行它。

【讨论】:

请注意,您不能依赖 __init__.py 文件的存在来解决此问题,尤其是在较新版本的 Python 中,单个包可以跨越文件系统上的多个不同文件夹。例如,请参阅 Python 中 namespace packages 上的文档。

以上是关于如何递归获取 python 包中的所有子模块?的主要内容,如果未能解决你的问题,请参考以下文章

如何检查 Python 包中的任何模块是不是从另一个包导入?

在子模块内添加 git 子模块(嵌套子模块)

如何在包中只导入没有 exec __init__.py 的子模块

获取当前包中所有模块的列表

如果放入命名空间包中,则子模块的python导入路径

python中有getpass包吗