Python 之旅 ()

/馬兒 (marr@linux.org.tw)

上期內容當中,讀者已經瀏覽過 Python 語言裡的一些基本資料型別元件,我們將接續地看到更多實用而有趣的範例。本期的重點擺在字串、函式、模組、系統資訊的處理上,這些學習經驗仍然是 Python 程式設計的重要基礎。

連續數值的指定: 內建函式 range()

range 是一個便利的內建函式,可以傳回一組整數值的串列。

>>> range(7) Œ

[0, 1, 2, 3, 4, 5, 6]

>>> (mon, tue, wed, thu, fri, sat, sun) = range(7) 

>>> mon 

0

>>> sun 

6

>>> range(-5, 5) Ž

[-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]

>>> range(0, 10, 3) 

[0, 3, 6, 9]

Œ range 的預設值由 0 為起始。

 將一個值組與 range 函式所產生的串列進行對映,所以指定結果是 mon 0,而 sun 6

Ž 可以為 range 函式指定起始值與終止值,如 (-5, 5) 是指 -5 4 所形成的整數值串列。

 先前的例子都省略了間隔值的設定,而使用其預設間隔值 1,在此我們可以加入間隔值的設定,如例中的最後一個參數 3

下列的簡單範例,可以將一組資料進行編號。

>>> weekday = ('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun')

>>> for day in range(len(weekday)):

... print day, weekday[day]

...

0 mon

1 tue

2 wed

3 thu

4 fri

5 sat

6 sun

實務程式寫作上,range函式 for 迴圈經常搭配。還記得上期內容裡的 ascii.py 範例嗎? 我們可以使用 for 迴圈與 range 函式加以改寫,來個溫故知新。

example$ cat ascii.py

#!/usr/bin/python

for i in range(256) :

print chr(i),

if i != 0 and i % 16 == 0 :

print

相較之下,程式碼本身顯得簡化了,但語法型態與 while 不同,讀者應用時大抵挑自己習慣者即可。

資源與效率的抵換

除了 range() 之外,還有個 xrange() 內建函式,不但名稱相近,功能可說完全一致,其傳回值最後是相同的。差別在於 range() 會以串列資料型別來儲存所產生的傳回值,實際在記憶體裡佔有完整空間,而 xrange() 則是傳回一個 XRangeType 的物件,並不會在記憶體裡佔有太多儲存空間,而是等到實際存取資料時才繼續算出其值。所以在大筆資料的實務應用上,使用 range() 會佔用較多記憶體,但效率快些,而 xrange() 則節省記憶體空間,而犧牲了效率。請讀者善用 Python 的設計特性,依場合需要自行選用合適的函式。

>>> range(1000000)

>>> xrange(1000000)

如果你的電腦系統資源豐富,上述兩個例子「看不出差別」,可以繼續嘗試更大的輸入值來測試。

函式 (function) 的使用

函式功能在程式語言裡是必備的,不管是函式或模組的寫作,都要求程式員盡可能做到易讀易懂、功能獨立、可重複使用的程式片段,「避免重造輪子」,這樣的設計理念,在 Python 語言裡便極受重視。相信讀者至少已經熟悉一種其他程式語言的函式功能,我們可以直接觀察下列的範例:

>>> def fact(n):

... """Return the factorial of the given number."""

... r = 1

... while n > 0:

... r = r * n

... n = n - 1

... return r

...

>>> fact.__doc__

'Return the factorial of the given number.'

>>> fact(5)

120

>>>

函式還是一個物件,其指定方式,是以保留字 def ( define 之意) 為首,餘下的函式內容,同樣要按照「Python 的程式碼縮排原則」,否則會產生「IndentationError」的錯誤訊息。慣例上,函式的第二行會是一段「三引號註釋字串」,即 """ 所括夾的文字,稱為「文件字串」(documentation strings),我們可以透過如 fact.__doc__ 的物件方法,再把文件字串內容顯示出來,這樣的實務慣例,通常在大型而正式的 Python 程式開發專案裡顯得有用。

fact() 是一個階乘函式範例,請注意到最後一行的保留字 return,如果少了 return 敘述 [1],則預設會以 None 值傳回。以範例函式 fact() 來看,n 是函式的參數 (argument),其傳回值 r n 的階乘結果。

保留字、識別字、內建函式名稱

程式語言裡的保留字 (reserved words) 與識別字 (identifiers),是用來識別變數、函式、類別、模組、物件的名稱,我們先前見過如 printiswhilefordefreturn 等,都是 Python 保留字的範例。另外,以 ___ 為開頭的識別字名稱,許多是 Python 的保留識別字,如 __doc____name____builtins__ 等,它們通常有特殊意義,日後我們會看到越來越多這樣的例子。至於內建函式名稱,讀者已經見過的,如 chr()id()len()max()range() 等。值得注意的是,上述識別字相關名稱,都是大小寫有別的 (case sensitive)。對於打算長期與 Python 廝混的朋友而言,應該有必要手邊準備一份保留字相關資訊。

Python 識別字指定規則:

http://www.python.org/doc/2.0/ref/identifiers.html

Python 所有保留字資訊:

http://www.python.org/doc/2.0/ref/keywords.html

>>> def fact(n=10):

... """Return the factorial of the given number, with the defult input value."""

... r = 1

... while n > 0:

... r = r * n

... n = n - 1

... return r

...

>>> fact(5)

120

>>> fact()

3628800

>>>

上述程式片段示範了函式預設輸入值的設定方式,試試以 fact(5) 呼叫範例函式,會得到傳回值 120,而以 fact() 呼叫,則會傳回以 10 為預設輸入值的 3628800

>>> def power(x, y=2):

... r = 1

... while y > 0:

... r = r * x

... y = y - 1

... return r

...

>>> power(2, 4)

16

>>> power(3)

9

>>> power()

Traceback (innermost list):

File "<stdin>", line 1, in ?

TypeError: not enough arguments: expected 1, got 0

>>> power(2, 4, 3)

Traceback (innermost list):

File "<stdin>", line 1, in ?

TypeError: too many arguments: expected 2, got 3

>>>

一個函式可以設定多個輸入值,上例 power() 函式可以接受兩個輸入值,但至少需要 (expect) 一個輸入值,因為參數 y 有個預設值 2,輸入參數時可以省略之。以 power(2, 4) 呼叫時,會成功傳回 16,以 power(3) 呼叫時,會自動以 power(3, 2) 為輸入值,傳回 9。如果以 power() 呼叫,則會產生 TypeError 的錯誤訊息,它會明確告知「參數不足」,你必須至少輸入幾個參數,如果是以 power(2, 4, 3) 來呼叫,則會得到「參數過多」的 TypeError 訊息。

>>> power(y=4, x=2)

16

>>>

另一個有用的應用技巧,稱為「關鍵字派定法」(keyword passing),是以類似 power(x=2, y=4) 方式來呼叫,明確地將參數值配合變數名稱通知函式,甚至也可以用 power(y=4, x=2) 方式來呼叫,如此一來,兩者的呼叫結果是完全相同,意即其參數值順序可以任意改變。

我們接著來看一些任意個輸入值的例子,相信其實用價值極高。

>>> def maximum(*numbers):

... if len(numbers) == 0:

... return(None)

... else:

... max = numbers[0]

... for n in numbers[1:]:

... if n > max:

... max = n

... return max

...

>>> maximum(3, 8, 5)

8

>>> maximum(2, -5, 9, 1, 7)

9

>>>

函式 maximum() 的用意很明顯,我們可以輸入任意個數的數值,而它最後會傳回最大值。例如 maximum(3, 8, 5) 會傳回 8,而 maximum(2, -5, 9, 1, 7) 會傳回 9。值得注意的地方,就是其處理不定參數個數的技巧,參數指定以 *numbers 方式代表,而 numbers 本身是一個值組 ( tuple,而非 list)。函式的演算規則倒簡單,先把第一個數 numbers[0] 設為最大值,再將剩下的數 numbers[1:] 所形成之值組,丟進 for 迴圈,以「暴力法」比大小。

下列的例子,將參數設定的可能狀況大抵做了整合介紹,我們可以一窺函式參數派定的相關細節,值得讀者反覆測試觀察。

>>> def args_func(a, b, c=9, *other1, **other2): Œ

... return [a, b, c, other1, other2.items()] 

...

>>> args_func(1, 2, 3) Ž

[1, 2, 3, (), []]

>>> args_func(b=1, a=2) 

[2, 1, 9, (), []]

>>> args_func(1, 2, 3, 4, 5, 6, 7, 8, 9) 

[1, 2, 3, (4, 5, 6, 7, 8, 9), []]

>>> args_func(1, c=3, b=2, d=4, e=5)

[1, 2, 3, (), [('d', 4), ('e', 5)]]

>>> args_func(d=4, e=5)

Traceback (innermost last):

File "<stdin>", line 1, in ?

TypeError: not enough arguments; expected 2, got 0

Œ 範例函式 args_func() 可以輸入三個 (以上的) 參數,參數名稱分別是 abc,其中 c 有預設值為 9a b 是必要的輸入參數。而 *other1 用以指定 abc 關鍵字之外的參數值 (不定個數)**other2 則是用以指定 abc 關鍵字之外的派定值 (同樣是不定個數)

 函式 args_func() 非常簡潔,直接把所有的輸入參數值以串列資料型別傳回。其中的 other1 屬於值組資料型別,而 other2 則是辭典集 (dictionary) 資料型別。別慌,稍後會為讀者解說辭典集的相關細節。

Ž 給定三個參數值,它們會分別成為函式 args_func() abc 的設定值,此時 other1 other2 都是空空如也。

 使用關鍵字派定法來指定 a b 的參數值,而使用 c 的預設參數值。

 給定了九個參數值,前三個依序成為 abc 的參數值,後六個數值則成為值組 other1 的元素內容。

給定了五個參數值,第一個成為 a 的參數值,b c 以關鍵字派定法來指定,而最後的 d e 則成為不定個數的關鍵字派定值,它們被辭典集 other2 所收留了。

內建資料型別 dictionary 的使用

辭典集是 Python 裡的內建映射物件 (mapping object),也就是由一個物件集合來作為另一個物件集合的鍵值索引。映射物件和之前談過的序數資料相較,在概念上有擴充、補強的涵意,善用映射物件的特性,可以協助我們將看似複雜的問題,以相當直覺的方式解決。兩者具有若干不同之處,例如序數以索引運算作為其取值方式,映射物件的元素成份來源很有彈性,映射物件是不做排序的,而且映射物件是可變物件。

TABLE_BEGIN

: 序數型別與映射型別的比較

序數型別

映射型別

以索引運算作為其取值方式。

keys()values()items() 等內建函式來取值。

以整數作為其索引(index)

以不可變物件作為其鍵(key)

元素具排序效果。

元素儲存時不做特定排序。

不一定都屬於可變物件。

辭典集是可變物件。

TABLE_END

在功能上,辭典集的實作相當於資料結構裡的雜湊表 (hash table) 或是相關陣列 (associative array),所以你會看到「鍵-值」(key-value pairs)的表示法。通常我們會以字串來做為辭典集的 key,因為有意義的字串可以帶來「望文生義」的效果,不過,一定要以不可變物件來做辭典集的 key,而 value 的部份就全無限制了。

>>> x = [] Œ

>>> y = {} Œ

>>> x[0] = 'Beatles' Œ

Traceback (innermost last):

File "<stdin>", line 1, in ?

IndexError: list assignment index out of range

>>> y[0] = 'John Lennon' 

>>> y[1] = 'Paul McCartney' 

>>> y[2] = 'George Harrison' 

>>> y[3] = 'Ringo Starr' 

>>> y[0] + " and Yoko Ono" Ž

'John Lennon and Yoko Ono'

>>> y 

{3: 'Ringo Starr', 2: 'George Harrison', 1: 'Paul McCartney', 0: 'John Lennon'}

>>> y.keys() 

[3, 2, 1, 0]

>>> y.values() 

['Ringo Starr', 'George Harrison', 'Paul McCartney', 'John Lennon']

>>> y.items() 

[(3, 'Ringo Starr'), (2, 'George Harrison'), (1, 'Paul McCartney'), (0, 'John Lennon')]

>>>

Œ 分別起始建立一個空串列 x 與一個空辭典集 y。由於無法指定不存在的索引值給串列,所以 x[0] 的指定動作宣告失敗,Python 回報 IndexError 錯誤訊息。

 對於辭典集而言,並無上述的困擾,修改或增添元素資料均無限制。此例中,我們分別指定了 y[0]y[1]y[2]y[3] 四筆元素資料。

Ž 可以針對元素進行各式運算,例如我們拿字串 y[0] 與另一字串合併顯示。

 注意到辭典集的儲存,並沒有特定的順序方式,如果想要依特定的排序方法處理資料,可以另尋變通方法。

 示範辭典集最常見的內建函式,即 keys()values()items(),它們都是傳回串列物件。

>>> Beatles = {'leader':'John','bass':'Paul','guitar':'George','drum':'Pete'} Œ

>>> Hurricanes = {'drum':'Ringo'} 

>>> Beatles.update(Hurricanes) Ž

>>> Beatles

{'drum': 'Ringo', 'leader': 'John', 'bass': 'Paul', 'guitar': 'George'}

>>> Beatles.get('leader', "Not available") 

'John'

>>> Beatles.get('manager', "Not available") 

'Not available'

>>>

Œ 我們為一個叫做 Beatles 的樂團建立辭典集,指定其鍵值及元素值,共計四項元素。

 另一個叫做 Hurricanes 的樂團,其辭典集元素,只有鼓手的設定資料。

Ž 透過 update() 物件方法,我們更新了 Beatles 鼓手的設定資料。

 get() 物件方法是詢問 Beatles 裡是否有 leader 此一鍵值,若存在則傳回其對應之元素值,否則會傳回後頭的字串資料。

相信讀者至此已對辭典集有了基礎認識,其他辭典集相關的物件方法及運算元,請參考表格內容說明。值得一提的是,Python 裡的辭典集實作得相當有效率,就算和串列型別相較,你也應該會滿意於它的便利與速度,適當的話,可以考慮多加使用。

TABLE_BEGIN

: 辭典集的方法和操作

項目

說明

len(dict)

傳回辭典集 dict 裡的元素個數。

dict[k]

傳回鍵值 k 的元素內容。

dict[k]=v

dict[k] 的內容設定為 v

del dict[k]

dict[k] 元素項目移除。

dict.clear()

將辭典集 dict 所有元素項目全部移除。

dict.copy()

將辭典集 dict 整個複製。

dict.has_key[k]

如果辭典集 dict 含有鍵值 k 的話,則傳回 1,否則傳回 0

dict.items()

以值組 (key, value) 的串列型別傳回辭典集中所有元素。

dict.keys()

傳回辭典集 dict 的所有鍵值。

dict.values()

傳回辭典集 dict 的所有元素值。

dict.update(other)

將辭典集 other 所有物件更新至辭典集 dict 當中。

dict.get(k [, other])

如果 dict[k] 存在則傳回 dict[k],否則傳回 other

TABLE_END

模組 (module) 的使用

簡單地說,模組代表著某個檔案名稱,該檔案名稱必須以 .py 延伸檔名作結,我們可以到 Python 的安裝目錄一窺究竟,以 Linux Mandrake 7.2 為例,其安裝目錄為 /usr/lib/python1.5。目錄裡包含類似 string.pyos.pyfind.py 的檔案,我們可以透過 import string, os, find 之類的程式語法呼叫這些模組內容。也可以直接閱讀這些 .py 檔案的程式碼,相信部份檔案的內容,對你而言已不再全是天書。

別誤會了,模組本身也可以是編譯過的檔案,如 .pyc .pyo 檔案即屬此類。在系統目錄裡存在這類檔案,通常可以達到執行加速的效果。

實際動手撰寫自己的模組之前,我們得先認識內建函式 dir() 的功能,它可以將許多物件內部的資訊顯示出來。

>>> dir() Œ

['__builtins__', '__doc__', '__name__']

>>> dir(__doc__) 

[]

>>> print __doc__ 

None

>>> print __name__ Ž

__main__

>>> type(__builtins__)

<type 'module'>

>>> dir(__builtins__) 

['ArithmeticError', 'AssertionError', 'AttributeError', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'FloatingPointError', 'IOError', 'ImportError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError', 'None', 'NotImplementedError', 'OSError', 'OverflowError', 'RuntimeError', 'StandardError', 'SyntaxError', 'SystemError', 'SystemExit', 'TypeError', 'ValueError', 'ZeroDivisionError', '_', '__debug__', '__doc__', '__import__', '__name__', 'abs', 'apply', 'buffer', 'callable', 'chr', 'cmp', 'coerce', 'compile', 'complex', 'delattr', 'dir', 'divmod', 'eval', 'execfile', 'exit', 'filter', 'float', 'getattr', 'globals', 'hasattr', 'hash', 'hex', 'id', 'input', 'int', 'intern', 'isinstance', 'issubclass', 'len', 'list', 'locals', 'long', 'map', 'max', 'min', 'oct', 'open', 'ord', 'pow', 'quit', 'range', 'raw_input', 'reduce', 'reload', 'repr', 'round', 'setattr', 'slice', 'str', 'tuple', 'type', 'vars', 'xrange']

>>> print __builtins__.__doc__ 

Built-in functions, exceptions, and other objects.

Noteworthy: None is the `nil' object; Ellipsis represents `...' in slices.

>>>

Œ 當我們身處新啟動之 Python 交談環境裡,輸入 dir() 可以顯示 local symbol table的名稱串列,共計三個。

 進一步提供 __doc__ dir() 做為參數,傳回空串列,表示 __doc__ 物件已無相關屬性 (attributes)print __doc__ 顯示其為 None 物件。

Ž __name__ 是一個字串物件,表示目前執行「程式」的名稱,其值為 __main__

 __builtins__ 則是一個模組物件,持續以 dir(__builtins__) 觀察,可以顯示模組 __builtins__ 的全部屬性。

 顯示 __builtins__ __doc__ 屬性內容。

注意到,每個正在執行的主要程式,其程式名稱 ( __name__ 屬性) 會是 __main__,如果是以模組型態被 import 進來,那麼該模組程式便會以原本檔案名稱為 __name__ 的值。請觀察下列程式範例的說明,兩個極其簡化的「土製模組」。

example$ cat other_mod.py

#!/usr/bin/python

print "this is from other_mod."

print __name__

example$ chmod 755 other_mod.py; ./other_mod.py

this is from other_mod.

__main__

example$ cat my_mod.py

#!/usr/bin/python

"""Simple module example for illustrating how __*__ works."""

import other_mod

print "this is from my_mod."

print __name__

example$ chmod 755 my_mod.py; ./my_mod.py

this is from other_mod.

other_mod

this is from my_mod.

__main__

 

import 的模組檔案,其內容會被執行,所以範例 my_mod.py 在執行之後,會先讀進 other_mod.py 的程式片段,接著才是 my_mod.py 的程式片段。請特別留意 __name__ 屬性值的變化,這項控制技巧經常被使用。

模組的搜尋路徑

Python 實際搜尋模組路徑的設定值,可以由 sys 模組裡的 path 變數值取得。

>>> import sys

>>> sys.path

['', '/usr/lib/python1.5/', '/usr/lib/python1.5/plat-linux-i386', '/usr/lib/python1.5/lib-tk', '/usr/lib/python1.5/lib-dynload', '/usr/lib/python1.5/site-packages', '/usr/lib/python1.5/site-packages/PIL']

>>> import marr

Traceback (innermost last):

File "<stdin>", line 1, in ?

ImportError: No module named marr

>>>

上述的搜尋路徑是有順序性的,也就是說,import 所呼叫的模組名稱,依序第一個找到的路徑便會馬上回傳使用。如果找不到 import 的模組名稱,則會傳回 ImportError 的錯誤訊息。以載入 foo 模組為例,其實際尋找模組的過程順序如下:

1. 是否存在名為 foo 的目錄,並且裡頭含有該模組的檔案。

2. 是否存在 foo.sofoomodule.sofoomodule.sl 或是 foomodule.dll

3. 是否存在 foo.pyo

4. 是否存在 foo.pyc

5. 是否存在 foo.py

以一個 .py Python 原始碼檔案而言,經過編譯後,會產生一個名為 .pyc bytecode執行檔,當尋找某個模組名稱時,要是 .py 檔案的日期不比 .pyc 檔案來得新,Python 直譯器會直接將編譯好的 .pyc 檔案載入,若是 .py 檔案的日期比 .pyc 檔案來得新,通常就表示 .py 檔案內容已更新,Python 直譯器會重新編譯之,以產生新的 .pyc 檔案,然後才進入載入動作。而 .pyo 檔案只有在直譯器以 -O 選項啟動之後才會產生,這類檔案裡的資訊通常比 .pyc 檔案來得多,包含有原始程式的行號以及除錯資訊,因此 .pyo 檔案的載入速度會較慢,但程式的執行速度會較快。

.pyc 或是 .pyo 檔案的編譯動作,是在程式裡頭呼叫 import 後才會發生,對 Python語言來說,模組檔案不止是設計概念的切割,它更從強化模組執行效率的角度,鼓勵程式員善用模組檔案功能。如果自製的模組檔案越來越多,其應用自然越顯重要,此時便要認真為自製模組找個適當的存放路徑,比較常見的方式之一,是設定相關的環境變數值,例如變數 PYTHONPATH

TABLE_BEGIN

: Python 相關環境變數設定

變數名稱

說明

PYTHONDEBUG

python -d 啟動模式相同。可產生 Python 的語法解析除錯資訊。

PYTHONHOME

與模組搜尋路徑設定相關的變數。

PYTHONINSPECT

python -i 啟動模式相同。以交談模式來執行 Python 程式。

PYTHONOPTIMIZE

python -O 啟動模式相同。以最佳化模執行 Python 程式。

PYTHONPATH

增加模組搜尋路徑。

PYTHONSTARTUP

交談模式就緒前所執行的程式路徑。

PYTHONUNBUFFERED

python -u 啟動模式相同。記錄未做緩衝的二元標準輸出輸入。

PYTHONVERBOSE

python -v 啟動模式相同。執行過程詳列相關處理資訊。

TABLE_END

名稱空間 (namespace) 及其有效領域 (scope)

回過頭來,我們再次透過 dir() 來觀察模組檔案載入後的物件變化情況。請將現行目錄設定在 my_mod.py 檔案存放的目錄,然後進入 Python 的交談環境。

●圖●

: 名稱空間階層示意

●圖●

example$ python

Python 1.5.2 (#1, Sep 30 2000, 18:08:36) [GCC 2.95.3 19991030 (prerelease)] on linux-i386

Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam

>>> dir()

['__builtins__', '__doc__', '__name__']

>>> import my_mod

this is from other_mod.

other_mod

this is from my_mod file.

my_mod

>>> dir()

['__builtins__', '__doc__', '__name__', 'my_mod']

>>> dir(my_mod)

['__builtins__', '__doc__', '__file__', '__name__']

>>> print my_mod.__doc__

Simple module example for illustrating how __*__ works.

>>> print my_mod.__file__

my_mod.py

>>>

整個運作過程,依序可以分解如下:

1. 每次 Python 啟動時,它會產生一個名為 __main__ 的模組物件,此物件本身當然有其相關資訊,所以我們可以透過 dir() 來取得。

2. 每次呼叫 import 內建函式之後,Python 會載入另一個模組物件,並將其物件相關資訊併進 __main__

3. 由於兩個模組物件的變數名稱,有若干重複之處,如上述例子裡的 __builtins____doc____name__,在取用變數資訊時,必須有辦法加以區別才行。

4. Python 區別變數資訊的方法,如上例所示,是類似 __doc__ my_mod.__doc__ 這樣的不同表示法。

5. 每個模組物件,都會伴隨一份物件資訊,以辭典集型別儲存在系統當中,而這樣的物件資訊內容,包括其變數、函式、物件等,都成為辭典集裡所記錄的鍵值或屬性值。

6. 上述的辭典集物件資訊,我們把它簡稱為「名稱空間」(namespace)

7. 內建函式 dir() 的功能,可以把名稱空間的名稱資訊全部列出。

除了基本的資料型別之外,Python 裡的每一個模組物件都擁有自己的名稱空間。下列是一個範例程式,可以協助列出模組物件的名稱資訊。

example$ cat namespace.py

#!/usr/bin/python

import sys

k = sys.modules.keys()

print "Keys:", k

print "-" * 30

for i in k:

if i == "__main__":

print ">>>", i, "__dict__", sys.modules[i].__dict__

print "-" * 30

print dir()

在實務設計上,Python 程式員會運用模組的方式,將物件做適當的切割,切割好的程式可以被包在一個 Python 類別庫 (package) 裡,以便進一步有效管理大型的軟體專案。有了函式、模組、類別庫等一系列的架構,我們便可更直覺地管理軟體專案的物件體系。著名的 Zope 系統便是這樣的專案管理實例 [2],其整套系統廣泛應用類別庫的技巧,至今仍在積極發展中。

其他有用的函式

為了方便接續的內容,我們先來認識一個實用的模組,稱為 string,顧名思義,可用於協助處理字串物件。

>>> import string

>>> date = "Fri May 18 CST 2001"

>>> piece1 = string.split(date)

>>> piece1

['Fri', 'May', '18', 'CST', '2001']

>>> time = "12:03:27"

>>> piece2 = string.split(time, ':')

>>> piece2

['12', '03', '27']

>>> string.digits

'0123456789'

上述範例,讓我們見識到模組 string 裡有個 split() 的物件方法,可以將一個字串變數值,依空白字元 (預設狀況) 為切割點,分解成數個小字串,形成一個字串串列傳回。如果切割條件不是空白字元時,在 split() 所接參數中予以指定,如 split(time, ':') 就是指定要以 ':' 字元為切割點。最後則是顯示模組 string 有個字串變數 digits,內容設定為 '0123456789'

如果我們想把上述字串串列裡的「數字」,如 '18' '2001',由字串型別轉換成數值型別,可以怎麼做呢? 下列是個方法。

>>> def try_ai(s):

... if s[0] in string.digits:

... return string.atoi(s)

... else:

... return s

...

>>> import string

>>> date = "Fri May 18 CST 2001"

>>> piece = string.split(date)

>>> finish_ai = map(try_ai, piece)

>>> print finish_ai

['Fri', 'May', 18, 'CST', 2001]

>>>

首先,定義一個叫做 try_ai() 的函式,它在讀進字串後,會比對字串的第一個字元,如果第一個字元是屬於阿拉伯數字,那麼就會嘗試將字串轉換成整數,最後傳回其整數型別資料。是的,你會發現它的演算規則有些天真,不過,我們暫時還不需要一個無懈可擊的轉換函式。

接著,我們載入模組 string 之後,利用內建函式 map() 將自製函式 try_ai 與字串串列 piece 連結起來,如此一來,便能如願將 piece 裡的部份字串,轉換成數值型別。顯然 map() 函式在此例中幫上大忙,簡潔地協助我們將自製函式與序列資料做了巧妙結合。

接下來,我們就可以進一步稍微改良原本天真的 try_ai() 函式。

>>> def try_ai(s):

... if ':' in s:

... ts = string.split(s, ':')

... return map(string.atoi,ts)

... if s[0] in string.digits:

... return string.atoi(s)

... else:

... return s

...

>>> import string

>>> date = "Fri May 18 12:03:27 CST 2001"

>>> piece = string.split(date)

>>> finish_ai = map(try_ai, piece)

>>> print finish_ai

['Fri', 'May', 18, [12, 3, 27], 'CST', 2001]

>>>

這個改良過的版本,可以進一步處理像 '12:03:27' 這樣的「數字」,否則原本更天真的版本會傳回 ValueError 的錯誤訊息。

>>> piece = ['Fri', 'May', '18', '12:03:24', 'CST', '2001']

>>> def strp(x, y):

... return x + ' ' + y

...

>>> r = reduce(strp, piece)

>>> r

'Fri May 18 12:03:24 CST 2001'

>>>

上述程式片段,處理效果剛好與之前的程式相反,它會把字串串列重組成一個長字串。重點就是利用了內建函式 reduce(),其運作方式同樣要輸入一個函式名稱及一個序數資料,不過,目的是要把序數資料的元素「合併減少」成一個。reduce() 也可以應用在數值串列上,以下便是這樣的範例。

>>> n = range(1, 11)

>>> def mult(x, y):

... return x * y

...

>>> f = reduce(mult, n)

>>> f

3628800

>>>

說穿了,它還是一個階乘的範例,每呼叫一次 mult() 函式,數值串列的個數會越來越少,最後傳回一個階乘結果,在此例中,即 10! 的答案。

下列是一個簡化版本的閏年判斷程式,我們將介紹另一個函式 filter()

>>> def leap(y):

... if (y%400) == 0:

... return 1

... elif (y%100) == 0:

... return 0

... elif (y%4) == 0:

... return 1

... return 0

...

>>> n = range(1900, 2001)

>>> leap_year = filter(leap, n)

>>> leap_year

[1904, 1908, 1912, 1916, 1920, 1924, 1928, 1932, 1936, 1940, 1944, 1948, 1952, 1956, 1960, 1964, 1968, 1972, 1976, 1980, 1984, 1988, 1992, 1996, 2000]

>>>

函式 filter() 同樣是接一個函式名稱與一個序數資料為參數,重點在於,在自製函式中,你必須把「想留下來的資料」,其函式傳回值設為 1 (代表 true),而把「不想留下來的資料」,其函式傳回值設為 0 (代表 false)。如此一來,filter() 函式在依序處理完序數資料後,還是會傳回一個序數資料,而且應該只留下你想要的資料。

lambda 表示式

lambda 表示式是個實用而重要的工具,其功能及本質還是函式,差別在於 lambda 沒有「明確的函式名稱」,而且通常只用於簡潔的函式敘述。

>>> n = range(1, 11)

>>> f = reduce(lambda x, y: x * y, n)

>>> f

3628800

>>>

又是熟悉的階乘函式,有趣的是,它的表示方式非常簡潔,乍看之下,初學者可能以為是天書了。lambda 表示式的語法如下:

lambda 參數串列: 表示式

例如 x + yx * ys[9] 都是表示式的例子。早期,lambda 表示式的概念是取自 Lisp 語言,使用上有其便利及優勢,不過,對初學者而言,使用 lambda 表示式通常還是得花時間熟悉,若是「畫虎不成反類犬」,搞到程式大亂就得不償失了。

相關說明

[1] 有人把缺少 return 敘述的「函式」稱為 procedure。本質上,由於函式不明確描述 return 的話,會自動以 None (空型別) 傳回,所以視之為函式並無不妥。

[2] Zope 是一套網站 Application Server 系統,完全由 Python 語言所開發出來,程式本身就是一個龐大的物件系統,有興趣的朋友,可到網站 http://www.zope.org/ 觀摩。

[3] 來點不一樣的吧,歡迎訪閱 Instant Python 網頁,網址 http://www.hetland.org/python/instant-python.php

[4] 想讓 Python 執行更有效率嗎? 試試Python Performance Tips 網頁,網址http://musi-cal.mojam.com/~skip/python/fastpython.html

[5] Python for Lisp Programmers 網頁,網址 http://www.norvig.com/python-lisp.html

[6] A Comparison of Python and LISP 網頁,網址http://www.strout.net/python/pythonvslisp.html

[7] A Python/Perl phrasebook 網頁,網址http://starship.python.net/~da/jak/cookbook.html

[8] Major Python changes 網頁,提供Python 改版增修重點說明,網址http://shell.rmi.net/~lutz/errata-python-changes.html

[9] Python Language Mapping 網頁,網址http://www.informatik.hu-berlin.de/~loewis/python/pymap.htm