好吧,我承認我又標題黨了,不該把Python拉出來和C比,我絕無輕視C語言的意思。我想說的只是,在解決某些問題是,比起用C,用Python真是太舒服了。
《Beautiful Code》開篇第一章是由大神Brian Kernighan講解另一位半神半程式員Rob Pike設計的C語言“正則”運算式引擎(“正則“二字打引號的原因下面會說明)。不能不說短短几十行代碼是讓人歎為觀止的大師傑作。 具體好在哪裡我也不重複了,如果我都能對這段代碼頭頭是道,那也不勞Kernighan親自出場讚不絕口了。總之,他說:I was amazed by how compact and elegant this code was... 我覺得,既然Kernighan都如此讚賞,論“compat and elegant”,這一定是C語言的最高境界了。估計這也是程式設計的最高境界了吧?一段時間裡,我深信不疑,直到有一天,我看見了這個Regular expression engine in 14 lines of Python。再次驚歎。
首先,整理一下這段代碼,實際上,原帖中的代碼還不夠精簡,因為它用3行實現了一個Python標準庫中已有的函數。取消這三行,改為用import引入庫函數,得到的代碼如下:
from itertools import chain as iconcat
def nil(s): yield s
def seq(l, r):
return lambda s: (sr for sl in l(s) for sr in r(sl))
def alt(l, r):
return lambda s: iconcat(l(s), r(s))
def star(e):
return lambda s: iconcat(nil(s), seq(e, star(e))(s))
def plus(e): return seq(e, star(e))
def char(c):
def match(s):
if s and s[0] == c: yield s[1:]
return match
就代碼量來看,和《Beautiful Code》中的C“正則”引擎相差不大,但此Python實現的正則引擎在功能和簡單性上都有無可比擬的優勢,在分析這個正則引擎如何使用,順帶說明其工作原理後,再來看它的好處。
使用這個正則引擎時首先用char, nil, sq, alt, star, plus這幾個函數構造出一個Regex,用BNF記號表示,格式是這樣的:
exp -> char(c) |
nil |
seq(exp, exp) |
alt(exp, exp) |
star(exp) |
plus(exp)
其語義為:
exp->char(c) 表示匹配以字母c開頭的字串;
exp->nil 表示匹配一個Null 字元串;
exp->seq(exp1, exp2)表示如果exp1匹配s1,exp2匹配s2,則exp匹配由s1和s2串連成的字串;
exp->alt(exp1, exp2)表示字串s必須匹配exp1或exp2中的一個;
exp->star(exp1)匹配Null 字元串,或由一個或多個匹配exp1的子字串串連成的字串;
exp->plus(exp1)匹配由一個或多個匹配exp1的子字串串連成的字串。
總之不論格式還是語義它都和教科書上的Regex完全一致。例如,Regexe=c(a|d)*r用這個正則引擎表到出來就是:
e = seq(char('c'),
seq(plus(alt(char('a'), char('d'))),
char('r'))
現在是使用的問題了。構造出來的Regex的Python類型是函數,此函數接受一個參數,即被匹配的字串。調用此函數得到的結果是一個集合,其中每一個元素為一個字串,對應一個Regex和目標字串的匹配,而這個字串的內容就是目標字串中剩下的無法匹配的部分。也就是說,如果Regex可以和目標字串匹配,則返回的集合不為空白,反之則得到一個空集合,因此,我們可以用:
if e(str):
來判斷Regexe是否和字串str匹配。
要理解這個正則引擎的工作原理,只要抓住前述的一點“調用此函數得到的結果是一個集合,其中每一個元素為一個字串,對應一個Regex和目標字串的匹配,而這個字串的內容就是目標字串中剩下的無法匹配的部分“。實際上nil, char, seq, alt, plus中的每一個都符合此定義。這也是此Python正則引擎設計的好處之一:簡單明了,易於理解,易於實現,易於確保正確。要寫出《Beautiful Code》中的正則引擎是相當難的。我承認,以我的水平,即使Rob Pike把他的設計告訴我讓我寫代碼,我也無法正確的寫出。迴圈、指標還有邊界條件,實在太容易出錯了,這樣的代碼,非出自大師之手不可。而Python版的正則引擎就不一樣,簡單清晰的語義,很好的模組性,即使讓我來寫也可一次寫對。
雖然代碼量相當,但是此Python 在功能上大大強過C版。首先,C版並不是一個真正的正則引擎。它無法表達“匹配exp1或匹配exp2”,這大大的限制它的實用性,而Python版是教科書式的標準正則引擎。其次,C版不能表達(abc)*這樣的運算式,即其Kleene closure中的內容只能是一個字母,而Python版Kleene closure中可以是任何錶達式。這就是為什麼開頭“正則”二字要加引號。
此Python版代碼的可組合性也比C版好。如果要在原有的基礎上添加功能,Python版的使用者只需要添加新的函數,而C版則要修改已有函數,難易程度不可同日耳語。比如注意到Python版沒有“匹配任一字元”的功能,我們只需添加兩行代碼即可實現此功能:
def any(s):
yield s[1:]
不僅實現簡單,而且因為不改動已有代碼,測試也容易。
為什麼在實現一個簡單的正則引擎這個任務上,Python比C版順手許多呢。看看Python版中用到的C所不具備的語言特性:generator function, generator expression, 高階函數,string slicing。string slicing不僅僅是一個簡單的產生子字串的運算式,支撐其易用性是Python的自動記憶體管理。