Python-Typ ohne zyklische Importe


107

Ich versuche meine große Klasse in zwei Teile zu teilen. Nun, im Grunde in die "Haupt" -Klasse und ein Mixin mit zusätzlichen Funktionen, wie so:

main.py Datei:

import mymixin.py

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

mymixin.py Datei:

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

Nun, obwohl dies gut funktioniert, kann der Typhinweis MyMixin.func2natürlich nicht funktionieren. Ich kann nicht importieren main.py, da ich einen zyklischen Import erhalten würde und mein Editor (PyCharm) ohne den Hinweis nicht sagen kann, was selfist.

Ich verwende Python 3.4 und bin bereit, auf 3.5 umzusteigen, wenn dort eine Lösung verfügbar ist.

Gibt es eine Möglichkeit, meine Klasse in zwei Dateien aufzuteilen und alle "Verbindungen" beizubehalten, sodass meine IDE mir weiterhin die automatische Vervollständigung und alle anderen Extras bietet, die daraus entstehen, wenn ich die Typen kenne?


2
Ich denke nicht, dass Sie normalerweise den Typ von kommentieren müssen self, da es immer eine Unterklasse der aktuellen Klasse sein wird (und jedes Typprüfsystem sollte in der Lage sein, dies selbst herauszufinden). Wird func2versucht, Anruf func1, der nicht in definiert ist MyMixin? Vielleicht sollte es sein (als abstractmethod, vielleicht)?
Blckknght

Beachten Sie auch, dass allgemein spezifischere Klassen (z. B. Ihr Mixin) in der Klassendefinition links von Basisklassen stehen sollten, class Main(MyMixin, SomeBaseClass)damit Methoden aus der spezifischeren Klasse diejenigen aus der Basisklasse
Anentropic

3
Ich bin mir nicht sicher, wie nützlich diese Kommentare sind, da sie die gestellte Frage tangieren. velis bat nicht um eine Codeüberprüfung.
Jacob Lee

Hinweise zum Python-Typ mit importierten Klassenmethoden bieten eine elegante Lösung für Ihr Problem.
Ben Mares

Antworten:


162

Ich fürchte, es gibt keine äußerst elegante Möglichkeit, Importzyklen im Allgemeinen zu handhaben. Sie können entweder Ihren Code neu gestalten, um die zyklische Abhängigkeit zu entfernen, oder, wenn dies nicht möglich ist, Folgendes tun:

# some_file.py

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from main import Main

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

Die TYPE_CHECKINGKonstante ist immer Falsezur Laufzeit, daher wird der Import nicht ausgewertet, aber mypy (und andere Tools zur Typprüfung) werten den Inhalt dieses Blocks aus.

Wir müssen auch das machen Main Typanmerkung auch in eine Zeichenfolge umwandeln und sie effektiv weiterleiten, da das MainSymbol zur Laufzeit nicht verfügbar ist.

Wenn Sie Python 3.7+ verwenden, können wir zumindest die Bereitstellung einer expliziten Zeichenfolgenanmerkung überspringen, indem wir PEP 563 nutzen :

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

Das from __future__ import annotations Import werden alle Typhinweise zu Zeichenfolgen und überspringen die Auswertung. Dies kann dazu beitragen, unseren Code hier etwas ergonomischer zu gestalten.

Alles in allem erfordert die Verwendung von Mixins mit mypy wahrscheinlich etwas mehr Struktur als derzeit. Mypy empfiehlt einen Ansatz , der im Grunde genommen decezebeschreibt - ein ABC zu erstellen, das sowohl Sie Mainals auch Ihre MyMixinKlassen erben. Es würde mich nicht wundern, wenn Sie etwas Ähnliches tun müssten, um Pycharms Kontrolleur glücklich zu machen.


3
Danke dafür. Mein aktuelles Python 3.4 hat es nicht typing, aber PyCharm war auch ziemlich zufrieden damit if False:.
Velis

Das einzige Problem ist, dass es MyObject nicht als Django-Modell erkennt. Modell und damit Nörgelei über Instanzattribute, die außerhalb von__init__
velis

Hier ist der entsprechende Pep für typing. TYPE_CHECKING : python.org/dev/peps/pep-0484/#runtime-or-type-checking
Conchylicultor

24

Für Personen, die mit zyklischen Importen zu kämpfen haben, wenn sie eine Klasse nur zur Typprüfung importieren: Sie möchten wahrscheinlich eine Weiterleitungsreferenz verwenden (PEP 484 - Typhinweise):

Wenn ein Typhinweis Namen enthält, die noch nicht definiert wurden, kann diese Definition als Zeichenfolgenliteral ausgedrückt werden, um später aufgelöst zu werden.

Also statt:

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

Sie machen:

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

Könnte PyCharm sein. Verwenden Sie die neueste Version? Hast du es versucht File -> Invalidate Caches?
Tomasz Bartkowiak

Vielen Dank. Entschuldigung, ich hatte meinen Kommentar gelöscht. Es hatte erwähnt, dass dies funktioniert, aber PyCharm beschwert sich. Ich habe mich entschlossen, den von Velis vorgeschlagenen if False-Hack zu verwenden . Durch die Ungültigmachung des Caches wurde das Problem nicht behoben. Es ist wahrscheinlich ein PyCharm-Problem.
Jacob Lee

1
@JacobLee Anstelle von if False:dir kannst du auch from typing import TYPE_CHECKINGund if TYPE_CHECKING:.
Luckydonald

11

Das größere Problem ist, dass Ihre Typen zunächst nicht gesund sind. MyMixinmacht eine fest codierte Annahme, dass es eingemischt wirdMain in eine beliebige Anzahl anderer Klassen eingemischt wird, in welchem ​​Fall es wahrscheinlich brechen würde. Wenn Ihr Mixin fest codiert ist, um in eine bestimmte Klasse gemischt zu werden, können Sie die Methoden auch direkt in diese Klasse schreiben, anstatt sie zu trennen.

Um dies mit vernünftiger Typisierung richtig zu tun, MyMixinsollte es gegen eine Schnittstelle oder eine abstrakte Klasse im Python-Sprachgebrauch codiert werden :

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
Nun, ich sage nicht, dass meine Lösung großartig ist. Es ist genau das, was ich versuche, um den Code übersichtlicher zu machen. Ihr Vorschlag könnte erfolgreich sein, aber dies würde in meinem speziellen Fall bedeuten, nur die gesamte Hauptklasse auf die Schnittstelle zu verschieben .
Velis

3

Es stellte sich heraus, dass mein ursprünglicher Versuch auch der Lösung ziemlich nahe kam. Folgendes verwende ich derzeit:

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

Beachten Sie den Import innerhalb der if FalseAnweisung, der nie importiert wird (aber IDE weiß es trotzdem), und verwenden Sie die MainKlasse als Zeichenfolge, da sie zur Laufzeit nicht bekannt ist.


Ich würde erwarten, dass dies eine Warnung vor totem Code auslöst.
Phil

@Phil: Ja, zu der Zeit habe ich Python 3.4 verwendet. Jetzt gibt es typing.TYPE_CHECKING
velis

-4

Ich denke, der perfekte Weg sollte sein, alle Klassen und Abhängigkeiten in eine Datei (wie __init__.py) und dann from __init__ import *in alle anderen Dateien zu importieren.

In diesem Fall bist du

  1. Vermeiden mehrfacher Verweise auf diese Dateien und Klassen und
  2. müssen auch nur eine Zeile in jede der anderen Dateien und hinzufügen
  3. Der dritte wäre der Pycharm, der alle Klassen kennt, die Sie verwenden könnten.

1
Es bedeutet, dass Sie alles überall laden. Wenn Sie eine ziemlich schwere Bibliothek haben, bedeutet dies, dass Sie für jeden Import die gesamte Bibliothek laden müssen. + Die Referenz wird sehr langsam arbeiten.
Omer Shacham

> es bedeutet, dass Sie alles überall laden. >>>> absolut nicht, wenn Sie viele " init .py" - oder andere Dateien haben und vermeiden import *, und dennoch können Sie diesen einfachen Ansatz nutzen
Sławomir Lenart
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.