這句簡單的話,我直到三十多歲,才說了出來。
我一直以為,學寫程式很簡單,找個問題,然後分解,然後找尋每個子問題當中,對應的語言特性與 API 來呼叫就好。
直到去年,某個靈光乍現的一刻,我突然意識到,其實我根本不會 Python。即使用它來寫過一些工具,即使用 Jupyter 玩過一些 machine learning 練習,況且我還有良葛格的 Python 技術手冊。但是這個語言的本質,其實我並不了解。
於是,人一旦承認自己的無知,便能開始從 0 到 1 的路。
那要如何掌握一個語言呢?
Language Spec
有些語言的規格書很厚,不容易印出來,像 Java 或 Haskell;有些語言的規格書比較薄,我通常會印出來讀,像 Scheme。但 Python,有語言規格書嗎?
有的,Python Language Reference (以下簡稱 「PyLR」)。請把它轉成電子書放在 Kindle 的第一本,或把它的連結放在 Facebook 的置頂,最後請把它印出來,放在馬桶旁、枕頭旁、還有通勤坐車上班吃飯,垂手可得之處。
為何要讀語言規格書,找一本天瓏排行榜上的書或去 StackoverFlow 不就好了嗎?
這很簡單,當你想了解貝多芬時,你會反而跑去聽布拉姆斯,舒伯特,或者是華格納嗎?不,你會聽貝多芬。又或者你會去看坊間一些音樂科普書,介紹貝多芬生平,介紹他指揮完第九號時的歡聲如雷嗎?你或許會,我也會,但最好的辦法仍然是:去聽貝多芬,聽不同時期、不同指揮下的貝多芬,若一次沒辦法聽太多首,就聽一首。如果有受過樂理的訓練,那麼能讀讀譜會更好。
當我想認識一個語言時,或許會讀 Learn You A XXX for Great Good,也會找找 StackoverFlow 或 GitHub 上的 Awesome XXX。但若要能真的掌握它,一定得回到規格書的正統脈絡下,使用規格書作者所使用的語彙來認識這個語言。
對我而言,有了語言規格書後,從哪裡開始呢?
Python 的 typing
拿到一個語言,我通常會先了解它資料的表示方式。這方式正式點的名稱,就是型別系統。透過型別,我們可以區分語言中的每一件事物是什麼與不是什麼。
而 Python 有哪些型別呢?其實 Python 的型別系統很豐富,從 PyLR 的分類看得出來,至少分為 13 大類:
- None
- NotImplemented
- Elipsis
- numbers
- Sequences
- Sets
- Mappings
- Callable types
- Modules
- Custom classes
- Class instances
- I/O Objects
- Internal types
至於為何會分成這麼多,我的理解是,Python 是一種腳本語言,因此它的設計本來就是出於彈性、靈活、方便,甚至是直觀。因此,許多在物件導向語言裡,透過類別、物件與註解 (annotation) 來達成的概念,在 Python 裡面直接化成語言的一部份,進入了型別系統。
Python 的 operator overwrite
在 Python 裡面,如同 PyLR 第三章開宗明義所說,每一個事物都被當成物件 (object) 來看待。因此,每個事物像是物件一般,有屬於自己的方法,而這其中有著一些共同、且特殊的方法。在這其中有一群很特別,它們提供物件可以在語法上轉為邏輯判斷或數值運算的作用:
方法 | 運算子 |
---|---|
__lt__() | < |
__le__() | <= |
__eq__() | == |
__ne__() | != |
__gt__() | > |
__ge__() | >= |
以及
方法 | 運算子 |
---|---|
__add__() | + |
__sub__() | - |
__mul__() | * |
__matmul__() | @ |
__truediv__() | / |
__floordiv__() | // |
__mod__() | % |
__divmod__() | divmod() |
__pow__() | pow() 或 ** |
__lshift__() | << |
__rshift__() | >> |
__and__() | & |
__xor__() | ^ |
__or__() | | |
了解這些 operator overwrite 的特殊函式,並理解 Python 型別的概念後,大概就不難理解在 Pandas 裡,如何用這種寫法,過濾出年紀大於 20 歲的人:
adult = people_df[people_df.age > 20]
# 或
adult = people_df[people_df['age'] > 20]
結語
雖然 Python 有這麼多型別跟特殊用法,但我仍想問一個問題,這些型別與特殊函式提供了豐富且彈性的語言特性,但能不能更加嚴謹些呢?
例如在 Python 裡,這樣寫是可以的:
def test():
a = 1
print('a: ', a) # a: 1
a = 'a'
print('a: ', a) # a: a
在具有型別推導的語言裡,當你把一個值 (value) 與一個名稱 (identity) 綁定 (binding) 在一起後,這個值的型別 (type) 就會成為這個名稱的型別。而在許多具有這類特性的語言裡頭,當一個名稱被綁定了值與型別後,它便無法再改變,例如 Kotlin:
var a = 1
println("a: " + a) // a: 1
a = "a"
println("a: " + a)
// error: type mismatch: inferred type is String but Int was expected
很遺憾的是,我無法找到可以限制這種寫法的方式,甚至連 Rust 對型別系統這麼嚴謹的語言 (繼承 OCaml 而來的靜態強型別),都允許開發者重覆定義同樣的 identity:
let a = 1;
// ...
let a = 'a'; // 這是可以的
// ...
我只希望在我寫 Python 的歲月裡,可以避開這些奇葩的語言特性,寫出嚴謹易讀的程式。
在發表看法之前...想先打個廣告XD
不好意思,我得先提出幾個認為有異意的地方:
Rust那個並不是真正意義上的重複宣告(重複定義),更像是宣告覆蓋。Rust有很嚴謹的生存域和擁有權檢查,對待變數的行為和其他程式語言不太一樣。那只是後者個生存預剛好覆蓋到前者之後的生存範圍,更正式的寫法會是:
https://pastebin.com/fcmHWNZB
Rust我認為說明的還算明確。
即使是你提的kotlin也有類似作法:
https://pastebin.com/NSbnVZu3
經過編譯是可以執行的:
> kotlinc hello.kt -include-runtime -d hello.jar > java -jar hello.jar 1 a b
編譯時會有警告沒錯,但我很清楚自己在做什麼。或者也可以改用內部函式的方式,應該就沒警告了。
此外在REPL的頂層環境下(top-level),下面行為也被允許(這只是為了方便,不在語言規範裡):
> kotlinc-jvm val a = 1 println(a) val a = "a" println(a)
就算是早期的C也有類似的作法:
int main(void){ int a = 1; printf("%d\n", a); do{ int a = "Hello, World"; printf(a); }while(0); }
後來C也接受了block的作法:
int main(void){ int a = 1; printf("%d\n", a); { int a = "Hello, World"; printf(a); } }
然後關於Python型別的檢查,在Python 3.6後有了相關的語法。也有人做出檢查器,像是mypy,下面程式使用
mypy
執行會出錯(關於numbers.Number
可以稍微參考這篇):# hello-mypy.py from numbers import Number num: Number = 1 print(num) num = r"String" print(num)
執行:
> mypy hello-mypy.py hello-mypy.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "Number") hello-mypy.py:7: error: Incompatible types in assignment (expression has type "str", variable has type "Number") Found 2 errors in 1 file (checked 1 source file)
我喜歡研究程式語言,感覺會和我很合得來XD
歡迎交流
(阿阿阿~好想推坑Common Lisp阿阿阿~)