递归填充__init__.py中的__all__

4 投票
2 回答
2168 浏览
提问于 2025-04-17 15:58

我在我的模块的 __init__.py 文件中使用以下代码来填充 __all__,我在想有没有更有效的方法。有什么想法吗?

import fnmatch
import os

__all__ = []
for root, dirnames, filenames in os.walk(os.path.dirname(__file__)):
    root = root[os.path.dirname(__file__).__len__():]
    for filename in fnmatch.filter(filenames, "*.py"):
        __all__.append(os.path.join(root, filename[:-3]))

2 个回答

2

我在处理一些复杂的包,这些包里面还有子包和子模块。我喜欢逐个模块地控制这些内容。我使用一个叫做 auto-all 的简单包,这让事情变得容易(顺便说一下,我是这个包的作者)。

https://pypi.org/project/auto-all/

这里有个例子:

from auto_all import start_all, end_all

# Define some internal stuff

start_all(globals())

# Define some external stuff

end_all(globals())

我使用这种方法的主要原因是因为导入(imports)。正如 alexis 提到的,你可以通过在对象名称前加下划线来隐式地让它变得私有,但这样做可能会变得混乱,或者对于导入的对象来说不太实用。考虑以下代码:

from pyspark.sql.session import SparkSession

如果这个出现在你的模块中,那么你就会隐式地让 SparkSession 可以从模块外部访问。另一种方法是给所有导入的项目加下划线,比如:

from pyspark.sql.session import SparkSession as _SparkSession

但这样也不理想,所以手动管理 __all__ 是我知道的唯一管理你想对外公开的内容的方法。

你可以通过明确设置 __all__ 变量的内容来轻松做到这一点(这也是符合 Python 风格的做法),但当你需要管理大量对象时,这可能会变得繁琐。如果开发者添加了一个新对象却没有把它添加到 __all__ 变量中,也可能会导致问题。这种情况在代码审查中可能会被忽略。使用简单的辅助函数来管理这个变量的内容会让事情变得容易得多。

5

你可能不应该这样做:默认情况下,import 的行为是相当灵活的。如果你不想让某个模块(或者其他变量)被自动导出,只需给它起个名字,以 _ 开头,Python 就不会导出它。这是标准的 Python 做法,重新发明轮子被认为是不符合 Python 风格的。此外,别忘了除了模块之外,还有其他东西可能需要导出;一旦你设置了 __all__,你还需要找到并导出它们。

不过,你还是想知道如何最好地生成一个可导出的模块列表。因为你不能导出不存在的东西,所以我建议你检查一下你自己的模块在主模块中是如何被识别的:

basedir = os.path.dirname(__file__)
for m in sys.modules:
    if m in locals() and not m.startswith('_'): # Only export regular names
        mod = locals()[m]
        if '__file__' in mod.__dict__  and mod.__file__.startswith(basedir):
            print m

sys.modules 包含了 Python 已加载的每个模块的名称,包括许多没有被导出到主模块的模块——所以我们要检查它们是否在 locals() 中。

这样做比扫描你的文件系统要快,而且比假设目录树中的每个 .py 文件都会以某种方式成为顶级子模块要更可靠。当然,你应该在 __init__.py 的末尾附近运行这段代码,这样一切都加载完毕后再执行。

撰写回答