2010-10-17 10 views
41

Consideremos Python (3.x): guionespitón importaciones circulares, una vez más (también conocido como lo que está mal con este diseño)

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/team.py:

from test.user import user 

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

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

Por lo tanto, al no ser pythonista, tengo tres preguntas. En primer lugar:

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

Y, sabiendo que alguien dirá inevitable "importaciones circulares siempre indica un problema de diseño", la segunda pregunta es:

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

Y finalmente, el tercero:

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

Para ser precisos, el tipo de comprobación como arriba es solo un ejemplo, también hay una capa de índice basado en clase, que permite decir. encontrar todos los usuarios que son miembros de un equipo (clase de usuario tiene muchas subclases, por lo que se duplica el índice, para los usuarios en general y para cada subclase específica) o todos los equipos que tengan determinado usuario como miembro

Editar:

Espero que un ejemplo más detallado aclare lo que intento lograr. Archivos omitidos por readibility (pero tener archivo de origen uno 300kb me asusta de alguna manera, así que por favor asuma que todas las clases se encuentra en 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 algunos 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 (detalles de implementación omitidos, pero no hay nada complicado) además de esta cosa impía circular de importación.

+2

Se considera una buena práctica capitalizar sus clases-- Equipo/Usuario. – snapshoe

+4

También: echa un vistazo a [propiedades] de python (http://docs.python.org/library/functions.html#property) para la alternativa preferida para declarar setters y getters. – intuited

+0

@intuited: YO SOY decoradores Amoroso, pero al parecer no funciona muy bien con __setattr__/__getattr__ (ejemplo anterior es bastante simplificado) –

Respuesta

77

importaciones circulares no son inherentemente malo es natural que el código team confiar en user mientras que el user hace algo con team.

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

En su lugar, importe solo módulos. Esto da como resultado un espacio de nombres más claro, posibilita el parche posterior de mono y resuelve el problema de importación. Como solo está importando el módulo en el momento de la importación, no le importa que la clase no esté definida aún. Para cuando consigas usar la clase, será.

Por lo tanto, la 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 teams.Team es también bien, si desea escribir test menos. Eso sigue importando un módulo, no un miembro de módulo.

Además, si Team y User son relativamente simples, colóquelos en el mismo módulo. No es necesario seguir la expresión idiomática de una clase por archivo de Java. Las pruebas isinstance y set también me gritan unpythonic-Java-wart; Dependiendo de lo que esté haciendo, es posible que sea mejor utilizar un @property normal sin verificación de tipo.

+9

Las importaciones circulares son inherentemente una mala cosa.Si alguna vez desea tomar una porción de su aplicación y, por ejemplo, crear una biblioteca, entonces lo importante de las dependencias de ese bloque es que todas deben ir de la aplicación a la biblioteca. Una biblioteca que tiene dependencias en su aplicación no sirve para nadie. Ni siquiera usted: no podrá realizar pruebas en la biblioteca sin agruparlas con la aplicación, lo que frustra el propósito de intentar desacoplarlas en primer lugar. Como las dependencias circulares funcionan AMBAS maneras, es imposible dividir el código en partes desacopladas. –

+13

Permítanme repetir esto: las importaciones circulares son inherentemente una mala cosa, si alguna vez quieren tomar una porción de su aplicación y, por ejemplo, crear una biblioteca. _Con un trozo desacoplado, puede ser perfectamente razonable usar referencias circulares. – mhsmith

+17

"Inherentemente" tiene un mal olor. Es como decir "obviamente" o "seguramente". Si el mundo tiene dependencias circulares y utiliza clases para modelar el mundo, entonces sus clases pueden tener dependencias circulares. Sí, a nivel de implementación, hacen las cosas difíciles y vale la pena discutir soluciones. – bootchk

3

i. Para que funcione, puede usar una importación diferida. Una forma sería dejar user.py solo 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. Para una alternativa, ¿por qué no poner las clases de equipo y usuario en el mismo archivo?

+1

anuncio. iii - Tengo más de 60 de esas clases y ponerlas en un archivo no es realmente la opción –

+0

ad i. - ¿No es ese el rendimiento de golpear? ¿alguien sabe si Python optimiza internamente ese tipo de importaciones? –

+3

sí, está optimizado. ver http://docs.python.org/library/sys.html#sys.modules – snapshoe

2

práctica Bad/mal olor son las siguientes cosas:

  • comprobación de tipos probaly innecesaria (see also here). Solo usa los objetos que obtienes como si fueran un usuario/equipo y genera 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, y sus importaciones circulares desaparecerán (al menos por ahora). Mientras los objetos se obtiene se comportan como un usuario/a del equipo, podrían ser cualquier cosa.(Duck Typing)
  • inferior clases de casos (esto es más o menos una cuestión de gustos, pero la norma aceptada general (PEP 8) hace de manera diferente
  • colocador en las que no sea necesario: usted podría decir: my_team.leader=user_b y user_b.team=my_team
  • problemas con la consistencia de los datos:?., ¿y si (my_team.leader.team!=my_team)
+0

En realidad captadores y definidores eran mucho simplified.Normally toman más responsabilidad, es decir, la comprobación de coherencia de datos (i no estoy publicando código completo aquí). Clases en minúsculas: vale, solo estoy leyendo PEP8;). Por último, pero no menos importante: escriba la verificación. El problema es que los objetos que se establecen aquí se propagan y su uso a menudo se difiere. Por lo tanto, si el objeto tiene un tipo incorrecto, debería retroceder toda la propagación, que puede ser completamente imposible. En lugar de eso, estoy validando objetos antes (sí, violando EAFP), y la validación se basa en la clase del objeto en lugar de las propiedades. ¿Alguna solución sensata aquí? –

+0

Usted podría simplemente agregar un miembro a sus instancias diciendo qué clase es: Entonces su prueba se vería así: 'si no es.is_a ==" usuario ":' que sería simplemente un control de cordura usando pato-tipado. –

+0

@ Michael: Parece que tengo que tratar con él de esa manera, aunque esta clase se duplica estructura de herencia –

1

Aquí hay algo que aún no he visto. ¿Es una mala idea/diseño usando 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 al question que enlaza con este.

Aquí es otra toma en 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 de archivo test/__init__.py estar vacío. La razón por la que esto funciona es porque primero se importa test.team. En el momento en que python está importando/leyendo un archivo, agrega el módulo al sys.modules.Cuando importamos test/user.py, el módulo test.team ya estará definido, ya que lo estamos importando en main.py.

estoy empezando a gustar esta idea para los módulos que crecen bastante grande, pero hay funciones y clases que dependen unos de otros. Supongamos que hay un archivo llamado util.py y este archivo contiene muchas clases que dependen unas de otras. Quizás podríamos dividir el código entre diferentes archivos que dependen el uno del otro. ¿Cómo salimos de la importación circular?

Pues bien, en el archivo util.py simplemente importar todos los objetos de los otros archivos "privadas", digo privada ya que esos archivos no están destinados a ser visitada directamente, en vez que accede 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 

ElLa llamadafuncionará siempre que intente importar primero el mymodule.util.

Por último, sólo señalar que esto se está haciendo para ayudar a los usuarios con una legibilidad (archivos) más cortos y por lo tanto yo no diría que las importaciones circulares son "intrínsecamente" malo. Todo podría haberse hecho en el mismo archivo, pero estamos usando esto para poder separar el código y no confundirnos al desplazarnos por el enorme archivo.

Cuestiones relacionadas