翻譯:《實用的Python程式設計》09_01_Packages

codists發表於2021-04-16

目錄| 上一節 (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.csvData/prices.csvporty-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

目錄| 上一節 (8.3 除錯) | 下一節 (9.2 第三方包)

注:完整翻譯見 https://github.com/codists/practical-python-zh

相關文章