setup.py和requirements.txt区别
这篇文章是一篇译文,翻译自https://caremad.io/posts/2013/07/setup-vs-requirement/。**翻译行为已经征求到原作者同意**,但是由于英文能力所限,翻译的不好,所以有能力的人还是请看原文。
另外,我注意到https://pyzh.readthedocs.io/en/latest/python-setup-dot-py-vs-requirements-dot-txt.html 也有原文的中文翻译,不过我翻译接近完成的时候才发现,但是还是把它翻译完了。
关于setup.py
和requirements.txt
,以及它们的职责,网上存在着很多误解。一大堆人觉得他们是重复的,甚至创造了一些工具,去处理这种重复的内容。
Python Libraries
在这篇文章中,一个Python 库
指的是一个被开发出来、被发布出来给人使用的东西。你可以在Pypi
上找到一大堆Python 库
。为了保证成功发布一个Python 库
,需要提供一大堆元数据,比如名称、版本、依赖等等。这些元数据就写在setup.py
里,比如像这样:
1 | from setuptools import setup |
这个(范例)很简单,你申明了所需的元数据。但是这个范例中,没有指定我们应该从哪里获取依赖。上述文件只是申明了我们有requests
和bcrypt
这两个依赖,但是没有告诉我们应该去哪个网站,或者哪个机器上获取它们。我把这种依赖叫做“抽象依赖”。这种抽象依赖只有一个名字,最多再有一个可有可无的版本号。这种思想类似于Python编程思想中的”鸭子类型”,你不在乎你获得的requests
是哪个,只要你获得的看起来像是requests
。
Python Applications
当我说到Python 应用
,我指的是一个你可以用来部署的东西。它可能在Pypi
上面,也可能不在,但它基本上没有太多的可复用性。一个存在于Pypi
的应用,基本上需要一个跟部署有关的配置文件,这个文件用来处理那些部署方面的问题。
一个Python应用
基本上都会有一些依赖,有时候甚至有一大堆依赖,这些依赖都是被测试过的。而一个已经部署好的Python应用
,是不会有名称或者打包相关的元数据的。这些元数据都被写在pip requirements
文件里了。一个典型的requirements
文件可能会是这个样子:
1 | # This is an implicit value, here for clarity |
这里你能够看到,每个依赖都有一个确切的版本号。一个Python库
倾向于有一个大范围的、模糊的版本号,但是一个Python应用
需要一个确切的版本号。你装了哪个版本的requests
并不重要,但你需要在生产环境中安装同样的版本,因为你已经在本地测试过了。
在这个文件上面,你可能也会看到一个--index-url https://pypi.python.org/simple/
。它是requirements.txt
很重要的一个部分,这一行把抽象的依赖,(比如requests==1.2.0
),转为确切的、来自 https://pypi.python.org/simple/“、而且版本号为1.2.0
的依赖。这一次就不是鸭子类型了,这个类似isinstance()
相等判断。
所以为什么抽象和确切很重要?
读到现在,你可能会说,ok我已经明白setup.py
是为那些“可再发行的东西“设计的,requirements.txt
是为那些“不可再发行的东西”设计的,但我已经有一些工具,它读取requirements.txt
,然后把读到的依赖,填写到setup.py
中的install_requires=[...]
里面去。那么我为什么还要在乎这些东西呢?
抽象和确切的区分还是重要的。这样的话,我们可以使用那些Pypi
的镜像站,一个公司也可以建立他们自己私有的包目录,甚至你可以fork一个库的分支,去修复一个bug或者增加一些新的特性。因为一个抽象的依赖只是一个名称和一个可有可无的版本号,所以你可以从Pypi
或者一些别的地方安装它,比如Crate.io
或者你自己的文件系统。你可以fork一个库,修改他的代码,只要它有正确的名字和版本号,Python库就会使用它。
当在一个本应该使用抽象依赖的地方,用了一个确切的依赖,会发生什么呢? Go 语言里面有一个更加极端的例子。在Go语言中,默认的包管理器 (go get
) 允许你通过代码里的URL去指定你的imports,这些代码是包管理器收集和下载的。具体如下:
1 | import ( |
这里你可以看到,这个URL指向了一个确切的依赖。现在如果我用了这个库,但是在它依赖的bar库中,有个bug影响我,或者我需要增加一个新特性,这时候我就需要修改这个名为“bar”的库。那么我不仅要fork那个名为bar的库,我还得fork那个依赖了bar的库(译者注:如果不修改依赖了bar的库,那么它还是用的有问题的版本)。更差劲的是,如果这个依赖是5层的,那么我可能会得fork5层中5个不同的库,仅仅为了让他们指向一个稍微有点不同的”bar”。
Setuptools 的一个缺陷
类似上面go语言的例子,setuptools也有一个相似的问题。这个问题叫 dependency links ,具体如下:
1 | from setuptools import setup |
setuptools 的dependency_links
参数将本应该是抽象的依赖,变成了具体的依赖了。这里所说的具体的依赖就是指上文代码里硬编码的Url。现在和go非常类似的是,如果我们想要修改一个包,我们必须深入代码,修改依赖链上的每个包,去修改dependency_links
。
译者注:我的理解是这样的:
1 | 如果python应用依赖了库C,C依赖了库B,B依赖了库A。而且都用了dependency_links这种具体的、写死的做法。 |
开发可复用的内容 or 如何不让自己重复做同一件事
库
和应用
的区别看起来没什么问题,但当你正在开发一个库的时候,某种意义上说它就是你的应用。在setup.py
中,你知道你应该使用抽象的依赖;在 requirements.txt
中,你知道你应该使用确切的依赖。但是你并不想维护两个独立的列表,否则它们两个难免会不同步。而requirements.txt
可以帮我们处理这种情况。给定一个目录,如果目录下有setup.py
文件,那么你就可以这么写你的requirements.txt
文件:
1 | --index-url https://pypi.python.org/simple/ |
现在命令pip install -r requirements.txt
还是会和之前一样正常工作。它会先:
- 安装路径
.
上面的库(我理解这一步是什么都没做的,因为你当前路径一般不会放什么库) - 然后找到
setup.py
文件里面的抽象依赖,再把抽象依赖和参数--index-url
里指示的源相结合,变为具体的、确切的依赖,然后安装他们。
还有一个有用的功能。比如说你有2个或者2个以上的库,开发时候你是一起开发的,但是你想分开发布;或者你把一个库分割成了几个部分但是还没有正式发布它们。如果你顶层的库仍然只是按照名字来依赖(前面说的这些库)的话,当你用requirements.txt
的时候,你可以安装开发版本;当你不用的时候,你可以安装发布版本。把文件这么写即可:
1 | --index-url https://pypi.python.org/simple/ |
这将会:
- 先从 https://github.com/foo/bar.git安装名为`bar`的库,名字就是bar;
- 然后会安装本地的包,一样地,会把本地的抽象依赖和
--index-url
里面指示的源相结合,结合成确切、具体的依赖再安装。
不过第二步的时候,因为bar
已经在第一步的时候安装过了,所以这次会跳过,然后继续用开发版本的bar
。
参考
https://docs.python.org/2/library/os.html
https://blog.csdn.net/u013061183/article/details/78525807
https://www.cnblogs.com/now-fighting/p/3534185.html
https://packaging.python.org/discussions/install-requires-vs-requirements/
https://www.reddit.com/r/Python/comments/3uzl2a/setuppy_requirementstxt_or_a_combination/
https://stackoverflow.com/questions/2477117/pip-requirements-txt-with-alternative-index