Python 2 Tutorial 第二堂(3)函式、模組、類別與套件



Python 2 Tutorial 第二堂(2)容器、流程、for 包含式 << 前情

在 Python 中,每個東西都是物件,那麼 Python 是以物件導向作為主要典範嗎?不,Python 之父 Guido van Rossum 曾在《Masterminds of Programming》書中談到:

Python supports procedural programming, to some extent, and OO. These two aren’t so different, and Python’s procedural style is still strongly influenced by objects (since the fundamental data types are all objects). Python supports a tiny bit of functional programming—but it doesn’t resemble any real functional language, and it never will.

基本上,無論採用何種典範,關鍵在於架構程式時應思考的幾個重點,像是…
  • 抽象層的封裝與隔離
  • 物件的狀態
  • 名稱空間(Namespace)
  • 資源的實體組織方式,像是原始碼檔案、套件(Package)等
只有在腦海中清楚地思考過這幾個重點,才能在程式語言中採用適當的機制來加以實現,或者是在程式語言不支援時,想辦法自行實作類似機制,像是在 JavaScript 中,即使沒有名稱空間及套件機制,仍有開發者依各自需求實現各種風格的機制,來解決對應的問題。

幸運地,Python 中對於這幾個思考重點,在實作時提供的機制頗為完整,提供了像是函式(Function)、模組(Module)、類別(Class)與套件等支援。

函式

當發現到兩個程式片段極為類似,只有當中幾個計算用到的數值或變數不同時,例如:
...
max1 = a if a > b else b
...
max2 = x if x > y else y
...

可以使用函式來封裝程式片段,將流程中引用不同數值或變數的部份設計為參數,例如:
def max(a, b):
    return a if a > b else b

函式是一種抽象,對流程的抽象
,在定義了 max 函式之後,客戶端對求最大值的流程,被抽象為 max(x, y) 這樣的函式呼叫,求值流程實作被隱藏了起來。 在上面的 Python 程式碼中定義了一個函式,在 Python 中,函式不單只是定義,也是個值,舉例而言,你可以如下將 max 指向的函式指定給 maximum 變數,透過 maximum 來呼叫:
maximum = max
maximum(10, 20) # 傳回 20

在 Python 中,可以使用 lambda 表示式來定義一個函式,像是...
lambda a, b: a if a < b else b

這樣的函式稱為 λ 函式或是匿名函式(Anonymous function),當然,你可以將函式指定給變數:
min = lambda a, b: a if a < b else b
minimum = min
min(10, 20) # 傳回10
minimum(10, 20) # 傳回10

模組

如果你有一大堆數學相關的函式與常數定義,像是:
def max(a, b):
    return a if a > b else b
def min(a, b):
    return a if a < b else b

def sum(*numbers): # numbers 接受可變長度引數
    total = 0
    for number in numbers:
        total += number
    return total

pi = 3.141592653589793
e = 2.718281828459045

該怎麼組織它們,讓它們有別於其他開發者撰寫的函式與常數定義?避免名稱空間衝突問題?像是其他開發者也在其他地方定義了自己的 max 函式?

在 Python 中,模組是幾個重要抽象層的機制之一,也許是最自然的機制之一
,只要你建立了一個原始碼檔案 modu.py,你就建立了一個模組 modu,原始碼主檔名就是模組名稱。

import modu 陳述句會在相同目錄下尋找 modu.py,如果沒找到,則會試著尋找在 sys.path 中遞迴地尋找 modu.py,如果還是沒有,則會引發 ImportError 例外。

模組提供了名稱空間。模組中的變數、函式與類別,基本上需透過模組的名稱空間來取得。在 Python 中,importimport asfrom import 是陳述句,可以出現在程式中陳述句可出現的任何位置,它們基本上用來在現有範疇(Scope)中匯入、設定名稱空間,舉例來說,如果先前程式範例是撰寫於 xmath.py 檔案中,那麼以下是一些 importimport asfrom import 的使用實例,假設這些程式是撰寫在與 xmath.py 相同目錄的另一個 .py 檔案:
import xmath
print '# import xmath'
print xmath.pi
print xmath.max(10, 5)
print xmath.sum(1, 2, 3, 4, 5)

print '# import xmath as math'
import xmath as math # 為 xmath 模組取別名為 math
print math.e

print '# from xmath import min'
from xmath import min  # 將 min 複製至目前模組,不建議 from modu import *,易造成名稱衝突
print min(10, 5)

結果應該會顯示:

 python-tutorial-the-2nd-class-3-1

類別

對於熟悉物件導向的開發者而言,可能會問:「那類別的應用場合呢?」…嗯…當打算將某些狀態功能黏在一起時…例如你可能原本有這樣的一個 bank.py:
def account(name, number, balance):
    return {'name': name, 'number': number, 'balance': balance}

def deposit(acct, amount):
    if amount <= 0:
         raise ValueError('amount must be positive')
    acct['balance'] += amount 

def withdraw(acct, amount):
    if amount > acct['balance']:
        raise RuntimeError('balance not enough')
    acct['balance'] -= amount

def to_str(acct):
    return 'Account:' + str(acct)

當中是有關於帳戶建立、存款、提款等函式,你會這麼使用:
import bank
acct = bank.account('Justin', '123-4567', 1000)
bank.deposit(acct, 500)
bank.withdraw(acct, 200)
print bank.to_str(acct)
實際上,bank 中的函式操作,都是與傳入的 dict 實例,也就是代表帳戶狀態的物件高度相關,何不將它們組織在一起呢?這樣比較容易使用些,因此你重新使用類別組織了 bank.py 中的函式:
class Account:
    def __init__(self, name, number, balance):
        self.name = name
        self.number = number
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
             raise ValueError('amount must be positive')
        self.balance += amount

    def withdraw(self, amount):
        if amount > self.balance:
            raise RuntimeError('balance not enough')
        self.balance -= amount

    def __str__(self):
        return 'Account({0}, {1}, {2})'.format(
            self.name, self.number, self.balance)

Account 類別中,__init__ 定義了物件的初始流程,取代了原本的 account 函式,注意到每個類別中的函式(或說是方法),首個參數「必定」接受物件本身,慣例上使用 self 名稱,相當於其他程式語言中 this 的概念,你可以說這是 Python 中「Explicit is better than implicit」的哲學,不過其實這還有讓類別動態調有時更為方便的實際意義,這是進階議題,這六個小時的課程中就不談了,有機會看看 Python 的專門書籍,應該都會談到…

在類別中還定義了 __str__,你應該還記得 Python 2 Tutorial 第二堂(1)數值與字串型態 中,談過 __str____repr__ 的差別吧?忘記的話,記得回頭複習一下… 在上面的程式中,實際示範了如何在類別中定義 __str__ 函式。 如此定義之後,客戶端在使用上就容易得多了…
import bank
acct = bank.Account('Justin', '123-4567', 1000)
acct.deposit(500)
acct.withdraw(200)
print acct

是的!容易使用!在討論物件導向時,大家總是愛談可重用性(Reusability),然而要談到重用性的話,函式的重用性還高上許多,在考量物件導向時,易用性(Usability)其實才是它的重點。

套件

假設現在你有一些 .py 檔案,別人同樣也有一堆 .py 檔案,你們的檔案現在得放在同一專案中,那麼檔案名稱衝突是有可能發生的,最好是為你們的 .py 檔案分別開設目錄。使用 Python 時,你可以在開設的目錄中放個 __init__.py 檔案,這樣 Python 就會將這個目錄視為一個套件,而目錄名稱就是套件名稱。

使用 import pack.modu 陳述時,Python 會尋找 pack 目錄,看看裏頭是否有 __init__.py 檔案,然後看看目錄中是否有個 modu.py 檔案。__init__.py 檔案空白也無所謂,實際上當中也可以寫些程式碼,用來執行這個套件中都會需要的初始工作,不過慣例上,除非你有真正不得已的理由,請保持 __init__.py 檔案空白。在找到模組後,實際上會執行其中頂層範疇中的程式碼,之後,模組中的變數、函式、類別等名稱,可以透過 pack.modu 來取得。

練習 5:運用模組、類別與套件來組織程式


在練習用的檔案中,有個 exercises/exercise5/main.py,裏頭草草寫了一堆函式與變數,以及執行結果輸出的程式碼,請利用這邊介紹的模組、類別與套件,來重新組織當中可重用的程式碼,讓它們可以位於 pycon 套件中適當的類別與模組,數學相關的函式,請置於 xmath 模組中,而帳戶相關的類別定義,請置於 bank 模組中 …

最後,你完成的程式在實體架構上,應該會像是以下的圖片示意(如果不知道怎麼完成實作,記得參考練習用檔案中 solutions/exercise5 的成果 ):

python-tutorial-the-2nd-class-3-2


完成這個練習後,第二堂應該時間就差不多到了,休息一下,接下來的第三堂課要來認識 Python 的社群、文件以及更多的 API ...

參考資源