Python模塊導(dǎo)入與包構(gòu)建最佳實(shí)踐

[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_asubpkg_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)試更方便。

參考:

  1. PEP 328: Absolute and Relative Imports
  2. Absolute vs Relative Imports in Python
  3. The import system
  4. Script vs. Module
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容