Подсказка типов Python без циклического импорта

123

Я пытаюсь разделить свой огромный класс на два; ну, в основном в "основной" класс и миксин с дополнительными функциями, например так:

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
также обратите внимание, что обычно более конкретные классы (например, ваш миксин) должны располагаться слева от базовых классов в определении класса, то есть, class Main(MyMixin, SomeBaseClass)чтобы методы из более конкретного класса могли переопределять методы из базового класса
Anentropic
3
Я не уверен, насколько полезны эти комментарии, поскольку они не имеют отношения к задаваемому вопросу. Велис не просил ревью кода.
Джейкоб Ли
Подсказки типа Python с импортированными методами класса обеспечивают элегантное решение вашей проблемы.
Бен Марес,

Ответы:

179

Боюсь, что в целом не существует очень элегантного способа обработки циклов импорта. Вы можете либо переделать свой код, чтобы удалить циклическую зависимость, либо, если это невозможно, сделать что-то вроде этого:

# 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, вероятно, потребует немного большей структуры, чем у вас есть сейчас. Mypy рекомендует подход, который в основном decezeописывает то, что описывает - создать ABC, который наследуют как ваш, так Mainи MyMixinклассы. Я не удивлюсь, если вам понадобится сделать что-то подобное, чтобы сделать программу проверки Pycharm счастливой.

Майкл0x2a
источник
4
Спасибо за это. В моем текущем python 3.4 его нет typing, но PyCharm тоже остался доволен if False:.
Велис
Единственная проблема заключается в том, что он не распознает MyObject как Django models.Model и, таким образом, ворчит об атрибутах экземпляра, определенных за пределами__init__
velis
Вот соответствующее сообщение для typing. TYPE_CHECKING : python.org/dev/peps/pep-0484/#runtime-or-type-checking
Conchylicultor
29

Для людей, которые борются с циклическим импортом при импорте класса только для проверки типа: вы, вероятно, захотите использовать прямую ссылку (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
Томаш Бартковяк
источник
Может быть PyCharm. Вы используете последнюю версию? Вы пробовали File -> Invalidate Caches?
Tomasz
Спасибо. Извините, я удалил свой комментарий. Он упомянул, что это работает, но PyCharm жалуется. Я решил использовать хак if False, предложенный Велисом . Аннулирование кеша не помогло. Вероятно, это проблема PyCharm.
Джейкоб Ли
1
@JacobLee Вместо if False:тебя можно также from typing import TYPE_CHECKINGи if TYPE_CHECKING:.
счастливчик
11

Более серьезная проблема в том, что ваши типы изначально не в здравом уме. MyMixinделает жестко запрограммированное предположение, что он будет смешан Main, тогда как он может быть смешан с любым количеством других классов, и в этом случае он, вероятно, сломается. Если ваш миксин жестко запрограммирован для смешивания в один конкретный класс, вы также можете записать методы непосредственно в этот класс, а не разделять их.

Чтобы правильно сделать это с разумной типизацией, 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')
обмануть
источник
1
Я не говорю, что мое решение отличное. Это просто то, что я пытаюсь сделать, чтобы сделать код более управляемым. Ваше предложение может быть принято, но на самом деле это будет означать просто перемещение всего класса Main в интерфейс в моем конкретном случае.
Велис
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
        ...

Обратите внимание на if Falseоператор import внутри, который никогда не импортируется (но IDE все равно знает об этом) и использует Mainкласс как строку, потому что он не известен во время выполнения.

Велис
источник
Я ожидал, что это вызовет предупреждение о мертвом коде.
Фил
@Phil: да, в то время я использовал Python 3.4. Теперь набираем
текст.
-4

Я думаю, что идеальный способ - импортировать все классы и зависимости в файл (например, __init__.py), а затем from __init__ import *во все другие файлы.

В этом случае вы

  1. избегая множественных ссылок на эти файлы и классы и
  2. также нужно добавить только одну строку в каждый из других файлов и
  3. третий - это pycharm, знающий обо всех классах, которые вы можете использовать.
Амир Хоссейн
источник
1
это означает, что вы загружаете все везде, а если у вас довольно тяжелая библиотека, это означает, что для каждого импорта вам нужно загружать всю библиотеку. + ссылка будет работать супер медленно.
Омер
> Значит, вы везде грузите все. >>>> Абсолютно нет, если у вас много " init .py" или других файлов, и избегайте import *, и все же вы можете воспользоваться этим простым подходом
Славомир Ленарт