我想把我庞大的班级分成两部分;好吧,基本上变成了“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仍然为我提供自动补全和所有其他来自它知道类型的好处?
一般来说,恐怕没有一种非常优雅的方法来处理导入周期。你的选择是重新设计你的代码以删除循环依赖,或者如果这是不可行的,就像这样做:
# 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,因此导入不会被计算,但myypy(和其他类型检查工具)将计算该块的内容。
我们还需要将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注释导入将使所有类型提示都成为字符串,并跳过对它们求值。这有助于使我们的代码稍微更符合人体工程学。
综上所述,在mypy中使用mixins可能需要比当前更多的结构。myypy推荐一种方法,基本上就是欺骗所描述的方法——创建一个Main类和MyMixin类都继承的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类作为字符串,因为它在运行时不知道。
从Python 3.5开始,将类分解为单独的文件就很容易了。
实际上可以在类ClassName: block中使用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。
要实现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"
在VS Code中,当悬停时,我可以看到从self.a()检测到的类型:
一切都按照预期运行:
>>> from class_def import C
>>> c = C()
>>> c.x()
'hello hello goodbye'
关于旧Python版本的注释
对于≤3.4的Python版本,TYPE_CHECKING没有定义,因此这个解决方案将不起作用。
对于≤3.6的Python版本,没有定义延迟注释。作为一种变通方法,省略__future__中的导入注释,并引用上面提到的类型声明。
对于那些在只为了类型检查而导入类时挣扎于循环导入的人:你可能会想要使用Forward Reference (PEP 484 - Type Hints):
当类型提示包含尚未定义的名称时,该定义可以表示为稍后解析的字符串字面值。
所以不要:
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
一般来说,恐怕没有一种非常优雅的方法来处理导入周期。你的选择是重新设计你的代码以删除循环依赖,或者如果这是不可行的,就像这样做:
# 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,因此导入不会被计算,但myypy(和其他类型检查工具)将计算该块的内容。
我们还需要将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注释导入将使所有类型提示都成为字符串,并跳过对它们求值。这有助于使我们的代码稍微更符合人体工程学。
综上所述,在mypy中使用mixins可能需要比当前更多的结构。myypy推荐一种方法,基本上就是欺骗所描述的方法——创建一个Main类和MyMixin类都继承的ABC。如果你最终需要做一些类似的事情来让Pycharm的检查者高兴,我不会感到惊讶。