Python circular importa una vez más (también conocido como lo que está mal con este diseño)

Consideremos los scripts de python (3.x):

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) 

prueba / user.py:

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

prueba / equipo.py:

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

Ahora, por supuesto, tengo importación circular y ImportError espléndido.

Entonces, no siendo pythonista, tengo tres preguntas. Ante todo:

yo. ¿Cómo puedo hacer que esto funcione?

Y, sabiendo que alguien dirá inevitablemente “Las importaciones circulares siempre indican un problema de diseño”, la segunda pregunta viene:

ii. ¿Por qué este diseño es malo?

Y finalmente, el tercero:

iii. ¿Cuál sería la mejor alternativa?

Para ser precisos, la comprobación de tipos como anteriormente es solo un ejemplo, también hay una capa de índice basada en la clase, que permite, por ejemplo. encuentre a todos los usuarios que son miembros de un equipo (la clase de usuario tiene muchas subclases, por lo que el índice se duplica, para los usuarios en general y para cada subclase específica) o todos los equipos que han dado usuario como miembro

Editar:

Espero que un ejemplo más detallado aclare lo que trato de lograr. Los archivos se omiten por motivos de legibilidad (pero tener un archivo fuente de 300 kb me asusta de alguna manera, así que suponga que cada clase está en un archivo diferente)

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

y ahora algo de uso:

 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] 

Por lo tanto, funciona muy bien (los detalles de la implementación se omiten, pero no hay nada complicado), además de esta cosa de importación circular profana.

Las importaciones circulares no son intrínsecamente malas. Es natural que el código del team dependa del user mientras que el user hace algo con el team .

La peor práctica aquí es from module import member . El módulo de team está intentando obtener la clase de user en el momento de la importación, y el módulo de user está tratando de obtener la clase de user . Pero la clase de team aún no existe porque todavía estás en la primera línea de team.py cuando se ejecuta user.py

En su lugar, importar solo módulos. Esto da como resultado un espacio de nombres más claro, hace posible la aplicación posterior de parches en el mono y resuelve el problema de importación. Debido a que solo está importando el módulo en el momento de la importación, no le importa que la clase que contiene no esté aún definida. Cuando llegues a usar la clase, será.

Entonces, prueba / users.py:

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

prueba / teams.py:

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

from test import teams y luego from test import teams teams.Team también está bien, si desea escribir la test menos. Eso sigue importando un módulo, no un miembro del módulo.

Además, si Team y User son relativamente simples, colóquelos en el mismo módulo. No es necesario seguir el lenguaje de una clase por archivo de Java. Los isinstance prueba y set instancias también me gritan unpythonic-Java-wart; Dependiendo de lo que esté haciendo, puede que sea mejor que utilice una @property simple y no verificada por el @property .

yo. Para que funcione, puede utilizar una importación diferida. Una forma sería dejar solo user.py y cambiar team.py a:

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

iii. Como alternativa, ¿por qué no poner las clases de equipo y usuario en el mismo archivo?

Mala práctica / maloliente son las siguientes cosas:

  • Verificación innecesaria de tipos ( ver también aquí ). Simplemente use los objetos que obtiene como un usuario / equipo y genere una excepción (o en la mayoría de los casos, uno se genera sin necesidad de código adicional) cuando se rompe. Deje esto fuera, y las importaciones circulares desaparecen (al menos por ahora). Siempre y cuando los objetos que obtienes se comporten como un usuario / un equipo, podrían ser cualquier cosa. ( Escribiendo pato )
  • clases de minúsculas (esto es más o menos una cuestión de gustos, pero el estándar generalmente aceptado ( PEP 8 ) lo hace de manera diferente
  • setter donde no sea necesario: simplemente podría decir: my_team.leader=user_b y user_b.team=my_team
  • problemas con la consistencia de los datos: ¿qué (my_team.leader.team!=my_team) si (my_team.leader.team!=my_team) ?

Aquí hay algo que no he visto todavía. ¿Es una mala idea / diseño usar sys.modules directamente? Después de leer la solución @bobince, pensé que había entendido todo el negocio de importación, pero luego me encontré con un problema similar a una pregunta que enlaza con esta.

Aquí hay otra toma de la solución:

 # main.py from test import team from test import user if __name__ == '__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 

y el archivo test/__init__.py está vacío. La razón por la que esto funciona es porque test.team se está importando primero. En el momento en que python está importando / leyendo un archivo, agrega el módulo a sys.modules . Cuando importamos test/user.py el módulo test.team ya estará definido ya que lo estamos importando en main.py

Me está empezando a gustar esta idea para los módulos que crecen bastante, pero hay funciones y clases que dependen unas de otras. Supongamos que hay un archivo llamado util.py y este archivo contiene muchas clases que dependen unas de otras. Tal vez podríamos dividir el código entre diferentes archivos que dependen unos de otros. ¿Cómo podemos sortear la importación circular?

Bueno, en el archivo util.py simplemente importamos todos los objetos de los otros archivos “privados”, digo privado ya que esos archivos no están destinados a ser accedidos directamente, en lugar de eso, accedemos a ellos a través del archivo original:

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

Luego en cada uno de los otros archivos:

 # 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 

La llamada a sys.modules funcionará siempre que se intente importar mymodule.util primero.

Por último, solo señalaré que esto se está haciendo para ayudar a los usuarios con la legibilidad (archivos más cortos) y, por lo tanto, no diría que las importaciones circulares son “inherentemente” malas. Todo podría haberse hecho en el mismo archivo, pero lo estamos usando para poder separar el código y no confundirnos mientras nos desplazamos por el enorme archivo.

Usted podría simplemente arreglar el gráfico de dependencia; por ejemplo, el usuario puede no tener que saber sobre el hecho de que es parte de un equipo. La mayoría de las dependencias circulares admiten tal refactorización.

 # 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 

Las dependencias circulares complican significativamente la refactorización, inhiben la reutilización del código y reducen el aislamiento en las pruebas.

Aunque en Python es posible sortear ImportError importando en tiempo de ejecución, importando a nivel de módulo o usando otros trucos mencionados aquí, estas estrategias se aplican a una falla de diseño. Vale la pena evitar las importaciones circulares si es posible.