Підказка типу Python без циклічного імпорту


110

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

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
Я не впевнений, наскільки ці коментарі корисні, оскільки вони стосуються поставленого питання. velis не просив перевірити код.
Jacob Lee

Підказки типу Python за допомогою імпортованих методів класів забезпечують елегантне рішення вашої проблеми.
Бен Марес

Відповіді:


168

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

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


4
Дякую за це. Мій поточний python 3.4 не має typing, але PyCharm також був цілком задоволений if False:.
Веліс

Єдина проблема полягає в тому , що він не визнає MyObject як Джанго models.Model і , таким чином , бурчить про атрибутах примірника визначається поза__init__
Velis

Ось відповідний бадьорість для typing. TYPE_CHECKING : python.org/dev/peps/pep-0484/#runtime-or-type-checking
Conchylicultor

25

Для людей, які борються з циклічним імпортом під час імпортування класу лише для перевірки типу: ви, швидше за все, захочете скористатися прямим посиланням (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?
Томаш

Дякую. Вибачте, я видалив свій коментар. Він згадав, що це працює, але PyCharm скаржиться. Я вирішив скористатися хакерською системою if False, запропонованою Velis . Недійсне кешування не допомогло. Ймовірно, це проблема PyCharm.
Jacob Lee

1
@JacobLee Замість if False:вас також можетеfrom typing import TYPE_CHECKING і if TYPE_CHECKING:.
luckydonald

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оператора, який ніколи не імпортується (але IDE все одно знає про це), і використовуючи Mainклас як рядок, оскільки він не відомий під час виконання.


Я очікую, що це спричинить попередження про мертвий код.
Філ

@Phil: так, на той час я використовував Python 3.4. Тепер набрано
тексту.

-4

Я думаю, що ідеальним способом повинен бути імпорт усіх класів та залежностей у файл (наприклад __init__.py), а потім from __init__ import *у всі інші файли.

У цьому випадку ви

  1. уникаючи багаторазових посилань на ці файли та класи та
  2. також потрібно лише додати один рядок у кожному з інших файлів і
  3. третім буде pycharm, який знає про всі класи, якими ви можете скористатися.

1
це означає, що ви завантажуєте все всюди, якщо у вас досить важка бібліотека, це означає, що для кожного імпорту вам потрібно завантажувати всю бібліотеку. + посилання буде працювати дуже повільно.
Omer

> це означає, що ви завантажуєте все скрізь. >>>> абсолютно ні, якщо у вас багато файлів " init .py" чи інших файлів, і уникайте import *, і все ж ви можете скористатися цим простим підходом
Славомір Ленарт,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.