模組與套件
在日常生活中,當你的個人收藏品越來越多的時候,你可能會想買一些箱子、櫃子來整理或分類。當箱子、櫃子越來越多的時候,有預算的話你說不定還會開始買貨櫃或倉庫(?)來放這些箱子、櫃子,可以更方便的找到想要找的東西。
寫程式也是,當函數越寫越多、功能越來越複雜,你可能會把想把一些相關或功能類似的函數放在一起,Python 的模組與套件的設計可以讓我們用簡單的方式管理這些程式碼。
什麼是模組?
「模組(Module)」是一個 Python 檔案,裡面可能會包含一些函數、變數和類別。透過模組的設計,可以讓程式碼更有組織性,也更容易維護。另外一個常會跟模組一起看到的名詞叫做「套件(Package)」,如果說剛才講到的模組是一個檔案的話,那麼套件就是一個目錄、資料夾的概念。一個套件裡面可以放很多的模組,或是放更多的子套件裡面再放更多的模組,基本上就是個像檔案系統一樣的結構,一個目錄裡能放很多檔案以及更多的子目錄一樣,待會就會看到實際的例子。
模組這個名詞聽起來有點抽象,或是感覺像什麼複雜的資料 結構,但在 Python 的模組就是一個檔案,在這個檔案裡包含了一些資料,可以給其他函數或模組使用。Python 的模組也不一定只能用 Python 寫,也有的模組是用 C 語言寫的,然後在執行時候再動態的載入。
但不管是用什麼程式語言寫,之所以會把程式碼分在不同的檔案或模組,都是希望讓程式碼更有組織性,或是把功能類似的函數放在同一個地方,維護起來也更容易。把程式碼分成不同的模組之後,不管是自己要用或是給其他人使用,只要把這些程式碼「匯入」就可以了。
使用模組
在 Python 中,要使用模組的話,主要是透過 import
關鍵字,這個關鍵字可以讓我們把模組匯入到目前的程式碼中,這樣就可以使用這個模組裡的東西了。
匯入模組
在 Python 匯入模組主要是使用 import
關鍵字,例如我們之前曾經介紹過的數學模組 math
:
>>> import math
模組匯入成功的話不會有任何反應,但如果匯入失敗,例如模組名字打錯了,或是根本沒有安裝過這個模組,就會出現錯誤訊息:
>>> import helloworld
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'helloworld'
錯誤訊息滿明顯的,就是說現在的環境裡並沒有叫做 helloworld
的模組。
問題是,到底 Python 是去哪裡找這些所謂的模組呢?這就要講到 Python 的模組搜尋路徑了。我們進到 REPL 環境進行以下操作:
>>> import sys
>>> sys.path
sys
模組是 Python 內建的模組,所以不需要另外安裝就能使用,可以看到這裡同樣也是使用 import
的方式把它匯入。在 sys
模組裡有不少跟系統相關的東西,其中 path
變數存放了 Python 搜尋模組的路徑。這個路徑是一個串列,我稍微整理排版過之後如下:
[
'',
'/Users/kaochenlong/.pyenv/versions/3.12.7/lib/python312.zip',
'/Users/kaochenlong/.pyenv/versions/3.12.7/lib/python3.12',
'/Users/kaochenlong/.pyenv/versions/3.12.7/lib/python3.12/lib-dynload',
'/Users/kaochenlong/.pyenv/versions/3.12.7/lib/python3.12/site-packages'
]
這個路徑可能會因為作業系統或 Python 版本不同而有些差異,最前面的空字串 ''
表示 Python 會先在當前目錄找看看有沒有這個模組,如果找到就載入,如果沒找到就繼續照著順序依序往下找,最後一個找的地方是 Python 安裝目錄下的 site-packages
。萬一這整串都沒找到,就會出現剛才的 ModuleNotFoundError
錯誤訊息。
模組搜尋的路徑是一個串列結構,它是可以被修改的,事實上我們前面介紹過的虛擬環境,不管是 venv
還是功能更完整的 Poetry,當啟動虛擬環境的時候,你可以去觀察 sys.path
印出來的結果,會發現虛擬環境的目錄也會被加進 sys.path
串列的最後面,變成模組搜尋的路徑之一,這也正是這些虛擬環境套件之所以能做到隔離環境的原因之一。
因為 Python 預設會先從當前的目錄開始找模組,所以如果我們有自己寫的模組,只要把這個模組放在當前目錄,就可以直接匯入使用了。我們就來動手做看看,我這裡有兩個檔案,分別叫做 app.py
以及 greeting.py
,我把這兩個檔案擺放在同一個目錄,我先在 greeting.py
裡寫上以下內容:
def helloworld():
print("Hello World")
def hi():
print("Hi!")
def hey():
print("Hey!")
def bye():
print("Bye!")
這些函數非常簡單也沒什麼營養,基本上就是一堆打招呼的函數。這個 greeting.py
檔案在 Python 我們就可以說它是一個模組。接下來如果我想在 app.py
裡使用這個模組,我可以在 app.py
裡這樣寫:
import greeting
import
關鍵字表示要匯入一個模組,這裡的 greeting
就是我們自己剛剛寫的模組。Python 會先在當前目錄找看看有沒有這個模組(也就是 greeting.py
檔案),找到之後就會把這個模組的內容載入,接下來就可以使用這個模組裡的東西了。注意在 import
的時候,greeting
不需要使用引號包起來,而且也不需要寫副檔名 .py
,像這樣:
import "greeting" # 這不行
import greeting.py # 這也不可以
硬是加上引號或附檔名會造成語法錯誤,Python 很聰明的,它會自動找到並載入我們想要的檔案,找不到也會透過錯誤訊息讓我們知道。模組匯入後就可以使用這個模組裡的東西了,使用的方式就是連名帶姓的呼叫模組以及函數的名稱,中間用 .
連接:
import greeting
greeting.helloworld()
greeting.hi()
這樣就行了。這裡可以匯入的不是只有函數,就算只是個簡單的變數也行,只要是在模組裡的東西,都可以這樣使用。
再繼續往下看之前,我想來看看這個匯進來的 greeting
是個什麼東西,來把它印出來看看:
import greeting
print(greeting)
# 印出 <module 'greeting' from '/demo/greeting.py'>
print(type(greeting))
# 印出 <class 'module'>
可以看到我們透過 import
關鍵字匯進來的是個 module
物件,還能隱約看的出來這個模組是寫在哪個檔案裡,我們回到 REPL 來看看內建的 sys
模組:
>>> import sys
>>> sys
<module 'sys' (built-in)>
>>> type(sys)
<class 'module'>
看的出來 sys
同樣也是個 module
物件,不過它的來源是寫著 (built-in)
,這表示 sys
模組是 Python 的一部分,這些模組跟著 Python 一起出廠,不需要另外安裝就能使用。
回到剛才的 greeting
模組,在 Python 如果我們想要看看某個物件身上有哪些東西,可以透過那個物件身上的 __dict__
屬性,或是直接使用內建函數 dir()
來查看。在 Python 很多東西都是物件,模組也是,所以:
import greeting
print(dir(greeting))
# 印出
# ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__',
# '__name__', '__package__', '__spec__', 'bye', 'helloworld', 'hey', 'hi']
除了那些兩個底線開頭的特別屬性外,可以看到我們自己定義的 bye
、helloworld
、hey
、hi
都在這裡面了,這也是為什麼當我們執行 greeting.hi()
的時候可以正常運作的原因。是說,如果知道 greeting
是個物件,那表示我們也可以在上面隨便加屬性,例如:
import greeting
greeting.yo = lambda: print("Yo!")
greeting.yo() # 印出 "Yo!"
這可以正常執行,但這沒什麼意義也不是什麼好習慣,我只是想透過這個例子讓大家知道模組本身其實就只是個物件而已,沒什麼特別的。
現在 greeting.py
跟 app.py
這兩個檔案都在同一個目錄裡,不過隨著模組越來越多,總不能把全部的雞蛋都放在同一個籃子裡,這時候可以用「套件(Package)」來幫模組做分類。套件這名字好像聽起來很厲害的樣子,但其實就是個目錄而已。我可以手動建立一個 utils
目錄,然後把剛才的 greeting.py
檔案移到這個目錄裡,現在的目錄結構看起來像這樣:
├── utils
│ └── greeting.py
└── app.py
這樣在匯入模組的時候,需要把套件的名字,也就是目錄名稱也加進去:
import utils.greeting
utils.greeting.hey()
在這裡我們會說 utils
是個套件,而 greeting
是個模組。題外話,所以在 Python 所謂的套件就是資料夾而已嗎?也不全然是,但現在可以先這樣理解就好,晚點會看到我為什麼這樣說。匯入模組之後,如果要呼叫模組裡的 hey()
函數,一樣是連名帶姓的把套件跟模組名稱帶上。覺得囉嗦嗎?我們可以幫匯入的模組取個小名:
import utils.greeting as g
g.hey()
使用 as
關鍵字可幫原本的 utils.greeting
取個小名,但這樣一來就不能再透過 utils.greeting
來取用 hey()
函數了。
另外要提醒一件有點重要但又不是那麼重要的事,就是大家別要被「匯入」這個動作給誤導了,在 Python 匯入模組的時候,一開始的確是去讀取模組相對應的檔案沒錯,但讀完之後 Python 會直接執行它。所以如果我在 greeting.py
檔裡面試著加上一行 print("Hello Kitty")
的話,就算只有匯入模組然後什麼事都不做,也會看到印出 "Hello Kitty"
的結果,正是那個模組被執行的效果。
也就是說,在 Python 所謂的匯入模組,其實就是執行那個模組裡的所有的程式碼!
只匯入部份功能
跟 import
還有另一個搭配的關鍵字叫做 from
,這個關鍵字可以讓我們只匯入模組裡的某些功能,而不是整個模組。以剛才的例子可以這樣改寫:
from utils.greeting import hey
hey()
語法其實還算滿清楚的,就是從 utils.greeting
這個模組裡匯入 hey()
函數,所以在目前的 Scope 裡就會有 hey()
函數可以用。要注意的是這裡我們執行 hey()
函數的時候不能也不需要連模組名稱一起使用,否則會出現錯誤訊息,畢竟我們這裡就只是從某個模組匯入了 hey()
這一個函數而已。
如果要一次要從同一個模組匯入多個東西,可以使用逗號分開:
from utils.greeting import hey, hi, helloworld
其實還有個更快(或說更偷懶?)的方式:
from utils.greeting import *
後面的 *
表示要匯入 utils.greeting
模組裡所有的東西,雖然這樣寫起來比較短,但除非你知道你在做什麼,否則不建議這樣做,因為這樣會不知道匯入哪些東西,而且當我們執行 hey()
函數的時候,會比較不容易到底這個函數是從哪來的。
你可能會認為,使用 from ... import ...
的寫法應該只會匯入指定的程式碼,不會讀取整個模組。事實上 Python 還是得把整個模組讀進來並且執行整個檔案,只是最後 Python 會從模組裡挑選指定的名稱(變數、函數、類別等)到當前的 Scope 裡而已。