解决循环导入语句的Pythonic方式?

68 投票
4 回答
73762 浏览
提问于 2025-04-16 16:12

我最近接手了一些代码,感觉有点不安:这里有一个测试库,里面有很多类,每个类对应我们网站上的一个网页,每个网页类都有一些方法来自动化该页面的功能。

这些方法可以用来点击页面之间的链接,并返回链接页面的类。下面是一个简化的例子:

文件 homePageLib.py:

class HomePage(object):
    def clickCalendarLink(self):
        # Click page2 link which navigates browswer to page2
        print "Click Calendar link"
        # Then returns the page2 object
        from calendarLib import CalendarPage
        return CalendarPage()

文件 calendarLib.py:

class CalendarPage(object):
    def clickHomePageLink(self):
        # Click page1 link which navigates browswer to page1
        print "Click Home Page link"
        # Then return the page2 object
        from homePageLib import HomePage
        return HomePage()

这样,脚本文件就可以点击页面,并从这个方法中获取对象作为返回值,这样脚本的作者在浏览网站时就不需要一直实例化新的页面对象。(我觉得这个设计有点奇怪,但我说不出具体的原因,只是觉得一个叫做'clickSomeLink'的方法返回一个结果页面的对象似乎不太对。)

以下脚本展示了一个脚本如何在网站上导航:(我插入了print page来显示页面对象是如何变化的)

脚本文件:

from homePageLib import HomePage

page = HomePage()    
print page
page = page.clickCalendarLink()
print page
page = page.clickHomePageLink()
print page

这会产生以下输出:

<homePageLib.HomePage object at 0x00B57570>
Click Calendar link
<calendarLib.CalendarPage object at 0x00B576F0>
Click Home Page link
<homePageLib.HomePage object at 0x00B57570>

我特别不安的部分是那些到处都是的from ____ import ____语句。我觉得这样不好,原因有以下几点:

  1. 我一直有个习惯,就是把所有的导入语句放在文件的顶部。
  2. 由于可能有多个链接指向同一个页面,这样就会导致在文件的多个地方出现相同的from foo import bar代码行。

问题是,如果我们把这些导入语句放在页面顶部,就会出现导入错误,因为(根据这个例子),HomePage导入了CalendarPage,而CalendarPage又反过来导入了HomePage:

文件 homePageLib.py

from calendarLib import CalendarPage

class HomePage(object):
    def clickCalendarLink(self):
        # Click page2 link which navigates browswer to page2
        print "Click Calendar link"
        # Then returns the page2 object

        return CalendarPage()

文件 calendarLib.py

from homePageLib import HomePage

class CalendarPage(object):
    def clickHomePageLink(self):
        # Click page1 link which navigates browswer to page1
        print "Click Home Page link"
        # Then return the page2 object
        return HomePage()

这就导致了以下错误:

>>> from homePageLib import HomePage
Traceback (most recent call last):
  File "c:\temp\script.py", line 1, in ?
    #Script
  File "c:\temp\homePageLib.py", line 2, in ?
    from calendarLib import CalendarPage
  File "c:\temp\calendarLib.py", line 2, in ?
    from homePageLib import HomePage
ImportError: cannot import name HomePage

(有没有更好的格式化Python输出的建议?)

我不想继续这种风格,想找个更好的方法。有没有什么Pythonic的方式来处理这样的循环依赖,同时还能把导入语句放在文件的顶部?

4 个回答

1

请查看Sebastian的回答,里面有详细的解释。这种方法是David Beazley在PyCon上提出的。

试着把导入的部分放在最上面,像这样:

try:
    from homePageLib import HomePage
except ImportError:
    import sys
    HomePage = sys.modules[__package__ + '.HomePage']

这样做会尝试导入你的HomePage,如果导入失败,它会尝试从缓存中加载。

10

我遇到了一个循环导入的问题,因为我在类型提示中引用了一个类。这个问题可以通过使用 from __future__ import annotations 来解决(我在 Python 3.9.x 中测试过)。

下面是一个例子:

AClass.py

from BClass import BClass
class AClass():
    def __init__(self) -> None:
        self.bClass = BClass(self)

BClass.py

from __future__ import annotations  # Without this, the type hint below would not work.
import AClass  # Note that `from AClass import AClass` would not work here.`
class BClass:
    def __init__(self, aClass: AClass.AClass) -> None:
        self.aClass = aClass
123

解决这些问题通常需要用到一些技术,比如依赖注入

不过,修复这个错误其实很简单:

在calendarLib.py文件中:

import homePageLib

class CalendarPage(object):
    def clickHomePageLink(self):
        [...]
        return homePageLib.HomePage()

模块级别的代码在导入时就会执行。使用from [...] import [...]这种语法时,模块必须完全初始化才能成功。

而简单的import [...]就不需要这样,因为没有访问任何符号,这样就打破了依赖关系。

撰写回答