【问题标题】:Are python circular imports an implementation detail?python循环导入是实现细节吗?
【发布时间】:2019-10-12 02:14:34
【问题描述】:

循环导入的行为是否在 Python 中明确指定,还是只是特定于实现?

例如在 cpython 中,导入子模块会导致子模块被分配为父模块的属性,但直到子模块完成执行之后。这在文档或 PEP 的任何地方都有说明吗?

通常情况下,python 循环导入只是起作用(在导入期间不执行任何函数等约定的辅助,并以与预期调用顺序相反的方式布置源代码以定义函数),但上述行为的结果是您不能通过父级引用部分导入的模块。例如,如果导入package.submodule 会导致导入othermodule,那么othermodule 不能执行import package.submodule as submod,但可以 执行from package import submodule as submod。这是由文档解释/指定的吗?如果不是,它可能会改变吗?

在这种情况下,差异是因为 import package.submodule as submod 被实现为对 __import__ 的调用(返回 package),然后是属性查找(检索 submodule 以便可以将其分配给 @987654331 @)。相反,from package import submodule as submod 导致__import__ 直接返回submodule(即使submodule 仍然只完成了一半,这似乎也有效)。如果您深入研究 cpython 字节码(记录在标准库的 dis 部分中)并研究 __import__ 语义,您可以弄清楚其中的一些。但是,由于解决循环导入是 python 中的一个常见问题,因此找到一个官方的高级摘要来说明预期会有什么用处?

例子:

mkdir package
touch package/__init__.py
cat > package/submodule.py << EOF
def f():
  print('OK')
import othermodule
othermodule.g()
def notyetdefined():
  pass
EOF
cat > othermodule.py << EOF
def g():
  try:
    import package.submodule as submod # this fails
  except AttributeError:
    print('FAIL')
  from package import submodule as submod # this works ok
  submod.f()
  #submod.notyetdefined() # this cannot be invoked
EOF
python -c 'import package.submodule'

python 3.6.7 中的输出:

FAIL
OK

【问题讨论】:

  • This question 是相关的,但是,虽然答案确实涉及循环导入的行为,但他们没有说明它是否是规范的一部分。我自己的假设(我相信其他问题的一些答案也支持这一点)是,导入的明确定义的行为通常会导致一致,即使它没有明确记录。
  • 另外值得注意的是,这仅在 Python 3.7 和 3.8 中打印“OK”。

标签: python python-3.x documentation python-import circular-dependency


【解决方案1】:

通过一些研究,听起来答案是涉及到一些规范和未记录的行为,与模块的初始化方式以及import 语句的不同形式如何解析(子)模块有关。总体而言,循环导入的行为看起来应该由系统很好地定义,但您看到的行为是“一个实现怪癖”。1

虽然我没有详细研究 Python 导入系统的规则,但我能够深入了解您观察到的特定问题。

为了到达那里,我首先注意到,您的代码的行为在 Python 3.7 中发生了变化:它现在只打印 OKthe changelog for Python 3.7 的这一点说明了原因:

现在支持涉及将子模块绑定到名称的绝对导入的循环导入。 (由 Serhiy Storchaka 在bpo-30024 中贡献。)

关于导致更改的 Python 问题的讨论包含一些关于之前发生的事情的讨论,以及一些指向早期讨论的链接,这些讨论为什么会在 3.7 之前的行为中起作用。我发现一些 cmets 和链接在这里特别有用:

第一条评论解释了您观察到的行为(this Stack Overflow answer 讨论了在类似情况下相同的错误是如何发生的):

这里的背景是http://bugs.python.org/issue17636 中的更改,它允许 IMPORT_FROM 在写为“from a.b import c as m”时回退到 sys.modules,而为“import a.b.c as m”生成的普通 LOAD_ATTR 失败。

请注意,bpo-17636 推动了对“[c] 涉及相对导入的循环导入”in Python 3.5 的支持。

更一般地说,对于您的回答,this comment(来自 Guido 本人)声明循环导入的行为定义:

循环情况下导入的语义有些复杂,但定义明确,只需考虑几条规则,从这些规则中可以推断出任何特定情况是否有效。

基于the documentation for the import system 中没有出现“圆”、“循环”及其任何明显变体这一事实,我假设有关循环导入的规则虽然一致,但是系统的紧急属性,而不是明确的行为。

(请注意,导入系统的文档也没有提到 3.7 中的更改,也没有提到任何关于 import ... as ...from ... import ... 的内容。虽然 the documentation for the import statement 确实讨论了不同形式的声明,它也没有讨论周期(并且大部分内容还没有在at least 3 years 中更新)。)


Python 3.8 中还有一个小变化,虽然 the changelog 中似乎没有。

如果您取消注释 othermodule 中的 submod.notyetdefined(),Python 3.6 和 3.7 都会引发以下错误:

AttributeError: 模块 'package.submodule' 没有属性 'notyetdefined'

在 Python 3.8.0b4 中,会生成这条更有用的消息:

AttributeError: 部分初始化的模块“package.submodule”没有属性“notyetdefined”(很可能是由于循环导入)

此消息似乎是simple check 在访问缺少的属性时是否正在初始化当前模块的结果;它实际上并没有做任何与循环导出相关的事情,除了知道它们是模块的未定义属性在初始化时可能被访问的最可能原因。


1 具有讽刺意味的是,该引用来自 this email,它在 bpo-30024 的讨论中给出,作为 bpo-17636 引起的更改的基本原理——他们修复了一个案例,但离开了另一个,这就是导致你的错误的原因。

【讨论】:

  • 所以 Guido 断言它是明确定义的,我们只是不确定在哪里? (只有哪几条规则?)
  • 我怀疑他的意思是它的明确定义与我所做的相同:文档没有说“循环导入像这样工作”,但他们确实说明了何时导入什么,并遵循这些规则,你可以弄清楚循环应该如何解决。
  • 它是否说明模块是否具有与部分初始化的子模块相对应的属性? (这是 3.8 的变化吗?)
  • 文档中没有说明,以及关于部分初始化子模块的任何其他内容,大概是因为不打算使用部分初始化的模块。但是,很容易编写一个测试来表明不存在这样的虚拟属性。关于 bpo-17636 的一些讨论也暗示了这一点。至于 3.8 中的行为,我已经更新了我的答案,说明该更改是如何工作的。
猜你喜欢
  • 2016-02-24
  • 1970-01-01
  • 1970-01-01
  • 2021-12-12
  • 2021-05-01
  • 2014-04-06
  • 1970-01-01
  • 1970-01-01
  • 2016-10-04
相关资源
最近更新 更多