【Python】包管理,弄明白import,package,module
前言
Python中的包管理,一直是我比较困惑的一点。
- python导入另一个文件,会不会执行另一个文件的?
- python是怎么找到具体的包的?包名不会重复吗?
- 目录层级结构比较复杂的时候,怎么导入包?
简单实例
同级目录下,main.py
可以按照如下方式导入mymath.py
中的变量和函数
目录结构如下
- python_package
--main.py
--mymath.py
# mymath.py
PI = 3.14def add(a,b):return a+b
# main.py
import mymathprint(mymath.add(1, 2))
import本质是什么?
那么我们需要深入研究一下,import
到底做了什么呢?
首先我们要看看mymath
到底是个什么?
print(type(mymath))
# <class 'module'>
可以看到mymath
其实是一个变量,指向了module
类的实例。所以import
就是生成了一个module
实例,然后赋值给了变量mymath
于是我们就像变量一样使用它,如下
mm = mymath
print(mymath.add(1, 2))
print(mm.add(1, 2))
相当于给mymath
起了一个别名,因为这种起别名的方式还挺常见的,所以Python有特殊的语法专门做这个事
# python在创建module后,就将其赋值给mm变量,然后就没有mymath变量了
import mymath as mm
print(mm.add(1, 2))
ok,我们知道了mymath是指向一个module实例的变量
那么import是怎么生成这个module实例的呢?
我们在遇到import实例的时候,python首先会查询,mymath
有没有被import
过。
如果没有:python就会将mymath.py
读取到内存中,并运行。
而如果已经被import
过了就找到当时创建的module
,并把它直接赋值给import
后面的变量。
所以如果import
了两次同样的文件,那这个文件只会在被第一次import
时,运行一次。
我们来验证一下
# 在mymath.py中加入打印
print("exeute mymath.py")
PI = 3.14def add(a,b):return a+b
# main.py多次引入
import mymath as mm
import mymath as mm2
# 分别打印id
print(id(mm))
print(id(mm2))
exeute mymath.py
2509003175824
2509003175824
可以看到mymath.py
只被运行了一次,且mm和mm2的ID是相同的。说明他们指向同一个module
对象
当mymath.py
运行时,import
创建的module
就会成为mymath.py
的全局命名空间。所以说mymath.py
中定义的全局变量,就会被定义在import
创建的module
中
我们可以通过dir()
函数来看到mm
定义的内容
# main.py
import mymath as mm
import mymath as mm2print(dir(mm))
['PI', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'add']
可以看到PI
和add
都在这个全局变量中
现在我们来个骚操作
# mymath.py
print("exeute mymath.py")
PI = 3.14def add(a,b):return a+bdef add_global(name,value):g = globals()g[name] = value
# main.py
import mymath as mm
import mymath as mm2mm.add_global("E",2.71)
print(mm.E)
mymath中本来是没有E这个变量的,但是通过调用方法给他添加了这个变量后,就有了。
这说明mm就是mymath.py
运行的全局命令空间。
python import还有另外一种写法。
# main.py
from mymath import PI
这样做还是会为mymath
创建module
并且运行mymath.py
但这次不会把module
赋值给mymath
变量了,而是把PI
的值直接赋值给当前命名空间的PI
变量
注意这里是一个赋值的操作,赋值之后mymath.py
中的PI
就和当前文件中的PI
没有任何的关系了
也就是说,如果我们修改当前的PI
值是不会修改mymath.py
中的PI
值的
from mymath import PI
import mymath as mmPI = 9999
print(f"PI in main: {PI}")
print(f"PI in mymath: {mm.PI}")
exeute mymath.py
PI in main: 9999
PI in mymath: 3.14
所以说import
的本质就是创建命名空间,也就是module
,然后对变量进行赋值的一个过程
代码从哪找?
好,现在我们创建了module
就应该找相应的Python文件来运行了。
那么从哪里找呢?
就是从sys.path
中查找
from mymath import PI
import mymath as mm
import sysprint(sys.path)
['d:\\pythonProject\\python_package', 'D:\\Develop\\python\\python310\\python310.zip', 'D:\\Develop\\python\\python310\\DLLs', 'D:\\Develop\\python\\python310\\lib', 'D:\\Develop\\python\\python310', 'D:\\Develop\\python\\python310\\lib\\site-packages']
# 可以看到sys.path的第一项,就是main脚本所在的目录
# 还包含了python自带的库的目录
# 以及安装的第三方的库的目录 D:\\Develop\\python\\python310\\lib\\site-packages
sys.path
就是一个普通的list
,所以我们是可以用代码对它进行更改的。
当然除了通过代码,我们还可以在运行Python的时候,指定环境变量PYTHONPATH
来改变sys.path
的值
如下
PYTHONPATH=D:\a\b\c python main.py$env:PYTHONPATH="D:\\a\\b\\c"; python main.pyPS D:\pythonProject\python_package> $env:PYTHONPATH="D:\\a\\b\\c"; python main.py
exeute mymath.py
['D:\\pythonProject\\python_package', 'D:\\a\\b\\c', 'D:\\Develop\\python\\python310\\python310.zip', 'D:\\Develop\\python\\python310\\DLLs', 'D:\\Develop\\python\\python310\\lib', 'D:\\Develop\\python\\python310', 'D:\\Develop\\python\\python310\\lib\\site-packages', 'D:\\Develop\\python\\python310\\lib\\site-packages\\win32']
可以看到D:\\a\\b\\c
已经被加到sys.path
变量中了。
于是在import
时python就可以从这些路径去寻找mymath.py
文件了
如果找到了皆大欢喜,如果找不到python就会去寻找mymath的目录
mymath目录中往往还有一个__init__.py
,这时python就会去运行__init__.py
带有__init__.py
目录的优先级是比文件要高的,
execute __init__.py
['D:\\pythonProject\\python_package', 'D:\\Develop\\python\\python310\\python310.zip', 'D:\\Develop\\python\\python310\\DLLs', 'D:\\Develop\\python\\python310\\lib', 'D:\\Develop\\python\\python310', 'D:\\Develop\\python\\python310\\lib\\site-packages', 'D:\\Develop\\python\\python310\\lib\\site-packages\\win32']
可以看到,我们的程序只打印出了__init__.py
的输出。这说明我们的程序确实加载了__init__.py
文件,没有加载mymath.py
文件
而这个目录其实就是所谓的package
,
注意:无论有没有__init__.py
文件,Python都会加载这个目录,只不过如有没有__init__.py
这个文件,Python就不运行任何代码了。但是module还是会照常被创建。
我们试试直接创建一个目录mydir
# main.py
import mydirprint(mydir)
<module 'mydir' (<_frozen_importlib_external._NamespaceLoader object at 0x00000170F723DA80>)>
可以看到mydir
还是可以被正常的加载
Python也是支持子目录的
我们在mydir
下创建subdir
。然后我们就可以用如下方式导入子目录
# main.py
import mydir.subdirprint(mydir)
print(mydir.subdir)
<module 'mydir' (<_frozen_importlib_external._NamespaceLoader object at 0x000001D607B1DA50>)>
这时Python会做两件事情
- 首先在当前的命名空间中导入
mydir
- 然后再在
mydir
的命名空间中导入subdir
我们可以使用dir
打印一下两个命名空间,看看里面分别都有什么
# main.py
import mydir.subdirprint(dir())
print(dir(mydir))['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'mydir']
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'subdir']
可以看到在第一个命名空间中有mydir
,在第二个命令空间中有subdir
当然我们也可以用一下方式导入子目录
这样就可以将subdir
直接导入到当前命名空间中,但这样依然会在mydir的命名空间中也导入subdir
from mydir import subdir
import mydirprint(dir())
print(dir(mydir))
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'mydir', 'subdir']
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'subdir']
可以看到当前命令空间和mydir
命名空间中都有了subdir
relative import 相对导入
我们需要在mydir目录中的a.py
引入subdir
import mydir.subdir
from . import subdir
总结
import mymath
导入包,会在os.path
列表下以此去找对应的目录
或者文件
。目录下带有__init__.py
的优先级大于文件
。- 带有
__init__.py
的目录,则被称为package
包 os.path
是一个地址路径列表,依次查找对应的目录
或者文件
以找到的第一个包为准。import mymath
如果是目录则会执行mymath
目录下的__init__.py
文件,如果是文件mymath.py
则执行该文件,然后创建module
类的实例赋值给mymath
变量dir()
可以查看命名空间中有哪些变量。