[TOC]
最開(kāi)始寫(xiě)程序的時(shí)候,都是一個(gè)文件里輸入幾行源碼(python 的一個(gè) web 框架bottle就特別強(qiáng)調(diào)自己是單文件框架)。隨著程程式變大變復(fù)雜,一個(gè)文件很難承載如此多的功能,因此將代碼拆分到不同的文件里,以模塊(module)或者包(package)形式組織。既方便管理,也利于復(fù)用。python的模塊和包非常簡(jiǎn)單,一個(gè)文件即模塊,一個(gè)文件夾即一個(gè)包。
文件夾包必須在文件夾內(nèi)聲明一個(gè)init.py文件,包被導(dǎo)入的時(shí)候,默認(rèn)會(huì)先執(zhí)行這個(gè)文件的代碼
導(dǎo)入方式
有了包機(jī)制,既可以把項(xiàng)目拆分封裝。也可以實(shí)現(xiàn)獨(dú)立的功能給別的項(xiàng)目使用。Python提供了 import 指令,鑒于歷史原因,import 有相對(duì)導(dǎo)入(implicit/explicit relative import)和絕對(duì)導(dǎo)入(absolute import)兩種方式。相對(duì)導(dǎo)入有隱式的導(dǎo)入(implicit)和顯示導(dǎo)入(explicit)。
模塊的導(dǎo)入都是相對(duì)于包而言。 import 指令是加載包的模塊,通過(guò)from import 語(yǔ)句不僅可以加載模塊,也可以導(dǎo)入 python 對(duì)象(類,函數(shù),變量等)。模塊最小組織單位是文件,import 后面的如果是文件則被認(rèn)為是模塊,若是文件里的 python 對(duì)象,則加載的不是模塊。
模塊搜索
Python的模塊搜索大致有三種步驟。
首先搜索
sys.modules:這是一個(gè)列表,她存儲(chǔ)了之前導(dǎo)入的所有模塊,新導(dǎo)入的模塊也會(huì)追加到這個(gè)列表里。若這里搜索不到,那么就會(huì)進(jìn)行第二步。其次搜索
built-in module:Python 的標(biāo)準(zhǔn)庫(kù)和安裝的第三方軟件包。如果還搜索不到模塊,就進(jìn)行最后一步。最后搜索
sys.path:被執(zhí)行的python文件,其所在的目錄會(huì)被追加到 sys.path 列表,也就是相對(duì)于被執(zhí)行的文件的目錄文件夾和系統(tǒng)在sys.path的也會(huì)被搜索。若任然找不到,最終會(huì)拋出一個(gè)ModuleNotFoundError錯(cuò)誤。
Python文件加載方式
Python的文件加載方式有兩種,直接運(yùn)行和以模塊方式加載。
以top-level方式直接加載:python + 文件名。如
python filename.py,或者python dir/filename.py,這樣的python文件是作為top-level腳本運(yùn)行,腳本文件的__name__屬性會(huì)被設(shè)置成__main__,同時(shí)其__package__屬性設(shè)置為None。因?yàn)榇藭r(shí)的文件作為頂層模塊,它不屬于任何一個(gè)包。直接運(yùn)行腳本,會(huì)把腳本所在的目錄追加到sys.path 之中。-
以模塊方式加載,使用
-m解釋參數(shù),然后跟著文件路徑,其中/替換成.。如python -m filename或者python dir.filename,filename.py 變成了一個(gè)模塊,其模塊名為所在python執(zhí)行目錄下的包.模塊.文件名。例如他的模塊名是filename或者dir.filename。這種方式,不會(huì)把腳本所在位置加載 sys.path 之中,而是會(huì)把執(zhí)行 python 命令所在的目錄加載 sys.path 中。但是被執(zhí)行的腳本肯定也在執(zhí)行命令所在文件或者子文件中,因此效果類似自身也屬于 sys.path 之中。
自2.6以后,python 的模塊名可以用下面的方式打?。?br>
module_name = '"{}.{}".format(__package__, __name__) if __package is not None else __name__
相對(duì)導(dǎo)入和絕對(duì)導(dǎo)入
上文介紹了 python 腳本加載和模塊搜索的基本方式?;诖?,python提供了以相對(duì)或絕對(duì)導(dǎo)入兩種包、模塊的import方式。因python2和3分裂的歷史,2默認(rèn)是相對(duì)導(dǎo)入,3則是絕對(duì)導(dǎo)入,并且日常開(kāi)發(fā)也推薦使用絕對(duì)導(dǎo)入方式。
那么它們兩種有什么區(qū)別呢?
導(dǎo)入都是針對(duì)包而言
- 絕對(duì)導(dǎo)入:文件的
import或者from import導(dǎo)入語(yǔ)句,都是從包的根路徑開(kāi)始 - 相對(duì)導(dǎo)入:導(dǎo)入的起始模塊未必是從包路徑開(kāi)始,使用
.或者..的方式是顯示的相對(duì)導(dǎo)入,否則是隱式的
python3針對(duì)隱式相對(duì)導(dǎo)入會(huì)直接拋錯(cuò)。
Case Study
文字都過(guò)于抽象,下面針對(duì)code進(jìn)行演示解析。文件目錄結(jié)構(gòu)如下,myproj 項(xiàng)目中,有一個(gè)pkg的包,包里有兩個(gè)文件夾subpkg_a和subpkg_b兩個(gè)子包,子包分別有幾個(gè)py模塊文件。
? myproj tree
.
└── pkg
├── __init__.py
├── main.py
├── subpkg_a
│ ├── __init__.py
│ ├── hello.py
│ └── world.py
└── subpkg_b
├── __init__.py
└── welcome.py
首先分析subpka_a包,即相對(duì) subpkg_a 來(lái)分析 hello.py 和 world.py 直接的導(dǎo)入方式。暫時(shí)可以忽略其他文件或文件夾。
下面是幾個(gè)文件的源碼
? pkg cat subpkg_a/__init__.py subpkg_a/hello.py subpkg_a/world.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
if __package__:
print('subpkg_a.__init__.py: ', '{}.{}'.format(__package__, __name__))
else:
print('subpkg_a.__init__.py: ', '{}'.format(__name__))
------------------------
#!/usr/bin/env python
# -*- coding:utf-8 -*-
if __package__:
print('hello.py: ', '{}.{}'.format(__package__, __name__))
else:
print('hello.py: ', '{}'.format(__name__))
import world
------------------------
#!/usr/bin/env python
# -*- coding:utf-8 -*-
if __package__:
print('world.py: ', '{}.{}'.format(__package__, __name__))
else:
print('world.py: ', '{}'.format(__name__))
def say_world():
return 'world'
if __name__ == '__main__':
print(say_world())
hello.py 文件直接導(dǎo)入了world模塊,然后運(yùn)行結(jié)果如下:
? pkg python subpkg_a/hello.py
('hello.py: ', '__main__')
('world.py: ', 'world')
可以看見(jiàn),hello.py 的模塊名為 __main__ 。作為 top-level 執(zhí)行的文件,其__package__是None, __name__是 __main__。 對(duì)于world.py 文件,因?yàn)樗潜患虞d的模塊,__name__就是文件名。
hello.py很好理解,作為top-level腳本執(zhí)行,其本身不屬于任何一個(gè)包。world.py是作為模塊被加載的,但是hello.py 里也沒(méi)指定從哪個(gè)包里加載。因此加載時(shí)候就按照模塊搜索方式。因?yàn)閔ello.py執(zhí)行的目錄被加入了sys.path,world.py 與 hello.py 同級(jí),因此自然能被搜索并成為模塊。
隱式相對(duì)導(dǎo)入
由于上面的導(dǎo)入方式,模塊都不屬于任何一個(gè)包,自然就沒(méi)有相對(duì)于絕對(duì)導(dǎo)入的說(shuō)法。刪掉subpka_a/__init__.py 文件也不會(huì)有影響。正如前面所介紹,python腳本若不是top-level,才有包概念的。修改執(zhí)行方式如下:
? pkg python -m subpkg_a.hello
('subpkg_a.__init__.py: ', 'subpkg_a')
('hello.py: ', 'subpkg_a.__main__')
('world.py: ', 'subpkg_a.world')
可以看到,subpak_a 包的__init__.py 文件也被加載執(zhí)行了,這表示包 subpak_a 被導(dǎo)入了。-m 的語(yǔ)法告訴了解釋器,把當(dāng)前執(zhí)行命令的目錄加入到 sys.path。以模塊的方式加載 hello.py 文件,并且指定了hello 模塊的父級(jí)是 subpkg_a,同理,處于同級(jí)的 world.py 文件也被隱式的包含在 subkag_a 包里,它的模塊名subpkg_a.world。
上面的 import 語(yǔ)句中沒(méi)有出現(xiàn)包 subpkg_a,所以是一種相對(duì)導(dǎo)入,也沒(méi)有使用.或者.. 符號(hào),所以是隱式的導(dǎo)入。隱式導(dǎo)入在python3下不支持,會(huì)拋錯(cuò):
? pkg python3 -m subpkg_a.hello
subpkg_a.__init__.py: subpkg_a.subpkg_a
hello.py: subpkg_a.__main__
Traceback (most recent call last):
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/runpy.py", line 193, in _run_module_as_main
"__main__", mod_spec)
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/runpy.py", line 85, in _run_code
exec(code, run_globals)
File "/Users/master/myproj/pkg/subpkg_a/hello.py", line 9, in <module>
import world
ModuleNotFoundError: No module named 'world'
顯示相對(duì)導(dǎo)入
subpak_a 包的層級(jí)很清楚,因此改為顯示相對(duì)導(dǎo)入也很簡(jiǎn)單,即:
? pkg cat subpkg_a/hello.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
if __package__:
print('hello.py: ', '{}.{}'.format(__package__, __name__))
else:
print('hello.py: ', '{}'.format(__name__))
from . import world
運(yùn)行結(jié)果也正確:
? pkg python -m subpkg_a.hello
('subpkg_a.__init__.py: ', 'subpkg_a')
('hello.py: ', 'subpkg_a.__main__')
('world.py: ', 'subpkg_a.world')
使用了python -m subpkg_a.hello 命令執(zhí)行,hello.py 和 world.py 分別屬于 subpkg_a 包,因此 hello.py 里的 . 表示在包 subpkg_a 內(nèi),相對(duì)于__main__模塊的導(dǎo)入同級(jí)模塊。即subpkg_a.world,.即subpkg_a.。因此不會(huì)報(bào)錯(cuò)。
需要注意,顯示的相對(duì)導(dǎo)入只有以模塊加載的方式才能使用,否則會(huì)拋 Attempted relative import in non-package的錯(cuò)誤
? pkg python subpkg_a/hello.py
('hello.py: ', '__main__')
Traceback (most recent call last):
File "subpkg_a/hello.py", line 9, in <module>
from . import world
ValueError: Attempted relative import in non-package
正如前文所述,以top-level 的運(yùn)行hello.py文件,hello.py的模塊名是__main__, world.py 的模塊名是world,兩者不屬于任何一個(gè)包,自然也沒(méi)有模塊的層級(jí)。. 是指相對(duì)與包下面的模塊的路徑進(jìn)行導(dǎo)入。正如這樣沒(méi)有包概念,因此拋錯(cuò)。
對(duì)于subpak_b包里的模塊,需要使用..操作符,修改hello.py 如下:
...
from . import world
from ..subpkg_b import welcome
需要注意的是,python -m subpkg_a.hello的執(zhí)行方式,最頂級(jí)的包是 subpkg_a,而subpkg_b 是搜索不到的,需要更上層的目錄來(lái)執(zhí)行:
? pkg python -m subpkg_a.hello
('subpkg_a.__init__.py: ', 'subpkg_a')
('hello.py: ', 'subpkg_a.__main__')
('world.py: ', 'subpkg_a.world')
Traceback (most recent call last):
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/runpy.py", line 162, in _run_module_as_main
"__main__", fname, loader, pkg_name)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/runpy.py", line 72, in _run_code
exec code in run_globals
File "/Users/master/myproj/pkg/subpkg_a/hello.py", line 10, in <module>
from ..subpkg_b import welcome
ValueError: Attempted relative import beyond toplevel package
? pkg cd ../
? myproj python -m pkg.subpkg_a.hello
('subpkg_a.__init__.py: ', 'pkg.subpkg_a')
('hello.py: ', 'pkg.subpkg_a.__main__')
('world.py: ', 'pkg.subpkg_a.world')
('welcome.py: ', 'pkg.subpkg_b.welcome')
綜上所述,相對(duì)導(dǎo)入,導(dǎo)入的路徑中,都沒(méi)有出現(xiàn)包名。
混用隱式和顯示
通常對(duì)于subpak_a 和subpak_b,它們自身實(shí)現(xiàn)邏輯可以使用顯示或者隱式導(dǎo)入。對(duì)于它的調(diào)用者,pkg下的main.py 可以直接引用這兩個(gè)包。此時(shí)的參考包是pkg。
if __package__:
print('main.py: ', '{}.{}'.format(__package__, __name__))
else:
print('main.py: ', '{}'.format(__name__))
from subpkg_a import hello
在main中隱式相對(duì)導(dǎo)入了subpak_a 和 hello 模塊
? myproj python pkg/main.py
('main.py: ', '__main__')
('welcome.py: ', 'subpkg_b.welcome')
('subpkg_a.__init__.py: ', 'subpkg_a')
('hello.py: ', 'subpkg_a.hello')
('world.py: ', 'subpkg_a.world')
Traceback (most recent call last):
File "pkg/main.py", line 12, in <module>
from subpkg_a import hello
File "/Users/master/myproj/pkg/subpkg_a/hello.py", line 11, in <module>
from ..subpkg_b import welcome
ValueError: Attempted relative import beyond toplevel package
from subpkg_b import welcome語(yǔ)句正常執(zhí)行了,from subpkg_a import hello也正常,這符合前面的說(shuō)明。
此時(shí)main是top-level,它不屬于任何一個(gè)包,但是subpkg_a,subpkg_b 也不屬于任何一個(gè)包,但是它本身是一個(gè)包,所以導(dǎo)入它是沒(méi)問(wèn)題,并且它里面的hello和world導(dǎo)入也正常。
執(zhí)行到 from ..subpkg_b import welcome 語(yǔ)句的時(shí)候報(bào)錯(cuò)了。正如前面的結(jié)果,subpkg_a 和 subpkg_b 是同級(jí),可是在 subpkg_a 包并不知道 subpkg_b 包的存在,因此需要把他們共有的包 pkg 引入到包層級(jí)中,即
? myproj python -m pkg.main
('main.py: ', 'pkg.__main__')
('welcome.py: ', 'pkg.subpkg_b.welcome')
('subpkg_a.__init__.py: ', 'pkg.subpkg_a')
('hello.py: ', 'pkg.subpkg_a.hello')
('world.py: ', 'pkg.subpkg_a.world')
鑒于 welcome 已經(jīng)被導(dǎo)入過(guò),因此 hello.py 將不會(huì)再導(dǎo)入 welcome 模塊。
絕對(duì)導(dǎo)入
隱式相對(duì)導(dǎo)入在py3被禁止了,顯式相對(duì)導(dǎo)入也不是默認(rèn),那么最好還是使用絕對(duì)導(dǎo)入。即從包的起始位置書(shū)寫(xiě)import路徑。
修改main.py,將import從跟包開(kāi)始書(shū)寫(xiě)路徑
? myproj cat pkg/main.py
if __package__:
print('main.py: ', '{}.{}'.format(__package__, __name__))
else:
print('main.py: ', '{}'.format(__name__))
from pkg.subpkg_b import welcome
from pkg.subpkg_a import hello
? myproj python pkg/main.py
('main.py: ', '__main__')
Traceback (most recent call last):
File "pkg/main.py", line 11, in <module>
from pkg.subpkg_b import welcome
ImportError: No module named pkg.subpkg_b
可以看到,與上次執(zhí)行不一樣,from pkg.subpkg_b import welcome 這一句就報(bào)錯(cuò)了。當(dāng)前的 sys.path 是 main.py 所在的目錄,并不包括 pkg 所在的目錄,因此搜索包的時(shí)候,搜索不到 pkg。解決方案也有兩種。
因?yàn)?sys.path 沒(méi)有,那么加上即可。在main.py 中加上
? myproj cat pkg/main.py
if __package__:
print('main.py: ', '{}.{}'.format(__package__, __name__))
else:
print('main.py: ', '{}'.format(__name__))
import sys
sys.path.append('./')
from pkg.subpkg_b import welcome
from pkg.subpkg_a import hello
? myproj python pkg/main.py
('main.py: ', '__main__')
('welcome.py: ', 'pkg.subpkg_b.welcome')
('subpkg_a.__init__.py: ', 'pkg.subpkg_a')
('hello.py: ', 'pkg.subpkg_a.hello')
('world.py: ', 'pkg.subpkg_a.world')
盡管針對(duì) sys.path 進(jìn)行 hack 可以實(shí)現(xiàn)絕對(duì)導(dǎo)入,可是這種方式始終一點(diǎn)也不 make sence。正如前面解決方式一樣,可以使用 -m 以模塊方式加載。畢竟 -m 可以把當(dāng)前執(zhí)行路徑加入到sys.path中,去掉 sys.path.append的語(yǔ)句,再運(yùn)行:
? myproj python -m pkg.main
('main.py: ', 'pkg.__main__')
('welcome.py: ', 'pkg.subpkg_b.welcome')
('subpkg_a.__init__.py: ', 'pkg.subpkg_a')
('hello.py: ', 'pkg.subpkg_a.hello')
('world.py: ', 'pkg.subpkg_a.world')
使用 -m 方式使用一個(gè)包,在python也是挺常見(jiàn)的,例如開(kāi)啟一個(gè)服務(wù)器和格式化json字符串
? myproj python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
? myproj echo '[{"hello": "world"}, {"python": "life is short"}]' | python -m json.tool
[
{
"hello": "world"
},
{
"python": "life is short"
}
]
然而,實(shí)際生產(chǎn)中,寫(xiě)一個(gè)package或者lib,更多是被 install 后再導(dǎo)入運(yùn)行。install 保證了它將會(huì)處在sys.path,被導(dǎo)入等同于以 -m 方式被加載執(zhí)行。因此不太會(huì)有 sys.path 和ModuleNotFoundError問(wèn)題。例如將hello改為
from subpkg_a import world
變成絕對(duì)導(dǎo)入之后,直接運(yùn)行會(huì)報(bào)錯(cuò):
? pkg python subpkg_a/hello.py
('hello.py: ', '__main__')
Traceback (most recent call last):
File "subpkg_a/hello.py", line 10, in <module>
from subpkg_a import world
ImportError: No module named subpkg_a
main.py 改為
from subpkg_a import hello
模擬subpkg_a作為一個(gè)獨(dú)立的lib,其本身使用絕對(duì)導(dǎo)入,然后pkg里的main使用這個(gè)包,直接運(yùn)行,并沒(méi)有報(bào)錯(cuò)
? myproj python pkg/main.py
('main.py: ', '__main__')
('subpkg_a.__init__.py: ', 'subpkg_a')
('hello.py: ', 'subpkg_a.hello')
('world.py: ', 'subpkg_a.world')
? myproj
因此使用絕對(duì)導(dǎo)入開(kāi)發(fā)一個(gè) lib 是更好的實(shí)踐??墒钦缟厦?subpkg_a 所面臨的問(wèn)題,開(kāi)發(fā)過(guò)程中,直接運(yùn)行,可能會(huì)報(bào)錯(cuò),不得不使用 -m 的方式。為了更好的開(kāi)發(fā),可以使用下面介紹的包結(jié)構(gòu)。
Python Lib 構(gòu)建推薦
帶有__init__.py 文件夾即成為一個(gè)包,包,模塊相互組織起來(lái)即成為lib。先看一個(gè)相對(duì)導(dǎo)入,即構(gòu)建包的時(shí)候。
? demo tree mylib
mylib
├── mylib
│ ├── __init__.py
│ ├── greet
│ │ ├── __init__.py
│ │ ├── hello.py
│ │ └── world.py
│ └── main.py
└── setup.py
2 directories, 6 files
其中 hello.py world.py 和 main.py 的內(nèi)容如下:
? mylib cat greet/hello.py greet/world.py main.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from . import world
def say_hello():
return 'hello ' + world.say_world()
if __name__ == '__main__':
print(say_hello())
#!/usr/bin/env python
# -*- coding:utf-8 -*-
def say_world():
return 'world'
if __name__ == '__main__':
print(say_world())
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from .greet import hello
def do_greet():
return hello.say_hello()
if __name__ == '__main__':
print(do_greet())
運(yùn)行 python main.py 也能正常運(yùn)行
然后使用 setup打包進(jìn)行安裝。
? mylib cat setup.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from setuptools import setup, find_packages
setup(
name='mylib',
version='1.0.0',
decription='simple lib demo',
long_description='README.md',
author='jiamin',
author_email='maojiamin@daixm.com',
licens='',
packages=find_packages(exclude=('tests', 'docs')),
test_suite='tests'
)
執(zhí)行 python setup.py bdist_wheel,會(huì)在 lib 下的 dist 文件夾生成一個(gè) mylib-1.0.0-py2-none-any.whl 包。使用 pip 可以直接安裝
(venv) ? myproj pip list
Package Version
---------- -------
pip 19.0.1
setuptools 40.6.3
wheel 0.32.3
(venv) ? myproj pip install ~/mylib/dist/mylib-1.0.0-py2-none-any.whl
DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 won't be maintained after that date. A future version of pip will drop support for Python 2.7.
Processing /Users/master/mylib/dist/mylib-1.0.0-py2-none-any.whl
Installing collected packages: mylib
Successfully installed mylib-1.0.0
(venv) ? myproj pip list
Package Version
---------- -------
mylib 1.0.0
pip 19.0.1
setuptools 40.6.3
wheel 0.32.3
(venv) ? myproj ipython
In [1]: import mylib
In [2]: from mylib.main import do_greet
In [3]: do_greet()
Out[3]: 'hello world'
In [4]:
使用相對(duì)導(dǎo)入也可以構(gòu)建一個(gè)包。
更好的python包構(gòu)建方式
使用顯示相對(duì)導(dǎo)入包構(gòu)建方式,一個(gè)好處就是,報(bào)名修改了,也會(huì)不用修改包內(nèi)模塊的導(dǎo)入語(yǔ)句。而絕對(duì)導(dǎo)入包含了包名。但是絕對(duì)導(dǎo)入對(duì)于本地包的處理,有更好的方式,因此也是python3的默認(rèn)方式。
構(gòu)建一個(gè)python lib。和包結(jié)構(gòu)和相對(duì)導(dǎo)入類似,下面增加更多的應(yīng)用場(chǎng)景。項(xiàng)目結(jié)構(gòu)目錄如下
? mylib tree
.
├── mylib
│ ├── __init__.py
│ ├── cron
│ │ ├── __init__.py
│ │ └── tasks.py
│ ├── greet
│ │ ├── __init__.py
│ │ ├── hello.py
│ │ └── world.py
│ └── main.py
├── setup.py
├── docs
├── README.md
└── tests
├── __init__.py
├── __init__.pyc
└── test_greet.py
4 directories, 11 files
增加了tests目錄和docs目錄以及README.md。幾個(gè)文件代碼如下,所有導(dǎo)入都使用絕對(duì)導(dǎo)入,即從 mylib開(kāi)始導(dǎo)入
? mylib cat mylib/greet/hello.py
from mylib.greet import world
...
? mylib cat mylib/cron/tasks.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from mylib.greet import hello
def do_task():
return 'do task : ' + hello.say_hello()
if __name__ == '__main__':
print(do_task())
? mylib cat tests/test_greet.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import unittest
from mylib.greet import world
from mylib.greet import hello
class TestGreet(unittest.TestCase):
def test_say_world(self):
self.assertEqual('world', world.say_world())
def test_say_hello(self):
self.assertEqual('hello world', hello.say_hello())
if __name__ == '__main__':
unittest.main()
在mylib內(nèi),若想要執(zhí)行 corn下面的task,必須以 -m 方式運(yùn)行,否則會(huì)拋錯(cuò)
? mylib python mylib/cron/tasks.py
Traceback (most recent call last):
File "mylib/cron/tasks.py", line 4, in <module>
from mylib.greet import hello
ImportError: No module named mylib.greet
? mylib python -m mylib.cron.tasks
do task : hello world
同樣的,執(zhí)行 tests 也是需要制定 -m。
hydra 里的cron,都是使用 sys.path.append方式,將執(zhí)行腳本追加到path。使用 -m 方式會(huì)比hack sys.path 更好
運(yùn)行測(cè)試
? mylib python -m tests.test_greet
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
測(cè)試 測(cè)試
但是,其實(shí)setup工具,提供了測(cè)試方法,并且setup里還可以指定不同的測(cè)試方式,即聲明 test_suite='tests'
? mylib python setup.py test
running test
running egg_info
creating mylib.egg-info
writing mylib.egg-info/PKG-INFO
writing top-level names to mylib.egg-info/top_level.txt
writing dependency_links to mylib.egg-info/dependency_links.txt
writing manifest file 'mylib.egg-info/SOURCES.txt'
reading manifest file 'mylib.egg-info/SOURCES.txt'
writing manifest file 'mylib.egg-info/SOURCES.txt'
running build_ext
test_say_hello (tests.test_greet.TestGreet) ... ok
test_say_world (tests.test_greet.TestGreet) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.000s
當(dāng)運(yùn)行了上面的測(cè)試之后,會(huì)發(fā)現(xiàn)當(dāng)前目錄多了一個(gè)文件夾mylib.egg-info
? mylib ls
mylib mylib.egg-info setup.py tests
安裝項(xiàng)目
Lib開(kāi)發(fā)完之后,自然是可以進(jìn)行打包然后分發(fā)后install。然而測(cè)試開(kāi)發(fā)的時(shí)候,如果執(zhí)行腳本,都需要加上 -m,整個(gè)開(kāi)發(fā)過(guò)程還是蠻繁瑣,因此python的setup.py 提供了一個(gè)develop參數(shù),進(jìn)行了一次mock安裝。
(venv) ? mylib pip list
Package Version
---------- -------
pip 19.0.1
setuptools 40.6.3
wheel 0.32.3
(venv) ? mylib python setup.py develop
running develop
running egg_info
writing mylib.egg-info/PKG-INFO
writing top-level names to mylib.egg-info/top_level.txt
writing dependency_links to mylib.egg-info/dependency_links.txt
reading manifest file 'mylib.egg-info/SOURCES.txt'
writing manifest file 'mylib.egg-info/SOURCES.txt'
running build_ext
Creating /Users/master/myproj/venv/lib/python2.7/site-packages/mylib.egg-link (link to .)
Adding mylib 1.0.0 to easy-install.pth file
Installed /Users/master/myproj/mylib
Processing dependencies for mylib==1.0.0
Finished processing dependencies for mylib==1.0.0
(venv) ? mylib pip list
Package Version Location
---------- ------- --------------------------
mylib 1.0.0 /Users/master/myproj/mylib
pip 19.0.1
setuptools 40.6.3
wheel 0.32.3
(venv) ? mylib python setup.py develop
running develop
running egg_info
writing mylib.egg-info/PKG-INFO
writing top-level names to mylib.egg-info/top_level.txt
writing dependency_links to mylib.egg-info/dependency_links.txt
reading manifest file 'mylib.egg-info/SOURCES.txt'
writing manifest file 'mylib.egg-info/SOURCES.txt'
running build_ext
Creating /Users/master/myproj/venv/lib/python2.7/site-packages/mylib.egg-link (link to .)
mylib 1.0.0 is already the active version in easy-install.pth
Installed /Users/master/myproj/mylib
Processing dependencies for mylib==1.0.0
Finished processing dependencies for mylib==1.0.0
(venv) ? mylib python mylib/cron/tasks.py
do task : hello world
由此可見(jiàn),執(zhí)行了 python setup.py develop, 會(huì)在環(huán)境的site-package創(chuàng)建一個(gè) mylib.egg-link文件,這個(gè)文件的內(nèi)容是 /Users/master/myproj/mylib,即指向當(dāng)前開(kāi)發(fā)環(huán)境包目錄,因此等價(jià)于安裝了包到環(huán)境中。自然可以通過(guò)pip list 查看。也就是可以直接使用腳本方式運(yùn)行,不再需要 -m了,并且也能再開(kāi)發(fā)的時(shí)候,進(jìn)行針對(duì)安裝以后的行為效果進(jìn)行調(diào)試。
總結(jié)
程序規(guī)模變大變復(fù)雜,通常進(jìn)行模塊拆分和封包復(fù)用。python文件及模塊的基本組織單位,文件夾則是基礎(chǔ)包。包或者模塊的引用可以使用 import 或者 from import 語(yǔ)法。
Import有相對(duì)導(dǎo)入和絕對(duì)導(dǎo)入,相對(duì)導(dǎo)入又有顯式和隱式兩種。顯式則使用.或者..操作符。相對(duì)還是絕對(duì),針對(duì)的是python文件被加載的方式。
直接運(yùn)行python文件則是以top-level方式,當(dāng)前文件模塊名是__main__,它本身就是頂級(jí)模塊,不存在包的概念。若使用-m參數(shù),則以模塊方式加載,模塊方式加載都是相對(duì)包而言。. 表示在同一個(gè)包內(nèi),被相對(duì)被加載文件的路徑進(jìn)行加載導(dǎo)入的模塊。
相對(duì)導(dǎo)入的文件里不會(huì)出現(xiàn)包名,絕對(duì)導(dǎo)入的文件里,import語(yǔ)句必須包含包名。同時(shí)所導(dǎo)入的包都必須從包名的根路徑開(kāi)始,寫(xiě)出完整的模塊路徑。
Python3不在支持隱式相對(duì)導(dǎo)入。官方也更推薦使用絕對(duì)導(dǎo)入。因此介紹了使用絕對(duì)導(dǎo)入構(gòu)建一個(gè)lib,所使用的方式包括項(xiàng)目源碼,文檔,測(cè)試等,這也是facebook 的 tornado 的方式。其中使用 python setup.py test 進(jìn)行單元測(cè)試。以及使用 pyton setup.py develop和mock安裝,使得開(kāi)發(fā)調(diào)試更方便。
參考: