今天想将自己的代码打包成一个wheel文件,然后pip安装一键搞定的事情。遇到了相对导包的问题。要彻底地解决这个问题需要重新认识一下python关于模块与包的相关概念。
一、什么是模块?
一个py文件就是一个模块
二、什么是包:
多个模块放在一个目录下,并且存在一个__init__.py文件的目录就是一个模块。
首先构建一个测试的项目,目录结构如下:
代码如下:
# hello.__init__.py
print('__package__={} | __file__={} | __name__={} '.format(__package__, __file__, __name__))
# hello.utils.py
print('__package__={} | __file__={} | __name__={} '.format(__package__, __file__, __name__))
def say(something):
print(something)
# hello.doing.py
import sys
from utils import say
def greet():
say('hello world')
if __name__ == '__main__':
print(sys.path)
greet()
# main.py
import sys
# sys.path.append('./hello')
from hello.doing import greet
if __name__ == '__main__':
print(sys.path)
greet()
在执行python doing.py
的脚本时,输出如下:
从上面打印的结果可知,hello.init.py并没有执行,doing.py导入utils模块时,输出了utils.py相关的内容。从sys.path的路径来看,我们可以发现,python会默认把当前文件夹test/hello添加到系统变量的路径当中,那么系统变量路径+当前模块的名称__name__,即可找到该模块,此种用法是最为常规的执行方式。
当我们尝试将doing.py中的from utils import say
修改成from .utils import say
,此时执行脚本,输出如下:
这就意味着系统没法找到包的路径,原因很简单系统路径./test/hello/拼接上.utils,确实没有这号人物啊。那如果我非要from .utils import say
如此操作呢?在模块之间的调用是无法实现的。那相对路径总得有啥作用吧?再看我们的main.py的模块,此时我们执行python main.py
,输出如下:
__package__=hello | __file__=/Users/xxx/workspace/test/hello/__init__.py | __name__=hello
['/Users/xxx/workspace/test', '/Users/xxx/workspace/test', '/Users/xxx/.pyenv/versions/3.6.5/lib/python36.zip', '/Users/xxx/.pyenv/versions/3.6.5/lib/python3.6', '/Users/xxx/.pyenv/versions/3.6.5/lib/python3.6/lib-dynload', '/Users/xxx/.virtualenvs/test/lib/python3.6/site-packages']
__package__=hello | __file__=/Users/xxx/workspace/test/hello/utils.py | __name__=hello.utils
hello world
可以看出,尽管我们没有调用hello.init.py,系统也默认执行了一次,此时sys.path中没有了test/hello的目录,但是依旧能调用到utils,此时将代码改回from utils import say
,输出如下:
原因就是系统路径没有hello与之拼接?于是在main.py导入hello.doing之前加上sys.path.append('./hello')
,再次运行就正常了。
看到这里,我们应该明白了,python提供了两种互补的方式进行导包,如果是包之间的导入,那么包内模块之间需要相对路径,所以即使你把包放到lib/python3.6/site-packages的目录下也能正常的导入,但如果是模块之间则需要绝对路径。包与模块之间的界限,就是是否是目录以及目录存在一个__init__,但是我删掉__inti__也如期正常运行。其实这个__init__是为了初始化模块的导入,在python中第一次导入之后就不在重复导入,后面的导入只是计数增加而已。也就是说__init__可以用来管理包目录下的所以模块的初始化,比如flask的init中就会一相对路径的形式把自己的模块一一进行初始化。
终于搞清楚了这个流程,所以如果你的代码要作为第三方包,给别人pip install之后调用,那么你的代码内部之间就需要用相对路径。我尝试去看了很多的模块的源码的目录结构,比如flask、numpy等都是如此。
使用相对路径的好处就是无论用户放在那个路径下,我们都可以通过指定添加sys.path来进行获取,所以以后开发还是尽量地使用相对路径吧,毕竟哪天说不准就要提供包了。关于项目打包wheel,等实践完成再补充了。
明白了上述的过程,打包wheel的流程就简单多了。
1.首先hello必须是一个包,即必须有__init__.py
2.在hello同级目录下,新建setup.py的代码如下:
from setuptools import find_packages, setup
setup(
name='hello',
version='1.0.0',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
py_modules=['hello'],
# install_requires=[
# 'flask',
# ],
)
name是生成的wheel的名称,py_modules是你的包名,可以多个包一起打包成一个wheel模块。具体参数查询setuptools模块的使用文档即可。执行:
# 生成.tar.gz的压缩包
python setup.py sdist
# 生成wheel的压缩包
python setup.py bdist_wheel
执行上述命令之后会在当前目录下生成dist的目录,目标文件就在那里,pip install 即可使用。
参考:https://blog.youkuaiyun.com/weixin_39523998/article/details/78040768?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.control
官方文档:https://docs.python.org/zh-cn/3/reference/import.html#packages