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

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

作者:Bruce小鬼原文地址:https://blog.csdn.net/m0_38039437/article/details/128138739

%s 个评论

要回复文章请先登录注册