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

7
在介绍了模块作用域变量__package__以允许子模块中的显式相对导入的PEP 366 - 主模块显式相对导入中,有如下摘录:

(英文链接)

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.

我尝试在以下场景中使用这个样板文件:
  • Directory layout:

    foo
    ├── bar.py
    └── baz.py
    
  • Contents of the bar.py submodule:

    if __name__ == "__main__" and __package__ is None:
        __package__ = "foo"
    
    from . import baz
    
在文件系统中执行子模块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/)吗?

1
对于你的第一个问题:https://dev59.com/bHRC5IYBdhLWcg3wD87r - Brian McCutchon
3
请注意,“from . import baz”与“import baz”并不等同。它使用不同的名称导入了“baz”,这可能会导致如果其他代码使用另一种导入形式,则“baz”存在两次。 - MisterMiyagi
3
在使用这种“简化”的替代方案之前,请记住需要保护foo.bazbaz的每个导入,而不仅仅是在foo内部。您还必须禁止使用任何第三方的baz模块,以避免名称冲突。这些都是需要考虑的事情。 - MisterMiyagi
@MisterMiyagi,我认为您的评论实际上已经回答了这个问题。也许Maggyero的困惑在于何时应该使用样板文件?(答案是“永远不要,除非您真的希望允许子模块直接作为脚本执行,但如果您认为您需要这样做,您应该重新考虑”。PEP没有说过这一点,因为它是一个技术设计文档,而不是最终用户文档) - ncoghlan
1
@ncoghlan 这个问题明确包含两个问题(主要的保护是否必要?None 保护是否正确?),而我的评论根本没有回答这些问题。它们作为现成的答案是不够充分的。“不要那样做”是对一个在这里没有被问到的问题的充分回答。 - MisterMiyagi
显示剩余10条评论
1个回答

5
正确的样板代码是没有的,只需编写显式相对导入并让异常逃逸,如果有人尝试将模块作为脚本运行或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. 第一个子表达式 __name__ == "__main__" 是否必要,还是已经包含在第二个子表达式 __package__ is None 中了?

用现代导入系统很难在其他位置得到 __package__ is None。但是它过去更加普遍,因为 __package__ 不是在模块加载时由导入系统设置,而是在第一个执行的相对导入中懒惰地设置。换句话说,这个样板只是试图让直接执行起作用 (1 到 4 的情况),但是 __package__ is None 曾经意味着直接执行 或导入语句 (第 7 种情况),所以要过滤掉第 7 种情况,就必须使用子表达式 __name__ == "__main__" (1 到 6 的情况)。

  1. 第二个子表达式 __package__ is None 是否应该改为 not __package__,以处理 __package__ 为空字符串的情况 (例如通过指定包含目录来执行文件系统中的 __main__.py 子模块的命令: PYTHONPATH=$(pwd) python3 foo/) ?
不行,因为这个样板只是试图让“直接执行”起作用(上面的1到4情况),它并没有试图让其他各种sys.path配置错误悄悄通过。

如果你在回答中提到了这个问题,我会很乐意接受它。 - Géry Ogam
1
不,因为模板只是试图让直接执行工作,它并没有试图让其他类型的sys.path配置错误静默通过。 - ncoghlan
1
解除防护:__name__ 检查的意思是“这是主模块还是导入模块?如果是导入模块,则不做任何操作”,而 __package__ 检查的意思是“这是直接运行还是通过 -m 开关运行的?如果是通过 -m 开关运行,则不做任何操作”。因此,该代码片段仅在直接执行时运行,并在其他情况下被跳过。 - ncoghlan
我已经通过以下方式检查了__package__ is None: 1. 除目录和zip文件之外的文件路径参数(python <file path except directory and zip file>). 2. -c 参数 (python -c <code>). 3. 交互式解释器 (python). 4. 标准输入 (python < <file path>). 但是没有使用以下方式: 5. 目录路径或zip文件路径参数 (python <directory path or zip file path>). 6. -m 参数 (python -m <module name>). 7. 导入语句 (import <module name>). - Géry Ogam
1
是的,使用现代导入系统很难在主模块以外的任何地方得到__package__ is None。过去这种情况更为常见,因为该属性不是在模块加载时由导入系统设置,而是在模块中执行第一个显式相对导入时才会被惰性地设置。 - ncoghlan
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接