移除Python循环导入

10

用户.py:

from story import Story

class User:
    ...
    def get_stories(self):
        story_ids = [select from database]
        return [Story.get_by_id(id) for id in story_ids]

故事.py

from user import User

class Story:
    ...
    def __init__(self, id, user_id, content):
        self.id = id
        self.user = User.get_by_id(user_id)
        self.content = content

正如您所看到的,这个程序中存在循环导入,导致 ImportError。我了解到,我可以将导入语句移动到方法定义中以防止此错误。但我仍然想知道,在这种情况下是否有一种方法来消除循环导入,或者说,是否有必要(为了良好的设计)?


1
将循环导入移至方法定义中以推迟导入,是一种合理的方式,无需为了良好的设计而删除循环导入。 - Raymond Hettinger
4个回答

2
在这种情况下,最明显的解决方案是完全打破对“User”类的依赖性,通过更改接口,以便“Story”构造函数接受实际的“User”,而不是“user_id”。这也会导致更有效的设计:例如,如果用户有很多故事,同一个对象可以提供给所有这些构造函数。
除此之外,整个模块的导入(即story和user而不是成员)应该是可行的 - 在第二个模块被导入时,先导入的模块将出现为空;但是这并不重要,因为这些模块的内容不会在全局范围内使用。
这比在方法中导入稍微优选一些。在方法中导入比仅在模块全局查找(story.Story)具有显着的开销,因为它需要针对每个方法调用进行操作; 在简单情况下,开销至少是30倍。

1
在网上有很多这样的关于python循环导入问题的讨论。我选择参与这个线程是因为Ray Hettinger的评论使得循环导入的使用情况合法化,但他推荐的解决方案我认为并不是特别好的做法——将导入移到一个方法中。
除了Hettinger的权威性外,还需要三个免责声明来回应普遍的反对意见:
1. 我从未在Java中编程,我不想使用Java风格。 2. 重构并不总是有用或有效的。逻辑API有时会决定一种结构,使得递归导入引用不可避免。请记住,代码存在于用户而非程序员。 3. 合并相当大的模块可能会导致可读性和可维护性问题,这可能比一个或两个递归导入更糟糕。
此外,我认为可维护性和可读性要求将导入分组放在文件顶部,每个所需名称只出现一次,并且from module import name风格是首选(也许除了具有许多函数的非常短的模块名,例如gtk),因为它避免了重复的语言混乱并使依赖关系明确。

在这个基础上,我将提出一个简化版的我的用例,并介绍我的解决方案。

我有两个模块,每个模块都定义了许多类。 surface 定义了几何表面,如平面、球体、双曲面等。 path 定义了平面几何图形,如直线、圆、双曲线等。从 API 要求的角度来看,这些逻辑上是不同的类别,重构并不是一个选项。然而,这两个类别是紧密相关的。

一个有用的操作是相交两个表面,例如,两个平面的相交是一条直线,或者一个平面和一个球体的相交是一个圆。

例如,如果在 surface.py 中进行直接导入以实现相交操作的返回值:

from path import Line

你得到:

你得到:

Traceback (most recent call last):
  File "surface.py", line 62, in <module>
    from path import Line
  File ".../path.py", line 25, in <module>
    from surface import Plane
  File ".../surface.py", line 62, in <module>
    from path import Line
ImportError: cannot import name Line

几何上,平面用于定义路径,毕竟它们可以在三维(或更多)中任意定位。回溯告诉您发生了什么以及解决方法。
只需将surface.py中的导入语句替换为:
try: from path import Line
except ImportError: pass # skip circular import second pass

跟踪回溯的操作仍在进行中。只是第二次通过时,我们忽略了导入失败。这并不重要,因为Line在模块级别上没有使用。因此,surface的必要命名空间被加载到path中。因此,path的命名空间解析可以完成,允许将其加载到surface中,完成首次遇到from path import Line。因此,surface的命名空间解析可以继续并完成,继续进行可能需要的任何其他操作。
这是一种简单而非常清晰的习惯用语。try: ... except ...语法清晰而简洁地记录了循环导入问题,减轻了未来可能需要的任何维护工作。每当重构确实是一个不好的想法时,请使用它。

1
另一种减轻循环依赖的方法是更改导入样式。将from story import Story更改为import story,然后将类引用为story.Story。由于您仅在方法内部引用该类,因此在调用该方法之前它不需要访问该类,而此时导入已成功完成。(根据导入顺序,您可能需要在其中一个或两个模块中进行更改。)
然而,设计似乎有些奇怪。您的设计使得UserStory类非常紧密耦合 - 两者都不能单独使用。在这种情况下,通常更明智的做法是将它们都放在同一个模块中。

是的,我也觉得这个设计有点奇怪...既然这是一个常见的情况,我想知道一个好的设计应该是什么样子的?谢谢。 - wong2
1
这可不是一个常见的情况 :) - Steinar Lima
建议的更改不会解决问题,仍然会存在循环导入。 - Daniel Roseman
它确实解决了问题的一部分,即“ImportError”。 - Antti Haapala -- Слава Україні

0
正如BrenBarn所说,最明显的解决方案是将User和Story放在同一个模块中,如果User需要了解Story的任何信息,这是非常合理的。现在,如果你真的需要将它们放在不同的模块中,你也可以在story.py中使用monkeypatch来添加get_stories方法。这是可读性/解耦合的权衡...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接