python编程中的circular import问题
循环引入,circular import是编程语言中常见的问题,在C语言中我们可以使用宏定义来处理,在c++语言中我们可以使用宏定义和类的预定义等方式来解决,那么在python编程中呢?
其实在python编程语言中出现circular import的时候还是毕竟少的,主要原因是python用来开发较大、较复杂的项目的场景有限,这一点不像C、C++等语言,但是随着AI领域的兴起python语言作为几乎是唯一的选择慢慢的应用多了起来。与此同时如网络编程、图形设计等中小规模的开发场景也开始逐渐选择python语言了,毕竟python语言的用户多了以后一些适合python的中小级别项目也会更倾向选择使用python语言开发了,但是随着python开发的项目大了起来以后这个circular import问题也就开始常见起来了。
=====================================
给出一个circular import的典型例子:
x2.py
print("this is x2.file, in ")
import x3
def f1():
print("x2.f1")
def f2():
x3.f3()
f2()
print("this is x2.file, out ")
x3.py
print("this is x3.file, in")
import x2
def f3():
print("x3.f3")
def f4():
x2.f1()
f4()
print("this is x3.file, out")
运行结果:


从上面的代码运行我们可以知道circle import问题其实简单的说就是一个python的module文件运行到import语句时跳转到了另一个module文件并执行该module文件,跳转到的module文件又import了原先没有运行完的module文件,但是由于原module文件已经加入到了引入模块中不会再次import了并且由于是执行被中断后跳转到现在的module中导致并没有完整的运行完。换句话说,跳转后的module文件查询已引入的模块发现原module已经被引入(但是此时的原module并没有完全引入)就不会再重新引入原模块,但是此时跳转后的module又调用了原模块中的变量,而由于原模块并没有完整引入因此该变量并不能成功调用,此时就会包circle import的错误。
知道了circle import 问题出现的原理,那么像上面的例子我们完全可以通过简单的修改import的位置来解决,给出修改后的文件:
x2.py
print("this is x2.file, in ")
def f1():
print("x2.f1")
import x3
def f2():
x3.f3()
f2()
print("this is x2.file, out ")
x3.py
print("this is x3.file, in")
def f3():
print("x3.f3")
import x2
def f4():
x2.f1()
f4()
print("this is x3.file, out")
运行结果:


可以看到对于circle import,只要解决对未初始化变量的调用就会解决报错问题,像上面的例子就是通过修改import的时机来解决的,但是在实际coding中出现circle import的情况都很复杂,对于不同情况也都需要不同的解决方法,但是基本准则就是解决对未初始化变量的调用。
-----------------------------------------------------
另外说一下,不论使用什么coding技巧对于出现circular import的python代码最终的必杀技就是代码重构,不过由于这样做时间损耗比较大所以不大万不得已还是不建议的。
在某些情况下可以使用稍稍修改来解决,在网上找到了一个针对两个文件内相互调用对方文件下的类来声明本文件下变量的解决方法:
https://www.bilibili.com/video/BV1E54y1R7wm/?vd_source=f1d0f27367a99104c397918f0cf362b7
=====================================
为了更详细的研究一下python中的circle import问题写了几个小DEMO,并把代码上传到网上,具体见:
https://gitee.com/devilmaycry812839668/circle_import_python
先进入到 circle_import_python_1 文件夹,看下文件:

文件内容:
main.py
import xyz
xyz.py
print("this is xyz.py file, in")
import submodule
submodule.s = submodule.s+1
def fun():
print("........ xyz.fun")
print("submodule.s: ", submodule.s)
print("this is xyz.py file, out")
submodule/__init__.py
print("this is __init__, in")
s = 0
import submodule.x
import submodule.y
print("this is __init__, out")
submodule/x.py
print("this is x.py file, in")
print("this is x.py file, out")
submodule/y.py
print("this is y.py file, in")
import xyz
xyz.fun()
print("this is y.py file, out")
在 circle_import_python_1 文件夹下执行:
python3 xyz.py

--------------------------------------------
python3 main.py

python3 main.py 时,xyz模块在main.py中被执行所以在y.py中import xyz时就不会被再次执行,但是此时main.py中对xyz模块的执行并没有完成因此y.py中调用xyz.fun就会由于未初始化而报错。
python3 xyz.py 时,xyz模块作为启动模块不会在启动时加入到已经在已经import的模块的路径集合中,所以在y.py中import xyz时就会被再次执行,这时再次跳转到xyz.py文件中,而由于y.py已经加入到已经import的模块的路径集合中,因此此时执行xyz.py模块可以顺利的对xyz.fun初始化,然后xyz执行完重新回到y.py中执行对xyz.fun的调用就不会报circle import的错误。
----------------------------------------------
从上面的circle_import_python_1的例子我们可以知道:
(python程序运行时会维护一个已经import的模块的路径集合,每次执行import语句时都会判断该模块是否已经在已经import的模块的路径集合中,如果不在才执行,如果已经存在则不执行)
1. 如果python程序以非交互的方式启动,那么启动文件(module)是不会加入到模块引入路径的,因此如果在其他模块中重新import启动文件那么就可能使启动文件被运行两次:一次是启动程序时作为启动文件,此时并没有加入import模块路径中;一次是在其他模块中被import,然后加入到import模块路径中。就如上面的submodule.s的最后输出数值为2。
(回答一个问题,python中有没有可能一个模块被重复执行两次?会的,如果这个模块是作为启动模块存在的,并且在其他模块中还import这个模块则这个模块会被执行两次)
2. 当import一个package里面的模块时都是要判断这个package的__init__.py模块是否被加入到import模块路径中,如果没有加入到已经import的模块的路径集合中则执行该__init__.py并加入到已经import的模块的路径集合中,只有package中的__init__.py被执行过才可以import该package下的模块。
3. 在一个module中执行import语句,如果判断需要import的模块并不在已经import的模块的路径集合中则会中断在现在的module中的运行并跳转到需要import的那个模块中运行,只有当import的那个模块执行结束才会恢复现模块的执行。
-------------------------------------------------------
对circle_import_python_1进行下修改,得到circle_import_python_2代码,修改的地方在submodule/__init__.py中的s=0的声明位置,我们将submodule/__init__.py中的s=0的声明位置放在import语句后。
修改后的 submodule/__init__.py :
print("this is __init__, in")
import submodule.x
import submodule.y
s = 0
print("this is __init__, out")
执行:
python3 xyz.py

可以看到同样也是报circle import错误。在python中如果项目大了起来出现circle import的机会就会增多,知道circle import出现的原理我们就可以根据实际情况来进行应对。就先前面说的,一旦出现circle import如果实在没有办法解决可以选择最后的方法,就是代码重构,不过重构的话一个是需要较大的时间耗费,一个就是会影响原代码的逻辑结构、影响代码风格。
可以说,对于circle import问题最好的解决方法就是在coding时对模块的层级做到较好的计划,在coding时有较好的规范可以很好的避免circle import问题的出现,不过在有些情况下该问题又难以通过改善coding规范的方法来解决(当然可以通过重构的方法来解决,不过需要尽量避免重构),给出以下的例子,参见:
https://www.bilibili.com/video/BV1E54y1R7wm/?vd_source=f1d0f27367a99104c397918f0cf362b7
参照上面视频中的代码重新写了各类似的代码,上传在:
x_class.py

y_class_3.py

对这个 typing.TYPE_CHECKING 个人理解的不是很多,个人的理解是 typing.TYPE_CHECKING 在编译时为true,在运行时为false。因此在编译时可以正常通过,在代码编辑时可以被识别出类型并给出很好的提示信息(value: int),而在执行时由于 typing.TYPE_CHECKING 为false,所以在执行时并不会执行import class语句因此不会造成circle import的错误。
而在y_class_1.py和y_class_2.py中虽然可以通过编译及运行,但是在代码编辑时还是会提示类型无法识别:


可以看到y_class_1.py和y_class_2.py中所使用的方法可以起到对程序员的提示功能,但是并不被IDE所识别,y_class_3.py中所使用的typing.TYPE_CHECKING方法为python的原生支持语法可以被IDE所识别。
------------------------------------------------------------------
typing.TYPE_CHECKING 的使用方法:
https://docs.python.org/zh-cn/3/library/typing.html?highlight=type_check#typing.TYPE_CHECKING

我们知道python语言在执行时是一边编译一边执行的,我们也可以这样理解,那就是python在执行一段代码之前是需要先对其进行编译的,编译好才会执行。python语言在编译时是不考虑对象和变量类型的,也就是不会去检查对象和变量类型的,至于会不会出现类型错误都是在执行的时候才会发现,python的该种特性导致在coding时是不会显示的声明类型的,这样不利于人类对代码的理解,说白了就是这样的代码在review的时候谁也搞不懂这个代码原先设计的含义是什么。为此python语言的解决方法就是大量的编写说明文档doc,但是doc的编写很耗费时间十分的不讨coding人员的喜爱,为此就出现了python语言的注解(typing下的annotation),说白了就是为python语言中的对象标记出类型,这个类型并不像c++、java语言那样提供给编译器使用而是只工程序员理解代码含义的,这一特性可以很好的减少doc文档的体量也便于理解。
而在 circle_import_python_3的例子中就是因为使用了注解(typing下的annotation)从而造成了circle import的问题,针对这种情况下的circle import问题python官方给出了typing.TYPE_CHECKING的解决方法,该方法在代码静态编辑的时候可以被pylance等第三方类型检查工具读取出typing.TYPE_CHECKING的值为true,以使执行import语句可执行从而保证编译成功,但是在代码执行的时候typing.TYPE_CHECKING的值为false,从而避免了circle import错误的出现。
=====================================
相关:
https://blog.csdn.net/xun527/article/details/108637094
python编程中的circular import问题的更多相关文章
- 【转载】Python编程中常用的12种基础知识总结
Python编程中常用的12种基础知识总结:正则表达式替换,遍历目录方法,列表按列排序.去重,字典排序,字典.列表.字符串互转,时间对象操作,命令行参数解析(getopt),print 格式化输出,进 ...
- Python编程中常用的12种基础知识总结
原地址:http://blog.jobbole.com/48541/ Python编程中常用的12种基础知识总结:正则表达式替换,遍历目录方法,列表按列排序.去重,字典排序,字典.列表.字符串互转,时 ...
- 解析Python编程中的包结构
解析Python编程中的包结构 假设你想设计一个模块集(也就是一个"包")来统一处理声音文件和声音数据.通常由它们的扩展有不同的声音格式,例如:WAV,AIFF,AU),所以你可能 ...
- 详解Python编程中基本的数学计算使用
详解Python编程中基本的数学计算使用 在Python中,对数的规定比较简单,基本在小学数学水平即可理解. 那么,做为零基础学习这,也就从计算小学数学题目开始吧.因为从这里开始,数学的基础知识列位肯 ...
- Python编程中 re正则表达式模块 介绍与使用教程
Python编程中 re正则表达式模块 介绍与使用教程 一.前言: 这篇文章是因为昨天写了一篇 shell script 的文章,在文章中俺大量调用多媒体素材与网址引用.这样就会有一个问题就是:随着俺 ...
- Python编程中NotImplementedError的使用
Python编程中raise可以实现报出错误的功能,而报错的条件可以由程序员自己去定制.在面向对象编程中,可以先预留一个方法接口不实现,在其子类中实现.如果要求其子类一定要实现,不实现的时候会导致问题 ...
- Python编程中的反模式
Python是时下最热门的编程语言之一了.简洁而富有表达力的语法,两三行代码往往就能解决十来行C代码才能解决的问题:丰富的标准库和第三方库,大大节约了开发时间,使它成为那些对性能没有严苛要求的开发任务 ...
- python编程中的if __name__ == 'main与windows中使用多进程
if __name__ == 'main 一个python的文件有两种使用的方法,第一是直接作为程序执行,第二是import到其他的python程序中被调用(模块重用)执行. 因此if __name_ ...
- python编程中的if __name__ == 'main': 的作用和原理
在大多数编排得好一点的脚本或者程序里面都有这段if __name__ == 'main': ,虽然一直知道他的作用,但是一直比较模糊,收集资料详细理解之后与打架分享. 1.这段代码的功能 一个pyth ...
- python编程中的一些有用插件或工具
windows监控 在python编程的windows系统监控中,需要监控监控硬件信息需要两个模块:WMI 和 pypiwin32 . 前端文件上传插件 krajee karkit 后台管理模板 ni ...
随机推荐
- 写的程序总是出 BUG,只好请佛祖前来镇楼啦
前言 自己之前写着玩的,在这做个备份,感觉不错的取走即可. 南无阿弥陀佛 佛祖镇楼,BUG 消失,永不怠机. ///////////////////////////////////////////// ...
- 警告: BASE64Decoder是内部专用 API, 可能会在未来发行版中删除
警告: BASE64Decoder是内部专用 API, 可能会在未来发行版中删除 import org.apache.commons.codec.binary.Base64; public class ...
- SpringBoot指标监控功能
SpringBoot指标监控功能 随时查看SpringBoot运行状态,将状态以josn格式返回 添加Actuator功能 Spring Boot Actuator可以帮助程序员监控和管理Spring ...
- java多线程-3-使用多线程的时机
许多人对于计算机的运行原理不了解,甚至根本不了解. 不幸的是,此类中的一部分人也参与了计算机的编码工作.可想而知,编写的效率和结果.听者伤心,闻者流泪. 此类同学的常见的误解: 并发就能加快任务完成 ...
- Typora行内公式识别不了
Typora行内公式识别不了,主要是因为行内公式属于LaTeX扩展语法,并非Markdown的通用标准 需要在Typora的"文件"-"偏好设置"-" ...
- 关于c指针的理解
1 #include<stdio.h> 2 { 3 int a= 100,b=10; 4 int *p1=&a,*p2=&b; 5 *p1=b; 6 *p2=a; 7 pr ...
- uCos 学习:0-有关概念
先说一下UCOSIII:Micrium在2009年推出了UCOSIII,相对于之前的UCOSII版本,在性能上有了进一步的提升,主要是支持时间片轮调度,极短的关中断事件等. 可剥夺多任务管理: 什么是 ...
- MinIO使用记录
探索MinIO:高性能.分布式对象存储解决方案 注:本文除代码外多数为AI生成 最近因为有项目需要换成Amazon S3的云存储,所以把之前做过的minio部分做一个记录,后面也会把基于这版改造的S3 ...
- Java中的栈、堆和常量池
Java程序是运行在JVM(Java虚拟机)上的,因此Java的内存分配是在JVM中进行的,JVM是内存分配的基础和前提. Java程序的运行会涉及以下的内存区域: 寄存器:JVM内部虚拟寄存器,存取 ...
- yb课堂 开发前端项目路由 《三十五》
Vue-Router开发前端项目路由 vue-router 是Vue.js官方的路由管理器,它和Vue.js的核心深度集成,让构建单页面应用变得易如反掌 官方文档:https://router.vue ...