Підказка типу 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.