最佳 Python 入門書——《Python 技術者們 - 練功!》心得與導讀
文章目錄
Python 技術者們 - 練功!
要說現在哪一門程式語言的入門書最多?恐怕就是 Python 了吧?
基於動態語言容易上手的特性,加上大數據、人工智慧的推波助瀾,Python 的熱度是一年比一年升溫,相關書籍也是一本接著一本推出。看看天瓏書局的 Python 分類,可謂汗牛充棟,多到讓你懷疑人生。
即使扣除特定領域應用和相對進階的內容,剩下通盤介紹 Python 基礎的入門書,無論本土還是翻譯作,加起的數量依舊可觀,這也成為了初學者入門選書時的一大困擾。
「少則得,多則惑」,本系列將繼續介紹,我認為值得一讀的 Python 入門佳作。
不求多,但求讓你讀完這 3 本書後,可以對 Python 有足夠的基本理解、避免常見的新手錯誤,更重要的是——能寫出「Pythonic」的程式碼。
系列:Python 書單推薦——Python 入門三部曲
- 《精通 Python 第二版》心得:給入門者的 Python 學習藍圖
- 最佳 Python 入門書——《Python 技術者們 - 練功!》心得與導讀
- 《Python 功力提升的樂趣》給 Python 開發者的 Clean Code 入門(待發表)
本文目錄
- 超越《精通 Python》的入門傑作
- 本文的寫作視角
- 優於《精通 Python》的內容編排
- 實用性滿滿的「老手帶路」
- 一、Python 中的變數:是水桶還是標籤?
- 二、使用可變物件作為引數時要小心
- 三、小心區域變數未賦值先使用
- 書中的經典案例
- 全書總評
- 結語:再會了!新手村
超越《精通 Python》的入門傑作
正如我上一篇心得所言:
《精通 Python》可能不是我心中最好的 Python 入門書,但仍不失為一本經典之作。
沒錯,因為我心中最佳的 Python 入門書,就是本書!
如果說《精通 Python》是一部佳作,那麼這本《Python 技術者們 - 練功!老手帶路教你精通正宗 Python 程式》則屬毫無疑問的傑作——用詞淺白,卻能直指痛處。
除了翻譯的品質明顯比《精通 Python》更好,作者 Naomi Ceder 也大有來頭,曾經是 Python 軟體基金會(Python Software Foundation)的主席。
雖然從銷量與知名度而言,本書和《精通 Python》可能略有差距。不過沒關係,既然都是好書,我們當然全都要!
本文的寫作視角
上一篇《精通 Python》心得,對於書中的具體內容,我往往點到為止,並沒有花太多篇幅加以介紹、著墨,只站在全書視野的高處,俯看整本書
會選擇如此「高視角」切入,第一個原因是:剛開始嘗試程式類閱讀心得寫作,還處於摸索階段,需要更多的實驗與練習。
其次,程式書籍不同於一般通俗讀物,性質上比較接近參考書——存在大量需要記憶的知識細節。如果文章以摘選或轉述書中內容為主,介紹可能會流於「繁瑣」,從而缺乏整體輪廓。對於尚在建立基礎的讀者而言,未必友好。
所以,第一篇以讓讀者先熟悉 Python 基本功為目標,也有利於系列後續的展開,故未提及書中太多具體內容,僅從全書的寫作風格、輪廓與推薦閱讀方式上手。
本文的視角與前提
接下來,本文則要擔當起「第二棒」的角色,寫作方式也會有小幅調整,改為「從高處俯看」與「介紹書中具體內容」兩個方向切入,增加書中教學的比例。
因為已是系列的第二篇,本文會有下列視角與前提:
- 前提:預設讀者已經讀完前篇介紹的《精通 Python》一書,或其它 Python 入門書籍。即認為本篇讀者已經大致了解 Python 基礎語法。
- 視角:從 Python 開發者的角度,表達我個人對書中內容的看法與心得。
好,有了上面的理解,我們來開始探索本書的迷人之處。
優於《精通 Python》的內容編排
首先,還是要從 high level 講起。上篇我提到《精通 Python》的一個明顯缺點,就是「第二部分所佔比例太高」:
簡單來說,我認為第二部分的篇幅有點太多了,減半可能會更理想。因為每一項都只能蜻蜓點水般介紹,容易讓人感覺零碎與不連貫,同時分散了學習的注意力。
而本書的篇幅分配則較為合理——Python 主體介紹佔全書 2/3,其餘套件應用部分則佔 1/3。兩相比較,我認為《精通 Python》各半的配置,對於「入門書」這個定位而言,難免有「喧賓奪主」之嫌。
本書合宜的內容編排,所帶來的閱讀與學習體驗提升,我認為是明顯的——全書讀起來更加緊湊、環環相扣,幾乎沒什麼多餘的章節。
當然,這只是一種取捨,沒有絕對的優劣,只是我更喜歡本書的做法。
實用性滿滿的「老手帶路」
本書最大的亮點,無疑是適時穿插於章節之中的「老手帶路」專欄。
篇如其名,它總結了一些 Python 學習與實作上,容易誤解或發生錯誤的地方。除了讓你預先對這些易於混淆的概念有個通盤理解,同時也從正反兩面相互比較,讓學習思路更加清晰,堪稱觀念與實用兼具。
甚至,如果你不知道哪些章節是重點,只要翻閱目錄,看看哪幾章的「老手帶路」環節特別多——那一章肯定是重要的。
接下來就舉書中 3 個我特別有感的重要觀念——且三者環環相扣、層層遞進——作為本書的亮點推薦及內容參考。
一、Python 中的變數:是水桶還是標籤?
參見本書:頁 4-9。
這是一個重要且影響無所不在的概念。
把變數和「變數值」的關係,想像成水桶和「水桶中的水」,無疑是符合人類直覺的思維——然而 Python 並非如此。Python 中的變數,就像 Git 的分支一樣,都只是一張「貼紙(標籤)」,而變數的實際內容,則另有參照主體——物件。
換句話說,在 Python 中,我們只是把變數這張貼紙,貼到「物件」上面而已。
比如number = 3
,number 是一個變數,而 3 是一個獨立的整數物件,是這個=
把兩者暫時綁定(參照)在一起了。但 3 依舊不是 number 的一部分,只是 number 變數的「參考(reference)」。
為什麼這個觀念很重要?
因為如果 Python 變數是水桶,則理論上每一個變數的值,應該是「各自獨立,互不影響」的存在,但實際並非如此,這就是水桶和標籤的主要差異。
實例說明
想要驗證變數是標籤而非水桶,最典型的例子,就是當多個變數同時參考(參照)到同一個可變物件(比如 list)時:
1 | a = [1 ,2 ,3] |
上面的 a、b、c,實際上都參考了同一個可變物件[1 ,2 ,3]
(標籤說),而不是各自擁有自己變數值(水桶說)。
因此,print
輸出結果會是:
1 | 1, 5, 3], [1, 5, 3], [1, 5, 3] [ |
上面第 3 行程式碼c[1] = 5
,僅僅是變更了 c 變數的內容,結果卻是 a、b、c 三者都受到了影響,正是因為 a、b、c 的參照,其實都是指向(參照)同一個物件所致。
此外,為何說 Python「變數即標籤」特性影響無所不在?因為只有理解了這個特性,才能進一步理解它所衍生出來的各種 Python 行為特徵。
其中最具代表性的,就屬接下來要登場的「使用可變物件作為引數」。
二、使用可變物件作為引數時要小心
參見本書:頁 9-16。
函式在呼叫時,其引數傳入的是物件的參照,然後參數就會參照到引數所參照的物件。請注意!這時候引數與參數所參照的是同一個同件。
本段引述可謂超級重要!
在理解這段內容之前,我們得先弄清楚,函式中參數和引數之間的差異 :
參數
參數代表程式預期您在呼叫它時傳遞的值。 程式的宣告會定義其參數。
每個參數的名稱都會當做程式中的區域變數 。 使用參數名稱的方式與使用任何其他變數的方式相同。
引數
引數代表您在呼叫程式時傳遞至程式參數的值。 呼叫程式碼會在呼叫程式時提供引數。
上述內容簡單理解就是:函式定義時,預先命名且代表引數傳入函式內部的區域變數,是為參數;而函式呼叫時實際傳入的變數,則為引數。
回到正題,本段開頭所引述書上的短短幾行,其中資訊量可一點也不少,足以讓你舉一反三,推導出下列有關 Python 變數的 3 個重要事實:
- 引數傳入後,如果函式內的參數重新參照了新物件(根據前述,引數和參數本來是參照同一個物件),由於重新參照,兩者的參照對象不再相同,所以作為引數的外部變數將不再受到影響。
- 但是,如果傳入的引數是「可變物件(mutable)」,且在函式內「修改參數的內容」,就會同時修改到函式外的引數——因為它們參照的是同一個物件。
- 這是典型的「副作用」,要盡可能避免。
- 如果引數是「不可變物件(immutable)」,則不會有此問題。因為不可變物件,特性上就是無法被修改。所有對其進行的操作,都是重新參照,而不是修改原物件。
- 第三,是這段話中沒有特別強調,但能夠推論得出的命題——Python 引數的傳入模式為「Call by Object Reference」,有興趣的讀者可參考這篇〈Python 是 Pass By Value, Pass by Reference, 還是 Pass by Sharing?〉,文中有詳細的例示與說明,限於篇幅,這裡就不展開。
實例
用書中實例來說明上述概念:
1 | def func(num, list1, list2): |
為了方便辨識,我用sep='\n'
讓print
的每一個結果換行,實際輸出如下:
1 | 5 |
x、y、z 變數狀態解說
x
在函式外部的值為 5,型別是int
,屬於不可變物件。作為引數傳入函式中,因為在函式中「重新參照(重新賦值)」了新物件 3。- 依上述說明,重新參照時,外部的變數不受影響,其值仍為原來的 5。
- 無論引數是可變或不可變物件,只要函式內部只是重新參照而非修改內容,外部變數都不受影響。
- 同理,
y
傳入函式後為參數list1
,雖然y
是「可變物件」,但其在函式內部的行為也只是重新參照(重新賦值)為[4, 5, 6]
,所以外部變數y
也不受影響。 - 但
z
就不同,list2[0] = [4, 5, 6]
並非重新賦值,而是對其內容進行修改。前面已經提過,因為引數與參數所參照的是「同一個物件」,對參數的修改,相當於「對共同參照物件的修改」。結果就是——外部的z
跟著一起改變!
若稍微修改一下上面程式碼的內容,把這兩行:
1 | y = [1 ,2 ,3] |
換成這一行「z = y = [1 ,2 ,3]
」,則輸出的結果會有所不同:
1 | 5 |
為什麼會這樣?參考上一段「Python 中的變數:是水桶還是標籤?」的例子,相信就不難明白。
同樣經典的,還有「不要使用可變物件作為預設參數」議題,可參考書中的頁 9-19,或是下面的相關文章。
三、小心區域變數未賦值先使用
參見本書:頁 9-23。
這也一個要先理解「Python 變數即標籤」才容易切入的問題,所以最開頭才會有所謂的「影響深遠」之說,而這一頁絕對是我重翻本書最多次的一頁!
開發人員對於 Python 區域變數常常是一知半解
要說 Python 簡單,那恐怕只是因為我們知道的還太少。就像我本來也以為 Django 很簡單一樣。😂
即便是使用 Python 一段時間的開發人員,還是很可能對函式內的區域變數只有模糊的概念,因而寫出一些乍看有點摸不著頭緒的內容,比如下面的程式碼:
1 | def get_total(list1): |
這是一個我在 code review 時很常遇到的問題,而且每隔一段時間就會反覆出現——total = None
這行純屬多餘。
在本例中,並不需要給定total
變數的預設值。換句話說,即使沒有total = None
這一行,函式的行為也不會受到影響。
在此我們先不考慮else
其實可以用衛語句(guard clause)替代,且假設list1
參數的元素內容不會有型別錯誤。以此為前提,我們只關注一個核心問題:
為什麼開發人員會覺得在
if
/else
判斷式之前,應該要先有一行total = None
作為預設值?
未賦值先使用?
當你詢問對方為何要加上這一行,得到的回答通常是:
因為怕產生未賦值先使用的錯誤。
但其實這裡不會。
會產生如此誤解的原因是:else
裡面雖然缺少關於total
的賦值行為,但因為是直接拋出錯誤,進了else
後就不會觸發函式的return
——不會有機會使用到total
。
換句話說,return
只可能發生在if
成立時。而if
內確實有total
的賦值行為,所以根本不會出現開發人員所擔心的未賦值先使用(UnboundLocalError
):
1 | # 不會發生,只是給你看一下想像中的錯誤訊息 |
反之,假設else
內沒有raise
,而是做了一些事,但不包括total
的賦值,那進了else
後,最後要return
時,才會出現上述「未賦值先使用」問題。
很小兒科的問題吧?但很遺憾,對新手 Python 工程師而言,它真的不算罕見。
開發人員沒有明確區分上述可能的不同情況,而一律加上預設值,往往是因為概念不夠清晰,或未能進一步思考使然。
或許你會問:「即使一律都加上預設值,也不會真的造成什麼大問題啊?只是程式碼有點冗餘而已」,這正是我們第三本書所要討論的議題,在此先按下不表。
書中的經典案例
上述開發人員如果看過接下來要介紹的,本書的經典案例,相信就會明白為何total
變數不需要預設值。
這個「老手帶路」範例,就像書中所述,是連老手都可能失足的所在。在此我們先來看一段關鍵引言:
只要函式內部(不論位置、前後順序)對該變數有賦值動作,該變數一律視為區域變數。
再看看書中的兩個實例,必能對這段引言有更深刻的理解,進而避免錯誤發生。
實例一
1 | a = 'one' |
執行上述程式碼,你將得到熟悉的:
1 | UnboundLocalError: local variable 'a' referenced before assignment |
理由是函式中有a = 1
這個賦值行為,且先未使用global
宣告,則無論a
在函式中的任何地方,都會被視為區域變數。
儘管a = 1
出現的順序在print(a)
之後——以致於可能讓你覺得print(a)
好像可以印出「one」字串(其實不然)——仍不影響a
早已被視為區域變數的事實。
換句話說,在這個情況下,區域變數a
必須先賦值再使用,且這個賦值必須先於任何取值行為,才不會發生錯誤。
本例中,print(a)
是典型的取值行為,而賦值卻在之後才發生,自然會出現「未賦值先使用」的錯誤。
實例二
實例一的簡單變形,讓你更清楚其中的概念,程式碼如下:
1 | a = 'one' |
上面的True
可以任意替換為1
、非空字串等等會被視為True
的值;[]
亦然,可以替換為False
或0
等等。
這個例子的重點是:
- 當引數是
True
時,if x:
會成立,a = 1
會被執行,故print(a)
會印出1
; - 而當引數是
False
時,if x:
不成立,a = 1
不會被執行——但是!區域變數a
仍然會被視為已經存在,只是未賦值而已。
執行結果:
1 | UnboundLocalError Traceback (most recent call last) |
不出所料,我們在執行func([])
時,得到了一個UnboundLocalError
。
有了實例一加持,這裡的func([])
為何會出錯,相信不難理解——因為沒有先賦值。
從我舉的例子與書中的範例可以看出,弄清楚 Python 變數的各種特性,如賦值、修改與參照的本質,真是為了學好 Python,所不得不下的功夫。
而本書在這方面,確實頗有助益。
書中的精彩之處還有很多,限於幅篇,無法一一列舉,從書的介紹頁面看來,至少還有以下這些重點:
- Python 全域變數不是真正全域?
- Python 物件沒有真正的私有屬性,資訊都不用封裝隱藏了嗎?
- 鴨子型別是什麼?我實在不懂這跟鴨子到底有什麼關係?
- Pythonic 這個詞好像很厲害,是什麼意思?
- Python 的 list[n:m] 切片為何要有頭無尾?
- Python 的型別與類別是同義詞?
這些議題,就留給讀者們自行探索了。
全書總評
原本書中的內容介紹我是打算寫 5 點的,但沒想到僅僅成文的這 3 點就耗費了超過 2000 字的幅篇,再多寫 2 點則整篇文章會顯得有點超載。
不過,我覺得上述 3 點已稱得上極具代表性,即使止步於此,也不算偷懶了。
本文所舉的內容,畢竟只是書中的一小部分,主要用意在於引發讀者閱讀此書的興趣。若看了文章覺得心動,建議趕緊前往天瓏書局網站為自己入手一本。因為它很有可能會是你這輩子所買過,最值得的一本 Python 參考書。
最佳 Python 入門書
《Python 技術者們 - 練功!》是一部值得一看再看、對學習與實際開發都頗有助益的優秀作品。就像開頭所說言,它就是我心中最佳的 Python 入門書,沒有之一。
全書讀下來,沒什麼多餘的部分,完全無愧於原文書名「The Quick Python Book」,讓你在有限的幅篇內,快速了解 Python 學習與應用上的重點所在。
好了,溢美之詞就到此為止。最後讓我對本書,表達深深的敬意與感謝。
結語:再會了!新手村
我相信,在你讀完這本《Python 技術者們 - 練功!》並將其中的內容——尤其是「老手帶路」部分——融會貫通後,你就再也不屬於 Python 新手村的一份子了。
從此你可以昂首闊步,大方地略過那些面向 Python 新手的「all-round」型入門書——因為你已經不太容易從它們身上,再獲得自己所不知道的 Python 知識。
然而,要成為一個「忠實 Python 信徒」的試煉,卻遠遠還沒有結束。
什麼?你說你不想?🥺,確實,這種事無法輕易強求。但如果「Yes」正是你的答案,那麼寫出符合 Python 風格的 Clean Code——即 Pythonic——則無疑是我們下一階段所要努力的目標。
不得不承認,入門 Python 或許很容易,但要把它寫得簡潔而優雅,卻得經歷非常多的辛苦與反思,堪稱一生的追求。
最好的路,往往只有一條
Python 的高度自由,容易讓人誤以為:好像怎麼寫都可以?
然而事實正好相反,如〈The Zen of Python〉所言:
There should be one– and preferably only one –obvious way to do it.
Python 的自由,很可能會成為你通往 Clean Code 的阻礙。上述這段話的核心精神,也是 Python 使用者容易忽略的部分,以致程式碼過於隨意,不利團隊協作。
如果你是有經驗的 Python 開發者,難免心有戚戚焉,這也是為什麼我們要特別學習,屬於 Python 的 Clean Code 風格。
這個議題,正是下一本書——《Python 功力提升的樂趣》——所要探討的。
相關文章