我正试图将我的大班分成两部分;好吧,基本上进入“主”类和带有附加功能的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 仍然为我提供自动完成功能以及所有其他来自它的知道类型的好东西?
self
的类型,因为它始终是当前类的子类(并且任何类型检查系统都应该能够自行解决)。 func2
是否正在尝试调用 func1
,而这在 MyMixin
中没有定义?也许应该是(也许是abstractmethod
)?
class Main(MyMixin, SomeBaseClass)
以便来自更具体类的方法可以覆盖来自基类的方法
恐怕没有一种非常优雅的方式来处理一般的导入周期。您的选择是重新设计代码以删除循环依赖,或者如果不可行,请执行以下操作:
# 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 类型提示成为字符串并跳过评估它们。这可以帮助我们的代码稍微更符合人体工程学。
综上所述,在 mypy 中使用 mixins 可能需要比目前更多的结构。 Mypy recommends an approach 基本上就是 deceze
所描述的 - 创建一个您的 Main
和 MyMixin
类都继承的 ABC。如果您最终需要做类似的事情来让 Pycharm 的检查器满意,我不会感到惊讶。
对于仅在为类型检查导入类时遇到循环导入问题的人:您可能希望使用 Forward Reference(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
File -> Invalidate Caches
吗?
if False:
,您还可以使用 from typing import TYPE_CHECKING
和 if TYPE_CHECKING:
。
更大的问题是你的类型一开始就不健全。 MyMixin
做了一个硬编码假设,即它会混入 Main
,而它可能会混入任意数量的其他类,在这种情况下它可能会中断。如果您的 mixin 被硬编码为混合到一个特定的类中,那么您最好将方法直接写入该类,而不是将它们分开。
要使用合理的输入正确执行此操作,应针对 interface 或 Python 用语中的抽象类对 MyMixin
进行编码:
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')
Main
和 MyMixin
分别在文件 main.py 和 mymixin.py 中分开,我想这必然意味着创建第三个文件 api.py 持有 MixinDependencyInterface
,不是吗?
typing.Protocol
可以代替 abc.ABC
使用,因为您实际上不需要子类化它来注册它。这是提供您计划使用的接口的正确方法,而 abc.ABC
更适合您提供部分完成的实现,即您实际上想要子类化它。
原来我最初的尝试也非常接近解决方案。这是我目前正在使用的:
# 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
语句中的 import 永远不会被导入(但 IDE 无论如何都知道它)并使用 Main
类作为字符串,因为它在运行时不知道。
从 Python 3.5 开始,将类分解为单独的文件很容易。
实际上,可以在 class ClassName:
块的 inside 中使用 import
语句,以便将方法导入类。例如,
class_def.py
:
class C:
from _methods1 import a
from _methods2 import b
def x(self):
return self.a() + " " + self.b()
在我的例子中,
Ca() 将是一个返回字符串 hello 的方法
Cb() 将是一个返回 hello goodbye 的方法
Cx() 将因此返回 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__
导入启用 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"
https://i.stack.imgur.com/FquCl.png
一切都按预期运行:
>>> 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.Foo
或 bar.Bar
,因为您的函数/方法应该只调用一次 foo.Foo
并且 bar.Bar
可以使用。
正如其他一些人所建议的那样,我建议重构您的代码。
我可以向您展示我最近遇到的一个循环错误:
前:
# 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
我认为完美的方法应该是将所有类和依赖项导入一个文件(如 __init__.py
),然后在所有其他文件中导入 from __init__ import *
。
在这种情况下,您是
避免对这些文件和类的多次引用,并且只需要在每个其他文件中添加一行,第三个是 pycharm 知道您可能使用的所有类。
import *
,则绝对不会,您仍然可以利用这种简单的方法
不定期副业成功案例分享
typing
,但 PyCharm 对if False:
也很满意。__init__
之外定义的实例属性typing. TYPE_CHECKING
的相应 pep:python.org/dev/peps/pep-0484/#runtime-or-type-checking