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

我想把我庞大的班级分成两部分;嗯,基本上变成了“main”;类和带有附加函数的mixin,如下所示:

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。

有没有办法我可以把我的类分成两个文件,并保留所有的“连接”;所以我的IDE仍然为我提供自动补全和所有其他的好东西,因为它知道类型?

78163 次浏览

更大的问题是,你们这种类型的人一开始就不理智。MyMixin做了一个硬编码的假设,它将被混合到Main中,而它可以混合到任何数量的其他类中,在这种情况下,它可能会崩溃。如果你的mixin被硬编码为混合到一个特定的类中,你也可以直接把方法写进那个类中,而不是把它们分开。

为了用正常类型正确地做到这一点,MyMixin应该针对接口或Python术语中的抽象类进行编码:

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导入将使所有类型提示为字符串,并跳过对它们求值。这有助于使我们的代码稍微更符合人体工程学。

综上所述,在mypy中使用mixins可能需要比当前更多的结构。Mypy 推荐一种方法,这基本上就是deceze所描述的——创建一个你的MainMyMixin类都继承的ABC。如果你最终需要做一些类似的事情来让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类作为字符串,因为它在运行时不知道。

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

在这种情况下,你是

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

对于那些在仅为类型检查而导入类时挣扎于循环导入的人:你可能会想要使用向前引用 (PEP 484 -类型提示):

当类型提示包含尚未定义的名称时,该定义可以表示为稍后解析的字符串字面值。

所以不要:

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)

一个脚本/模块只能被一个脚本导入。

后:

# 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 ClassName:块的内部import语句来将方法导入到类中。例如,

class_def.py:

class C:
from _methods1 import a
from _methods2 import b


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

在我的例子中,

  • C.a()将是一个返回字符串hello的方法
  • C.b()将是一个返回hello goodbye的方法
  • C.x()将返回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__导入启用了推迟了注释。这是可选的;如果没有它,你必须引用类型注释(即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()检测到的类型: enter image description here < / p >

一切都按照预期运行:

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

关于旧Python版本的注释

对于≤3.4的Python版本,没有定义TYPE_CHECKING,因此此解决方案将不起作用。

对于≤3.6的Python版本,没有定义延迟注释。作为一种变通方法,省略from __future__ import annotations并引用上面提到的类型声明。

与其强迫自己搞typing.TYPE_CHECKING的恶作剧,还有一个避免循环类型提示的简单方法:不要使用from导入,使用from __future__ import annotations或字符串注释。

# 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需要在函数/方法中使用,因为你的函数/方法只应该被调用一次foo.Foobar.Bar可以被使用。