Suggerimenti sul tipo Python senza importazioni cicliche


108

Sto cercando di dividere la mia enorme classe in due; beh, fondamentalmente nella classe "principale" e un mixin con funzioni aggiuntive, in questo modo:

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

Ora, mentre funziona bene, il suggerimento sul tipo MyMixin.func2ovviamente non può funzionare. Non posso importare main.py, perché otterrei un'importazione ciclica e senza il suggerimento, il mio editor (PyCharm) non può dire cosa selfsia.

Sto usando Python 3.4, disposto a passare a 3.5 se una soluzione è disponibile lì.

C'è un modo per dividere la mia classe in due file e mantenere tutte le "connessioni" in modo che il mio IDE mi offra ancora il completamento automatico e tutte le altre chicche che ne derivano conoscendo i tipi?


2
Non penso che normalmente dovresti aver bisogno di annotare il tipo di self, poiché sarà sempre una sottoclasse della classe corrente (e qualsiasi sistema di controllo del tipo dovrebbe essere in grado di capirlo da solo). Sta func2tentando di chiamare func1, che non è definito in MyMixin? Forse dovrebbe essere (come abstractmethod, forse)?
Blckknght

nota anche che generalmente le classi più specifiche (ad esempio il tuo mixin) dovrebbero andare a sinistra delle classi base nella definizione della classe, cioè in class Main(MyMixin, SomeBaseClass)modo che i metodi della classe più specifica possano sovrascrivere quelli della classe base
Anentropic

3
Non sono sicuro di quanto questi commenti siano utili, poiché sono tangenziali alla domanda che viene posta. velis non stava chiedendo una revisione del codice.
Jacob Lee

I suggerimenti di tipo Python con metodi di classe importati forniscono un'elegante soluzione al tuo problema.
Ben Mares

Risposte:


166

Non esiste un modo estremamente elegante per gestire i cicli di importazione in generale, temo. Le tue scelte sono di riprogettare il tuo codice per rimuovere la dipendenza ciclica o, se non è fattibile, fare qualcosa del genere:

# some_file.py

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from main import Main

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

La TYPE_CHECKINGcostante è sempre Falsein fase di esecuzione, quindi l'importazione non verrà valutata, ma mypy (e altri strumenti di controllo del tipo) valuteranno il contenuto di quel blocco.

Abbiamo anche bisogno di trasformare l' Mainannotazione del tipo in una stringa, dichiarandola effettivamente in avanti poiché il Mainsimbolo non è disponibile in fase di esecuzione.

Se stai usando Python 3.7+, possiamo almeno evitare di dover fornire un'annotazione di stringa esplicita sfruttando 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):
        ...

Il from __future__ import annotations importazione renderà tutti i suggerimenti di tipo stringhe e ignorerà la loro valutazione. Questo può aiutare a rendere il nostro codice qui leggermente più ergonomico.

Detto questo, l'uso di mixin con mypy richiederà probabilmente un po 'più di struttura rispetto a quella che hai attualmente. Mypy consiglia un approccio che è fondamentalmente ciò che decezesta descrivendo: creare un ABC che ereditano sia le tue Mainche le MyMixinclassi. Non sarei sorpreso se finissi per dover fare qualcosa di simile per rendere felice il controllore di Pycharm.


3
Grazie per questo. Il mio attuale Python 3.4 non ha typing, ma anche PyCharm era abbastanza soddisfatto if False:.
velis

L'unico problema è che non riconosce MyObject come un modello Django.Model e quindi si lamenta degli attributi di istanza definiti al di fuori di__init__
velis

Ecco il pep corrispondente per typing. TYPE_CHECKING : python.org/dev/peps/pep-0484/#runtime-or-type-checking
Conchylicultor

24

Per le persone che lottano con le importazioni cicliche durante l'importazione della classe solo per il controllo del tipo: probabilmente vorrai usare un riferimento diretto (PEP 484 - Suggerimenti sul tipo):

Quando un suggerimento di tipo contiene nomi che non sono stati ancora definiti, quella definizione può essere espressa come una stringa letterale, per essere risolta in seguito.

Quindi invece di:

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

tu fai:

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

Potrebbe essere PyCharm. Stai usando la versione più recente? Hai provato File -> Invalidate Caches?
Tomasz Bartkowiak

Grazie. Scusa, avevo cancellato il mio commento. Aveva detto che questo funziona, ma PyCharm si lamenta. Ho risolto utilizzando l'hack if False suggerito da Velis . L'invalidazione della cache non l'ha risolto. Probabilmente è un problema di PyCharm.
Jacob Lee

1
@JacobLee Invece di if False:puoi anche from typing import TYPE_CHECKINGe if TYPE_CHECKING:.
luckydonald

11

Il problema più grande è che i tuoi tipi non sono sani di mente all'inizio. MyMixinfa un'ipotesi hardcoded che verrà mescolata inMain , mentre potrebbe essere mescolata in un numero qualsiasi di altre classi, nel qual caso probabilmente si spezzerebbe. Se il tuo mixin è codificato per essere mescolato in una classe specifica, puoi anche scrivere i metodi direttamente in quella classe invece di separarli.

Per farlo correttamente con una digitazione sana, MyMixindovrebbe essere codificato su un'interfaccia o una classe astratta nel linguaggio 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
Beh, non sto dicendo che la mia soluzione sia ottima. È proprio quello che sto cercando di fare per rendere il codice più gestibile. Il tuo suggerimento potrebbe passare, ma questo significherebbe in realtà spostare l'intera classe Main sull'interfaccia nel mio caso specifico .
velis

3

È venuto fuori che anche il mio tentativo originale era abbastanza vicino alla soluzione. Questo è quello che sto usando attualmente:

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

Nota l' if Falseistruzione import all'interno che non viene mai importata (ma IDE lo sa comunque) e utilizza la Mainclasse come stringa perché non è nota in fase di esecuzione.


Mi aspetto che questo provochi un avviso sul codice morto.
Phil

@Phil: sì, all'epoca stavo usando Python 3.4. Ora c'è la digitazione.TYPE_CHECKING
velis

-4

Penso che il modo perfetto dovrebbe essere quello di importare tutte le classi e le dipendenze in un file (come __init__.py) e poi from __init__ import *in tutti gli altri file.

In questo caso lo sei

  1. evitando più riferimenti a quei file e classi e
  2. anche solo aggiungere una riga in ciascuno degli altri file e
  3. il terzo sarebbe il pycharm che conosce tutte le classi che potresti usare.

1
significa che stai caricando tutto ovunque, se hai una libreria piuttosto pesante significa che per ogni importazione devi caricare l'intera libreria. + il riferimento funzionerà molto lentamente.
Omer Shacham

> significa che stai caricando tutto ovunque. >>>> assolutamente no se hai molti file " init .py" o altri, ed eviti import *, e puoi comunque sfruttare questo facile approccio
Sławomir Lenart
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.