web-dev-qa-db-fra.com

python Importations circulaires à nouveau (alias ce qui ne va pas avec cette conception)

Considérons python (3.x) scripts:

main.py:

from test.team import team
from test.user import user

if __name__ == '__main__':
    u = user()
    t = team()
    u.setTeam(t)
    t.setLeader(u)

test/utilisateur.py:

from test.team import team

class user:
    def setTeam(self, t):
        if issubclass(t, team.__class__):
            self.team = t

test/Team.py:

from test.user import user

class team:
    def setLeader(self, u):
        if issubclass(u, user.__class__):
            self.leader = u

Maintenant, bien sûr, j'ai une importation circulaire et une splendide impororror.

Donc, pas être Pythonista, j'ai trois questions. Tout d'abord:

je. Comment puis-je faire ce problème?

Et, sachant que quelqu'un dira inévitablement que "les importations circulaires indiquent toujours un problème de conception", la deuxième question vient:

iI. Pourquoi ce design est-il mauvais?

Et le final, troisième:

iII. Quelle serait une meilleure alternative?

Pour être précis, le contrôle de type comme ci-dessus n'est qu'un exemple, il existe également une couche d'index basée sur la classe, qui permet à c'est-à-dire. Retrouvez tous les utilisateurs membres d'une équipe (classe d'utilisateurs possède de nombreuses sous-classes. L'index est donc doublé, pour les utilisateurs en général et pour chaque sous-classe spécifique) ou toutes les équipes ayant reçu un utilisateur en tant que membre.

Edit :

J'espère que cet exemple plus détaillé clarifiera ce que j'essaie d'atteindre. Fichiers omis pour la réadapibilité (mais avoir un fichier source de 300 ko me fait peur d'une manière ou d'une autre, alors s'il vous plaît supposer que chaque classe est dans un fichier différent)

# ENTITY

class Entity:
    _id    = None
    _defs  = {}
    _data  = None

    def __init__(self, **kwargs):
        self._id   = uuid.uuid4() # for example. or randint(). or x+1.
        self._data = {}.update(kwargs)

    def __settattr__(self, name, value):
        if name in self._defs:
            if issubclass(value.__class__, self._defs[name]):
                self._data[name] = value

                # more stuff goes here, specially indexing dependencies, so we can 
                # do Index(some_class, name_of_property, some.object) to find all   
                # objects of some_class or its children where
                # given property == some.object

            else:
                raise Exception('Some misleading message')
        else:
            self.__dict__[name] = value    

    def __gettattr__(self, name):
        return self._data[name]

# USERS 

class User(Entity):
    _defs  = {'team':Team}

class DPLUser(User):
    _defs  = {'team':DPLTeam}

class PythonUser(DPLUser)
    pass

class PerlUser(DPLUser)
    pass

class FunctionalUser(User):
    _defs  = {'team':FunctionalTeam}

class HaskellUser(FunctionalUser)
    pass

class ErlangUser(FunctionalUser)
    pass

# TEAMS

class Team(Entity):
    _defs  = {'leader':User}

class DPLTeam(Team):
    _defs  = {'leader':DPLUser}

class FunctionalTeam(Team):
    _defs  = {'leader':FunctionalUser}

et maintenant une certaine utilisation:

t1 = FunctionalTeam()
t2 = DLPTeam()
t3 = Team()

u1 = HaskellUser()
u2 = PythonUser()

t1.leader = u1 # ok
t2.leader = u2 # ok
t1.leader = u2 # not ok, exception
t3.leader = u2 # ok

# now , index

print(Index(FunctionalTeam, 'leader', u2)) # -> [t2]
print(Index(Team, 'leader', u2)) # -> [t2,t3]

Cela fonctionne donc bien (les détails de la mise en œuvre envahis, mais il n'y a rien de compliqué) Outre cette chose impérieuse importation circulaire.

46
ts.

Les importations circulaires ne sont pas intrinsèquement une mauvaise chose. C'est naturel pour le code team sur le code user _ _ user fait quelque chose avec team.

La pire pratique ici est from module import member. Le module team tente d'obtenir la classe user à l'importation-temps, et le module user tente d'obtenir la classe team. Mais la classe team n'existe pas encore parce que vous êtes toujours à la première ligne de team.py lorsque user.py est couru.

Au lieu de cela, importer uniquement des modules. Il en résulte que les espaces de noms plus clairs permettent de corriger la pose de singe ultérieure possible et résout le problème d'importation. Parce que vous n'importez que le module à l'importation, vous ne vous souciez pas que le classe à l'intérieur, il n'est pas encore défini. Au moment où vous vous entamez à l'aide de la classe, ce sera.

Donc, test/utilisateurs.py:

import test.teams

class User:
    def setTeam(self, t):
        if isinstance(t, test.teams.Team):
            self.team = t

test/équipes.py:

import test.users

class Team:
    def setLeader(self, u):
        if isinstance(u, test.users.User):
            self.leader = u

from test import teams puis teams.Team est également correct, si vous voulez écrire test moins. Cela importe toujours un module et non un membre du module.

De plus, si Team et User sont relativement simples, mettez-les dans le même module. Vous n'avez pas besoin de suivre Java Idiome d'une classe-Per-fichier. Les tests isinstance tests et set méthodes SCYTHONIC-JAVA-WART Pour moi; en fonction de ce que vous faites, vous pouvez très bien être mieux en train d'utiliser une simple vérification de type @property.

79
bobince

je. Pour le faire fonctionner, vous pouvez utiliser une importation différée. Une façon serait de laisser user.py seul et de changer d'équipe.py to:

class team:
    def setLeader(self, u):
        from test.user import user
        if issubclass(u, user.__class__):
            self.leader = u

iII. Pour une alternative, pourquoi ne pas mettre l'équipe et les classes d'utilisateurs dans le même dossier?

3
snapshoe

Bad Pratique/Smelly sont les choses suivantes:

  • Vérification de type inutile probaly ( voir aussi ici ). Utilisez simplement les objets que vous obtenez en tant qu'utilisateur/équipe et soulevez une exception (ou dans la plupart des cas, on est soulevé sans avoir besoin de code supplémentaire) lorsqu'il se casse. Laissez-le loin, et vos importations circulaires disparaissent (au moins pour l'instant). Tant que les objets que vous obtenez se comportent comme un utilisateur/une équipe, ils pourraient être n'importe quoi. ( typing canard )
  • classes minuscules (c'est plus ou moins une question de goût, mais la norme générale acceptée ( PEP 8 ) le fait-elle différemment
  • setter où il n'est pas nécessaire: vous pourriez juste dire: my_team.leader=user_b et user_b.team=my_team
  • problèmes liés à la cohérence des données: Et si (my_team.leader.team!=my_team)?
2
knitti

Vous pouvez simplement réparer le graphique de dépendance; Par exemple, l'utilisateur peut ne pas avoir à savoir sur le fait qu'il fait partie d'une équipe. La plupart des dépendances circulaires admettent un tel refactoring.

# team -> user instead of team <-> user
class Team:
    def __init__(self):
        self.users = set()
        self.leader = None

    def add_user(self, user):
        self.users.add(user)

    def get_leader(self):
        return self.leader

    def set_leader(self, user):
        assert user in self.users, 'leaders must be on the team!'
        self.leader = user

Les dépendances circulaires compliquent de manière significative le refactoring, inhibent la réutilisation du code et réduisent l'isolement dans les tests.

Bien que dans Python= Il est possible de contourner un ImportError en importation au moment de l'exécution, importer au niveau du module ou en utilisant d'autres astuces mentionnées ici, ces stratégies font du papier sur une faille de conception . Il vaut la peine d'éviter les importations circulaires si possible.

0
chadlagore

Voici quelque chose que je n'ai pas encore vu. Est-ce une mauvaise idée/conception en utilisant sys.modules directement? Après avoir lu @bobince Solution, j'ai pensé que j'avais compris l'ensemble de l'importation, mais j'ai rencontré un problème similaire à A question qui relie celui-ci.

Voici une autre prise sur la solution:

# main.py
from test import team
from test import user

if __== '__main__':
    u = user.User()
    t = team.Team()
    u.setTeam(t)
    t.setLeader(u)

# test/team.py
from test import user

class Team:
    def setLeader(self, u):
        if isinstance(u, user.User):
            self.leader = u

# test/user.py
import sys
team = sys.modules['test.team']

class User:
    def setTeam(self, t):
        if isinstance(t, team.Team):
            self.team = t

et le fichier test/__init__.py Fichier étant vide. La raison pour laquelle cela fonctionne est parce que test.team est d'être importé en premier. Le moment python Importe/lise un fichier il append le module à sys.modules. Quand nous importons test/user.py le module test.team sera déjà défini puisque nous l'importrons dans main.py.

Je commence à aimer cette idée pour les modules qui grandissent assez gros, mais il existe des fonctions et des cours qui dépendent l'un de l'autre. Supposons qu'il existe un fichier appelé util.py Et ce fichier contient de nombreuses classes qui dépendent l'une de l'autre. Peut-être pourrions-nous diviser le code parmi différents fichiers qui dépendent les uns sur les autres. Comment pouvons-nous contourner l'importation circulaire?

Eh bien, dans le util.py Fichier Nous importatons simplement tous les objets des autres fichiers "privés", dis-je privé car ces fichiers ne sont pas destinés à être accessibles directement, nous y accédons à la place via le fichier d'origine:

# mymodule/util.py
from mymodule.private_util1 import Class1
from mymodule.private_util2 import Class2
from mymodule.private_util3 import Class3

Puis sur chacun des autres fichiers:

# mymodule/private_util1.py
import sys
util = sys.modules['mymodule.util']
class Class1(object):
    # code using other classes: util.Class2, util.Class3, etc

# mymodule/private_util2.py
import sys
util = sys.modules['mymodule.util']
class Class2(object):
    # code using other classes: util.Class1, util.Class3, etc

Les sys.modules L'appel fonctionnera aussi longtemps que le mymodule.util est tenté d'être importé en premier.

Enfin, je ne ferai que souligner que cela se fait pour aider les utilisateurs à la lisibilité (fichiers plus courts) et je ne dirais donc pas que les importations circulaires sont "intrinsèquement" mauvaises. Tout aurait pu être fait dans le même dossier, mais nous l'utilisons afin que nous puissions séparer le code et non confondu nous-mêmes tout en faisant défiler le fichier énorme.

0
jmlopez