Python 没有循环导入的类型提示

Python type hinting without cyclic imports

我想把我巨大的 class 一分为二;好吧,基本上进入“主要” class 和具有附加功能的混合,如下所示:

main.py 文件:

import mymixin.py

class Main(object, MyMixin):
    def func1(self, xxx):
        ...

mymixin.py 文件:

class MyMixin(object):
    def func2(self: Main, xxx):  # <--- note the type hint
        ...

现在,虽然这工作得很好,但 MyMixin.func2 中的类型提示当然不能工作。我无法导入 main.py,因为我会得到一个循环导入,如果没有提示,我的编辑器 (PyCharm) 无法分辨 self 是什么。

我正在使用 Python 3.4,但如果那里有可用的解决方案,我愿意迁移到 3.5。

有什么方法可以将我的 class 分成两个文件并保留所有“连接”,以便我的 IDE 仍然为我提供自动完成以及来自的所有其他好东西它知道类型吗?

更大的问题是您的类型一开始就不正常。 MyMixin 硬编码假设它将混合到 Main 中,而它可以混合到任意数量的其他 classes 中,在这种情况下它可能会中断。如果你的 mixin 被硬编码为混合到一个特定的 class 中,你最好将方法直接写入那个 class 而不是将它们分开。

为了通过合理的打字正确地做到这一点,MyMixin 应该根据 接口 或 Python 说法中的抽象 class 进行编码:

import abc


class MixinDependencyInterface(abc.ABC):
    @abc.abstractmethod
    def foo(self):
        pass


class MyMixin:
    def func2(self: MixinDependencyInterface, xxx):
        self.foo()  # ← mixin only depends on the interface


class Main(MixinDependencyInterface, MyMixin):
    def foo(self):
        print('bar')

恐怕没有一种非常优雅的方法来处理一般的导入周期。您的选择是重新设计代码以消除循环依赖,或者如果不可行,请执行以下操作:

# some_file.py

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from main import Main

class MyObject(object):
    def func2(self, some_param: 'Main'):
        ...

TYPE_CHECKING 常量在运行时始终是 False,因此不会评估导入,但 mypy(和其他类型检查工具)将评估该块的内容。

我们还需要将 Main 类型注释变成一个字符串,有效地向前声明它,因为 Main 符号在运行时不可用。

如果您使用的是 Python 3.7+,我们至少可以利用 PEP 563:

跳过必须提供显式字符串注释的步骤
# some_file.py

from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from main import Main

class MyObject(object):
    # Hooray, cleaner annotations!
    def func2(self, some_param: Main):
        ...

from __future__ import annotations 导入将使 all 类型提示成为字符串并跳过对它们的评估。这有助于使我们的代码更符合人体工程学。

综上所述,将 mixins 与 mypy 结合使用可能需要比您目前拥有的更多的结构。 Mypy recommends an approach 这基本上就是 deceze 所描述的——创建一个 ABC,您的 MainMyMixin 类 都继承了它。如果您最终需要做类似的事情来让 Pycharm 的检查员高兴,我不会感到惊讶。

原来我最初的尝试也很接近解决方案。这是我目前正在使用的:

# main.py
import mymixin.py

class Main(object, MyMixin):
    def func1(self, xxx):
        ...
# mymixin.py
if False:
    from main import Main

class MyMixin(object):
    def func2(self: 'Main', xxx):  # <--- note the type hint
        ...

注意 if False 语句中的导入永远不会被导入(但 IDE 知道它)并使用 Main class 作为字符串,因为它不知道在运行时。

我认为完美的方法应该是在一个文件中导入所有 类 和依赖项(如 __init__.py),然后在所有其他文件中导入 from __init__ import *

在这种情况下你是

  1. 避免多次引用这些文件以及 类 和
  2. 也只需要在每个其他文件中添加一行
  3. 第三个是 pycharm 了解您可能使用的所有 类。

对于在导入 class 仅用于类型检查时遇到循环导入问题的人:您可能希望使用 Forward Reference(PEP 484 - 类型提示):

When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later.

所以代替:

class Tree:
    def __init__(self, left: Tree, right: Tree):
        self.left = left
        self.right = right

你这样做:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right

我建议按照其他人的建议重构您的代码。

我可以向您展示我最近遇到的循环错误:

之前:

# person.py
from spell import Heal, Lightning

class Person:
    def __init__(self):
        self.life = 100

class Jedi(Person):
    def heal(self, other: Person):
        Heal(self, other)

class Sith(Person):
    def lightning(self, other: Person):
        Lightning(self, other)

# spell.py
from person import Person, Jedi, Sith

class Spell:
    def __init__(self, caster: Person, target: Person):
        self.caster: Person = caster
        self.target: Person = target

class Heal(Spell):
    def __init__(self, caster: Jedi, target: Person):
        super().__init__(caster, target)
        target.life += 10

class Lightning(Spell):
    def __init__(self, caster: Sith, target: Person):
        super().__init__(caster, target)
        target.life -= 10

# main.py
from person import Jedi, Sith

一步一步:

# main starts to import person
from person import Jedi, Sith

# main did not reach end of person but ...
# person starts to import spell
from spell import Heal, Lightning

# Remember: main is still importing person
# spell starts to import person
from person import Person, Jedi, Sith

控制台:

ImportError: cannot import name 'Person' from partially initialized module
'person' (most likely due to a circular import)

一个script/module只能被一个脚本导入。

之后:

# person.py
class Person:
    def __init__(self):
        self.life = 100

# spell.py
from person import Person

class Spell:
    def __init__(self, caster: Person, target: Person):
        self.caster: Person = caster
        self.target: Person = target

# jedi.py
from person import Person
from spell import Spell

class Jedi(Person):
    def heal(self, other: Person):
        Heal(self, other)

class Heal(Spell):
    def __init__(self, caster: Jedi, target: Person):
        super().__init__(caster, target)
        target.life += 10

# sith.py
from person import Person
from spell import Spell

class Sith(Person):
    def lightning(self, other: Person):
        Lightning(self, other)

class Lightning(Spell):
    def __init__(self, caster: Sith, target: Person):
        super().__init__(caster, target)
        target.life -= 10

# main.py
from jedi import Jedi
from sith import Sith

jedi = Jedi()
print(jedi.life)
Sith().lightning(jedi)
print(jedi.life)

执行行的顺序:

from jedi import Jedi  # start read of jedi.py
from person import Person  # start AND finish read of person.py
from spell import Spell  # start read of spell.py
from person import Person  # start AND finish read of person.py
# finish read of spell.py

# idem for sith.py

控制台:

100
90

文件组成是关键 希望对你有帮助:D

从 Python 3.5 开始,将您的 class 分成单独的文件很容易。

实际上可以在 class ClassName: 块的 中使用 import 语句 以便将方法导入 class。例如,

class_def.py:

class C:
    from _methods1 import a
    from _methods2 import b

    def x(self):
        return self.a() + " " + self.b()

在我的示例中,

  • C.a() 将是 return 字符串 hello
  • 的方法
  • C.b() 将是 returns hello goodbye
  • 的方法
  • C.x() 将因此 return hello hello goodbye.

要实施 ab,请执行以下操作:

_methods1.py:

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from class_def import C

def a(self: C):
    return "hello"

解释:当类型检查器读取代码时,TYPE_CHECKINGTrue。由于类型检查器不需要执行代码,因此循环导入在 if TYPE_CHECKING: 块内发生时没有问题。 __future__ 导入启用 postponed annotations。这是一个可选的;没有它,您必须引用类型注释(即 def a(self: "C"):)。

我们类似地定义_methods2.py

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from class_def import C

def b(self: C):
    return self.a() + " goodbye"

在 VS Code 中,悬停时我可以看到从 self.a() 检测到的类型:

一切都按预期运行:

>>> from class_def import C
>>> c = C()
>>> c.x()
'hello hello goodbye'

旧 Python 版本的注释

对于Python版本≤3.4,TYPE_CHECKING没有定义,所以这个解决方案是行不通的。

对于Python版本≤3.6,延期注解未定义。作为解决方法,省略 from __future__ import annotations 并引用上面提到的类型声明。

与其强迫自己搞 typing.TYPE_CHECKING 恶作剧,有一个简单的方法可以避免循环 type-hints:不要使用 from 导入,使用 [=14] =] 或字符串注释。

# foo.py
from __future__ import annotations
import bar


class Foo:
    bar: bar.Bar
# bar.py
import foo


class Bar:
    foo: "foo.Foo"

这种导入方式是“惰性评估”,而使用 from foo import Foo 会强制 Python 到 运行 整个 foo 模块以获得 Foo 立即在导入行。如果您还需要在 运行 时间使用它,它会非常有用,例如如果 foo.Foobar.Bar 需要在 function/method 中使用,因为您的 functions/methods 应该只调用一次 foo.Foo 并且可以使用 bar.Bar .