显式相对导入的正确样板是什么?

What is the correct boilerplate for explicit relative imports?

PEP 366 - Main module explicit relative imports 中引入了模块范围变量 __package__ 以允许在子模块中显式相对导入,摘录如下:

When the main module is specified by its filename, then the __package__ attribute will be set to None. To allow relative imports when the module is executed directly, boilerplate similar to the following would be needed before the first relative import statement:

if __name__ == "__main__" and __package__ is None:
    __package__ = "expected.package.name"

Note that this boilerplate is sufficient only if the top level package is already accessible via sys.path. Additional code that manipulates sys.path would be needed in order for direct execution to work without the top level package already being importable.

This approach also has the same disadvantage as the use of absolute imports of sibling modules - if the script is moved to a different package or subpackage, the boilerplate will need to be updated manually. It has the advantage that this change need only be made once per file, regardless of the number of relative imports.

我尝试在以下设置中使用此样板文件:

从文件系统执行子模块 bar.py 时样板有效(PYTHONPATH 修改使包 foo/ 可在 sys.path 上访问):

PYTHONPATH=$(pwd) python3 foo/bar.py

当从模块命名空间执行子模块 bar.py 时,样板文件也有效:

python3 -m foo.bar

然而,以下替代样板在两种情况下都与 bar.py 子模块的内容一样有效:

if __package__:
    from . import baz
else:
    import baz

此外,这个替代样板更简单,当它与子模块 baz.py 一起移动到不同的包时不需要对子模块 bar.py 进行任何更新(因为它不对包进行硬编码姓名 "foo").

下面是我关于 PEP 366 样板的问题:

  1. 第一个子表达式 __name__ == "__main__" 是必需的还是第二个子表达式 __package__ is None 已经隐含了?
  2. 第二个子表达式 __package__ is None 不应该改为 not __package__ ,以便处理 __package__ 是空字符串的情况(就像在 __main__.py 子模块中一样通过提供包含目录从文件系统执行:PYTHONPATH=$(pwd) python3 foo/)?

正确的样板文件是 none,只需编写显式相对导入,如果有人试图 运行 将模块作为脚本或 sys.path 配置错误,则让异常转义:

from . import baz

PEP 366 中给出的样板只是为了表明提议的更改足以让用户直接执行*工作,如果他们真的想要的话,它不是'无意暗示让直接执行工作是一个好主意(它不是,这是一个坏主意,它几乎不可避免地会导致其他问题,即使使用 PEP 的样板也是如此)。

您提出的替代样板重现了 Python 2 中隐式相对导入引起的问题:"baz" 模块从 __main__ 作为 baz 导入,但将在其他地方作为 "foo.baz" 导入,所以你最终在 sys.modules 中以不同的名称得到两个副本。

除其他问题外,这意味着如果其他模块抛出 foo.baz.SomeException 而您的 __main__ 模块试图捕获 baz.SomeException,它将无法工作,因为它们将是两个来自两个不同模块的不同异常对象。

相比之下,如果您使用 PEP 样板文件,那么 __main__ 将正确地将 baz 导入为 "foo.baz",您唯一需要担心的是可能导入的其他模块foo.bar.

如果你想要更简单的样板来明确防止“无意中以不同的名称制作同一模块的两个副本”错误而不对包名称进行硬编码,那么你可以使用这个:

if not __package__:
    raise RuntimeError(f"{__file__} must be imported as a package submodule")

但是,如果您打算这样做,您也可以按照上面的建议无条件地执行 from . import baz,如果有人试图直接 运行 脚本,则让底层异常逃逸通过 -m 开关。


* 直接执行表示执行代码来自:

  1. 除目录和 zip 文件路径 (python <file path>) 之外的文件路径参数。
  2. 一个-c参数(python -c <code>)。
  3. 交互式解释器(python)。
  4. 标准输入(python < <file path>)。

间接执行表示从以下位置执行代码:

  1. 目录或 zip 文件路径参数 (python <directory or zip file path>)。
  2. 一个-m参数(python -m <module name>)。
  3. 导入语句(import <module name>

现在具体回答你的问题:

  1. Is the first subexpression __name__ == "__main__" necessary or is it already implied by the second subexpression __package__ is None?

除了使用现代导入系统的 __main__ 模块外,很难在任何地方获得 __package__ is None。但它过去更常见,因为 __package__ 不是由导入系统在模块加载时设置,而是由模块中执行的第一个显式相对导入延迟设置。换句话说,样板只是试图让直接执行工作(上面的案例 1 到 4),但是 __package__ is None 用于 暗示直接执行 或导入statement(上面的案例 7),所以要过滤掉案例 7,子表达式 __name__ == "__main__"(上面的案例 1 到 6)是必要的。

  1. Shouldn’t the second subexpression __package__ is None be not __package__ instead, in order to handle the case where __package__ is the empty string (like in a __main__.py submodule executed from the file system by supplying the containing directory: PYTHONPATH=$(pwd) python3 foo/)?

不,因为样板只是试图让 直接执行 工作(上面的案例 1 到 4),它没有试图让 sys.path 的其他风格错误配置默默通过。