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

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

我正试图将我的大班分成两部分;好吧,基本上是进入“主”类和具有其他功能的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。

有什么办法可以将我的Class分成两个文件并保留所有“连接”,以便我的IDE仍然可以自动完成以及知道类型的所有其他优点。

I’m trying to split my huge class into two; well, basically into the “main” class and a mixin with additional functions, like so:

main.py file:

import mymixin.py

class Main(object, MyMixin):
    def func1(self, xxx):
        ...

mymixin.py file:

class MyMixin(object):
    def func2(self: Main, xxx):  # <--- note the type hint
        ...

Now, while this works just fine, the type hint in MyMixin.func2 of course can’t work. I can’t import main.py, because I’d get a cyclic import and without the hint, my editor (PyCharm) can’t tell what self is.

I’m using Python 3.4, willing to move to 3.5 if a solution is available there.

Is there any way I can split my class into two files and keep all the “connections” so that my IDE still offers me auto completion & all the other goodies that come from it knowing the types?


回答 0

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

# 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的检查器满意,我不会感到惊讶。

There isn’t a hugely elegant way to handle import cycles in general, I’m afraid. Your choices are to either redesign your code to remove the cyclic dependency, or if it isn’t feasible, do something like this:

# some_file.py

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from main import Main

class MyObject(object):
    def func2(self, some_param: 'Main'):
        ...

The TYPE_CHECKING constant is always False at runtime, so the import won’t be evaluated, but mypy (and other type-checking tools) will evaluate the contents of that block.

We also need to make the Main type annotation into a string, effectively forward declaring it since the Main symbol isn’t available at runtime.

If you are using Python 3.7+, we can at least skip having to provide an explicit string annotation by taking advantage of 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):
        ...

The from __future__ import annotations import will make all type hints be strings and skip evaluating them. This can help make our code here mildly more ergonomic.

All that said, using mixins with mypy will likely require a bit more structure then you currently have. Mypy recommends an approach that’s basically what deceze is describing — to create an ABC that both your Main and MyMixin classes inherit. I wouldn’t be surprised if you ended up needing to do something similar in order to make Pycharm’s checker happy.


回答 1

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

For people struggling with cyclic imports when importing class only for Type checking: you will likely want to use a Forward Reference (PEP 484 – Type Hints):

When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later.

So instead of:

class Tree:
    def __init__(self, left: Tree, right: Tree):
        self.left = left
        self.right = right

you do:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right

回答 2

更大的问题是,您的类型一开始并不理智。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')

The bigger issue is that your types aren’t sane to begin with. MyMixin makes a hardcoded assumption that it will be mixed into Main, whereas it could be mixed into any number of other classes, in which case it would probably break. If your mixin is hardcoded to be mixed into one specific class, you may as well write the methods directly into that class instead of separating them out.

To properly do this with sane typing, MyMixin should be coded against an interface, or abstract class in Python parlance:

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')

回答 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类用作字符串,因为在运行时不知道。

Turns out my original attempt was quite close to the solution as well. This is what I’m currently using:

# 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
        ...

Note the import within if False statement that never gets imported (but IDE knows about it anyway) and using the Main class as string because it’s not known at runtime.


回答 4

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

在这种情况下

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

I think the perfect way should be to import all the classes and dependencies in a file (like __init__.py) and then from __init__ import * in all the other files.

In this case you are

  1. avoiding multiple references to those files and classes and
  2. also only have to add one line in each of the other files and
  3. the third would be the pycharm knowing about all of the classes that you might use.