引言
之前我們項目組在使用 python 的過程中戳晌,經(jīng)常遇到用 pip install -e .
的方式安裝 python package 的情況泞辐,這些 package 中都有一個 setup.py
文件笔横,里面指定了 package 的依賴、描述性的信息等咐吼。
但是我們從來沒有為自己的 package 寫過類似的 setup.py 文件吹缔。如果需要在程序中調(diào)用自己寫的 package,一般是設(shè)定環(huán)境變量 PYTHONPATH
或者在 python 程序中設(shè)置 sys.path
來具體指定 package 的搜索路徑锯茄。如果只是在本地調(diào)用自己的 package厢塘,路徑相對固定茶没,這種方式也沒什么問題,如果要將 package 分享給別人甚至分享到 PyPI 上晚碾,這種路徑設(shè)置的方式就有些簡陋了抓半。如果我們?yōu)?package 編寫了 setup.py
文件,那么 package 路徑的問題格嘁、依賴的問題以及描述性的信息管理就可以交給 pip (package installer for Python) 處理笛求。 用戶只需要通過 pip install -e .
的方式安裝即可。
在學(xué)習(xí)編寫 setup.py
文件的過程中糕簿,我們感覺這篇文章講解的非常透徹探入,所以將它的內(nèi)容整理總結(jié)出來,目的是方便以后自己和其他研究人員參考冶伞。
一個簡單的 package 例子
如果還不太了解 module 和 package 的概念和用法新症,可以參考 python 官方的 tutorial步氏。
在這里我們先創(chuàng)建一個文件夾 setuppy_tutorial
响禽,后邊所有的操作都在這個文件夾中進(jìn)行。
假設(shè)在這個文件夾中有一個 package 名為 greeting_pkg
荚醒,文檔路徑結(jié)構(gòu)如下:
.
└── greeting_pkg
├── greeting_module.py
└── __init__.py
其中 __init__.py
為空文件芋类,greeting_module.py
文件內(nèi)容如下:
def greeting_func(name):
print("Hello, ", name)
也就是說,這個 greeting_pkg
package 包含一個 module 文件 greeting_module.py
界阁,其中的函數(shù) greeting_func
可以打印出輸入的名字并問候侯繁。
例如,當(dāng) greeting_pkg
package 處于當(dāng)前目錄時泡躯,進(jìn)入 python 環(huán)境贮竟,調(diào)用 module 并執(zhí)行其中的函數(shù):
$ python
>>> from greeting_pkg.greeting_module import greeting_func
>>> greeting_func("De Gang")
Hello, De Gang
如果 greeting_pkg
package 沒有處于當(dāng)前目錄,在調(diào)用 module 時就會出現(xiàn)如下路徑錯誤
>>> from greeting_pkg.greeting_module import greeting_func
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'greeting_pkg'
python 搜索 module 的路徑由 sys.path
決定较剃,可以通過如下命令查看:
>>> import sys
>>> print('\n'.join(sys.path))
<--- 這里有空字符串
/home/automan/pyenv/testsetuppy/lib/python36.zip
/home/automan/pyenv/testsetuppy/lib/python3.6
/home/automan/pyenv/testsetuppy/lib/python3.6/lib-dynload
/usr/lib/python3.6
/home/automan/pyenv/testsetuppy/lib/python3.6/site-packages
(這里用了虛擬環(huán)境咕别,名稱為 testsetuppy)
顯示的路徑列表中包括
1. 當(dāng)前路徑 (以空字符串表示)
2. PYTHONPATH 中的路徑(如果沒有設(shè)置環(huán)境變量 PYTHONPATH,這一項就不存在)
3. 與 python 安裝位置相關(guān)的其他路徑
可以在 python 程序中擴(kuò)展 sys.path
中的路徑写穴,例如在另一篇介紹 SUMO TraCI 的文章中惰拱,為了調(diào)用 TraCI,我們在程序中將 TraCI 所在的路徑加入到 sys.path
中:
if 'SUMO_HOME' in os.environ:
tools = os.path.join(os.environ['SUMO_HOME'], 'tools')
sys.path.append(tools)
else:
sys.exit("please declare environment variable 'SUMO_HOME'")
import traci
如果自己編寫的 module 不在上述 sys.path
路徑中,就會出現(xiàn)找不到 module 的錯誤。
setup.py
文件的作用之一是提供了一種通過 pip 標(biāo)準(zhǔn)化安裝 package 的方式袁梗,避免了額外設(shè)置路徑的麻煩粱玲。
添加一個最簡潔的 setup.py 文件
在剛才的 setuppy_tutorial
文件夾中 (即與 my_greetings
package 在同一目錄中) 編寫如下的 setup.py
文件:
from setuptools import setup, find_packages
setup(
name='greeting_pkg',
packages=find_packages()
)
其中,name
是在 pip 中注冊的名字(如果我們將這個 package 分享到 PyPI备籽,必須保證這個名字與 PyPI 中已有 package 注冊的名字不重復(fù)),與 package 文件夾的名字可以不同,但一般只要不涉及到重名勾怒,最好用與 package 相同的名字,方便用戶記憶。packages
后邊跟的是這個 package 的名字以及它內(nèi)部的 subpackage 的名字控硼,這里我們只有 greeting_pkg
一個 package泽论。
現(xiàn)在文檔路徑結(jié)構(gòu)如下:
.
├── greeting_pkg
│ ├── greeting_module.py
│ └── __init__.py
└── setup.py
用如下命令安裝這個 package
pip install -e .
確認(rèn)一下是否安裝成功:
$ pip list
Package Version Location
------------- ------- -------------------------
greeting-pkg 0.0.0 /home/automan/setuppy_tutorial
pip 19.2.1
pkg-resources 0.0.0
setuptools 41.0.1
wheel 0.33.4
此時我們的 package greeting-pkg
可以像其他通過 pip 安裝的 package 一樣使用,不必考慮路徑問題卡乾。
這里再回頭說一下 pip install -e .
安裝命令翼悴,其中參數(shù) -e
表示以 editable 方式安裝,這樣對于原 python 文件的改動可以直接反映到安裝的 package 中幔妨,不必重新安裝鹦赎。
例如,將 greeting_module.py
文件改成如下內(nèi)容:
def greeting_func(name):
print("Cheers, ", name) # "Cheers" 替換了原本的 "Hello"
再調(diào)用該 module 時效果如下:
$ python
>>> from greeting_pkg.greeting_module import greeting_func
>>> greeting_func("De Gang")
Cheers, De Gang # 馬上反映了對原 python 文件的修改
擴(kuò)展 setup.py
添加 vesion
在上邊 pip list
命令返回結(jié)果中 greeting_pkg
對應(yīng)的版本是 0.0.0误堡,這是默認(rèn)的版本號古话。我們可以在 setup.py
文件中設(shè)置 version 參數(shù),更好的反映 package 的開發(fā)進(jìn)度锁施。
修改 setup.py
內(nèi)容如下:
from setuptools import setup, find_packages
setup(
name='greeting_pkg',
packages=find_packages(),
version='0.1.0'
)
用 pip install -e .
命令重新安裝陪踩,再用 pip list
命令顯示如下:
$ pip list
Package Version Location
------------- ------- -------------------------
greeting-pkg 0.1.0 /home/automan/setuppy_tutorial
List 1.3.0
pip 19.2.1
pkg-resources 0.0.0
setuptools 41.0.1
wheel 0.33.4
版本號已更新。
添加 package 依賴
如果自己編寫的 package A 調(diào)用了某個 package B悉抵,但是在其他用戶的機子上沒有安裝 package B肩狂,那么在執(zhí)行 package A 時會報錯,找不到 module姥饰。為了避免這種問題傻谁,可以在 setup.py
文件中設(shè)置好依賴的其他 package,讓別人在用 setup.py
安裝時就將所有依賴的 package 一起安裝了列粪。
例如审磁,我們將 greeting_module.py
修改為如下內(nèi)容:
import pyjokes
def greeting_func(name):
print("Hello,", name)
print("Here is a joke for you:\n", pyjokes.get_joke())
這里調(diào)用了一個 module pyjokes
,它的功能是隨機產(chǎn)生一個 joke岂座。這個 module 需要額外安裝态蒂。可以將 setup.py
文件修改成如下形式:
from setuptools import setup, find_packages
setup(
name='greeting_pkg',
packages=find_packages(),
version='0.1.0',
install_requires=[ # 添加了依賴的 package
'pyjokes'
]
)
在設(shè)置依賴 package 時可以指定版本號掺逼,例如
pyjokes==0.5.0
pyjokes>=0.3.0
pyjokes>=0.3.0,<0.5.0
重新用 pip install -e .
方式安裝吃媒,然后在 python 中再次調(diào)用 greeting_func()
函數(shù),效果如下:
$ python
>>> from greeting_pkg.greeting_module import greeting_func
>>> greeting_func("De Gang")
Hello, De Gang
Here is a joke for you:
How to explain the movie Inception to a programmer? When you run a VM inside another VM, inside another VM ... everything runs real slow!
另外吕喘,還可以根據(jù)使用環(huán)境有選擇的安裝某些依賴 package赘那,例如修改 setup.py
文件如下:
from setuptools import setup, find_packages
setup(
name='greeting_pkg',
packages=find_packages(),
version='0.1.0',
install_requires=[
'pyjokes'
],
extras_require={ # 添加了可選安裝的依賴 package
'interactive': ['matplotlib>=2.2.0,<3.0.0', 'jupyter']
}
)
其中 extras_require
部分是可以選擇安裝的 package。
使用 pip install -e .
命令并不會安裝 extras_require
里面的 package氯质。
如果要安裝募舟,需要用如下命令:
pip install -e .[interactive]
在命令行中執(zhí)行 module 中的函數(shù)
如果我們希望 module 中的函數(shù)不僅僅只是被其他 python 程序通過 import 調(diào)用,還可以直接在命令行中執(zhí)行闻察,那么可以做如下修改:
-
在 greeting_module.py 文件中的修改
import pyjokes def greeting_func(name): print("Hello,", name) print("Here is a joke for you:\n", pyjokes.get_joke()) def main(): import sys arg = sys.argv[1] greeting_func(arg)
-
在 setup.py 文件中的修改:
from setuptools import setup, find_packages setup( name='greeting_pkg', packages=find_packages(), version='0.1.0', install_requires=[ 'pyjokes' ], extras_require={ 'interactive': ['matplotlib>=2.2.0,<3.0.0', 'jupyter'] }, entry_points={ # 設(shè)置了在命令行中如何使用 greeting_module 中的 main 函數(shù) 'console_scripts': [ 'greeting=greeting_pkg.greeting_module:main' ] } )
這里需要注意的是拱礁,我們的 greeting_func()
是需要送入?yún)?shù)的琢锋,但是在命令行中執(zhí)行函數(shù)不能添加參數(shù),只能以 sys.argv
的形式讀進(jìn)去呢灶,再進(jìn)行后續(xù)的處理吴超。所以當(dāng)作函數(shù)使用和當(dāng)作命令行中的命令使用時,"入口" 是不一樣的鸯乃!
做了以上修改之后鲸阻,再用 pip install -e .
命令安裝一下,然后測試缨睡。在命令行中輸入:
$ greeting "De Gang"
Hello, De Gang
Here is a joke for you:
3 Database Admins walked into a NoSQL bar. A little while later they walked out because they couldn’t find a table.
添加獨立的 module
除了以 package 的形式存在鸟悴,還可以允許不屬于任何 package 的 module 存在。例如我們在 package 的外邊添加兩個 module奖年,文檔路徑結(jié)構(gòu)如下:
$ tree -L 2
.
├── greeting_pkg
│ ├── greeting_module.py
│ └── __init__.py
├── isolated_greeting_module_1.py
├── isolated_greeting_module_2.py
└── setup.py
1 directory, 5 files
其中
- isolated_greeting_module_1.py 內(nèi)容如下:
def greeting_func(name):
print("Hi,", name, ', greetings from isolated_greeting_module_1.')
- isolated_greeting_module_2.py 內(nèi)容如下:
def greeting_func(name):
print("Hi,", name, ', greetings from isolated_greeting_module_2.')
對應(yīng)的 setup.py 中的內(nèi)容如下:
from setuptools import setup, find_packages
setup(
name='greeting_pkg',
packages=find_packages(),
py_modules=[ # 在 package 之外添加兩個獨立的 module
'isolated_greeting_module_1',
'isolated_greeting_module_2'
],
version='0.1.0',
install_requires=[
'pyjokes'
],
extras_require={
'interactive': ['matplotlib>=2.2.0,<3.0.0', 'jupyter']
},
entry_points={
'console_scripts': [
'greeting=greeting_pkg.greeting_module:main'
]
}
)
用 pip install -e .
安裝之后细诸,可以像普通的 module 一樣調(diào)用,例如:
$ python
>>> from isolated_greeting_module_1 import greeting_func
>>> greeting_func("De Gang")
Hi, De Gang , greetings from isolated_greeting_module_1.
添加描述性的條目
如果編寫 setup.py 文件的目的是希望將整個 package 分享給其他研究人員或者分享到 PyPI 上陋守,那么還要添加一些描述性的信息震贵,以便別人更好的理解這個 package。常見的描述如下:
from setuptools import setup, find_packages
setup(
...
# metadata to display on PyPI
author="Me",
author_email="me@example.com",
description="This is an Example Package",
keywords="hello world example examples",
url="http://example.com/HelloWorld/", # project home page, if any
project_urls={
"Documentation": "https://docs.example.com/HelloWorld/",
"Source Code": "https://code.example.com/HelloWorld/",
},
classifiers=[
'License :: OSI Approved :: Python Software Foundation License'
]
)
以上是基本的 setup.py
編寫方法嗅义。還有其他更多的條目以及對 setup.py 更深入的介紹可以在這里查閱屏歹。