Python 技術者們 - 練功!Python 技術者們 - 練功!

要說現在哪一門程式語言的入門書最多?恐怕就是 Python 了吧?

基於動態語言容易上手的特性,加上大數據、人工智慧的推波助瀾,Python 的熱度是一年比一年升溫,相關書籍也是一本接著一本推出。看看天瓏書局的 Python 分類,可謂汗牛充棟,多到讓你懷疑人生。

即使扣除特定領域應用和相對進階的內容,剩下通盤介紹 Python 基礎的入門書,無論本土還是翻譯作,加起的數量依舊可觀,這也成為了初學者入門選書時的一大困擾

少則得,多則惑」,本系列將繼續介紹,我認為值得一讀的 Python 入門佳作。

不求多,但求讓你讀完這 3 本書後,可以對 Python 有足夠的基本理解、避免常見的新手錯誤,更重要的是——能寫出「Pythonic」的程式碼。

系列:Python 書單推薦——Python 入門三部曲

  1. 《精通 Python 第二版》心得:給入門者的 Python 學習藍圖
  2. 最佳 Python 入門書——《Python 技術者們 - 練功!》心得與導讀
  3. 《Python 功力提升的樂趣》給 Python 開發者的 Clean Code 入門(待發表)

本文目錄

  1. 超越《精通 Python》的入門傑作
  2. 本文的寫作視角
  3. 優於《精通 Python》的內容編排
  4. 實用性滿滿的「老手帶路」
  5. 一、Python 中的變數:是水桶還是標籤?
  6. 二、使用可變物件作為引數時要小心
  7. 三、小心區域變數未賦值先使用
  8. 書中的經典案例
  9. 全書總評
  10. 結語:再會了!新手村

超越《精通 Python》的入門傑作

正如我上一篇心得所言:

《精通 Python》可能不是我心中最好的 Python 入門書,但仍不失為一本經典之作。

沒錯,因為我心中最佳的 Python 入門書,就是本書

如果說《精通 Python》是一部佳作,那麼這本《Python 技術者們 - 練功!老手帶路教你精通正宗 Python 程式》則屬毫無疑問的傑作——用詞淺白,卻能直指痛處

除了翻譯的品質明顯比《精通 Python》更好,作者 Naomi Ceder 也大有來頭,曾經是 Python 軟體基金會(Python Software Foundation)的主席。

雖然從銷量與知名度而言,本書和《精通 Python》可能略有差距。不過沒關係,既然都是好書,我們當然全都要!

本文的寫作視角

上一篇《精通 Python》心得,對於書中的具體內容,我往往點到為止,並沒有花太多篇幅加以介紹、著墨,只站在全書視野的高處,俯看整本書

會選擇如此「高視角」切入,第一個原因是:剛開始嘗試程式類閱讀心得寫作,還處於摸索階段,需要更多的實驗與練習。

其次,程式書籍不同於一般通俗讀物,性質上比較接近參考書——存在大量需要記憶的知識細節。如果文章以摘選或轉述書中內容為主,介紹可能會流於「繁瑣」,從而缺乏整體輪廓。對於尚在建立基礎的讀者而言,未必友好。

所以,第一篇以讓讀者先熟悉 Python 基本功為目標,也有利於系列後續的展開,故未提及書中太多具體內容,僅從全書的寫作風格、輪廓與推薦閱讀方式上手。

本文的視角與前提

接下來,本文則要擔當起「第二棒」的角色,寫作方式也會有小幅調整,改為「從高處俯看」與「介紹書中具體內容」兩個方向切入,增加書中教學的比例。

因為已是系列的第二篇,本文會有下列視角與前提:

  1. 前提:預設讀者已經讀完前篇介紹的《精通 Python》一書,或其它 Python 入門書籍。即認為本篇讀者已經大致了解 Python 基礎語法
  2. 視角:從 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
2
3
4
a = [1 ,2 ,3]
c = b = a
c[1] = 5
print(a, b, c)

上面的 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 個重要事實

  1. 引數傳入後,如果函式內的參數重新參照了新物件(根據前述,引數和參數本來是參照同一個物件),由於重新參照,兩者的參照對象不再相同,所以作為引數的外部變數將不再受到影響
  2. 但是,如果傳入的引數是「可變物件(mutable」,且在函式內「修改參數的內容」,就會同時修改到函式外的引數——因為它們參照的是同一個物件
    1. 這是典型的「副作用」,要盡可能避免。
    2. 如果引數是「不可變物件(immutable)」,則不會有此問題。因為不可變物件,特性上就是無法被修改。所有對其進行的操作,都是重新參照,而不是修改原物件。
  3. 第三,是這段話中沒有特別強調,但能夠推論得出的命題——Python 引數的傳入模式為「Call by Object Reference」,有興趣的讀者可參考這篇〈Python 是 Pass By Value, Pass by Reference, 還是 Pass by Sharing?〉,文中有詳細的例示與說明,限於篇幅,這裡就不展開。

實例

用書中實例來說明上述概念:

1
2
3
4
5
6
7
8
9
10
11
def func(num, list1, list2):
num = 3
list1 = [4, 5, 6]
list2[0] = [4, 5, 6]

x = 5
y = [1 ,2 ,3]
z = [1 ,2 ,3]
func(x, y, z)

print(x, y, z, sep='\n')

為了方便辨識,我用sep='\n'print的每一個結果換行,實際輸出如下:

1
2
3
5
[1, 2, 3]
[[4, 5, 6], 2, 3]

x、y、z 變數狀態解說

  1. x在函式外部的值為 5,型別是int,屬於不可變物件。作為引數傳入函式中,因為在函式中「重新參照(重新賦值)」了新物件 3。
    1. 依上述說明,重新參照時,外部的變數不受影響,其值仍為原來的 5。
    2. 無論引數是可變或不可變物件,只要函式內部只是重新參照而非修改內容,外部變數都不受影響。
  2. 同理,y傳入函式後為參數list1,雖然y是「可變物件」,但其在函式內部的行為也只是重新參照(重新賦值)為[4, 5, 6],所以外部變數y也不受影響。
  3. z就不同,list2[0] = [4, 5, 6]並非重新賦值,而是對其內容進行修改。前面已經提過,因為引數與參數所參照的是「同一個物件」,對參數的修改,相當於「對共同參照物件的修改」。結果就是——外部的z跟著一起改變!

若稍微修改一下上面程式碼的內容,把這兩行:

1
2
y = [1 ,2 ,3]
z = [1 ,2 ,3]

換成這一行「z = y = [1 ,2 ,3]」,則輸出的結果會有所不同

1
2
3
5
[[4, 5, 6], 2, 3]
[[4, 5, 6], 2, 3]

為什麼會這樣?參考上一段「Python 中的變數:是水桶還是標籤?」的例子,相信就不難明白。

同樣經典的,還有「不要使用可變物件作為預設參數」議題,可參考書中的頁 9-19,或是下面的相關文章。

相關文章:《Python 功力提升的樂趣》筆記(二)Pythonic、行話、陷阱


三、小心區域變數未賦值先使用

參見本書:頁 9-23。

這也一個要先理解「Python 變數即標籤」才容易切入的問題,所以最開頭才會有所謂的「影響深遠」之說,而這一頁絕對是我重翻本書最多次的一頁!

開發人員對於 Python 區域變數常常是一知半解

要說 Python 簡單,那恐怕只是因為我們知道的還太少。就像我本來也以為 Django 很簡單一樣。😂

即便是使用 Python 一段時間的開發人員,還是很可能對函式內的區域變數只有模糊的概念,因而寫出一些乍看有點摸不著頭緒的內容,比如下面的程式碼:

1
2
3
4
5
6
7
8
def get_total(list1):
"""取得傳入的list中的數值總合,如果為空list,則拋出錯誤"""
total = None
if list1:
total = sum(list1)
else:
raise ValueError("Empty list values!")
return total

這是一個我在 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
2
# 不會發生,只是給你看一下想像中的錯誤訊息
UnboundLocalError: local variable 'total' referenced before assignment

反之,假設else內沒有raise,而是做了一些事,但不包括total的賦值,那進了else後,最後要return時,才會出現上述「未賦值先使用」問題。

很小兒科的問題吧?但很遺憾,對新手 Python 工程師而言,它真的不算罕見。


開發人員沒有明確區分上述可能的不同情況,而一律加上預設值,往往是因為概念不夠清晰,或未能進一步思考使然。

或許你會問:「即使一律都加上預設值,也不會真的造成什麼大問題啊?只是程式碼有點冗餘而已」,這正是我們第三本書所要討論的議題,在此先按下不表。

書中的經典案例

上述開發人員如果看過接下來要介紹的,本書的經典案例,相信就會明白為何total變數不需要預設值。

這個「老手帶路」範例,就像書中所述,是連老手都可能失足的所在。在此我們先來看一段關鍵引言:

只要函式內部(不論位置、前後順序)對該變數有賦值動作,該變數一律視為區域變數

再看看書中的兩個實例,必能對這段引言有更深刻的理解,進而避免錯誤發生。

實例一

1
2
3
4
5
a = 'one'
def func():
print(a)
a = 1
func()

執行上述程式碼,你將得到熟悉的:

1
UnboundLocalError: local variable 'a' referenced before assignment

理由是函式中有a = 1這個賦值行為,且先未使用global宣告,則無論a在函式中的任何地方,都會被視為區域變數

儘管a = 1出現的順序在print(a)之後——以致於可能讓你覺得print(a)好像可以印出「one」字串(其實不然)——仍不影響a早已被視為區域變數的事實。

換句話說,在這個情況下,區域變數a必須先賦值再使用,且這個賦值必須先於任何取值行為,才不會發生錯誤。

本例中,print(a)是典型的取值行為,而賦值卻在之後才發生,自然會出現「未賦值先使用」的錯誤。

實例二

實例一的簡單變形,讓你更清楚其中的概念,程式碼如下:

1
2
3
4
5
6
7
8
a = 'one'
def func(x):
if x:
a = 1
print(a)

func(True)
func([])

上面的True可以任意替換為1、非空字串等等會被視為True的值;[]亦然,可以替換為False0等等。

這個例子的重點是:

  • 當引數是True時,if x:會成立,a = 1會被執行,故print(a)會印出1
  • 而當引數是False時,if x:不成立,a = 1不會被執行——但是!區域變數a仍然會被視為已經存在,只是未賦值而已。

執行結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-20-30399ac74ec7> in <module>
5 print(a)
6 func(True)
----> 7 func([])

<ipython-input-20-30399ac74ec7> in func(x)
3 if x:
4 a = 1
----> 5 print(a)
6 func(True)
7 func([])

UnboundLocalError: local variable 'a' referenced before assignment

不出所料,我們在執行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 功力提升的樂趣》——所要探討的。