前言
Python项目的路径管理是一个让人头疼的问题。在写python项目的时候,明明 import
了文件A,代码运行时却收到 ModuleNotFoundError
,仔细一看,是引用路径不对,很是气人。又或者,当项目中出现了重名的packages时,发现引用的函数并不是自己想要的,而是其他同名packages中的函数。这些问题归根结底都是Python路径管理的问题。那么今天我们一起来看看Python的路径管理到底是怎么做的,了解原理后,以后自然不会被路径问题所困扰了!
路径索引顺序
如我们在Python专题(二)Python二三事中讲的,Python2和Python3在路径索引的顺序上有些许不同,感兴趣的同学可以参考上篇专题内容。本文仅讲Python3版本的情况。首先,在Python中有内建函数(built-in)、第三方库(site-packages)以及自义库三种可以 import
的模块。然后,在 import
模块时,Python解释器的搜索顺序是先搜索built-in模块,然后搜索 sys.path
这个路径列表中的模块。那么Python的built-in模块又有哪些呢?我们可以在Python中用如下命令查看:
import sys
print(sys.builtin_module_names)
你会看到一长串builtin模块的名字,这些模块名称是 import
动作最先搜索到的。
我们在来看看sys.path中又有哪些东西呢?
sys.path
是一个路径列表,里面保存了解释器可以索引的所有路径。这个路径列表可分为如下部分:
- 当前脚本路径
- PYTHONPATH路径
- 虚拟环境路径
- site-packages路径
一般来说,第三方库会安装在site-packages路径下,当前脚本路径则是一些自定义模块,而PYTHONPATH和虚拟环境路径则是当前系统的环境变量和Python虚拟环境保存的路径。
所以当来了一个 import
命令时,Python解释器的搜索顺序就是:
当然,这个sys.path中的索引顺序只是一个默认顺序,你完全可以在代码中通过sys模块修改这个顺序,在后文中你会看到如何对这个索引顺序进行修改。当完成 import
动作后,Python会把这些模块的名字和所在路径保存在一个字典里,相当于一个缓存,在后面需要运行这个模块代码时可以迅速查找到该部分代码。你可以通过 print(sys.modules)
来查看当前Python解释器缓存(导入)了哪些模块。
from 和 import
老生常谈的话题了,但是很容易忽略。多数情况下,
from module import fun
a = fun()
和
import module
a = module.fun()
在效果上是等价的。区别是第一种方式只引用了 module
中的 fun
函数,而第二种方式引用了整个 module
。只引用 fun
函数,可能造成代码中的变量名混乱,譬如你的代码中本来就有一个名为 fun
的函数,这时候用第一种方式导入,会悄无声息地替换掉代码中原本的 fun
函数,从而引起命名空间混乱。而引用整个 module
时,解释器会运行 module
中的所有代码,如果 module
中有很耗时而我们又不需要的运算,第二种方式会存在冗余资源消耗。
还有一种导入模块的方式:
from module import *
a = fun()
这种导入模块的方式是官方不提倡的,因为刚才们提到用 fromimport
的方法会产生变量名混乱,但是 frommoduleimportfun
毕竟还是指定了导入的函数名,开发者还是可以很容易地察觉到问题。而 frommoduleimport*
这种方式会让开发者导入 module
中的所有公有类,函数,变量,从而使当前脚本中被导入了很多未知的变量名,让代码的管理变得更加复杂和不可控。不过,我们还是有办法控制 frommoduleimport*
的行为的——用 __all__
属性。如果在 module
脚本中定义了 __all__
属性,那么 frommoduleimport*
就只会导入 __all__
中的变量名:
# module.py
__all__ = ["fun"] # from module import * 只会导入fun
def fun():return True
def fun1(): # from module import * 不会导入fun1pass
var1 = False # from module import * 不会导入var1
sys.argv[0]和_file_
sys.argv[0]
用来获取入口执行文件的路径。__file__
用来获取当前脚本文件的路径。为了加深理解,做个小试验:
假设我的目录:
|-Users
|--myname
|----test1.py
|----test2.py
test1.py:
import sys
print(__file__)
print(sys.argv[0])
test2.py:
import test1
执行test1.py:
python test1.py
得到结果:
test1.py test1.py
执行test2.py:
python test2.py
得到结果:
/Users/myname/test1.py test2.py
在上一级目录执行test1.py:
python myname/test1.py
得到结果:
myname/test1.py myname/test1.py
在上一级目录执行test2.py:
python myname/test2.py
得到结果:
/Users/myname/test2.py myname/test2.py
05令人困扰的自引用问题
上面介绍了很多关于Python内部的路径管理的规则和原理。其实介绍这些规则和原理的终极目标就是解决令人困扰的自引用问题。在你写一个项目时,假设你的文件结构如下:
|-myproject
|----tools
|--------_init_.py
|--------trainer.py
|----utils
|--------_init_.py
|--------trans.py
|----test.py
如果在 test.py
中需要用到 tools/trainer.py
中的函数,那么在 test.py
中直接引用即可:
import tools.trainer
这种方式其实是用了前面我们聊到的当前路径索引,解释器从当前目录出发,查找相对路径为 tools/trainer
的模块。这时候我们可以完美运行 test.py
。
注意:文件夹下必须有_init_.py文件,解释器才能够找到模块。
但是如果在 utils/trans.py
中需要用到 tools/trainer
中的函数:
import tools.trainer
就会收到 ModuleNotFoundError
的错误信息。因为解释器查找当前脚本路径时,找不到 tools/trainer.py
这个文件,正确的路径应该是 ../tools/trainer.py
,因为当前的执行脚本是 utils/trans.py
。那么我们修改一下路径:
import sys
sys.path.insert(0, "../")
import tools.trainer
sys.path.insert(0,"../")
会把上级目录 ../
插入 sys.path
列表的首位,这也就是前文说的,通过sys模块来修改默认的索引路径。这样会强制解释器搜索当前脚本路径的上级路径,解释器就可以找到我们需要的模块。
上文所述的方法是一种比较方便但是并不是很规范的方式,因为如果按照PEP-8的规范, import
语句要在代码最前面, sys.path.insert(0,"../")
这条语句插在两个 import
语句中间,其实是违反了PEP-8规范。所以,更加规范的方式是什么呢?
笔者自己也很困惑这个问题,于是参考了一些开源的Python项目,发现他们一般用 site_packages
路径索引而非用当前脚本路径索引,由于 site_packages
路径索引会直接把 myproject
路径加入索引列表,所以 tools/trainer.py
可以在系统的任何路径下被索引到,问题自然得到解决。那么如何让Python解释器知道某条 import
语句用哪种索引方法呢?回顾一下 路径索引顺序
这一节,我们提到解释器会在sys.path这个路径列表搜索模块,默认情况下,会先从当前路径开始搜索,然后搜索环境变量,最后搜索site-packages路径。所以要让解释器用 site_packages
路径索引,我们需要确保:
- 当前路径下没有与所导入模块重名的文件
- 所导入的模块(文件)在
site_packages
路径下
第一个条件很容易达成,第二个条件一般有两种办法实现。如果当前项目myproject是一个纯Python项目,可以直接把myproject文件夹复制到 site_packages
目录下,虽然可以使用,不过这种方式非常不规范。更规范的方式是用 setup.py
把项目安装在本地,关于 setup.py
的使用我会在后面的专题中详细介绍。
到此,我们就解决了这个令人困扰的自引用问题。
结语
本专题从Python路径管理的原理出发,介绍了模块导入的路径索引顺序、from和import导入的区别、sys.argv[0]和_file_的含义。最后为大家提供了令人困扰的自引用问题的两种解决办法。其实对于笔者来说,在项目中导入自己的模块产生的路径问题困扰了我很久,直到看了一些开源的项目以及跟大佬们交流学习才知道如何处理这类问题。希望读者朋友们看完这篇专题后可以有所收获,如果你对本文有任何疑问或者建议,欢迎留言交流!
【Python专题(二)】Python二三事mp.weixin.qq.com【Python专题(一)】python环境搭建mp.weixin.qq.com