2022-10-06|閱讀時間 ‧ 約 9 分鐘

這樣寫好嗎?

製作fractal圖案,由匈牙利生物學家Aristid Lindenmayer所開發的L-system是個好用的工具。在研究L-system時,在一個網站(https://understanding-recursion.readthedocs.io/en/latest/15%20L-System.html)中看到了一段程式碼,一段乍看之下覺得挺詭異,懷疑是不是寫錯,但搞清楚之後卻拍案叫絕,冷靜下來後卻覺得這樣寫不怎麼好的程式碼。在研究這段程式碼的過程中,也才發現以前在讀官網的文件時,有點自以為是的忽略掉了一些東西,更激起是不是該再來從頭讀一讀官網文件的念頭。
那段程式碼要做的事情其實很單純,就是要把一串字串中的某些字元,代換成另一些字元。一個簡單的例子是:把一個字串裡頭的a、b、c分別以x、y、z來取代,其他的字元則不變。所以如果原本的字串是a-b+c*d,取代過後就會變成x-y+z*d。根據那個網站的寫法,程式會長這樣:
rule = {'a': 'x', 'b': 'y', 'c': 'z'}
string = 'a-b+c*d'
ans = ''.join([rule.get(char) or char for char in string])
讓人覺得詭異的部分是
rule.get(char) or char
邏輯運算子「or」得到的結果不是True就是False,怎麼可能最後可以得到字串?可是程式實際執行的結果,還真的可以得到正確答案。這究竟是怎麼一回事?
在網路上看了幾篇相關討論的文章,總算搞清楚了,不過要解釋的話,就得話說從頭了。
在進行邏輯判斷時,會有true和false兩種可能的結果。一般會用T或1來表示true;F或0來表示false。而在說明and、or、not、xor等邏輯運算子時,通常都會提到真值表。以or來說,真值表長這樣子:
A  B    A or B
F  F    F
F  T    T
T  F    T
T  T    T
稍微觀察一下就會發現,對於or來說,只要A、B中有任何一個是T,那最後的結果就是T。所以當A是T的時候,就已經可以確定結果是T了,後面的B就不用管它了。Python的官網文件中(Library Reference/Built-in Types/Boolean Operations),特別註明and、or是short-circuit operator,指的就是這種只要做到可以確定結果了,後面的部分就不做了的做法。就好像三戰兩勝制的比賽,比完前兩場,已經有一方贏了兩場,那就不用再比第三場了,因為勝負已定。這可是完全違反孔老夫子「行不由徑」的訓示。不過話又說回來,孔老夫子不會寫程式,不然應該也會同意這樣的做法。
因為and、or是short-circuit operator,所以它們的運算結果就不需要像真值表那樣,鉅細靡遺地列出所有狀況,而可以加以簡化。以or 來說,A or B的結果,可以寫成這樣的一條運算規則
if A is false, then B, else A
以前讀文件看到這條規則的時候並沒有太在意,覺得反正就是換湯不換藥,沒什麼特別的。沒想到,貓膩就藏在其中,一切的一切,都可以在這裡找到答案。不過,要想找出貓膩在哪,還得先瞭解一些東西。
前面提到過,false、true也可以用0、1來表示。有些程式語言比較大器,把除了0之外的整數,都當作true。至於Python呢,就更大開大闔了,把看起來和0會物以類聚的東西,例如None,都當作false,而其他的一切一切,都當作是true。在文件(Library Reference/Built-in Types)中,還特別有個小節「True Value Testing」專門在談這件事,並列出會被當成false的內建物件。
搞清楚Python對false、true的態度之後,再回頭看看那條規則就會發現,A or B所得到的,可能是A或B,而不非得是一直以來認為的true或false而已。所以啊,那條規則其實不是簡化版的真值表,而是在short-circuit操作下,or的廣義化運算規則。
萬事俱備了,可以來看看,為什麼那段讓人覺得詭異的程式,居然可以得到正確的結果。
先來看看rule.get(char)會得到什麼。
要從dictionary中取出某個key對應的value,可以直接指定key。例如rule['a']會得到'x',但如果指定的key不存在,則會傳回KeyError。另一種寫法是使用get()方法。例如rule.get('a')一樣會得到'x',不過,如果key不存在,傳回來的會是None。
這樣就很清楚了。當char不在rule裡頭時,rule.get(char)會得到None,而None會被視為false。根據or的那條廣義化運算規則,當A是false時,A or B會得到B,也就是說rule.get(char) or char會得到char。反之,當char在rule裡頭時,rule.get(char)會得到對應的value,而對於所有不是false的東西,都是true。根據or的廣義化運算規則,當A是true時,A or B會得到A,也就是說 rule.get(char) or char會得到rule中對應於char的字元。
九彎十八拐,繞了好大一圈,總算搞清楚這看起來很有學問的寫法了。不過一開始相遇時的驚艷,卻在激情過後逐漸轉為懷疑:這樣寫好嗎?寫成
''.join([rule[char] if char in rule else char for char in string])
不是更直接了當清楚易懂嗎?難怪網路上有人說用or的這種寫法,不是Pythonic的寫法。
除了讓人覺得不夠清楚易懂外,用or的這種寫法,其實是有點危險的,有可能會造成很難察覺的問題。
有天晚上剛躺上床準備去找老周泡茶聊天時,突然想到,如果rule裡頭某個key的value,是個會被當成是false的物件時,會發生什麼事?還能得到正確結果嗎?和老周泡茶時聊到這話題,他老人家一副莫測高深不置可否的樣子,只說:「夢裡尋他千百度,驀然回首……」,居然打起啞謎來了。其實啊,想也知道他不知道答案,畢竟他的專長領域是睡眠,不是程式。不過,與其在夢裡想他千百遍,不如實際電腦跑一遍。不動手,再大的夢想,都是空想。
實際測試的結果:真的會出問題!如果把rule裡頭'a'這個key的value改成整數0或空字串'',那'a'會被保留,不會被置換。使用or的寫法,雖然讓人覺得挺炫的,但還是少用為妙。
在葉李華的科科網(http://yehleehwa.net/index.htm)中,有篇「樞紐與轉捩點──小兵立大功的銀河帝國系列」(http://yehleehwa.net/empire.htm),提到關於科幻大師艾西莫夫的一則小故事:
一九五○年初,剛過完三十歲生日,他終於出版了生平第一本書──長篇科幻小說《蒼穹一粟》。這件事帶給他極大的鼓勵,為其職業作家生涯埋下重要的伏筆。
根據艾西莫夫自己的說法,這本書出版後,他正式將自己視為作家。因此,在撰寫下一部長篇小說的時候,他刻意捨棄之前的筆法,嘗試將一字一句寫得足夠有文學味。好在剛寫完兩篇樣章,一位編輯(後來成為他的好友)及時給了他當頭棒喝。
那位編輯說:「你可知道,『第二天早上太陽出來了』這句話,海明威會怎麼寫?」
艾西莫夫承認只聽說過海明威,但從未讀過他的小說。於是那位編輯宣佈答案:「第二天早上太陽出來了。」
這段僅僅十秒鐘的對話,給了艾西莫夫絕大的啟示。其後整整四十年,他始終堅守這個原則,盡量將文句寫得通俗易懂,從不刻意賣弄辭藻或文采(搞笑時例外)。不知不覺間,這種風格便成了他的金字招牌。
「第二天早上太陽出來了」,這寫法還真是有The Zen of Python的味道。原來,寫作跟寫程式,骨子裡還真是挺像的啊!
分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.