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

作者:Bruce小鬼 时间:2023-03-19 12:22:14 

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语句毕竟是有开销的,如果它出现在需要频繁执行的循环体里面,那么这种开销会更大。另外,由于动态引入会推迟代码的执行时机,有可能你代码启动很久之后,如果因为动态引入其他模块发生异常而奔溃。

来源:https://blog.csdn.net/m0_38039437/article/details/128138739

标签:python,循环,依赖
0
投稿

猜你喜欢

  • python PIL和CV对 图片的读取,显示,裁剪,保存实现方法

    2022-02-06 09:35:07
  • Python 实用技巧之利用Shell通配符做字符串匹配

    2021-07-18 22:57:16
  • CentOS安装mysql5.7 及简单配置教程详解

    2024-01-21 08:18:51
  • 如何导出python安装的所有模块名称和版本号到文件中

    2022-07-26 15:38:59
  • Hive-SQL查询连续活跃登录用户思路详解

    2024-01-22 08:29:47
  • 基于SQLAlchemy实现操作MySQL并执行原生sql语句

    2024-01-18 17:16:05
  • python去掉字符串中重复字符的方法

    2022-11-23 09:17:35
  • python连接PostgreSQL数据库的过程详解

    2023-08-24 03:42:31
  • Sql Server中存储过程中输入和输出参数(简单实例 一看就懂)

    2024-01-26 17:20:54
  • JS脚本实现网页自动秒杀点击

    2024-04-16 09:36:09
  • python应用文件读取与登录注册功能

    2023-04-17 17:04:03
  • PHP中curl_setopt函数用法实例分析

    2023-11-22 22:07:22
  • asp如何判断偶数和奇数?

    2010-01-12 20:16:00
  • PHP一些常用的正则表达式字符的一些转换

    2024-05-13 09:22:19
  • 一篇文章带你了解python标准库--os模块

    2023-06-23 01:19:10
  • Python 矩阵转置的几种方法小结

    2023-01-20 15:51:56
  • Python MySQLdb 使用utf-8 编码插入中文数据问题

    2023-07-31 11:04:13
  • Python 找出出现次数超过数组长度一半的元素实例

    2023-06-07 05:50:33
  • 公网远程访问局域网SQL Server数据库

    2024-01-22 01:38:21
  • vue 项目中当访问路由不存在的时候默认访问404页面操作

    2024-04-30 10:41:49
  • asp之家 网络编程 m.aspxhome.com