python 循环导入再次(也就是这个设计有啥问题)

Posted

技术标签:

【中文标题】python 循环导入再次(也就是这个设计有啥问题)【英文标题】:python circular imports once again (aka what's wrong with this design)python 循环导入再次(也就是这个设计有什么问题) 【发布时间】:2011-04-26 17:28:57 【问题描述】:

让我们考虑一下 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。

所以,我不是 pythonista,我有三个问题。首先:

我。我怎样才能使这件事起作用?

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

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

最后,第三个:

三。有什么更好的选择?

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

编辑:

我希望更详细的示例能够阐明我试图实现的目标。为便于阅读而省略了文件(但有一个 300kb 的源文件让我感到害怕,所以请假设每个类都在不同的文件中)

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

所以,除了这个邪恶的循环导入之外,它的效果很好(省略了实现细节,但没有什么复杂的)。

【问题讨论】:

大写您的类被认为是一种很好的做法——团队/用户。 另外:查看 python 的 properties 以获得声明 setter 和 getter 的首选替代方法。 @intuited:我喜欢装饰器,但显然它不适用于 setattr / getattr(上面的示例相当简化)跨度> 【参考方案1】:

循环导入本质上并不是一件坏事。 team 代码依赖于 user 是很自然的,而 userteam 做一些事情。

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

相反,只导入模块。这导致更清晰的命名空间,使以后的猴子修补成为可能,并解决了导入问题。因为您只是在导入时导入 module,所以您不必关心其中尚未定义的 class。到你开始使用这个类的时候,它会是。

所以,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 teamsteams.Team也可以,如果要少写test。那仍然是导入模块,而不是模块成员。

另外,如果TeamUser比较简单,把它们放在同一个模块里。您不需要遵循 Java 一个文件一个类的习惯用法。 isinstance testing 和 set 方法也让我尖叫 unpythonic-Java-wart;根据您的操作,您最好使用普通的、未经类型检查的@property

【讨论】:

循环导入本质上是一件坏事。如果您想要获取应用程序的一部分,例如,从中创建一个库,那么关于该块的依赖关系的重要一点是它们必须全部从应用程序转到库。依赖于您的应用程序的库对任何人都没有用。甚至您也不行——如果不将其与应用程序捆绑在一起,您将无法在库上运行测试,这违背了首先尝试将它们解耦的目的。由于循环依赖是双向的,因此不可能将代码拆分成解耦的块。 让我为您重申:循环导入本质上是一件坏事,如果您想要占用您的应用程序的一部分,例如,制作一个库其中。 解耦块中,使用循环引用可能是完全合理的。 “天生”有难闻的气味。这就像说“显然”或“肯定”。如果世界具有循环依赖关系,并且您使用类对世界进行建模,那么您的类可能具有循环依赖关系。是的,在实施层面,它们使事情变得困难,值得讨论解决方案。【参考方案2】:

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

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

三。作为替代方案,为什么不将团队和用户类放在同一个文件中?

【讨论】:

广告。 iii - 我有 60 多个这样的类,将它们放入一个文件并不是真正的选择 广告我。 - 这不是打击性能吗?有谁知道python是否在内部优化了这种导入?【参考方案3】:

不良做法/臭味如下:

可能不必要的类型检查 (see also here)。只需使用您作为用户/团队获得的对象并在异常发生时引发异常(或者在大多数情况下,引发异常而不需要额外的代码)。离开这个,你的循环导入就会消失(至少现在是这样)。只要您获得的对象表现 像用户/团队,它们就可以是任何东西。 (Duck Typing) 小写类(这或多或少是个人喜好问题,但普遍接受的标准 (PEP 8) 会有所不同 不需要的地方的setter:你可以说:my_team.leader=user_buser_b.team=my_team 数据一致性问题:如果(my_team.leader.team!=my_team) 怎么办?

【讨论】:

实际上 getter 和 setter 被简化了很多。通常他们承担更多的责任,即检查数据一致性(我没有在这里发布整个代码)。小写类 - 好的,我只是在阅读 PEP8 ;)。最后但并非最不重要的 - 类型检查。问题是在这里设置的对象被传播并且它们的使用通常被推迟。因此,如果对象的类型错误,我应该回溯所有可能完全不可能的传播。而不是我之前验证对象(是的,违反 EAFP),并且验证基于对象的类而不是属性。这里有什么合理的解决方案吗? 您可以在您的实例中添加一个成员,说明它是什么类:然后您的测试将如下所示:if not u.is_a == "user": 这将只是使用鸭子类型的健全性检查。 ***.com/questions/510972/… 有一个稍微整洁的解决方案u.__class__.__name__ == "user" @MichaelAnderson:我不明白为什么if u.__class__.__name__ == "user"if isinstance(u, user) 更整洁。前者看起来很“hackish”,并且对于user 的子类失败。如果你想把类名作为一个字符串,很好,但为了比较,我会用isinstance @knitti:投反对票。您的批评并非针对所提出的问题。我不认为这是对发布的代码示例进行一般批评的合适论坛。【参考方案4】:

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

这是解决方案的另一种看法:

# 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

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

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

【讨论】:

【参考方案5】:

你可以修复依赖图;例如,用户可能不必知道它是团队的一部分这一事实。大多数循环依赖都承认这种重构。

# 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,但这些策略确实掩盖了设计缺陷。尽可能避免循环导入是值得的。

【讨论】:

以上是关于python 循环导入再次(也就是这个设计有啥问题)的主要内容,如果未能解决你的问题,请参考以下文章

有啥方法可以在for循环中保存多个图而不用python覆盖?

来自包导入模块的python,这有啥问题吗?

python是一门程序设计语言,学习python有啥好的视频教程?

将 python 模块导入例程或类定义有啥问题吗? [复制]

用python写GPU上的并行计算程序,有啥库或者编译器

Python循环导入?