深入理解Python中import机制

551次阅读  |  发布于4年以前

大型项目中为了维护方便,通常使用模块化开发,模块化的过程中,就会涉及到各种包或者模块的相互导入,即使是对于有多个项目的Python开发者来说,import也会让人困惑!本文带你深入了解python中import的内在机制,从而避免import导入引发的异常。

概念

模块(module)

任何.py文件都可以称为模块

包(package)

可以将多个模块放入一个包中,就像电脑中的文件夹,但与文件夹的区别是,package包含__init__.py文件

Python import 的搜索路径

当我们执行python xx.py时,python是如何帮我们正确定位包所在的目录呢?其实系统是按照以下顺序来寻找的:

1.系统内置模块,比如os, sys模块2.入口文件所在的目录,比如main.py所在的目录3.Python环境变量,也就是我们平时pip install后的包所在的目录,如Anaconda下的site-packages目录

在Python中,如果遇到了import错误,我们可以通过以下命令查看搜索路径:

import sys
print(sys.path)

结果:

sys.path: [
'/Users/root/Python/project',
'/Users/root/anaconda3/lib/python36.zip',
'/Users/root/anaconda3/lib/python3.6',
'/Users/root/anaconda3/lib/python3.6/lib-dynload',
'/Users/root/.local/lib/python3.6/site-packages',
'/Users/root/anaconda3/lib/python3.6/site-packages',
'/Users/root/anaconda3/lib/python3.6/site-packages/Sphinx-1.5.6-py3.6.egg',
'/Users/root/anaconda3/lib/python3.6/site-packages/aeosa',
'/Users/root/anaconda3/lib/python3.6/site-packages/mdr-0.0.1-py3.6-macosx-10.7-x86_64.egg'
]

可以看到,其中第一个目录是我们运行的文件所在目录,其他都是Python环境变量中的目录

执行python xx.py发生了什么?

直接被Python解释器运行的文件,称之为程序的入口文件,在Python中,入口文件有且仅有一个

我们经常使用以下语句:

# in main.py
if __name__=="__main__":
    run()

__name__=="__main__"这个语句就是检测当前脚本是否被当作入口文件使用,如果被当做入口文件使用,那么就运行if __name__=="__main__":下面的代码块,如果只是当做模块被其他的模块import进去使用,那就不应该运行这部分代码块。

因此,判断一个文件是否被python解释器当做入口文件对待,是可以打印脚本的__name__变量的。

# in main.py
print(__main__)

入口文件和import路径有什么关系呢?

如果我们把xx.py当做入口文件,那么Python解释器,就会将xx.py所在的目录,加入到sys.path中,作为import时搜索的根目录。简言之,你用python filename.py执行哪个文件,Python解释器就会将那个文件所在目录加入import的搜索目录。

新问题:在模块化的项目中,如何让Python解释器搜索到所有的模块呢?

这引入了一个项目结构合理性的问题:入口文件的目录层级应该是顶层的,也就是入口文件目录层级不应该低于任何模块或者包,一个常见的目录结构通常如下:

$ tree
./project
├── package
│   ├── __init__.py
│   ├── sub_pkg1
│   │   ├── __init__.py
│     │     ├── module_X.py
│   │   └── module_Y.py
│   └── sub_pkg2
│       ├── __init__.py
│       └── module_Z.py
└── main.py(入口文件)

如果我们执行python main.py,那么main.py就会被当做入口文件,所在目录project就会被加入import的搜索路径,那么我们将能够搜索package1package2中的模块;现在假设我们直接运行python module_X.py,此时入口文件变成了module_X.py,那么我们import搜索的根目录为sub_pkg1,当我们试图去import sub_pkg2中的文件时,就会引发异常。

上述问题引入了一个准则:除了入口文件之外,其他文件都不应该通过python xx.py来运行,那如果我们想运行某个模块怎么办?

答案是:通过python -m sub_pkg1.module_X来运行!

python -m的意思是将module当做模块来运行,同时sub_pkg1.module_X是导入路径,这样子就不会找不到模块了。

上述解决方案引入一个比较好的实践:

如果在一个大型项目中,你经常使用cd命令跳转到各种不同层级的目录中Python xx.py运行某个模块,那你可能得尝试使用Python -m了,也就是我们最好在项目的根目录下完成所有的模块的测试。当然,完善的项目,应该有专门的测试目录如tests, 这个以后有机会再讲。

Python中的相对导入和绝对导入

在Python中,存在相对导入和绝对导入两种import机制,但无论是绝对导入还是相对导入,都需要一个参照物,不然「绝对」与「相对」的概念就无从谈起。绝对导入的参照物是项目的根文件夹,相对导入参照物是当前模块,当我们使用相对导入时,需要给出相对于当前模块,想导入模块所在的位置。

# 相对导入
from . import fool
from .package import fool
from ..module import spam

# 绝对导入,项目根目录是app
from app.package import fool

相对导入可以避免硬编码带来的维护问题,例如我们改了某一顶层包的名字,那么其子包所有的导入就都不能用了。除非我们手动修改顶层包名。但是存在相对导入语句的模块,不能直接使用python xx.py的方式运行,否则会有异常:

ValueError: Attempted relative import in non-package

•如果是绝对导入,一个模块只能导入自身的子模块或和它的顶层模块同级别的模块及其子模块•如果是相对导入,一个模块必须有包结构(意味着有__init__.py)且只能导入它的顶层模块内部的模块 所以,如果一个模块被直接运行,Python会将该模块当做顶层模块(top level),不再当做一个包来对待,因此不存在层次结构,所以找不到其他的相对路径,所以如果直接运行python xx.py ,而xx.py有相对导入就会报错

看下面例子:

$ tree
./project
├── package
│   ├── __init__.py
│   ├── sub_pkg1
│   │   ├── __init__.py
│     │     ├── module_X.py
│   │   └── module_Y.py
│   └── sub_pkg2
│       ├── __init__.py
│       └── module_Z.py
└── main.py(入口文件)

module_X.py

from . import module_Y

print  "X __name__", __name__

module_Y.py

print  "Y __name__", __name__

当我们直接运行 python sub_pkg1/module_X.py的时候,会报错

ValueError: Attempted relative import in non-package

当我们这样运行的时候 python -m sub_pkg1.module_X, 才能正常运行

Y __name__ sub_pkg1.moduleY
X __name__ __main__

为什么会这样?简单地说,直接运行 .py 文件和 import 这个文件有很大区别。Python 解释器判断一个 py 文件属于哪个 package 时并不完全由该文件所在的文件夹决定。它还取决于这个文件是如何 load 进来的(直接运行 or import)。

有两种方式加载一个 py 文件:

•作为 top-level 脚本 作为 top-level 脚本指的是直接运行脚本,比如 python xx.py。有且只能有一个 top-level 脚本,就是最开始执行的那个(比如 python xx.py 中的 xx.py)。

•作为 module 作为 module 是指,执行 python -m xx,或者在其它 py 文件中用 import 语句来加载,那么它就会被当作一个 module。

当一个 py 文件被加载之后,它会被赋予一个名字,保存在 __name__属性中。如果是 top-level 脚本,那么名字就是__main__。如果是作为 module,名字就是把它所在的 packages/subpackages 和文件名用 . 连接起来。

例如,moduleX 被 import 进来,它的名字就是 package.subpackage1.moduleX。如果 import 了 moduleA,它的名字是 package.moduleA。如果直接运行 moduleX 或 moduleA,那么名字就都是__main__了。

所以上面的module_X__name____main__, 因为他是直接运行的, module_Y__name__sub_pkg1.module_Y,因为他是被import 来使用的。

常见几种import需求

module_X.py

# module_X导入module_Y

# 相对导入
from . import module_Y
# 绝对导入
from package.sub_pkg1 import module_Y

module_X.py

# module_X导入module_Z

# 相对导入
from ..sub_pkg2 import module_Z
# 绝对导入
from package.sub_pkg2 import module_Z

main.py

# main.py导入module_Y

from package.sub_pkg1 import module_Y

特别需要注意的是,虽然上述模块导入路径是对的,除了main.py之外,都不可以通过python xx.py的方式运行,而是通过前面讨论过个python -m方式运行。

相对导入和绝对导入的优缺点

相对导入可以避免硬编码,对于包的维护是友好的。其缺点是可读性较差,让人很难清楚地了解到资源所在的位置。

绝对路径导入由于其直观往往是大家的首选。只要看一下导入语句你就能知道资源是从什么位置导入的。再者,就算当前import语句的位置发生了变化,此绝对路径导入的资源依然有效。实际上,官方也推荐使用绝对路径导入。

《Python之禅》中提到:

明确 优于 隐晦

纵观一些优秀的开源项目,绝对导入的使用也是更为普遍的。我个人通常以绝对导入为主,相对导入为辅,只会在同一个模块层级中使用一些相对导入,如:from .fool import spam,很少会使用from ...fool import spam 这样让人迷惑的相对导入。

解决import错误的调试思路

1.区分是文件夹还是包(有无__init__.py)? 2.非入口文件是否是使用python -m运行的? 3.入口文件的层级,是否高于任何包或者模块?

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8