【问题标题】:How does Python inheritance detection work?Python 继承检测是如何工作的?
【发布时间】:2017-03-19 18:55:18
【问题描述】:

我有一个基类和几个从它继承的子类。我正在尝试动态检测哪些子类动态地从基类继承。我目前是通过动态导入基类__init__()中的所有子类,然后使用__subclasses__()方法来实现的。

我的文件结构如下:

proj/
|-- __init__.py
|-- base.py
`-- sub
    |-- __init__.py
    |-- sub1.py
    |-- sub2.py
    `-- sub3.py

base.py:

import importlib

class Base(object):
    def __init__(self):
        importlib.import_module('sub.sub1')
        importlib.import_module('sub.sub2')
        importlib.import_module('sub.sub3')

    @classmethod
    def inheritors(cls):
        print(cls.__subclasses__())

b = Base()

b.inheritors()

sub1.py:

import sys
import os

sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from base import Base

class Sub1(Base):
    pass

sub2.py:

import sys
import os

sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from base import Base

class Sub2(Base):
    pass

最后是 sub3.py:

import sys
import os

class Sub3(object):
    pass

您会注意到sub.sub1.Sub1sub.sub2.Sub2 都继承自base.Base,而sub.sub3.Sub3 没有。

当我打开 IPython3 并运行 import base 时,我得到以下输出:

In [1]: import base
[<class 'sub.sub1.Sub1'>, <class 'sub.sub2.Sub2'>]

上面的输出完全符合我的预期。当我使用 Python 命令行运行 base.py 时,它变得很奇怪:

python3 base.py
[<class 'sub.sub2.Sub2'>]
[]

现在我认为我理解在第二种情况下有两个打印,因为 Python 导入器最初在 sys.modules 全局变量中没有看到 base.py,所以当导入子类时它将再次导入 base.py并且代码将被第二次执行。这个解释没有解释为什么它第一次打印[&lt;class 'sub.sub2.Sub2'&gt;]而不是[&lt;class 'sub.sub1.Sub1'&gt;],因为sub.sub1.Sub1是首先导入的,也没有解释为什么__subclasses__()中只有sub.sub2.Sub2出现,而sub.sub1.Sub1没有。

任何有助于我理解 Python 在这方面如何工作的解释都将不胜感激!

编辑:我想使用python base.py 运行模块,所以也许我可以指出正确的方向?

【问题讨论】:

  • 简短回答:不要使用循环导入,也不要将同一文件视为脚本和可导入模块。当你做这些事情时,就会发生这种疯狂。此外,__subclasses__ 也不是很有用,因为它只知道实际执行的子类定义。
  • 谢谢,但我想了解这种行为。这不是生产就绪代码。总的来说,我知道并同意你所说的,但我要求对 Python 的内部有更深入的了解。
  • 你有替代 __subclasses__() 的替代方案,它在执行之前知道我的子类吗?
  • 静态代码分析工具可以帮助解决这个问题。

标签: python inheritance python-import


【解决方案1】:

你打了个结。 一个复杂的、不需要的结。我可以弄清楚 - 但我不知道我是否可以记住以清晰的方式解释正在发生的事情:-)

但首先有一点:这与“继承检测”关系不大,而与导入系统有关——你把它打成一个复杂的结。

所以,你得到了意想不到的结果,因为当你做python base.py 时,base 的内容被记录为sys.modules 中名为__main__ 的模块。 通常,Python 永远不会导入模块并再次运行相同的代码:在尝试导入现有模块的导入语句时,它只会创建一个指向现有模块的新变量。如果该模块尚未完成其主体的执行,则并非所有类或变量都会出现在第二个 import 语句的位置。对 importlib 的调用并没有更好 - 他们只是不自动化可变投标部分。当您进行循环导入时,更改导入路径,并从另一个文件中导入名为 base 的模块,Python 不知道这与 base 相同,即 __main__。所以,新的有一个新的导入,并且在 sys.modules 中有第二个条目,如base

如果你只是在你的继承方法中打印__class__,那就很清楚了:

@classmethod
def inheritors(cls):
    print("At class {}. Subclasses: {}".format(__class__, cls.__subclasses__()))

然后你会看到“base.Base”有“sub2”子类,而__main__.Base没有子类。

现在,让我试着把它的时间表:

  1. base.py 被导入为 __main__ 并运行到 b = Base() 行。此时 Base 的__init__ 方法将导入 子模块
  2. 子模块 sub1 运行,更改 sys.path,并且 将 base.py 重新导入为 base 模块。
  3. 的内容 基本模块一直运行,直到满足 base.Base 中的 __init__ 方法; 其中,它导入了sub.sub1,Python 发现这个模块有 已导入并位于sys.modules。它的代码没有 已完成,但 Sub1 基础尚未定义。
  4. 在 base 的 sub1 导入中,__init__ 尝试导入 sub.sub2。那 是 Python 的一个新模块,所以它被导入了
  5. 关于导入 sub2,当遇到import base时,Python将模块识别为 已经导入(虽然,同样,不是所有的初始化代码 已完成)- 它只是将名称别名带到 sub2 全局变量中,并且 继续
  6. Sub2 被定义为base.Base 的子类
  7. sub.sub2 导入完成,Python 继续执行第 (4) 步的 __init__ 方法; Python 导入 sub.sub3 并恢复到 b.inheritors() 调用 (来自base,而不是来自main)。此时唯一的子类 base.Basesub2 - 打印出来
  8. 的导入 base.py 随着 base 完成,Python 继续执行 bodu sub.sub1 的类Sub1 被定义为base.Base 的子类
  9. Python 恢复 __main__.base.__init__ 执行,导入 sub.sub2 - 但它已经运行,sub.sub3 也一样
  10. __main__.Base.inheritors__main__ 中被调用,并打印 no 子类。

这就是一段复杂历史的终结。

你应该做什么

首先:如果你需要做sys.path.append 诡计,你的包有问题。让您的包为proj,并指向proj.__init__ 导入base,如果您希望它运行(并动态导入其他模块) - 但停止摆弄 sys.path 以在您自己的包中查找内容。

秒: cls.__subclasses__ 调用没什么用,因为它只会告诉你cls 的直接子类——如果有一个孙子类,它会被忽视,

最常见的模式是注册您的 Base 的子类 - 在创建它们时,只需将新类添加到此记录中。这可以通过 Python __init_subclass__ 方法来完成。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-04-08
    • 1970-01-01
    • 2012-12-18
    • 2012-12-13
    • 2012-12-16
    • 1970-01-01
    • 2015-10-16
    相关资源
    最近更新 更多