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


108

我正试图将我的大班分成两部分;好吧,基本上是进入“主”类和具有其他功能的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仍然可以自动完成以及知道类型的所有其他优点。


2
我认为您通常不需要注释的类型self,因为它总是将成为当前类的子类(并且任何类型检查系统都应该能够自己找出原因)。是否在func2尝试调用func1,未在中定义MyMixin?也许应该(作为一个abstractmethod,也许)?
Blckknght

还应注意,通常,更特定类的类(例如您的mixin)应移到类定义中基类的左侧,即,class Main(MyMixin, SomeBaseClass)以便更特定类的方法可以覆盖基类中的方法
Anentropic

3
我不确定这些评论的用处,因为它们与提出的问题有关。velis并未要求进行代码审查。
雅各布·李

带有导入类方法的Python类型提示为您的问题提供了一种优雅的解决方案。
Ben Mares

Answers:


166

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

# 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所描述的方法-创建一个ABC,您的类MainMyMixin类都继承。如果您最终需要做一些类似的事情以使Pycharm的检查器满意,我不会感到惊讶。


3
谢谢你 我目前的python 3.4没有typing,但PyCharm也很满意if False:
velis

唯一的问题是,它无法将MyObject识别为Django的models.Model,因此关于实例属性在__init__
velis

这是对应的pep typing. TYPE_CHECKING python.org/dev/peps/pep-0484/#runtime-or-type-checking
Conchylicultor

24

对于仅在导入类以进行类型检查时陷入困境的人们:您可能希望使用前向引用(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

可能是PyCharm。您使用的是最新版本吗?你试过了File -> Invalidate Caches吗?
Tomasz Bartkowiak

谢谢。抱歉,我已删除我的评论。它曾提到这样做有效,但是PyCharm抱怨。我使用Velis建议的if False hack解决。使缓存无效不能解决它。这可能是PyCharm问题。
雅各布·李

1
@JacobLee而不是if False:您也可以from typing import TYPE_CHECKINGif TYPE_CHECKING:
luckydonald

11

更大的问题是,您的类型一开始并不理智。MyMixin进行硬编码的假设是将其混合到中Main,而可以将其混合到任何其他数量的类中,在这种情况下,它可能会损坏。如果将mixin硬编码为混合到一个特定的类中,则不妨将方法直接写入该类中,而不用将它们分开。

要使用合理的输入方式正确执行此操作,MyMixin应使用Python的说法对interface或abstract 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')

1
好吧,我并不是说我的解决方案很棒。这就是我为了使代码更易于管理而正在尝试做的事情。您的建议可能会通过,但这实际上意味着在我的特定情况下,只是将整个Main类移至接口。
velis

3

事实证明,我最初的尝试也非常接近解决方案。这是我目前正在使用的:

# 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 inside if False语句永远不会被导入(但IDE仍然知道它),并且将该Main类用作字符串,因为在运行时不知道。


我希望这会引起有关无效代码的警告。
Phil

@Phil:是的,当时我正在使用Python 3.4。现在有typing.TYPE_CHECKING
威利斯

-4

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

在这种情况下

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

1
这意味着您要在所有地方加载所有内容,如果您有一个非常庞大的库,这意味着对于每次导入,您都需要加载整个库。+参考将超级慢。
Omer Shacham '19年

>这意味着您正在到处加载内容。>>>>绝对不行,如果你有很多的“ 初始化的.py”或其他文件,并避免import *,而且还可以利用这个简单的方法
斯瓦沃米尔Lenart
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.