为什么循环导入使用`isinstance`会导致对象标识出现问题?

时间:2018-12-06 19:19:30

标签: python python-3.x

在隔离了几个小时的错误之后,我提出了以下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中某些长期存在的行为。

3 个答案:

答案 0 :(得分:7)

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

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

这已经在here中进行了讨论。

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

答案 1 :(得分:4)

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

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'>

答案 2 :(得分:2)

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

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

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

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

由于循环导入,因此在b.py中将Foo导入到函数中,该函数在__main__中被调用。如果在终端中定义一个类,也会发生类似的事情-它的名称空间为__console__