引入
Python的模块导入对于编程来说可以是家常便饭了,几乎每次写代码前都要进行导包。可你真的了解Python怎么导包的吗?为什么我写的程序总是会出现ModuleNotFoundError: No module named XXX?这Python的导包真的是令人头疼,包括我在网上搜的一些文章也都含糊不清地介绍下,本质上并未解决问题。所以写下这篇来解决这无处不在的导包问题。看这篇文章之前请带上以下两个问题:
- 模块内的__name__是什么,它是怎么变化的?
- 什么时候用绝对导入,又什么时候该用相对导入?
如果你不能很确定地知道答案,那请认真耐心地看完该篇,相信你一定会有所收获。
在此之前,要先从最基本的知识点说起
导入语句的风格
Python的官方风格指南—PEP 8,在编写导入语句时有几个忠告。总结如下:
- 导入总是位于文件的顶部,在任何模块注释和文档字符串之后。
- 导入应该根据导入情况来划分。通常有三类:
- 标准库导入(Python的内置模块)
- 相关第三方库导入
- 本地模块导入
- 每一组导入应该被空行隔开。
示例如下:
"""
说明文档内容....
"""
import re # Python的标准库
import sys
# 空行隔开
import requests # 安装的第三方库
# 空行隔开
import my_module # 自定义模块
模块与包
- 模块:用于组织Python代码(变量、函数、类)可以理解为就是一个以.py结尾的文件
- 包:用于组织模块的,可以理解为多个模块组成的目录,它还有一个
__init__.py
文件(实际上在Python3.3后,无论有无__init__.py
,Python都会将该目录视为包,但为了规范,应该要在包中添加__init__.py
) - 导入模块的本质:就是将导入的.py文件内容用解释器执行一遍
- 导包的本质:就是将__init__.py文件内容用解释器执行一遍
特别提醒:自定义模块一定不要与Python的标准库重名,也不要与第三方库重名,否则就会出现导包的问题(比如Python的内置模块abc
和test
,如果你写的目录名或是文件名与该名称一致,那么程序如果使用import
就会出现问题。)
导包的方式
import XXX[.XX.X....][as X] # as是起别名
from XXX[.XX.X....] import xxx
# from XXX import * # 这种导包的方式是不建议的,是一种非常不好的习惯
应该用 from XXX import xxx1, xxx2, xxx3来代替
绝对导入的问题
为了更好地说明,先创建如下的目录结构
outer
│ main1.py
│ __init__.py
│
└─middle
│ main2.py
│ __init__.py
│
├─inner1
│ module1.py 初始文件内容:print('in the module1')
│ test1.py
│ __init__.py
│
└─inner2
module2.py
__init__.py
其对应在PyCharm的文件结构为:
先从最里层的inner1
文件开始:
要在test1.py
中尝试导入module1
我们一般会这样做:
import module1
实际上在PyCharm是会给出红色警告的。
但我们尝试在test.py
模块下运行,是可以得到结果的
实际上大可不必管红色警告,因为Python解释器是可以运行的,有强迫症的可以右键父目录,找到Mark Directoy as --> Sources Root。
那么这是一种怎样的导入方式?它是绝对导入。
为什么?来看一下sys.path
修改module1.py
import sys
print(sys.path)
print("in the module1")
然后在test1.py
中运行,得到运行结果:
['C:\\project_name\\outer\\middle\\inner1', 'C:\\project_name', 'C:\\Python36\\python36.zip', 'C:\\Python36\\DLLs', 'C:\\Python36\\lib', 'C:\\Python36', 'C:\\Python36\\lib\\site-packages', 'C:\\Program Files\\JetBrains\\PyCharm 2018.3.4\\helpers\\pycharm_matplotlib_backend']
in the module1
我们主要关注的是前两个路径,后面的路径其实是一开始配置Python环境变量加入的
第一个路径实际上是当前运行模块的父目录路径
那第二个路径哪来的?我觉得应该是在PyCharm下创建项目时加入的,我在VsCode上试过是没有该路径的。(所以为了程序的可移植性,这个路径当做没有)
首先说明:import 能否导入成功,决定权是在sys.path中。 而日常我们遇到的很多导包问题就出在sys.path[0]
中。
模块要想被执行,你其实需要给Python解释器一个完整的路径名。
那什么是绝对导入:就是以sys.path[0]为基准,顺序往里导入的路径就是绝对导入。(注意不能跳,否则报错)
知道后,现在让我们尝试在main2.py
中导入module1.py
, 你应该会这么写:
from inner1 import module1
ok, 程序正常运行,得到结果, 但细心的你应该要发现sys.path[0]
改变了,
变为'C:\\project_name\\outer\\middle'
,这点非常重要。
在test1.py
下运行和main2.py
运行,同样的module1.py
下 print(sys.path())
,为什么结果却不一样?
根据前面写的导入模块的本质你就该知道,sys.path[0]
是会随着当前执行的模块而改变的。
好,那在main2.py
中导入test1.py
呢,根据上面的导入方式,你又会这么写:
from inner1 import test1
然后程序一运行,给你一个报错:
ModuleNotFoundError: No module named 'module1'
why? 之前在test1.py
下运行不是没有错误吗?当它被导入的时候就给我报错?
因为此时的sys.path[0]已经变了,是'C:\\project_name\\outer\\middle'
, 该目录下只有main2.py
,__init__.py
,inner1和inner2目录,所以python解释器是找不到module1
的,那要怎么解决这个问题呢?
你可能会说,那在sys.path
中添加'C:\\project_name\\outer\\middle\\inner1'
不就好了,那如果这个文件目录很复杂,导包的情况也有很多,这时你也要一个个的往里添加吗?这显然违背了 python之禅 ,代码不够优美。
那修改一下test1.py
的内容,改为
from inner1 import module1
修改之后,程序正常运行,但又会出现一个问题,当你在main1.py
中导入test1.py
下并运行的时候,程序又会报ModuleNotFoundError: No module named 'inner1'
,这些本质的原因其实都是sys.path[0]
改变造成Python解释器找不到路径而造成的。
为了解决以上问题,在此引出相对导入。
相对导入
相对导入分为隐式相对和显式相对,但隐式相对在Python3中已经废弃,所以现在说的相对导入都是显示相对
为了解决以上的问题,需将test1.py
文件内容改为
"""
相对导入的.代表当前文件夹的路径, ..则是再往上一层。
"""
from . import module1
这时你无论是在main1.py
还是在main2.py
中导入test1.py
模块时,各自都能正常运行。
因为.
会随着运行文件改变而改变,当在main1.py
运行时,.
就代表middle.inner1
; 而在main2.py
运行时,.
就代表inner1
。
但不要高兴地太早,这时又会出现一个问题,如果在test1.py
下直接运行程序
又会给你一个报错:ImportError: cannot import name 'module1'
是路径的问题吗?print(sys.path[0])
发现确实是C:\\study\\outer\\middle\\inner1
,当前路径下是有module1的。
我们可以发现错误是发生了变化的,但就是给你报错,这又是为啥??
实际上如果是相对导入,就只能导入其顶层模块内部的模块,
当一个模块被直接运行,它自己作为顶层模块,不存在层次结构,所以找不到其他的相对路径。
一句话解释就是如果一个模块有相对导入的语句,它只能被其他模块导入,本身不能直接运行
回到最初问题
为了回答最初的两个问题,需要修改文件的内容,让程序告诉我们答案
其中main2.py
import sys
import inner1.test1
print("main2的__name__--> ", __name__)
print(sys.path[0])
test1.py
from . import module1
print("test1的__name__--> ", __name__)
module1.py
print("module1的__name__--> ", __name__)
现在在main2.py
中运行程序,得到结果:
module1的__name__--> inner1.module1
test1的__name__--> inner1.test1
main2的__name__--> __main__
C:\project_name\outer\middle
从中我们可以得到结论:
- 当一个模块直接运行时,它的
__name__
为__main__
- 当一个模块被调用时,它的完整路径为
sys.path[0]
+__name__
那什么时候用绝对路径?
当该模块需要调用其内部的子模块时,一般这个模块位于顶级目录下,由它负责项目中各个模块的调用
那什么时候用相对路径?
当该模块在包内部并且希望被其他模块调用时。
到此就模块导入的问题就结束了吗?不,还有问题
__init__.py
的作用
为了说明它的作用,各模块的内容修改如下:
module2.py
def foo():
print("in the module2")
main2.py
from inner2 import module2
module2.foo()
当运行main2.py
时,ok,程序正常运行
但这时修改main2.py
的内容,换一种调用方式
import inner2
inner2.module2.foo()
这次再次运行main2.py
时,程序报错:AttributeError: module 'inner2' has no attribute 'module2'
为什么仅仅导入inner2
在调用时就会报错,inner2
下不是有module2
吗?
其实inner2是一个包,当导入一个包时,实际上是导入__init__.py
,但是__init__.py
文件没有内容,所以inner2
没有module2
这个属性。
所以需要在__init__.py
文件中添加
from . import module2
这时运行main2.py
就没有问题了。
现在我们再来看看Python内置的模块的__init__.py
中都写了什么
其中在Lib
下的json
包的__init__.py
内容如下:
from .decoder import JSONDecoder, JSONDecodeError
from .encoder import JSONEncoder
import codecs
所以当导入json
时,是可以调用json.encoder
下的方法,可以发现__init__.py
导入包内的模块时一般采用的是相对导入
再来看一下Lib
下的urllib
中的__init__.py
内容
发现它竟然为空!!!那我们就不能import urllib,再根据urllib.parse调用了, 什么,不相信?
那就试试吧,在inner2
下创建test2.py
,文件内容
import urllib
data = {'a': '1'}
print(urllib.parse.urlencode(data))
为了更精确地测试,注意不要直接在PyCharm下直接运行,在inner2下打开命令行执行python test2.py
,程序报错!!
你还可以再看看其他包下__init__.py
文件内容。
OK,到此文章结束,但模块导入的问题还没有结束,以后所遇到的错误就应该运用所学的知识去解决了。