无需循环导入的 Python 类型提示
- 2025-01-16 08:37:00
- admin 原创
- 163
问题描述:
我正在尝试将我的大班级分成两部分;基本上分成“主”班级和具有附加功能的混合班级,如下所示:
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 仍然可以为我提供自动完成功能以及了解类型的所有其他好东西?
解决方案 1:
恐怕一般来说,没有一种非常优雅的方式来处理导入循环。你的选择是重新设计代码以消除循环依赖,或者如果不可行,请执行以下操作:
# 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
将使所有类型提示都变成字符串并跳过对它们的求值。这可以帮助我们的代码更加符合人体工程学。
尽管如此,使用 mixin 和 mypy 可能需要比您目前拥有的更多的结构。Mypy推荐的方法基本上就是deceze
所描述的那样——创建一个您的Main
和MyMixin
类都继承的 ABC。如果您最终需要做类似的事情才能让 Pycharm 的检查器满意,我不会感到惊讶。
解决方案 2:
对于在仅为类型检查而导入类时遇到循环导入困难的人来说:您可能需要使用前向引用(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
解决方案 3:
更大的问题是您的类型从一开始就不合理。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')
解决方案 4:
与其强迫自己参与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.Foo`bar.Bar
解决方案 5:
从 Python 3.5 开始,将类分成单独的文件很容易。
实际上,可以使用块内import
的语句来将方法导入类中。例如,class ClassName:
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
。
要实现a
和b
,请执行以下操作:
_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_CHECKING
是True
类型检查器读取代码时的情况。由于类型检查器不需要执行代码,因此当循环导入出现在块内时,它们是可以的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"
self.a()
在 VS Code 中,我可以看到悬停时
检测到的类型:
一切按预期进行:
>>> 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
并引用上述类型声明。
解决方案 6:
事实证明,我最初的尝试也非常接近解决方案。这是我目前正在使用的:
# 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
...
请注意,import withinif False
语句永远不会被导入(但 IDE 无论如何都知道它)并且使用该类Main
作为字符串,因为它在运行时是未知的。
解决方案 7:
我建议重构你的代码,就像其他人建议的那样。
我可以向你展示我最近遇到的一个循环错误:
前:
# 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
扫码咨询,免费领取项目管理大礼包!