参考于:https://www.bilibili.com/video/BV1EK411g7Ff
在python中有一些常见的概念,并且这些概念可能会被混淆:
- 脚本(script):一个python文件,可以直接运行用于实现特定的功能。通常不包含类和函数,只是用来执行。
- 模块(module):也是一个python文件,通常包含了一些类和函数,用来被其他文件引用,包括被脚本文件引用,或是被其它模块引用。
- 包(package):一些相关的模块的集合,本质上是一个文件夹,但通常需要一个名为
__init__.py
的文件(python较高版本也可以不需要该文件),内容可以为空,表明这是一个python的包,而不是一个普通的文件夹。
接着,是一些查看python文件中搜索路径、命名空间和模块的方法:
-
sys.path
:其是一个列表,包含了python解释器在导入模块时会搜索的所有目录路径。当尝试导入一个模块时,python就会按照这个列表中提供的路径来顺讯查找这个模块是否在路径下。其会将运行的脚本文件的所在目录添加在该列表的首部,并且其是python解释器的全局变量,会在脚本和各个模块之间共享。因此,python只知道脚本文件所在的位置,而不知道其它文件所在的位置,直接运行某个文件可能会导致其它文件中的引用失效。 -
dir()
:是一个内置函数,用来返回指定对象或当前局部作用域的有效属性名列表。当不提供任何参数时,就会返回当前局部作用域的名称列表,对于一个文件来说,就会返回其包含的所有的全局变量、函数、类等。可以用来查看一个文件中可用的名称。 -
sys.modules
:是一个字典,保存了已经导入到当前会话中的模块,用来查看导入的模块有哪些,也是一个全局的变量,在所有涉及当前程序的python文件的模块都会保存在该字典中。其中键的名称是模块名,根据引用的方式可能只有模块名,也可能带有包名。也就是说同一个模块可能因为引用方式的不同而在该字典中有两个不同的名称,被引用两次。
绝对引入
首先,我们先探索绝对引入,其方式就是import xxx
或from xxx import xxx
,使用的路径不包含相对的层级结构,会直接从sys.path
下搜索模块。
我们在名为Temp1
的文件夹下创建m1.py
文件,定义了一个简单的函数,其内容为:
def f1():
print("this is f1 in m1")
f1()
并且也在Temp1
下创建run.py
文件,将m1.py
导入到本文件中,其内容为:
import m1
m1.f1()
此时的文件结构为:
Temp1
m1.py
run.py
然后我们运行run.py
文件,得到的结果如下:
E:\WorkSpace\Temp1>python run.py
this is f1 in m1
this is f1 in m1
结果中,有两个this is f1 in m1
,说明f1()
执行了两次,之所以会出现两次,是因为在import m1
时,执行了m1.py
中的代码,而其中包含了f1
的使用,因此执行了两次。就算使用from m1 import f1
,同样也会执行两次,因为在加载一个模块或模块中的东西的时候会先执行整个模块文件中的所有代码,以确保模块能够被正确初始化,并且所有变量、函数、类等都能够被定义和初始化。
解决上述问题的方法就是在m1.py
中增加一个判断语句,用来判断当前文件的名称__name__
是否为__main__
,因为当一个文件作为脚本执行的时候其__name__
会为__main__
,而当做文件被当做模块引用时,则为模块名。其中,__name__
是python中内置的变量,每个python文件都会有一些内置的变量,通过dir()
可以查看模块中所有的变量、函数、类,包括内置变量。
我们先将m1.py
文件的内容修改为如下:
def f1():
print("this is f1 in m1")
print(__name__)
if __name__ == "__main__":
f1()
然后我们执行m1.py
:
E:\WorkSpace\Temp1>python m1.py
__main__
this is f1 in m1
我们发现m1.py
的名称确实为__main__
,并且可以执行f1
方法。
而我们执行run.py
文件得到的结果如下所示:
E:\WorkSpace\Temp1>python run.py
m1
this is f1 in m1
我们可以看到,__name__
变为了m1
,为m1.py
的名称,并且f1
方法只执行了一次,即在run.py
中执行的,而没有在m1.py
中执行。
下面我们来介绍一下python是如何找到m1
的,因为我们可以通过import m1
,也可以使用from m1 import f1
来使用f1
,说明python是知道m1
在哪里的,才能够将其导入到其它文件中。
python是通过sys.path
中包含的路径来寻找模块的,我们对run.py
文件进行修改,在其中增加上打印sys.path
的内容:
import m1
m1.f1()
import sys
print(sys.path)
然后我们运行run.py
文件并查看sys.path
的内容:
E:\WorkSpace\Temp1> python run.py
m1
this is f1 in m1
['E:\\WorkSpace\\Temp1', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin']
我们可以看到,sys.path
中第一个路径就是run.py
所在的目录(注意,该路径为运行的脚本所在的目录,所以当该脚本引用其它模块时,其它模块的第一个路径仍然为脚本的目录,而不是模块的所在目录),然后是所安装的python环境自带的一些路径。因此,python可以找到m1
模块,因为m1
与run.py
位于同一目录下。我们直接引用某一个包时,必须保证这个包位于sys.path
中某一个路径下,否则python就找不到该包:我们在Temp1
下创建一个新的文件夹,名为pkg
,并在其中创建一个文件为m2.py
,内容为:
import sys
print(f"m2 sys.path is: {sys.path}")
def f2():
print("this is f2 in pkg->m2")
print(f"m2.py name: {__name__}")
此时的文件结构为:
Temp1
pkg
m2.py
m1.py
run.py
我们修改run.py
的内容,让其直接引用m2
:
import m1
import m2
m1.f1()
import sys
print(sys.path)
运行run.py
:
E:\WorkSpace\Temp1>python run.py
m1
Traceback (most recent call last):
File "run.py", line 2, in <module>
import m2
ModuleNotFoundError: No module named 'm2'
我们发现,python能够找到m1
但找不到m2
,尽管m2
所在的pkg
文件夹与run
在同一目录下,即在Temp1
下,但是m2
本身不在Temp1
下,因此python没有找到它。解决的方法有两个:(1)既然pkg
是能通过sys.path
找到的,可以通过pkg.m2
的形式将m2
模块引入;(2)既然pkg
不在sys.path
下,就手动将其加入进去。
修改run.py
,使其内容如下:
import m1
import pkg.m2
m1.f1()
import sys
sys.path.append("E:\WorkSpace\Temp1\pkg")
print(sys.path)
import m2
然后我们运行run.py
得到内容如下:
E:\WorkSpace\Temp1>python run.py
m1
m2 sys.path is: ['E:\\WorkSpace\\Temp1', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin']
m2.py name: pkg.m2
this is f1 in m1
['E:\\WorkSpace\\Temp1', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin', 'E:\\WorkSpace\\Temp1\\pkg']
m2 sys.path is: ['E:\\WorkSpace\\Temp1', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin', 'E:\\WorkSpace\\Temp1\\pkg']
m2.py name: m2
我们可以看到,m2
模块通过上述两种方式都可以引入进去,如果使用第二种方式,我们发现pkg
已经在sys.path
中了,所以可以直接找到m2
。并且我们发现,这两种方式打印出的m2
的__name__
有所不同,使用第一种方式打印的为pkg.m2
,而第二种则为m2
。模块的名称与是通过sys.path
中哪个路径找到的有关系,在第一种方法中,是在E:\WorkSpace\Temp1
找到的pkg.m2
,所以模块名为pkg.m2
,而第二种方法是在E:\WorkSpace\Temp1\pkg
中找到的,所以模块名为m2
。因为名称的不同,导致同一个包引用了两次。通过sys.modules
可以查看当前加载的模块有哪些:
print(sys.modules)
{'builtins': <module 'builtins' (built-in)>, ..., 'pkg.m2': <module 'pkg.m2' from 'E:\\WorkSpace\\Temp1\\pkg\\m2.py'>, 'm2': <module 'm2' from 'E:\\WorkSpace\\Temp1\\pkg\\m2.py'>}
可以看到同一个模块m2
被加载了两次,虽然名称不同,但是值都为E:\\WorkSpace\\Temp1\\pkg\\m2.py
。
相对引入
还是使用上述的文件结构和文件内容,我们继续研究相对引入。相对导入的语法规则是基于 from
语句的,其格式为from xxx import xxx
,不能是import xxx
。并且会包含.
以表示相对的层级结构,每使用一个.
就表示进入上一级的文件夹进行寻找,当只有一个.
时,表示从当前目录寻找。
我们在pkg
下创建m3.py
文件,内容为:
def f3():
print("this is f3 in m3")
目前的文件结构为:
Temp1
pkg
m2.py
m3.py
m1.py
run.py
我们将m2.py
的内容修改为如下所示,实现对m3
的相对引用:
import sys
print(f"m2 sys.path is: {sys.path}")
def f2():
print("this is f2 in pkg->m2")
print(f"m2.py name: {__name__}")
from .m3 import f3
将run.py
的内容修改为如下所示,通过对m2
的引用,同时调用f2
和f3
方法:
import pkg.m2 as m2
m2.f2()
m2.f3()
运行run.py
:
E:\WorkSpace\Temp1>python run.py
m2 sys.path is: ['E:\\WorkSpace\\Temp1', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin']
m2.py name: pkg.m2
this is f2 in pkg->m2
this is f3 in m3
可以看到run.py
可以运行成功,说明m2
成功将f3
引入到自身,所以run
可以通过m2
调用f3
。
但是,我们不能直接运行m2.py
:
E:\WorkSpace\Temp1>python pkg/m2.py
m2 sys.path is: ['E:\\WorkSpace\\Temp1\\pkg', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin']
m2.py name: __main__
Traceback (most recent call last):
File "pkg/m2.py", line 8, in <module>
from .m3 import f3
ModuleNotFoundError: No module named '__main__.m3'; '__main__' is not a package
我们可以看到,在引入m3
的时候发生了错误,因为在相对引用时,会通过模块的名称,即__name__
来寻找包和模块。当一个文件作为脚本运行时,其名称为__main__
,而from .m3 import f3
则是说在__main__
下寻找f3
,所以会报错 ModuleNotFoundError: No module named '__main__.m3'; '__main__' is not a package
。而作为模块被引用时,其名称为pkg.m2
,因此可以通过pkg.m3
找到该模块。
解决上述问题的方式就是修改m2
的名称,将其修改为pkg.xxx
,以告诉python当前的路径为pkg
,至于xxx
则可以任意,因为python会在当前模块所在的包中寻找,所以和当前模块的名称没有关系。但是只修改名称,python只知道可以通过pkg
寻找到m3
,但是不知道pkg
在哪里,我们可以看到上述输出中,sys.path
中第一个路径为E:\\WorkSpace\\Temp1\\pkg
,而不是E:\\WorkSpace\\Temp1
,并且python只会在路径下寻找包,所以尽管有Temp1\\pkg
,但python仍然不知道在哪里可以寻找到pkg
,所以还需要将E:\\WorkSpace\\Temp1
加入到sys.path
中。因此,将m2.py
的内容修改为:
import sys
print(f"m2 sys.path is: {sys.path}")
def f2():
print("this is f2 in pkg->m2")
print(f"m2.py name: {__name__}")
__name__ = "pkg.abc"
sys.path.append("E:\\WorkSpace\\Temp1")
from .m3 import f3
运行m2.py
:
E:\WorkSpace\Temp1>python pkg/m2.py
m2 sys.path is: ['E:\\WorkSpace\\Temp1\\pkg', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin']
可以看到,可以成功运行。接着,我么你将sys.path
中的E:\\WorkSpace\\Temp1
去掉,看是否还能正常运行:
import sys
print(f"m2 sys.path is: {sys.path}")
def f2():
print("this is f2 in pkg->m2")
print(f"m2.py name: {__name__}")
__name__ = "pkg.abc"
# sys.path.append("E:\\WorkSpace\\Temp1")
from .m3 import f3
E:\WorkSpace\Temp1>python pkg/m2.py
m2 sys.path is: ['E:\\WorkSpace\\Temp1\\pkg', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin']
m2.py name: __main__
Traceback (most recent call last):
File "pkg/m2.py", line 9, in <module>
from .m3 import f3
ModuleNotFoundError: No module named 'pkg'
可以看到,python已经找不到pkg
在哪里了。所以,在脚本中基本上不会使用到相对引用。
在相对引用时,还会遇到一个问题就是beyond top-level package
,就是通过相对引用时,每加一个.
就会往上找一层,那么python能够向上找多少层取决于模块的名称有多少层。例如,如果模块名为pkg.m4
,那么它最多找到pkg
,因为再往上就不知道其父目录是什么了。
我们在pkg
下创建一个文件夹名为subpkg
,并在下面创建一个文件,名为m4.py
,其内容为:
from ..m3 import f3
print(__name__)
此时的文件结构为:
Temp1
pkg
m2.py
m3.py
subpkg
m4.py
m1.py
run.py
我们修改run.py
的内容,使其如下:
from pkg.subpkg.m4 import f3
f3()
然后运行run.py
:
E:\WorkSpace\Temp1>python run.py
pkg.subpkg.m4
this is f3 in m3
因为我们是通过pkg.subpkg
找到的m4
,所以其模块名为pkg.subpkg.m4
,而m3
又在pkg
下,所以m4
可以找到m3
。
我们将m4
的内容修改为以下内容:
from ...m1 import f1
print(__name__)
将run
的内容修改为以下内容:
from pkg.subpkg.m4 import f1
f1()
然后运行run.py
:
E:\WorkSpace\Temp1>python run.py
Traceback (most recent call last):
File "run.py", line 1, in <module>
from pkg.subpkg.m4 import f1
File "E:\WorkSpace\Temp1\pkg\subpkg\m4.py", line 1, in <module>
from ...m1 import f1
ValueError: attempted relative import beyond top-level package
然后我们就看到了报错信息beyond top-level package
,因为最高级已经是pkg
了,再往上找已经不知道是什么了。
那么解决上述问题的方法就是使用绝对引用,如果担心所要寻找的包的路径不在sys.path
下,可以手动加入。