【问题标题】:Why do circular imports cause problems with object identity using `isinstance`?为什么循环导入会导致使用“isinstance”的对象标识问题?
【发布时间】:2018-12-06 19:19:29
【问题描述】:

在隔离了一个错误几个小时后,我提出了以下 MCVE 示例来演示我遇到的问题:

a.py:

from b import get_foo_indirectly

class Foo:
    pass

if __name__ == '__main__':
    print("Indirect:", isinstance(get_foo_indirectly(), Foo))
    print("Direct:", isinstance(Foo(), Foo))

b.py:

def get_foo_indirectly():
    from a import Foo
    return Foo()

a.py 的预期输出为:

Indirect: True
Direct: True

实际输出为:

Indirect: False
Direct: True

另外,如果我创建一个单独的模块c.py,输出如预期:

from a import Foo
from b import get_foo_indirectly

if __name__ == '__main__':
    print("Indirect:", isinstance(get_foo_indirectly(), Foo))
    print("Direct:", isinstance(Foo(), Foo))

很明显,isinstance 和导入机制之间的交互行为并不像我预期的那样。似乎循环进口的使用让我很难受。为什么?这是 Python 的预期行为吗?

请注意,这对我遇到此行为的实际上下文过于简单了;模块 a 和 b 都是大模块,而 b 是分开的,因为它与 a 有不同的用途。既然我已经看到了循环导入的后果,我可能会将它们结合起来,也许会降低 b 中的一些冗长的行为。

【问题讨论】:

  • @ehacinom 因为循环导入的性质(虽然我显然不完全理解);如果同时在b 中导入a 和在a 中的b,则会出现错误:ImportError: cannot import name 'get_foo_indirectly' from 'b'
  • 如果您在a.py 中打印出Foo(),您将得到__main__.Foo instance。如果你在b.py 中打印出Foo(),你会得到a.Foo instance
  • @John 很好的发现。但是,我的示例更简洁,并提出了更一般的问题。我不确定是否值得将其标记为重复(尽管如果您感觉不同,您和其他任何人当然可以标记并投票关闭作为重复。)

标签: python python-3.x


【解决方案1】:

当您运行 Python 脚本时,它会自动采用名称 __main__。在您在b.py 中导入a.py 时,Python 假定了通常的模块名称(即文件名),并且在运行时Python 更改为__main__,因为它是入口点脚本;所以,就像Foo 类在两个不同的地方声明:__main__ 模块和a 模块。

然后您将比较a.Foo(在get_foo_indirectly 内部创建)和__main__.Foo 的实例。

这已经在here讨论过。

如果您需要进行循环导入,请不要将入口点脚本放在循环中。这样你就可以避免 Python 的这种——非常令人困惑的——行为。

【讨论】:

  • 不要把“入口点脚本放在循环中”,你的意思是像我的c.py例子那样使用一个单独的模块吗?
  • @Graham 是的!入口点模块是你在终端中调用的模块,带有if __name__ == '__main__'的模块。
【解决方案2】:

我运行了相同的脚本,我发现你可以添加一些行来显示一些有趣的差异:

from b import get_foo_indirectly

class Foo:
    pass

if __name__ == '__main__':
    print("Indirect:", isinstance(get_foo_indirectly(), Foo))
    print(type(get_foo_indirectly()))
    print("Direct:", isinstance(Foo(), Foo))
    print(type(Foo()))

输出:

Indirect: False
<class 'a.Foo'>
Direct: True
<class '__main__.Foo'>

现在,对于您的 c.py 示例,两者都是 a.Foo,因此它们的评估结果相同。这里似乎推断出对象也被追踪到它们来自的文件/模块路径。

这是一个重要的区别,它超越了__main__ 的调用,(将路径分配为__main__,而不是来自PATH、@Gabriel、@ehacinom 的当前路径)。假设您在不同的文件中定义了完全相同的类,例如,d.py

class Foo:
    pass

你尝试将它们导入到同一个类e.py

from a import Foo
from d import Foo as Fooo

print(type(Foo()))
print(type(Fooo()))

你会得到:

<class 'a.Foo'>
<class 'd.Foo'>

这就是python命名空间类的方式。此外,如果您将d.py 移动到目录/d 与目录中的__init__.py,类将变为

<class 'd.d.Foo'>

所有路径都相对于 python PATHsite_packages 中安装的模块将在PATH 上可用,type 将返回从基目录开始的对象路径等:

<class 'matplotlib.figure.Figure'>

【讨论】:

    【解决方案3】:

    __main__ 中进行导入必须更改类命名空间的位置。

    __name__ 是一个内置变量,计算结果为当前模块的名称。您将覆盖 a.py 文件末尾的特殊变量 __name__ == __main__,并在该上下文中导入 Foo

    如果你在a.py __main__() 中打印出Foo(),你会得到__main__.Foo instance

    如果你在b.py get_foo_indirectly() 中打印出Foo(),你会得到a.Foo instance

    由于循环导入,在b.py 中,您在函数内导入Foo,该函数在__main__ 中调用。如果您在终端中定义一个类,也会发生类似的事情——它的命名空间为 __console__

    【讨论】:

      猜你喜欢
      • 2013-04-30
      • 2020-11-05
      • 1970-01-01
      • 2012-01-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-06
      • 2013-06-17
      相关资源
      最近更新 更多