python解决循环依赖的问题分析

 更新时间:2022年12月02日 09:30:31   作者:Bruce小鬼  
在使用python开发过程中在引入其他模块时可能都经历过一个异常就是循环引用most likely due to a circular import,它的意思就是A引用了B,反过来B又引用了A,导致出现了循环引用异常,下面来介绍如何避免循环引用异常,感兴趣的朋友跟随小编一起看看吧

python解决循环依赖

1.概述

在使用python开发过程中在引入其他模块时可能都经历过一个异常就是循环引用most likely due to a circular import,它的意思就是A引用了B,反过来B又引用了A,导致出现了循环引用异常。下面来介绍如何避免循环引用异常。

2.循环引用介绍

2.1.python引入模块原理

下面通过一个循环引用示例,来介绍python引入模块的原理。示例中创建了三个模块,它的引用关系如下

  • dialog.py模块引入了app模块的prefs类的get方法
  • app模块引入了dialog模块的show方法

创建一个python文件,命名为dialog.py

import app

class Dialog:
    def __init__(self, save_dir):
        self.save_dir = save_dir

save_dialog = Dialog(app.prefs.get('save_dir'))

def show():
    print('Showing the dialog!')

创建一个python文件,命名为app.py

import dialog

class Prefs:
    def get(self, name):
        pass

prefs = Prefs()
dialog.show()

创建一个python文件,命名为main.py

import app

运行上面循环引用代码,抛出了异常

AttributeError: partially initialized module 'app' has no attribute 'prefs' (most likely due to a circular import)

要明白上面为什么会抛出循环引用异常,首先要明白python是如何引入模块的。在引入模块的时候,python系统会按照深度优先的顺序,对模块执行以下五步:

  • 1.在sys.path里寻找模块的位置
  • 2.把模块的代码加载进来,并确认这些代码能编译
  • 3.创建响应的空白模块对象表示该模块
  • 4.把这个模块插入sys.modules字典
  • 5.运行模块对象之中的代码定义该模块的内容

循环依赖之所以会出错,原因在于,执行完第4步骤之后,这个模块已经位于sys.modules之中了,然而它的内容还没有得到定义,要等到执行完第5步骤,才能齐备。

可是python在执行import语句的时候,如果发现要引用的模块已经出现在了sys.modules之中,(也就是执行完第4个步骤),那么就会继续执行importd 下一条语句,而不会顾及模块之中的内容是否的得到了定义。

例如上面的例子,app模块在执行自己第5步骤时,首先遇到的就是引入dialog模块的这条语句,而此刻他还没有把自己的内容定义出来,他只不过执行完了前4步骤,让自己出现在了sys.dodules字典里面而已。
等到dialog模块反过来要引入app的时候,由于app模块已经出现在了sys.modules字典中,python就会认为这个模块已近引入,于是继续执行dialog模块其他代码,而不会考虑app里面的内容到底有没有定义。
这样的话,执行到save_dialog = Dialog(app.prefs.get('save_dir')) 这一句的时候,就会因为app里面找不到prefs属性而出错。(这个属性必须等app执行完第5步骤才能够得到定义)

3.解决循环引用方法

如果要解决上面的循环引用异常,有四种解决办法。

3.1.重构引入关系

例如把prefs内容提取到一个单独的工具模块中,把它放在依赖体系最底层,这样app与dialog分别引入这个模块。他们的关系如下

  • app 引入 prefs
  • dialog 引入 prefs

有时候这种重构引入关系需要拆分代码,对于大型的项目可能不太好拆分,还可以通过其他的方式解决

3.2.调整import语句

调整import位置,例如我们可以让app模块不要那么早就引入dialog模块,而是等到prefs等其他内容都创建出来之后,在引入dailog,这样的话,等待dialog返回来使用app中的属性时,就不会因为该属性还没有定义出来而发生AttributeError

class Prefs:
    def get(self, name):
        pass

prefs = Prefs()

import dialog  # Moved
dialog.show()

这种写法虽然可行,但是它违背了PEP8规范,依照建议,所有的import语句都应该出现在文件开头。这种方式有个弊端,在执行了一半,才发现自己要使用的那个模块还没有加载进来,因此不建议使用这种方法。

3.3.把模块分成引入-配置-运行三个环节

循环引入可以通过劲量缩减引用时所要执行的操作。我们可以让模块只把函数、类、与常量定义出来,而不真正去执行,这样python在引入本模块的时候,就不会由于操作其他模块而出错了。
我们可以把本模块里,需要用到其他模块的那种操作放在configure函数中,等到模块彻底引入完毕后,再去调用。

dialog.py模块把调用的操作放在configure函数中

import app

class Dialog:
    def __init__(self):
        pass

save_dialog = Dialog()

def show():
    print('Showing the dialog!')

def configure():
    save_dialog.save_dir = app.prefs.get('save_dir')

app.py模块把调用的操作放在configure函数中

import dialog

class Prefs:
    def get(self, name):
        pass

prefs = Prefs()

def configure():
    pass

main.py模块按照引入-配置-运行的顺序先把那两个模块引入进来,然后调用各自的configure函数,最后运行dialog模块的show函数

import app
import dialog

app.configure()
dialog.configure()

dialog.show()

这种写法能适应许多种情况,而且便于我们运用依赖注入模式来替换受依赖模块之中的内容。
但是有时候不太容易从代码中抽离出这样一个configure配置环节,因为他把该模块定义的对象与这些对象的配置逻辑分别写到了两个环节里面。

3.4.动态引入

动态引入比前几个方法要简单,也就是把import语句从模块级别下移到函数或方法里面,这样就解决了循环依赖关系了。
这种import并不会在程序启动并初始化本模块时执行,而是等到相关函数真正运行的时候才得以触发,因此又叫做动态引入

下面我们用动态引入办法修改dialog模块,他只会在dialog.show函数真正运行的时候去引入import模块,而不像原来那样,模块刚初始化,就要引入app

class Dialog:
    def __init__(self):
        pass

# Using this instead will break things
# save_dialog = Dialog(app.prefs.get('save_dir'))
save_dialog = Dialog()

def show():
    import app  # Dynamic import
    save_dialog.save_dir = app.prefs.get('save_dir')
    print('Showing the dialog!')

app模块修改

import dialog

class Prefs:
    def get(self, name):
        pass

prefs = Prefs()
dialog.show()

main模块

import 

这样写,实际上与刚才那种先引入,再配置,然后运行的办法是类似的。区别仅仅在于,这次不调整代码的结构,也不修改模块的定义与引入方式,只是把形成循环依赖的那条import语句推迟到真正需要使用另外一个模块的那一刻。

一般来说还是劲量避免动态引入,因为import语句毕竟是有开销的,如果它出现在需要频繁执行的循环体里面,那么这种开销会更大。另外,由于动态引入会推迟代码的执行时机,有可能你代码启动很久之后,如果因为动态引入其他模块发生异常而奔溃。

到此这篇关于python解决循环依赖的文章就介绍到这了,更多相关python循环依赖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Python基于BeautifulSoup和requests实现的爬虫功能示例

    Python基于BeautifulSoup和requests实现的爬虫功能示例

    这篇文章主要介绍了Python基于BeautifulSoup和requests实现的爬虫功能,结合实例形式分析了Python使用BeautifulSoup和requests库爬取网站指定信息的相关操作技巧,需要的朋友可以参考下
    2019-08-08
  • python条件语句和while循环语句

    python条件语句和while循环语句

    这篇文章主要介绍了python条件语句和while循环语句,文章基于python的相关资料展开对其条件语句及while循环语句的详细内容介绍,需要的小伙伴可以参考一下
    2022-04-04
  • 在Linux上安装Python的Flask框架和创建第一个app实例的教程

    在Linux上安装Python的Flask框架和创建第一个app实例的教程

    这篇文章主要介绍了在Linux上安装Python的Flask框架和创建第一个app实例,包括创建一个HTML模版和利用Jinja2模板引擎来做渲染的步骤,需要的朋友可以参考下
    2015-03-03
  • pip install命令安装扩展库整理

    pip install命令安装扩展库整理

    这篇文章主要介绍了pip install命令安装扩展库整理,文中代码和图文讲解的很详细,有需要的同学可以参考下
    2021-03-03
  • python梯度下降法的简单示例

    python梯度下降法的简单示例

    这篇文章主要为大家详细介绍了Python梯度下降法的简单示例,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-08-08
  • Python extract及contains方法代码实例

    Python extract及contains方法代码实例

    这篇文章主要介绍了Python extract及contains方法代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-09-09
  • Python unittest单元测试openpyxl实现过程解析

    Python unittest单元测试openpyxl实现过程解析

    这篇文章主要介绍了Python unittest单元测试openpyxl实现过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-05-05
  • python3 BeautifulSoup模块使用字典的方法抓取a标签内的数据示例

    python3 BeautifulSoup模块使用字典的方法抓取a标签内的数据示例

    这篇文章主要介绍了python3 BeautifulSoup模块使用字典的方法抓取a标签内的数据,结合实例形式Fenix了python3 BeautifulSoup模块进行数据的抓取相关操作技巧,需要的朋友可以参考下
    2019-11-11
  • 使用Python第三方库发送电子邮件的示例代码

    使用Python第三方库发送电子邮件的示例代码

    本文主要介绍了使用Python第三方库发送电子邮件的示例代码,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • Python中的shutil模块使用详解

    Python中的shutil模块使用详解

    这篇文章主要介绍了Python中的shutil模块使用详解,shutil库作为os模块的补充,提供了复制、移动、删除、压缩、解压等操作,这些 os 模块中一般是没有提供的,需要的朋友可以参考下
    2023-09-09

最新评论