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

Mag*_*ero 6 python boilerplate relative-import

PEP 366 -主模块显式相对导入中引入了模块范围变量__package__以允许在子模块中显式相对导入,有以下摘录:

当主模块由其文件名指定时,该 __package__属性将设置为None. 为了在直接执行模块时允许相对导入,在第一个相对导入语句之前需要类似于以下的样板:

if __name__ == "__main__" and __package__ is None:
    __package__ = "expected.package.name"
Run Code Online (Sandbox Code Playgroud)

请注意,仅当顶级包已经可以通过 sys.path. 需要额外的操作代码sys.path才能直接执行工作,而无需导入顶级包。

这种方法也有与使用兄弟模块的绝对导入相同的缺点——如果脚本被移动到不同的包或子包,样板将需要手动更新。它的优点是每个文件只需进行一次此更改,而不管相关导入的数量如何。

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

当从文件系统执行子模块 bar.py 时,样板工作(PYTHONPATH修改使包 foo/ 可访问sys.path):

PYTHONPATH=$(pwd) python3 foo/bar.py
Run Code Online (Sandbox Code Playgroud)

当从模块命名空间执行子模块 bar.py 时,样板也可以工作:

python3 -m foo.bar
Run Code Online (Sandbox Code Playgroud)

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

if __package__:
    from . import baz
else:
    import baz
Run Code Online (Sandbox Code Playgroud)

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

所以这里是我关于 PEP 366 样板的问题:

  1. 第一个子表达式是__name__ == "__main__"必需的还是第二个子表达式已经暗示了它__package__ is None
  2. 不应第二子表达式__package__ is Nonenot __package__代替,以便处理其中的情况下__package__是空字符串(如在一个__main__.py从通过供应含目录的文件系统执行的子模块:PYTHONPATH=$(pwd) python3 foo/)?

nco*_*lan 5

正确的样板是无,只需编写显式相对导入,并在有人尝试将模块作为脚本运行或有异常时让异常转义sys.path配置错误时让异常转义:

\n
from . import baz\n
Run Code Online (Sandbox Code Playgroud)\n

PEP 366 中给出的样板只是为了表明建议的更改足以允许用户使直接执行*工作(如果他们确实愿意),它并不是 \xe2\x80\x99t 旨在表明使直接执行工作是一个好主意(它不是\xe2\x80\x99t,这是一个坏主意,几乎不可避免地会导致其他问题,即使使用来自 PEP 的样板)。

\n

您提出的替代样板文件重现了由 Python 2 中的隐式相对导入引起的问题:模块作为from"baz"导入,但会像其他地方一样导入,因此最终会以不同的名称获得两个副本。baz__main__"foo.baz"sys.modules

\n

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

\n

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

\n

如果您想要更简单的样板文件,明确防止“无意中以不同名称创建同一模块的两个副本”错误,而无需对包名称进行硬编码,那么您可以使用以下命令:

\n
if not __package__:\n    raise RuntimeError(f"{__file__} must be imported as a package submodule")\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
\n

*直接执行意味着执行代码:

\n
    \n
  1. 文件路径参数,目录和 zip 文件路径 ( python <file path>) 除外。
  2. \n
  3. 一个-c论点(python -c <code>)。
  4. \n
  5. 交互式解释器 ( python)。
  6. \n
  7. 标准输入 ( python < <file path>)。
  8. \n
\n

间接执行意味着执行以下位置的代码:

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

现在具体回答一下大家的问题:

\n
\n
    \n
  1. 第一个子表达式是__name__ == "__main__"必要的还是第二个子表达式已经隐含了__package__ is None
  2. \n
\n
\n

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

\n
\n
    \n
  1. __package__ is None\xe2\x80\x99t是否应该改为第二个子表达式not __package__,以便处理__package__是 空字符串的情况(例如在__main__.py通过提供包含目录来从文件系统执行的子模块中:\n PYTHONPATH=$(pwd) python3 foo/)?
  2. \n
\n
\n

不,因为样板文件只是试图让直接执行工作(上面的情况 1 到 4),它并不是\xe2\x80\x99 试图让其他类型的错误sys.path配置默默地通过。

\n