缘起
循环import是很多Python初学者都会遇到问题,网上有也有很多文章讲解决方法,比如这篇,不清楚的可以自行查阅,这里就不赘述了。
那么,为啥老司机也会遇到这个问题呢?这段时间一直在搞把redis复刻一个python版本,在复刻代码时就遇到了这个问题。而且我也使用了延迟import,却没能解决。
下面我们来详细分析下
症状
先看两段代码
run.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import logging import sys
logging.basicConfig(level='DEBUG', format='[{asctime} {module}.{funcName:<11}] {message}', style='{') logging.info(sys.modules['__main__']) logging.info('begin load')
class Server: pass
server = Server()
def start(): from foo import do_someting logging.info('call') assert not hasattr(server, 'name') server.name = 'aaa' logging.info(repr(server)) logging.info('%s\t%s', repr(Server), id(Server)) do_someting()
logging.info('end load')
if __name__ == '__main__': start()
|
foo.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import logging import sys
logging.info('begin load')
def do_someting(): logging.info('begin call') import run
logging.info(repr(run.server)) logging.info('%s\t%s', (run.Server), id(run.Server)) if hasattr(run.server, 'name'): logging.info('found attr name') else: logging.info('not found attr name') logging.info('end call')
logging.info('end load')
|
再看执行python run.py的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| [2020-06-20 10:57:03,659 run.<module> ] <module '__main__' from '/Volumes/study/Projects/code_snippet/circular_import/run.py'> [2020-06-20 10:57:03,659 run.<module> ] begin load [2020-06-20 10:57:03,659 run.<module> ] end load [2020-06-20 10:57:03,662 foo.<module> ] begin load [2020-06-20 10:57:03,662 foo.<module> ] end load [2020-06-20 10:57:03,662 run.start ] call [2020-06-20 10:57:03,662 run.start ] <__main__.Server object at 0x1064eb940> [2020-06-20 10:57:03,662 run.start ] <class '__main__.Server'> 140250245614784 [2020-06-20 10:57:03,662 foo.do_someting] begin call [2020-06-20 10:57:03,663 run.<module> ] <module '__main__' from '/Volumes/study/Projects/code_snippet/circular_import/run.py'> [2020-06-20 10:57:03,663 run.<module> ] begin load [2020-06-20 10:57:03,663 run.<module> ] end load [2020-06-20 10:57:03,663 foo.do_someting] <run.Server object at 0x1065d8b50> [2020-06-20 10:57:03,663 foo.do_someting] <class 'run.Server'> 140250247610512 [2020-06-20 10:57:03,663 foo.do_someting] not found attr name [2020-06-20 10:57:03,663 foo.do_someting] end call
|
这里已经用延迟导入,这个典型方法,解决了执行时报错的问题
但是,还是可以发现几个问题
- run.py 被加载了两次
- 在run模块中的server实例和Server类,与foo模块中的id一样,也就是不是同一个对象。(第8行和14行)
分析
先复习下import机制
import 语句结合了两个操作;它先搜索指定名称的模块,然后将搜索结果绑定到当前作用域中的名称。 import 语句的搜索操作定义为对 __import__() 函数的调用并带有适当的参数。 __import__() 的返回值会被用于执行 import 语句的名称绑定操作。
对 __import__() 的直接调用将仅执行模块搜索以及在找到时的模块创建操作。 不过也可能产生某些副作用,例如导入父包和更新各种缓存 (包括 sys.modules),只有 import 语句会执行名称绑定操作。
sys.modules是一个字典,缓存了已加载的模型,以模块名称为key,模块对象为value。
执行import 语句时,先在sys.modules缓存中查询该模块,如已存在者返回该对象,否则从文件系统中加载该模块。
1
| [2020-06-20 10:57:03,662 run.start ] <__main__.Server object at 0x1064eb940>
|
从上面的这行输出可以看出,当run作为程序入口时,模块名称变为了__main__, 查看 sys.modules,也只发现了__main__,没有发现run.
所以, 当do_someting import run 模块时,肯定是发现没有加载,最终导致加载了两次,Server类id不一致也可以理解了。
解决
所以只要能从sys.modules正确地找到run模块,问题就可以解决。
具体来说有三种方法
方法A
修改foo.py, 把import run改为import __main__ as run
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import logging import sys
logging.info('begin load')
def do_someting(): logging.info('begin call') import __main__ as run
logging.info(repr(run.server)) logging.info('%s\t%s', (run.Server), id(run.Server)) if hasattr(run.server, 'name'): logging.info('found attr name') else: logging.info('not found attr name') logging.info('end call')
logging.info('end load')
|
方法B
修改sys.modules,增加keyrun,指向__main__模块
1
| sys.modules['run'] = sys.modules['__main__']
|
方法C(推荐)
启动文件单独使用一个文件,里面不包含其他代码。
这时__main__模块变成了bar, 这时run模块的名称就不会改变了,import行为也就正常了
bar.py
1 2 3
| from run import start
start()
|
输出结果
上面三种方法,殊途同归,结果都是一样的。
1 2 3 4 5 6 7 8 9 10 11 12
| [2020-06-20 11:43:31,508 run.<module> ] begin load [2020-06-20 11:43:31,508 run.<module> ] end load [2020-06-20 11:43:31,509 foo.<module> ] begin load [2020-06-20 11:43:31,509 foo.<module> ] end load [2020-06-20 11:43:31,509 run.start ] call [2020-06-20 11:43:31,509 run.start ] <run.Server object at 0x10f542fa0> [2020-06-20 11:43:31,509 run.start ] <class 'run.Server'> 140660950954304 [2020-06-20 11:43:31,509 foo.do_someting] begin call [2020-06-20 11:43:31,509 foo.do_someting] <run.Server object at 0x10f542fa0> [2020-06-20 11:43:31,509 foo.do_someting] <class 'run.Server'> 140660950954304 [2020-06-20 11:43:31,509 foo.do_someting] found attr name [2020-06-20 11:43:31,509 foo.do_someting] end call
|
总结
因为C是编译型语言,可以理解为模块的导入在编译期就完成了,也就不会出现模块的循环依赖,而且全局对象的内存位置也在编译期就固定了。
而Python作为解释型语言,模块的导入加载和执行是混在一起的,所有对象都是可以更改的,也就容易出现这种问题。
切记:
复杂Python程序的入口文件最好保持单一的文件,不要混入其他对象定义,谨慎使用if __name__ == '__main__'写法。
参考
- https://docs.python.org/zh-cn/3/reference/import.html
- https://docs.python.org/zh-cn/3/library/sys.html?#sys.modules