Python 之旅 ()

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

本期將介紹幾個 Unix/Linux 上常見的延伸應用,諸如 grepfindregular expression 等,這些模組的實作方式,以 Mandrake Linux 7.2 為例,讀者都可以在 /usr/lib/python1.5/ 目錄下找到原始碼。另外,我們也將接觸 Python 物件導向語言裡的類別與個體物件,透過一些簡單的範例,慢慢走入物件導向程式的核心世界。

與字串、檔案名稱有關的模組

grep 模組

Python grep 模組裡,同樣包含 grep()egrep()emgrep()ggrep() 等不同的 grep 物件方法,為了簡便學習,讀者先熟悉 grep() 即可。

>>> import grep Œ

>>> grep.grep("lib", "/etc/passwd") 

16: gopher:x:13:30:gopher:/usr/lib/gopher-data:

20: htdig:x:101:104::/var/lib/htdig:

23: postgres:x:40:235:PostgreSQL Server:/var/lib/pgsql:/bin/bash

>>> grep.grep("bin", "/etc/passwd", "/etc/group") Ž

/etc/passwd: 1: root:x:0:0:root:/root:/bin/bash

/etc/passwd: 2: bin:x:1:1:bin:/bin:

/etc/passwd: 3: daemon:x:2:2:daemon:/sbin:

/etc/passwd: 6: sync:x:5:0:sync:/sbin:/bin/sync

/etc/passwd: 7: shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown

/etc/passwd: 8: halt:x:7:0:halt:/sbin:/sbin/halt

/etc/passwd: 23: postgres:x:40:235:PostgreSQL Server:/var/lib/pgsql:/bin/bash

/etc/group: 2: bin:x:1:root,bin,daemon

/etc/group: 3: daemon:x:2:root,bin,daemon

/etc/group: 4: sys:x:3:root,bin,adm

>>>

Œ 使用時請載入 grep 模組。

 由上述範例,可以得知模組 grep 裡的 grep() 物件方法,其工作方式和 Unix/Linux 上的grep 指令一致,將一個字串當作搜尋值,再接所要進行搜尋的檔案名稱。其傳回值會包括所找到的行內容,並在行內容前標明其所在行數。

Ž 值得注意的是,所接檔案名稱可以超過一個以上,如上例以檔案名稱字串接續輸入,其不同檔案內所搜尋到的內容,會在輸出結果前,標明其檔案名稱以作區別。

glob 模組

如果我們想要使用類似 "*.txt" 這樣的檔案名稱描述方式,可以藉助於 glob 模組的服務,這在之前的檔案系統服務說明部份,已是簡單介紹過,在此我們再溫故知新一番。若忘了先 import glob 模組的話,很可能會回報錯誤訊息,請參考下列範例。

>>> import grep

>>> grep.grep("John", "*.txt")

Traceback (innermost last):

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

File "/usr/lib/python1.5/grep.py", line 12, in grep

return ggrep(RE_SYNTAX_GREP, pat, files)

File "/usr/lib/python1.5/grep.py", line 31, in ggrep

fp = open(filename, 'r')

IOError: [Errno 2] No such file or directory: '*.txt'

>>> import glob

>>> grep.grep("John", glob.glob("*.txt"))

beatles.txt: 1: John Lennon

smiths.txt: 2: Johnny Marr

>>> glob.glob('[0-9].*')

['1.gif', '2.txt']

>>> glob.glob('*.gif')

['1.gif', 'card.gif']

>>> glob.glob('?.gif')

['1.gif']

>>>

不過,要分清楚的是,glob 模組用於檔案名稱的解析,其解析規則通常與 Unix/Linux shell 的檔名解析規則相同,因為 glob 模組的實作方式,使用了 fnmatch 模組。而正規表示式則是使用於字串的解析,通常與檔名解析的規則不完全相同。

find 模組

Python 裡的 find 模組,其所提供的功能並不如 Unix/Linux 環境下的 find 程式一般豐富,而且實作方式有其特別之處。

>>> import find

>>> find.find("*") Œ

['./.dot_file.txt', './1.gif', './2.txt', './beatles.txt', './card.gif', './smiths.txt']

>>> find.find("*.txt")

['./.dot_file.txt', './2.txt', './beatles.txt', './smiths.txt']

>>> find.find("*.txt", "..") 

['../bob_dylan.txt', '../british/.dot_file.txt', '../british/2.txt', '../british/beatles.txt', '../british/smiths.txt']

>>>

Œ find() 物件方法最簡單的範例,便是以 "*" 為輸入參數,它會傳回現行目錄下的所有檔案名稱。值得注意的是,包括以「點符號」(.) 為首的檔案名稱,也會被符合比對。因為其實作方法,是將所有的檔案名稱最前面加上 ./ (代表 os.curdir),然後再進行比對。

 find() 物件方法可以接第二個參數,指定開始搜尋的起始目錄,如例中的 "..",代表上一層目錄。

正規表示式 (regular expression)

如果讀者之前已有 Unix 的使用經驗,很可能已經透過 shell Perl 的學習過程,認識到正規表示式的功能。正規表示式的內容相當龐雜,如果按部就班、從頭學習至尾,將近是一本書的份量,在此並不打算將正規表示式的內容悉數介紹,重點在於讓讀者了解,Python 一般的正規表示式功能,和 Perl 相較並無遜色之處。

正規表示式非常實用,系統管理員、資料庫管理員、與網頁設計員,相信於此感受更深。其主要應用場合包括「搜尋」「取代」「解析」(複雜之) 文字字串,值得說明的是,之前已介紹過的 string 模組,該模組中也包括字串搜尋 (indexfindcount)、取代 (replace)、解析 (split) 等功能,但大抵僅限於基本而簡單的部份,諸如單一字串、明確字串、字母大小區分的場合,讀者必須適當分辨應用場合,以便採用最佳的解決方案。

>>> import string Œ

>>> s = '100 NORTH MAIN ROAD' 

>>> string.replace(s, 'ROAD', 'RD.') 

'100 NORTH MAIN RD.' 

>>> s = '100 NORTH BROAD ROAD' Ž

>>> string.replace(s, 'ROAD', 'RD.') Ž

'100 NORTH BRD. RD.' Ž

>>> s[:-4] + string.replace(s[-4:], 'ROAD', 'RD.') 

'100 NORTH BROAD RD.'

>>> import re 

>>> re.sub('ROAD$', 'RD.', s)

'100 NORTH BROAD RD.'

>>>

Œ 先試試 string 模組的功能,將其 import 來使用。

 string 模組裡的 replace() 物件方法,可以接受三個參數,分別是等待處理的字串 s,以及取代前的字串 'ROAD' 和取代後的字串 'RD.',目的是讓 'ROAD' 以縮寫字 'RD.' 來取代。

Ž 之前的範例很順利地達到需求,但此例可就把事情搞砸了,照本宣科的話,會把原本不該取代的 BROAD 也換掉。

 直覺的變通方法,可以將字串 s 先分成兩段處理,由於 'ROAD' 4 個字母長度,而且是位於字串最後面,利用字串分割,將倒數 4 個字母前的字串保留,而最後 4 個字母的字串進行取代動作。

 上述的變通方法顯然缺乏彈性,比如說,若是遇到 'STREET' 要改成縮寫字 'ST.' 時,字串分割的字母數必須改為 6,很容易會讓人疲於奔命。比較理想的方式,便是利用正規表示式的功能,在此我們載入 re 模組。

re 模組裡有個 sub() 物件方法,其接受三個參數,分別是「想要取代的字串樣版」,在此例中為 'ROAD$',而第二個參數是「取代後的字串」'RD.',最後的參數是字串 s。字串樣版 'ROAD$' 表示 'ROAD' 將出現在待搜尋字串的結尾處,此時 'BROAD' 因為不在字串結尾處,所以不會被取代,符合我們的需求。

Python 裡的正規表示式功能由 re 模組提供,其伴隨幾個有用的模組方法,配合字串樣版來進行搜尋、取代、解析等動作。字串樣版 (pattern) 是指一組包含有一般文字以及特殊字元序列的字串,例如 '?(P<int>\d+)\.(\d*)' 就是一個複雜點的字串樣版,由於字串樣版裡經常會包含有星號 (*) 或反斜線 (\) 等特殊符號,因此慣例上,會配合原始字串 (raw string) 表示法來處理它,這點我們在之前也提示過。

常見的正規表示式功能介紹

下列的特殊字元最簡單且常用,可以先單獨予以記憶,並相互對照其使用意義。

特殊字元

功能說明

.

代表除了換行字元以外的所有字元。

^

代表字串位於開頭。

$

代表字串位於結尾。

*

代表一組出現一次或多次以上的表示模式。

+

代表一組出現零次或多次以上的表示模式。

?

代表一組出現零次或一次的表示模式。

下列再整理另一組字串樣版表示法範例,易學易懂。

字串樣版範例

功能說明

hello|Hello

代表helloHello兩個字串均符合條件。

(h|H)ello

代表helloHello兩個字串均符合條件。

[hH]ello

代表helloHello兩個字串均符合條件。

[0-9]

代表09的數字均符合條件。

[^0-9]

代表09數字之外的字元符合條件。

另外,用於字串樣版中的特殊字元可以在前面加上反斜線,用以代表特殊字元本身,但有些反斜線的特別應用,值得額外注意,將常見者整理如下:

字元

功能說明

\number

代表除了換行字元以外的所有字元。

\d

代表字串位於開頭。

\D

代表字串位於結尾。

\s

代表空格字元,即r'[ \t\n\r\f\v]'

\S

代表非空格字元,即r'[^ \t\n\r\f\v]'

\w

代表英數字,即[0-9a-zA-Z]

\W

代表\w定義以外的所有字元。

example$ cat dirbook.txt

Beatles, Liverpool

Lennon, John Winston: 0800-123456

McCartney, James Paul: 0204-123999

Harrison, George: 0800-999333

Starkey, Richard: 0204-123777

Starr, Ringo: 0204-456777

Smiths, Manchester

Morrissey, Steven Patrick: 0928-987654

Marr, Johnny: 0928-849952

Rourke, Andy: 0938111999

Joyce, Mike: 0936555444

 

上述是一個記錄姓名、電話的檔案,我們藉其內容先來練習 re 模組所附簡單的物件方法,例如搜尋及取代。

>>> import re

>>> s = "Johnny Marr"

>>> re.sub("Marr", "Maher", s)

'Johnny Maher'

>>> t = open("dirbook.txt").read()

>>> pat = re.compile(r'John.*:')

>>> re.findall(pat, t)

['John Winston:', 'Johnny:']>>>

根據上述 dirbook.txt 檔案的內容,我們再繼續來測試正規表示式的其他功能。其內容格式大致為:

Last_Name, First_Name Middle_Name: Phone_Number

觀察 Last_Name 的組成,只有英文字母而無數字,可用 [a-zA-Z] 來表示,而完整的 Last_Name 包括一個以上的英文字母,所以用 [a-zA-Z]+ 來表示。Last_Name 後緊跟著一個逗號 (,)

First_Name 部份原則上如法泡製,但 Middle_Name 算是可有可無,因此可用 [a-zA-Z]+( [a-zA-Z]+)? 來表示。之後再緊跟著一個冒號 (:)

Phone_Number 部份,本範例所呈現者並不算複雜,可分成 4 個數字 (數字可用 \d 表示),緊跟一個可有可無的 - 符號,再接 6 個數字,可以用 \d\d\d\d-?\d\d\d\d\d\d 來表示。

檔案中,除了兩行 "Beatles, Liverpool" "Smiths, Manchester" 之外,都符合上述的表示式分析。

import re

regexp = re.compile(r"[a-zA-Z]+,"

r" [a-zA-Z]+"

r"( [a-zA-Z]+)?"

r": \d\d\d\d-?\d\d\d\d\d\d")

file = open("dirbook.txt", 'r')

for line in file.readlines():

if regexp.search(line):

print "found"

file.close()

執行上述的程式內容,可以得到 9 "found" 字串回應,表示從記錄檔的內容中,成功解析出 9 個符合正規表示式的字串。

實務上,透過正規表示式所解析出來的資料,都會希望額外做些加工,再重新以使用者想要的格式來呈現。有個 ?P<name> 的應用技巧,可以幫助我們達到這樣的功能,請仔細觀察下列的範例:

(?P<last>[a-z A-Z]+), (?P<first>[a-zA-Z]+)( (?P<middle>([a-zA-Z]+)))?: (?P<phone> (\d\d\d\d-?\d\d\d\d\d\d)

也就是說,每一組具備意義的正規表示式,我們可以個別賦予其一個「別名」,如 <last><first><middle><phone> 等。值得注意的是,?P<name> 表示法中的問號 (?),與代表「可有可無」的特殊字元 ? 符號,兩者各自獨立運作,並無相關,請不要混淆。

認識了上述的「別名」設定技巧,我們就可以再透過 group() 物件方法來取得符合的字串資料。可參考下面的例子:

example$ cat re_group.py

import re

regexp = re.compile(r"(?P<last>[a-zA-Z]+),"

r" (?P<first>[a-zA-Z]+)"

r"( (?P<middle>([a-zA-Z]+)))?"

r": (?P<phone>\d\d\d\d-?\d\d\d\d\d\d)"

)

file = open("dirbook.txt", 'r')

for line in file.readlines():

result = regexp.search(line)

if result == None:

print "Not found."

else:

last_name = result.group('last')

first_name = result.group('first')

middle_name = result.group('middle')

if middle_name == None:

middle_name = ""

phone_no = result.group('phone')

print 'Name: ' + first_name + ' ' \

+ middle_name + ' ' \

+ last_name + '\n' \

+ 'Phone: ' + phone_no

file.close()

 

執行上述程式,可以得到重新整理過的資料內容。

example$ python re_group.py

Not found.

Name: John Winston Lennon

Phone: 0800-123456

Name: James Paul McCartney

Phone: 0204-123999

Name: George Harrison

Phone: 0800-999333

Name: Richard Starkey

Phone: 0204-123777

Name: Ringo Starr

Phone: 0204-456777

Not found.

Name: Steven Patrick Morrissey

Phone: 0928-987654

Name: Johnny Marr

Phone: 0928-849952

Name: Andy Rourke

Phone: 0938111999

Name: Mike Joyce

Phone: 093655544

 

類別功能 (class)

Java 語言一樣,Python 是一個物件導向式程式語言,其類別功能及類別體系,是物件導向式語言的重要成份。不過,在此我們很難逐步詳述物件導向語言的細節內容,以下的說明或範例,重點不在於概念或基礎名詞的介紹,而是帶出 Python 的語法結構。

如何定義一個類別

Python 使用 class 敘述來定義一個類別,而由呼叫某一類別名稱 (類似函式的呼叫方式) 而建立的新物件,被稱為該類別的一個「個體」(instance),例如:

class MyClass:

body

instance = MyClass()

慣例上,類別在定義時會以大寫字母來表示。而一個 instance 可以內含「資料結構」或「資料記錄」,感覺上與 C 語言的結構語法很像,但實際用法上不必像 C 語言必須事先將結構完成宣告。我們可以直接觀察下列的簡單範例:

class Circle: Œ

pass Œ

myCircle = Circle() 

myCircle.radius = 5 Ž

print 2 * 3.14 * myCircle.radius

 

Œ 定義一個 Circle 類別,其內容「空空如也」。

 建立一個新個體,名為 myCircle

Ž 設定 myCircle radius 值為 5。其設定方式,是在個體物件名稱後加上 . 符號,成為 myCircle.radius 這樣的描述方式。

個體物件的設定值,可由 __init__ 這個起始函式自動完成,通常 __init__ 又被稱為類別的「建構子」(constructor)。每次有新的個體物件被建立,都會找尋 __init__ 的設定內容,自動完成新個體物件的起始設定值。

class Circle: Œ

def __init__(self): Œ

self.radius = 1 Œ

def area(self): 

return self.radius * self.radius * 3.14159 

myCircle = Circle() 

print 2 * 3.14 * myCircle.radius 

myCircle.radius = 5 Ž

print 2 * 3.14 * myCircle.radius Ž

print myCircle.area()

 

Œ 類別 Circle 裡有個 __init__() 函式,設定其 radius 值為 1。注意到 self 這個參數,慣例上它就是 __init__() 函式的第一個參數,當 __init__ 起始設定時,self 就會被設定為新建立的個體上。

 新個體物件建立後,有自己的個體變數 (instance variables),如 radius 即為一例。

Ž 個體變數的值可以重新設定。

 可以為個體物件設定物件方法 (method),其方式就像定義函式一般。

class Circle:

def __init__(self, r=1):

self.radius = r

def area(self):

return self.radius * self.radius * 3.14159

c = Circle(5)

print c.area()

 

上述則是另一個改進版本的 Circle 類別內容設定,增加預設變數 r,並指定其預設值為 1。如此一來,我們便可透過 Circle(5) 這樣的方式來指定新個體的建立。

繼承功能 (inheritance)

繼承功能是物件導向式語言的另一項特性,Python 的繼承功能可由下列範例中觀察了解:

class Square:

def __init__(self, side=1, x=0, y=0):

self.side = side

self.x = x

self.y = y

class Circle:

def __init__(self, rad=1, x=0, y=0):

self.radius = rad

self.x = x

self.y = y

 

上述並列的兩個類別分別是 Square Circle,注意到它們的設定內容有部份雷同,我們可以利用此特性,為它們建立一個「母類別」。

class Shape:

def __init__(self, x, y):

self.x = x

self.y = y

class Square(Shape):

def __init__(self, side=1, x=0, y=0):

Shape.__init__(self, x, y)

self.side = side

class Circle(Shape):

def __init__(self, rad=1, x=0, y=0):

Shape.__init__(self, x, y)

self.radius = rad

 

我們定義了一個母類別 Shape,以 class Square(Shape) class Circle(Shape) 宣告方式,即可完成繼承母類別的第一步驟。值得注意的是,在 Square Circle 繼承 Shape 母類別後,記得要明確地使用 Shape.__init__(self, x, y) 物件方法呼叫,使之生效。

物件、類別的內容很重要,而且也很容易混淆,在此我們先簡單介紹上述基本的 Python 類別、物件功能設定,讀者務必要練習熟悉,奠定好基礎。

類別庫功能 (package)

我們之前已介紹過模組 (module),簡單地看,模組就是一個 Python 程式碼檔案,裡頭包含一些函式與物件功能。而呼叫模組時,就是以程式碼檔案的檔案名稱來命名。一旦上述的運作模式熟悉了,則類別庫 (package) 便不難理解,因為類別庫只是在一個目錄之下,將一堆功能相近而能相互引用的程式碼檔案收集在一起。而呼叫類別庫時,則是以其上層目錄名稱來命名。

類別庫呈現檔案目錄架構

類別庫的概念及設計方式,原則上就是模組的擴充,在大型的程式開發專案中,類別庫能夠整理一組相關的函式、物件、變數,分門別類地安排於目錄之下,讓程式人員容易進行管理。底下即是一個簡略的範例。

: 類別庫範例示意

圖中的 mathproj 即代表最上層目錄,其底下有一個 __init__.py 檔案,以及一個 comp 目錄,而 comp 目錄下,又類推有 __init__.py 檔案、c1.py 檔案、numeric 目錄,在 numeric 目錄下,則有 __init__.py 檔案、n1.py 檔案、n2.py 檔案。

__init__.py 檔案,其功能也就是類似類別內容設定裡的 __init__() 函式,在類別庫的設計方式裡,以檔案型式存在。下列是非常簡略的類別庫範例的程式內容,用以示範其運作概念。

example$ cat mathproj/__init__.py

print "Hello from mathproj init"

__all__ = ['comp']

version = 1.03

example$ cat mathproj/comp/__init__.py

__all__ = ['c1']

print "Hello from mathproj.comp init"

example$ cat mathproj/comp/c1.py

x = 1.00

example$ cat mathproj/comp/numeric/__init__.py

print "Hello from nemeric init"

example$ cat mathproj/comp/numeric/n1.py

from mathproj import version

from mathproj.comp import c1

from n2 import h

def g():

print "version is", version

print h()

example$ cat mathproj/comp/numeric/n2.py

def h():

print "Called function h in module n2"

 

執行來測試上述的類別庫程式內容時,必須確認 mathproj 目錄位於 Python 的搜尋路徑中,最簡單的方式,便是使用者的現行目錄設定在 mathproj 目錄之上,然後再開始一個 Python 的對話環境。

>>> import mathproj

Hello from mathproj init

>>> mathproj.version

1.03

>>> mathproj.comp.numeric.n1 Œ

Traceback (innermost last):

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

AttributeError: comp

>>> import mathproj.comp.numeric.n1

Hello from mathproj.comp init

Hello from nemeric init

>>> mathproj.comp.numeric.n1.g()

version is 1.03

Called function h in module n2

None

 

Œ 上述的錯誤訊息,在於 mathproj 載入後,並不代表也自動將 compnumeric 等子目錄裡的檔案設定都載入,因此使用者必須手動額外載入 mathproj.comp.numeric.n1 才能真正使用到子目錄裡的設定值。

注意到前述 __init__.py 檔案程式碼裡所出現的 __all__ 屬性值,我們可以看到其以一個字串串列為設定值,而該字串串列即子目錄名稱,明確地說,就是用於當 from ... import * 語法出現時,所要載入的目錄項目是哪些。由於不同的作業系統,對於檔案名稱大小寫的處理方式不同,以 Unix/Linux 為例,檔案名稱大小寫是視為不同的,而 MS Windows 的環境,則將檔案名稱大小寫視為相同。如此一來,載入檔案時便可能造成混淆,而 __all__ 屬性值的設定,即是要明確地指定所要載入的目錄項目有哪些。

如果讀者取得 Zope 這套以 Python 所寫成的網站應用程式,安裝之後便可發現其程式開發,大量應用了類別庫功能,有興趣深入了解類別庫設計的讀者,可以多多參考 Zope product 內容。

相關說明

[1] Regular Expression HOWTO 網頁,網址 http://py-howto.sourceforge.net/regex/regex.html

[2] PERL5 Regular Expression Description 網頁,網址http://www.cpan.org/doc/FMTEYEWTK/regexps.html

[3] Regular Expression 中文介紹,網址 http://www.cyut.edu.tw/~ckhung/olbook/gnulinux/regexp.shtml

[4] 完整的 Python Regular Expression 語法,網址http://www.python.org/doc/current/lib/module-re.html

[5] Regular Expression Matching 效能比較網頁,網址http://www.bagley.org/~doug/shootout/bench/regexmatch/

[6] How to think like a computer scientist -- Python Version 網頁,教你如何一窺程式設計的堂奧,網址http://www.ibiblio.org/obp/thinkCSpy/

[7] Python 與其他程式語言之比較,網址http://www.python.org/doc/Comparisons.html

[8] 物件導向式語言之簡介,網址 http://catalog.com/softinfo/objects.html

[9] Python Script 能夠設計哪些遊戲,網址 http://www.digivision.com.tw/DGAdvan/Animation/Ani11/Ani11.htm