dependencies:再次导入python循环(也就是这个设计有什么问题)

让我们考虑python (3.x)脚本:

main.py:

1
2
3
4
5
6
7
8
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:

1
2
3
4
5
6
from test.team import team

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

测试/ team.py:

1
2
3
4
5
6
from test.user import user

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

现在,当然,我有循环导入和精彩的导入。

所以,不是毕达哥拉斯,我有三个问题。首先:

我怎么才能把这东西修好呢?

而且,知道有人会不可避免地说"循环导入总是表明设计问题",第二个问题就来了:

二世。为什么这个设计不好?

最后,第三个问题:

三世。还有更好的选择吗?

准确地说,上面的类型检查只是一个例子,还有一个基于类的索引层,它允许ie。查找所有属于一个团队的用户(user类有许多子类,因此索引加倍,对于一般用户和每个特定子类)或所有已将user作为成员的团队

编辑:

我希望更详细的例子将阐明我试图实现的目标。可达性省略了文件(但是有一个300kb的源文件让我有些害怕,所以请假设每个类都在不同的文件中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 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}

现在来看一些用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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]

因此,除了这个邪恶的循环导入之外,它工作得很好(省略了实现细节,但没有什么复杂的)。


循环进口本身并不是一件坏事。team代码依赖于user,而user使用team做一些事情,这是很自然的。

这里最糟糕的做法是from module import memberteam模块试图在重要时间获取user类,而user模块试图获取team类。但是team类还不存在,因为在运行user.py时,仍然在team.py的第一行。

相反,只导入模块。这将导致更清晰的命名空间,使以后的猴子补丁成为可能,并解决了导入问题。因为您只在导入模块时导入模块,所以您并不关心模块中还没有定义的类。等到你开始使用这门课的时候,你就会明白了。

因此,测试/ users.py:

1
2
3
4
5
6
import test.teams

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

测试/ teams.py:

1
2
3
4
5
6
import test.users

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

from test import teamsteams.Team也可以,如果你想写test less。它仍然在导入模块,而不是模块成员。

此外,如果TeamUser相对简单,则将它们放在同一个模块中。您不需要遵循Java每个文件一个类的习惯用法。isinstance测试和set方法也向我发出了非python - java -wart的尖叫;根据您正在做的工作,使用普通的、没有类型检查的@property可能会更好。


要使它工作,您可以使用一个延迟导入。一种方法是不使用user.py,将team.py更改为:

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

三世。另一种选择是,为什么不将团队和用户类放在同一个文件中呢?


不好的习惯/难闻的气味有以下几点:

可能不必要的类型检查(请参阅这里)。只要使用您作为用户/团队获得的对象,并在它中断时引发异常(或者在大多数情况下,在不需要额外代码的情况下引发异常)。如果不这样做,循环导入就会消失(至少现在是这样)。只要您获得的对象表现得像用户/团队,它们可以是任何对象。(Duck Typing)小写类(这或多或少取决于个人喜好,但是一般公认的标准(PEP 8)却有不同的做法在不需要的地方设置setter:您只需说:my_team.leader=user_buser_b.team=my_team数据一致性问题:如果(my_team.leader.team!=my_team)?


你可以修正依赖关系图;例如,用户可能不需要知道它是团队的一部分。大多数循环依赖关系都允许这样的重构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 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,但是这些策略确实掩盖了设计缺陷。尽可能避免循环导入是值得的。


这是我还没见过的东西。直接使用sys.modules是个坏主意/设计吗?在阅读了@bobince解决方案后,我以为我已经了解了整个进口业务,但后来我遇到了一个类似的问题,这个问题与这个问题有关。

下面是另一种解决方案:

1
2
3
4
5
6
7
8
9
# 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)
1
2
3
4
5
6
7
# test/team.py
from test import user

class Team:
    def setLeader(self, u):
        if isinstance(u, user.User):
            self.leader = u
1
2
3
4
5
6
7
8
# 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文件中,我们只是从其他"私有"文件中导入所有对象,我说私有是因为这些文件不打算直接访问,而是通过原始文件访问它们:

1
2
3
4
# mymodule/util.py
from mymodule.private_util1 import Class1
from mymodule.private_util2 import Class2
from mymodule.private_util3 import Class3

然后在其他文件上:

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

只要先尝试导入mymodule.utilsys.modules调用就会工作。

最后,我只指出这样做是为了帮助用户提高可读性(更短的文件),因此我不会说循环导入"天生"不好。所有的事情都可以在同一个文件中完成,但是我们使用它是为了分离代码,并且在滚动这个巨大的文件时不会混淆。