Indication de type Python sans importations cycliques

109

J'essaye de diviser ma classe énorme en deux; enfin, essentiellement dans la classe "main" et un mixin avec des fonctions supplémentaires, comme ceci:

main.py fichier:

import mymixin.py

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

mymixin.py fichier:

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

Maintenant, bien que cela fonctionne très bien, l'indication de type MyMixin.func2ne peut bien sûr pas fonctionner. Je ne peux pas importer main.py, car j'obtiendrais une importation cyclique et sans l'indice, mon éditeur (PyCharm) ne peut pas dire ce que selfc'est.

J'utilise Python 3.4, prêt à passer à 3.5 si une solution y est disponible.

Est-il possible que je puisse diviser ma classe en deux fichiers et conserver toutes les "connexions" afin que mon IDE me propose toujours la complétion automatique et tous les autres avantages qui en découlent en connaissant les types?

velis
la source
2
Je ne pense pas que vous devriez normalement avoir besoin d'annoter le type de self, car ce sera toujours une sous-classe de la classe actuelle (et tout système de vérification de type devrait être capable de le comprendre seul). Est la func2tentative d'appel func1, qui n'est pas défini dans MyMixin? Peut-être que cela devrait être (en tant que abstractmethod, peut-être)?
Blckknght
notez également que les classes généralement plus spécifiques (par exemple votre mixin) doivent aller à gauche des classes de base dans la définition de classe, c'est-à-dire class Main(MyMixin, SomeBaseClass)que les méthodes de la classe plus spécifique peuvent remplacer celles de la classe de base
Anentropic
3
Je ne sais pas en quoi ces commentaires sont utiles, car ils sont tangentiels à la question posée. velis ne demandait pas de révision de code.
Jacob Lee
Les conseils de type Python avec des méthodes de classe importées fournissent une solution élégante à votre problème.
Ben Mares

Réponses:

167

Il n'y a pas de moyen extrêmement élégant de gérer les cycles d'importation en général, j'en ai peur. Vos choix sont soit de reconcevoir votre code pour supprimer la dépendance cyclique, soit si ce n'est pas faisable, faites quelque chose comme ceci:

# 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_CHECKINGconstante est toujours Falseà l'exécution, donc l'importation ne sera pas évaluée, mais mypy (et d'autres outils de vérification de type) évaluera le contenu de ce bloc.

Nous devons également transformer l' Mainannotation de type en une chaîne, en la déclarant effectivement car le Mainsymbole n'est pas disponible au moment de l'exécution.

Si vous utilisez Python 3.7+, nous pouvons au moins éviter d'avoir à fournir une annotation de chaîne explicite en tirant parti de 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):
        ...

L' from __future__ import annotationsimportation fera de toutes les indications de type des chaînes et ignorera leur évaluation. Cela peut aider à rendre notre code ici légèrement plus ergonomique.

Cela dit, l'utilisation de mixins avec mypy nécessitera probablement un peu plus de structure que celle que vous avez actuellement. Mypy recommande une approche qui est essentiellement ce que decezedécrit - pour créer un ABC dont votre Mainet vos MyMixinclasses héritent. Je ne serais pas surpris si vous deviez faire quelque chose de similaire pour rendre le vérificateur de Pycharm heureux.

Michael0x2a
la source
3
Merci pour cela. Mon python 3.4 actuel ne l'a pas typing, mais PyCharm en était également très satisfait if False:.
velis
Le seul problème est qu'il ne reconnaît pas MyObject comme Django models.Model et bourrins donc sur les attributs d' instance étant définie en dehors de__init__
velis
Voici le pep correspondant pour typing. TYPE_CHECKING : python.org/dev/peps/pep-0484/#runtime-or-type-checking
Conchylicultor
25

Pour les personnes aux prises avec des importations cycliques lors de l'importation d'une classe uniquement pour la vérification de type: vous souhaiterez probablement utiliser une référence directe (PEP 484 - Indices de type):

Lorsqu'un indice de type contient des noms qui n'ont pas encore été définis, cette définition peut être exprimée sous forme de chaîne littérale, à résoudre ultérieurement.

Donc au lieu de:

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

tu fais:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right
Tomasz Bartkowiak
la source
Peut-être PyCharm. Utilisez-vous la dernière version? Avez-vous essayé File -> Invalidate Caches?
Tomasz Bartkowiak
Merci. Désolé, j'avais supprimé mon commentaire. Il avait mentionné que cela fonctionne, mais PyCharm se plaint. J'ai résolu en utilisant le hack if False suggéré par Velis . L'invalidation du cache ne l'a pas résolu. C'est probablement un problème avec PyCharm.
Jacob Lee
1
@JacobLee Au lieu de if False:vous pouvez aussi from typing import TYPE_CHECKINGet if TYPE_CHECKING:.
luckydonald
11

Le plus gros problème est que vos types ne sont pas sains d'esprit au départ. MyMixinfait une hypothèse codée en dur dans laquelle il sera mélangé Main, alors qu'il pourrait être mélangé dans n'importe quel nombre d'autres classes, auquel cas il se briserait probablement. Si votre mixin est codé en dur pour être mélangé dans une classe spécifique, vous pouvez aussi bien écrire les méthodes directement dans cette classe au lieu de les séparer.

Pour faire cela correctement avec un typage sain, MyMixindoit être codé par rapport à une interface ou à une classe abstraite dans le langage 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')
déceler
la source
1
Eh bien, je ne dis pas que ma solution est excellente. C'est exactement ce que j'essaye de faire pour rendre le code plus gérable. Votre suggestion pourrait passer, mais cela signifierait en fait simplement déplacer toute la classe Main vers l'interface dans mon cas spécifique .
velis
3

Il s'avère que ma tentative initiale était également assez proche de la solution. Voici ce que j'utilise actuellement:

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

Notez l' if Falseinstruction import within qui n'est jamais importée (mais IDE le sait de toute façon) et utilisez la Mainclasse comme chaîne car elle n'est pas connue au moment de l'exécution.

velis
la source
Je m'attendrais à ce que cela provoque un avertissement sur le code mort.
Phil
@Phil: oui, à l'époque j'utilisais Python 3.4. Maintenant, il y a taper.TYPE_CHECKING
velis
-4

Je pense que le moyen idéal devrait être d'importer toutes les classes et dépendances dans un fichier (comme __init__.py), puis from __init__ import *dans tous les autres fichiers.

Dans ce cas, vous êtes

  1. éviter de multiples références à ces fichiers et classes et
  2. il suffit également d'ajouter une ligne dans chacun des autres fichiers et
  3. le troisième serait le pycharm connaissant toutes les classes que vous pourriez utiliser.
AmirHossein
la source
1
cela signifie que vous chargez tout partout, si vous avez une bibliothèque assez lourde, cela signifie que pour chaque importation, vous devez charger toute la bibliothèque. + la référence fonctionnera très lentement.
Omer Shacham
> cela signifie que vous chargez tout partout. >>>> absolument pas si vous avez beaucoup de " init .py" ou d'autres fichiers, et évitez import *, et vous pouvez toujours profiter de cette approche facile
Sławomir Lenart