目錄| 上一節 (8.3 除錯) | 下一節 (9.2 第三方包)
9.1 包
如果編寫一個較大的程式,我們並不真的想在頂層將其組織為一個個獨立檔案的大型集合。本節對包(package)進行介紹。
模組
任何一個 Python 原始檔稱為一個模組(module)。
# foo.py
def grok(a):
...
def spam(b):
...
一條 import
語句載入並執行 一個模組。
# program.py
import foo
a = foo.grok(2)
b = foo.spam('Hello')
...
包 vs 模組
對於較大的程式碼集合,通常將模組組織到包中。
# From this
pcost.py
report.py
fileparse.py
# To this
porty/
__init__.py
pcost.py
report.py
fileparse.py
首先,選擇一個名字並用該名字建立頂級目錄。如上述的 porty
(顯然,第一步最重要的是選擇名字)。
接著,新增 __init__.py
檔案到該目錄中。__init__.py
檔案可以是一個空檔案。
最後,把原始檔放到該目錄中。
使用包
包用作匯入的名稱空間。
這意味著現在有了多級匯入。
import porty.report
port = porty.report.read_portfolio('port.csv')
匯入語句還有其它變體:
from porty import report
port = report.read_portfolio('portfolio.csv')
from porty.report import read_portfolio
port = read_portfolio('portfolio.csv')
兩個問題
這種方法存在兩個主要的問題:
- 同一包內不同檔案之間的匯入無效。
- 包中的主指令碼無效。
因此,基本上一切匯入都是無效的,但是,除此之外,程式還是可以工作的。
問題:匯入
現在,在匯入的時候,同一包內的不同檔案之間的匯入必須包含包名。請記住這個結構:
porty/
__init__.py
pcost.py
report.py
fileparse.py
根據上述規則(同一包內的不同檔案之間的匯入必須包含包名)修改後的匯入示例:
# report.py
from porty import fileparse
def read_portfolio(filename):
return fileparse.parse_csv(...)
所有的匯入都是絕對的,而不是相對的。
# report.py
import fileparse # BREAKS. fileparse not found
...
相對匯入
除了使用包名直接匯入,還可以使用使用 .
引用當前的包。
# report.py
from . import fileparse
def read_portfolio(filename):
return fileparse.parse_csv(...)
語法:
from . import modname
使用上述語法使得重新命名包變得容易。
問題:主指令碼
將包內的子模組作為主指令碼執行會導致程式中斷:
bash $ python porty/pcost.py # BREAKS
...
原因:你正在執行單個指令碼,而 Python 不知道包的其餘部分(sys.path
是錯誤的)。
所有的匯入都會中斷。要想解決這個問題,需要以不同的方式執行程式,可以使用 -m
選項。
bash $ python -m porty.pcost # WORKS
...
__init__.py
檔案
該檔案的主要目的是將模組組織在一起。
例如:
# porty/__init__.py
from .pcost import portfolio_cost
from .report import portfolio_report
這使得匯入的時候名字出現在頂層。
from porty import portfolio_cost
portfolio_cost('portfolio.csv')
而不是使用多級匯入:
from porty import pcost
pcost.portfolio_cost('portfolio.csv')
指令碼的另一種解決方案
如前所述,需要使用 -m package.module
執行包內的指令碼。
bash % python3 -m porty.pcost portfolio.csv
還有另一種選擇:編寫一個新的頂級指令碼。
#!/usr/bin/env python3
# pcost.py
import porty.pcost
import sys
porty.pcost.main(sys.argv)
指令碼位於包外面。目錄結構如下:
pcost.py # top-level-script
porty/ # package directory
__init__.py
pcost.py
...
應用結構
程式碼組織和檔案結構是應用程式可維護性的關鍵。
對於 Python 而言,沒有“放之四海而皆準”的方法,但是一個適用於多種問題的結構就是這樣:
porty-app/
README.txt
script.py # SCRIPT
porty/
# LIBRARY CODE
__init__.py
pcost.py
report.py
fileparse.py
頂級 porty-app
目錄是所有其他內容的容器——這些內容包括文件,頂級指令碼,用例等。
同樣,頂級指令碼(如果有)需要放置在程式碼包之外(包的上一層)。
#!/usr/bin/env python3
# porty-app/script.py
import sys
import porty
porty.report.main(sys.argv)
練習
此時,我們有了一個包含多個程式的目錄:
pcost.py # computes portfolio cost
report.py # Makes a report
ticker.py # Produce a real-time stock ticker
同時,還有許多具有各種功能的支援模組:
stock.py # Stock class
portfolio.py # Portfolio class
fileparse.py # CSV parsing
tableformat.py # Formatted tables
follow.py # Follow a log file
typedproperty.py # Typed class properties
在本次練習中,我們將整理這些程式碼並將它們放入一個通用包中。
練習 9.1:建立一個簡單的包
請建立一個名為 porty
的目錄並將上述所有的 Python 檔案放入其中。另外,在 porty
目錄中建立一個空的 __init__.py
檔案。最後,檔案目錄看起來像這樣:
porty/
__init__.py
fileparse.py
follow.py
pcost.py
portfolio.py
report.py
stock.py
tableformat.py
ticker.py
typedproperty.py
請將 porty
目錄中的 __pycache__
目錄移除。該目錄包含了之前預編譯的 Python 模組。我們想重新開始。
嘗試匯入包中的幾個模組:
>>> import porty.report
>>> import porty.pcost
>>> import porty.ticker
如果這些匯入失敗,請進入到合適的檔案中解決模組匯入問題,使其能夠包括相對匯入。例如,import fileparse
語句可以像下面這樣進行修改:
# report.py
from . import fileparse
...
如果有類似於 from fileparse import parse_csv
這樣的語句,請像下面這樣修改程式碼:
# report.py
from .fileparse import parse_csv
...
練習 9.2:建立應用目錄
對應用而言,將所有程式碼放到“包”中通常是不夠的。有時,支援檔案,文件,指令碼等檔案需要放到 porty/
目錄之外。
請建立一個名為 porty-app
的新目錄。然後將我們在練習 9.1 中建立的 porty
目錄移動到 porty-app
目錄中。接著,複製測試檔案 Data/portfolio.csv
和 Data/prices.csv
到 porty-app
目錄。另外,在 porty-app
目錄下建立一個 README.txt
檔案,該檔案包含一些有關自己的資訊。現在,程式碼的組織結構像下面這樣:
porty-app/
portfolio.csv
prices.csv
README.txt
porty/
__init__.py
fileparse.py
follow.py
pcost.py
portfolio.py
report.py
stock.py
tableformat.py
ticker.py
typedproperty.py
要執行程式碼,需要確保你現在正在頂級目錄 porty-app/
下。例如,從終端執行:
shell % cd porty-app
shell % python3
>>> import porty.report
>>>
嘗試將之前的指令碼作為主程式執行:
shell % cd porty-app
shell % python3 -m porty.report portfolio.csv prices.csv txt
Name Shares Price Change
---------- ---------- ---------- ----------
AA 100 9.22 -22.98
IBM 50 106.28 15.18
CAT 150 35.46 -47.98
MSFT 200 20.89 -30.34
GE 95 13.48 -26.89
MSFT 50 20.89 -44.21
IBM 100 106.28 35.84
shell %
練習 9.3:頂級指令碼
使用 python -m
命令通常有點怪異。可能需要編寫一個頂級指令碼來處理奇怪的包。請建立一個生成上述報告的指令碼 print-report.py
:
#!/usr/bin/env python3
# print-report.py
import sys
from porty.report import main
main(sys.argv)
然後把指令碼 print-report.py
放到頂級目錄 porty-app/
中。並確保可以在 porty-app/
目錄下執行它:
shell % cd porty-app
shell % python3 print-report.py portfolio.csv prices.csv txt
Name Shares Price Change
---------- ---------- ---------- ----------
AA 100 9.22 -22.98
IBM 50 106.28 15.18
CAT 150 35.46 -47.98
MSFT 200 20.89 -30.34
GE 95 13.48 -26.89
MSFT 50 20.89 -44.21
IBM 100 106.28 35.84
shell %
最後,程式碼的組織結構應該下面這樣:
porty-app/
portfolio.csv
prices.csv
print-report.py
README.txt
porty/
__init__.py
fileparse.py
follow.py
pcost.py
portfolio.py
report.py
stock.py
tableformat.py
ticker.py
typedproperty.py