這題很考驗對 python 內建模組的理解程度,特別是正規表達式
這兩個函數分別導入 re 和 string 兩個內建模組
re 是 python 用來做正規表達式 (Regular Expression) 的模組,可以用來分析字串結構。
string 也可以用來分析字串,包含一些常量和函數,功能相對單純一些。
(注意這個 string 和內建的 str 物件是不同的東西,別搞混了! string 是模組 module,str 是內置類型 built-in type)
--
先講 Dave 的函數,比較簡單
他從 string 裡面 import 了 digits 這個常量,這個常量其實就相當於 "0123456789"
,它記錄了所有的十進制的阿拉伯數字數字。(要特別強調是因為有十進制但不是阿拉伯數字的文字,不懂的可以參考 k318 的解題報告)
所以他的函數其實是先檢查傳入的參數 s
是否為空字串,若不為空就遍歷該字串,逐一檢查每個字元是否都在 string.digits 內。
--
Carol 的函數就比較複雜了,他用了正規表達式,如果沒有學過的話基本上可以直接放棄這題,詳細介紹可以參考 wiki,我就不細說它的用法了,真細講下去沒完沒了。
re.match 這個函數接受三個參數,分別是 pattern, string, flag
re.match 如果有找到符合規則的字串,就會直接返回他找到的第一個字串,若找不到的話會回傳 None;所以這邊才會寫成 is not None。
一個字一個字解釋這個正規表達式 r'^\d+$'
r
是 python 中特別的字串表達方式,意思是原始字符串 row string,主要作用是避免反斜線 \
被當成跳脫字元處理,有興趣可以自己試試看 print(r'\\aaaa')
和 print('\\aaaa')
的區別,這邊要特別弄 row string 是因為在正規表達式中,反斜線 \
是很重要的符號,有特別的意義,不能當成轉譯字符看待,所以在字串最前面添加 r
。接下來被引號包起來的部分就是正規表達式的規則。^
意思是一定得從字串的開頭開始匹配,如果開頭就不符合規則,直接返回 None。\d
是指匹配數字字符,也就是我們熟悉的 0123456789,但其他也能用於表達數字的字符它也能接受,例如全形數字 0123456789、孟加拉數字 ০১২৩৪৫৬৭৮৯、泰米爾語數字 ௦௧௨௩௪௫௬௭௮௯ ......等一大堆稀奇古怪的東西。+
的意思是匹配前面的子運算式一次或多次,在這裡和 \d
組合在一起,變成 \d+
,相當於匹配連續不間斷的數字字符。$
就是匹配到字串的結束位置,注意對 $
來說,字串的結束位置並不等於最後一個字符,如果字串結尾是 \n
就會被它自動忽略(沒加這個就不忽略)。(這也是出bug的地方,對,就這一個字,讓程式出bug)
用人話來說, r'^\d+$'
意思就是「匹配從字串開頭到字串結尾都是數字字符的字串」
Carol 在函數的最後還傳入了一個 flag 參數 re.A
,意思是僅匹配在 ASCII 範圍內的字符,一共 128 個,這會修改 \d
對數字的定義,所以那些全形數字、孟加拉數字、......各種神祕語言的數字就都不包含在內了,因為他們都不在 ASCII 的範圍內。
看起來似乎很完美,如果字串從頭到尾都是數字才會回傳 True,也把條件篩很清楚,只匹配在 ASCII 內的數字。
傳入 "123456" -> 返回 True
傳入 "123abc" -> 返回 False
傳入 "௪௫௬555" -> 返回 False
傳入 "a1b2c3" -> 返回 False
無敵了......吧?
但其實字串並非只能是這些東西,ASCII 的表格裡面有好多奇怪的字符呢!我們也可以傳入
"123\t" -> 返回 False (\t
就是 tab)
"123\r" -> 返回 False (\r
是換行符的一種)
"123\n" -> 返回 True (\n
應該...不用解釋吧,就是我們習慣的換行符)
看起來似乎沒問題?又似乎有點不對勁?
回頭看一下題目的要求
該作業要求學生實作一個函數
isNumber(s)
,其輸入參數s
為一個字串(str
),該函數需要回傳一個布林值(bool
)用以判斷字串s
是否為「數字」?若一個字串至少包含一個字元,且之中的每個字元皆為阿拉伯數字0123456789
其中之一,則我們稱該字串為「數字」。
必須要每個字元都是數字才是正確答案,但在 "123\n"
中顯然並非如此,\n
並不是數字,這個字串並非所有字元都是數字,可為什麼 Carol 的函數會判定它為 True?
問題就出在它的正規表達式中的 $
前面有提到,$
是「匹配到字串的結尾為止」,對 $
來說,字串的結尾就是 \n
,如果遇到 \n
就會忽略它,只判斷在 \n
前面的內容,所以在讀取 "123\n" 時,邏輯是這樣的:
"1"
,是數字沒問題"2"
,是數字,且它前面也是數字,確實是連續的數字"3"
,是數字,且它前面也是數字,確實是連續的數字"\n"
,規則提到只需要檢查到「字串的結尾就可以」,\n
就是結尾,所以忽略掉"123"
,確實是從頭到尾都是數字的字符串,所以 re.match
返回 "123"
於是就這麼戲劇化的輸出錯誤結果了。
--
找到 bug 了,那能修嗎?當然能,沒有問題的。
簡單的刪掉 $
肯定是不行,因為我們要的就是「匹配從字串開頭到字串結尾都是數字字符的字串」,把 $
刪掉的話就是匹配從字串開頭開始連續的字串,會讓像 "123abc"
這種格是的字串也返回 True ,那就不是我們所期望的了。
$
不能用,但同時我們需要另一個能表達「匹配到字串結尾」且「不會忽略換行符 \n
」的東西替代它,那就可以使用 \Z
。
\Z
的用途和 $
差不多,區別在於它不會忽略換行符 \n
,所以我們只需要把原本的規則改成 r'^\d+\Z'
就是正確的函數了。
參考資料:
string | Common string operations | Python 3.13.1 documentation
re | Regular expression operations | Python 3.13.1 documentation
regex101: build, test, and debug regex (這是用來測試正規表達式用的網站)