Python круговой импорт еще раз (иначе, что не так с этим дизайном) - PullRequest
44 голосов
/ 18 октября 2010

Давайте рассмотрим скрипты на 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)

тест / user.py:

from test.team import team

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

тест / team.py:

from test.user import user

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

Теперь, конечно, у меня есть круговой импорт и великолепная ошибка ImportError.

Итак, не будучи питонистом, у меня три вопроса. Прежде всего:

я. Как я могу заставить эту вещь работать?

И, зная, что кто-то неизбежно скажет «Круговой импорт всегда указывает на проблему проектирования», возникает второй вопрос:

II. Почему этот дизайн плох?

И, наконец, третий:

III. Что было бы лучшей альтернативой?

Если быть точным, проверка типов, как указано выше, является только примером, также есть индексный слой, основанный на классе, который позволяет т.е. найти всех пользователей, являющихся членами одной команды (пользовательский класс имеет много подклассов, поэтому индекс удваивается, для пользователей в целом и для каждого конкретного подкласса) или все команды, имеющие пользователя в качестве члена

Edit:

Я надеюсь, что более подробный пример прояснит, чего я пытаюсь достичь. Файлы опущены для удобства чтения (но наличие одного исходного файла размером 300 КБ меня как-то пугает, поэтому, пожалуйста, предположите, что каждый класс находится в отдельном файле)

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

и теперь немного использования:

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]

Итак, он прекрасно работает (детали реализации опущены, но ничего сложного), кроме этой нечестивой циклической импорта.

Ответы [ 5 ]

79 голосов
/ 18 октября 2010

Круговой импорт по своей сути не является плохой вещью.Для кода team естественно полагаться на user, в то время как user делает что-то с team.

Наихудшая практика здесь - from module import member.Модуль team пытается получить класс user во время импорта, а модуль user пытается получить класс team.Но класс team еще не существует, потому что вы все еще на первой строке team.py, когда запускается user.py.

Вместо этого импортируйте только модули.Это приводит к более четкому пространству имен, делает возможным позднее исправление обезьян и решает проблему импорта.Поскольку вы импортируете только модуль во время импорта, вам все равно, что класс внутри него еще не определен.К тому времени, когда вы приступите к использованию класса, это будет.

Итак, test / users.py:

import test.teams

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

test / teams.py:

import test.users

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

from test import teams, а затем teams.Team также в порядке, если вы хотите написать test меньше.Это все еще импорт модуля, а не члена модуля.

Кроме того, если Team и User относительно просты, поместите их в один и тот же модуль.Вам не нужно следовать идиоме Java «один класс на файл».Методы isinstance тестирования и set также вызывают у меня непонятную Java-бородавку;в зависимости от того, что вы делаете, вам может быть лучше использовать простую, без проверки типа @property.

3 голосов
/ 18 октября 2010

я. Чтобы заставить его работать, вы можете использовать отложенный импорт. Одним из способов было бы оставить user.py в покое и изменить team.py на:

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

III. В качестве альтернативы, почему бы не поместить командный и пользовательский классы в один файл?

2 голосов
/ 18 октября 2010

Плохой практикой / вонючей являются следующие вещи:

  • Вероятно, ненужная проверка типов ( см. Также здесь ).Просто используйте объекты, которые вы получаете как пользователь / команда, и вызывайте исключение (или в большинстве случаев оно вызывается без необходимости дополнительного кода), когда оно ломается.Оставьте это, и ваш круговой импорт исчезнет (по крайней мере, пока).Пока объекты, которые вы получаете , ведут себя как пользователь / команда, они могут быть чем угодно.( Duck Typing )
  • строчные классы (это более или менее дело вкуса, но общепринятый стандарт ( PEP 8 ) делает это иначе
  • сеттер там, где это не нужно: вы просто можете сказать: my_team.leader=user_b и user_b.team=my_team
  • проблемы с согласованностью данных: что делать, если (my_team.leader.team!=my_team)?
0 голосов
/ 25 марта 2019

Вы можете просто исправить график зависимости;например, пользователь может не знать о том, что он является частью команды.Большинство циклических зависимостей допускают такой рефакторинг.

# 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

Круговые зависимости значительно усложняют рефакторинг, запрещают повторное использование кода и уменьшают изоляцию при тестировании.

Хотя в Python можно обойти ImportError, импортируя во время выполнения, импортируя на уровень модуля или используя другие приемы, упомянутые здесь, эти стратегии устраняют недостатки проекта.Стоит избегать кругового импорта, если это вообще возможно.

0 голосов
/ 13 марта 2014

Вот то, что я еще не видел.Это плохая идея / дизайн, использующий sys.modules напрямую?После прочтения решения @bobince я подумал, что понял весь бизнес, связанный с импортом, но затем столкнулся с проблемой, похожей на вопрос , который ссылается на этот вопрос.

Вот еще один вариант решения:

# 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

и файл test/__init__.py файл пуст.Это работает потому, что test.team импортируется первым.В тот момент, когда python импортирует / читает файл, он добавляет модуль к sys.modules.Когда мы импортируем test/user.py, модуль test.team уже будет определен, поскольку мы импортируем его в main.py.

. Мне начинает нравиться эта идея для модулей, которые становятся достаточно большими, но есть функции иклассы, которые зависят друг от друга.Предположим, что существует файл с именем util.py, и этот файл содержит много классов, которые зависят друг от друга.Возможно, мы могли бы разделить код между различными файлами, которые зависят друг от друга.Как нам обойти циклический импорт?

Что ж, в файле util.py мы просто импортируем все объекты из других «приватных» файлов, я говорю «приватные», поскольку эти файлы не предназначены для прямого доступа.вместо этого мы обращаемся к ним через исходный файл:

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

Затем для каждого из остальных файлов:

# 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

Вызов sys.modules будет работать какПока mymodule.util пытаются импортировать первыми.

Наконец, я лишь укажу, что это делается для того, чтобы помочь пользователям с удобочитаемостью (более короткие файлы), и, следовательно, я бы не сказал, что циклический импорт"по своей сути" плохо.Все могло бы быть сделано в одном и том же файле, но мы используем это, чтобы мы могли отделить код и не запутаться, просматривая огромный файл.

...