目录

导出

模块中可以使用哪些对象?一个常见的约定是用单个下划线开头来表示“私有”(尽管“隐藏”可能更能描述它通常是如何处理的 - 大多数工具会隐藏这些对象,除非您开始输入 _)。

以这种方式命名任何您不希望外部用户使用的对象是一个好习惯。对于库作者来说,在实践中这意味着您可以修改或删除任何 _* 对象,而不用担心会破坏谁的代码。对于库用户来说,尽量不要使用任何以单个 _ 开头的对象,因为这可能会随时更改。

但是,此约定有其局限性。导入呢?您通常不会(也不应该)重命名您的导入。但它们没有以下划线开头,那么这是否意味着它们是公开的?即使 from __future__ import annotations 也会向您的项目添加一个 annotations 对象,该对象对您的项目公开可见!

有时尝试的第二个解决方案是在使用完对象后将其删除。但是,由于 Python 的延迟绑定,这在某些情况下会导致意想不到的问题。由于 del 语句位于模块的末尾,远离使用位置,因此很容易忘记删除诸如导入之类的内容。

设置全部

此问题的解决方案是 __all__ 属性。这是一个导出 API 的公开声明。它看起来像这样

__all__ = ["object1", "Class1", "some_reexport"]

设置此属性会执行以下操作

  • 它控制如果用户执行 from module import * 将导入哪些内容。
  • 它提供了一个模块公共 API 的人类可读列表,而无需查看整个文件(理想情况下将其放置在文件顶部附近)。
  • 它通知静态工具(如类型检查器)有关公共 API 的信息,包括您导入内容的重新导出。
  • 可以用于控制 dir(module)(以及因此的选项卡补全)看到的内容。

如果您想改进选项卡补全/dir() 调用,您可以将此小型样板函数添加到您的模块中

def __dir__() -> list[str]:
    return __all__

这会导致选项卡补全仅显示您的公共 API!您仍然可以访问模块中的所有内容,只是不会显示给用户。

__dir__() 技巧在 __init__.py 模块上效果不佳,因为理想情况下,如果已导入子模块,则希望显示它们。最好使 __init__.py 模块保持最简化。在 __init__.py 中从子模块导入内容很诱人,但请记住,导入任何子模块都会始终运行所有父 __init__.py,因此您可能会遇到导入性能下降,并且可能不得不处理循环导入问题以节省用户几个按键。

有一些动态解决方案可以构建 __all__ 变量,而无需在文件顶部附近将所有项目列在一个列表中。但是,这样做会失去一些功能,例如模块内容的人类可读列表和静态类型检查器支持。