我們從約維安計畫:Python 的資料型別一直到約維安計畫:Python 的模組介紹並且討論了學習任何一個程式語言必要的主題:資料型別、資料結構、流程控制以及程式碼組織機制,本篇文章會簡介幾個 Python 使用者常會遭遇到的技巧,透過它們可以撰寫更簡潔且有效率的程式碼,為學習 Python 基礎程式設計能力階段畫下完美的句點,這些技巧包括:Comprehensions、生成器、具有生成器特性的內建函數以及讀取檔案。
Comprehensions
Comprehensions 目前還沒有一致的中文翻譯,常見的中文翻譯有解析式、生成式、建構以及理解,這種情況我比較傾向直接使用原本的英文名詞,Comprehensions 指的是依賴序列來建立資料結構的一種語法,在 Python 2 推出了 List comprehension,亦即透過序列來建立串列(list),在 Python 3 除了同樣有 List comprehension 之外,也新增了 Dictionary comprehension 以及 Set comprehension,亦即透過序列來建立字典(dict)以及集合(set)。
首先我們回顧傳統透過序列來建立串列的方式,在走訪這個序列之前先建立一個長度為零、空的串列,然後在走訪過程中一一將想要存放的元素透過串列的 append()
方法添加。
primes = [2, 3, 5, 7, 11]
squared_primes = []
for p in primes:
squared_primes.append(p**2)
print(squared_primes)
如果是採取 List comprehension 的語法,我們可以透過所謂的 Python 一列程式(Python one-liner)將建立串列與走訪序列兩個步驟合而為一。
primes = [2, 3, 5, 7, 11]
squared_primes = [p**2 for p in primes] # list comprehension
print(squared_primes)
假使在走訪過程中有條件判斷的需求,傳統透過序列來建立串列的方式必須在迴圈的程式區塊下再新增一個 if
敘述的程式區塊,如此一來就會有兩層的縮排、兩個程式區塊。
primes = [2, 3, 5, 7, 11]
squared_odd_primes = []
for p in primes:
if p % 2 == 1:
squared_odd_primes.append(p**2)
print(squared_odd_primes)
若是採取 List comprehension 的寫法,可以進一步將建立串列、走訪序列以及條件判斷三個步驟合而為一。
primes = [2, 3, 5, 7, 11]
squared_odd_primes = [p**2 for p in primes if p % 2 == 1] # list comprehension
print(squared_odd_primes)
除了能容納 if
敘述,更可以延伸為 if-else
敘述,傳統透過序列來建立串列的方式必須在迴圈的程式區塊下再新增一個 if-else
敘述的程式區塊,如此一來就會有兩層的縮排、三個程式區塊。
primes = [2, 3, 5, 7, 11]
is_even_primes = []
for p in primes:
if p % 2 == 0:
is_even_primes.append(True)
else:
is_even_primes.append(False)
print(is_even_primes)
若是採取 List comprehension 的寫法,同樣可以進一步將建立串列、走訪序列以及條件判斷三個步驟合而為一。
primes = [2, 3, 5, 7, 11]
is_even_primes = [True if p % 2 == 0 else False for p in primes] # list comprehension
print(is_even_primes)
兩相對比之下,假設在走訪過程中有 if-else
敘述的設計,改寫為 List comprehension 可以將六列程式碼壓縮為一列,程式碼精簡的成果十分可觀。
同樣的寫法,也可以更改為 Set comprehension 與 Dict comprehension 以一列程式碼透過序列建立集合與字典。
primes = [2, 3, 5, 7, 11]
squared_primes = {p**2 for p in primes} # set comprehension
print(squared_primes)
print(type(squared_primes))
squared_primes = {p: p**2 for p in primes} # dict comprehension
print(squared_primes)
print(type(squared_primes))
到這裡,中級約維安可能會產生一個疑問:為什麼有 List、Set 與 Dictionary comprehensions,獨缺了 Tuple comprehension?於是將 Comprehension 的寫法改為了小括號嘗試看看。
primes = [2, 3, 5, 7, 11]
squared_primes = (p**2 for p in primes)
print(squared_primes)
print(type(squared_primes))
沒有 Tuple comprehension 的原因是這個寫法得到了稱為生成器(Generators)的類別,也是本篇文章會涵蓋的第二個技巧。
生成器
生成器(Generators)是與 Comprehensions 相似的觀念,都是透過序列來生成資料的機制,不同的地方在於 Comprehensions 所產出的資料結構中儲存的是「資料值」,生成器則是儲存「資料值的生成規則」,這樣子的「資料值生成規則」必須要經過實例化才能將資料值儲存到資料結構中,就如同自行定義好類別中的資料與函數之後,必須透過一個物件作為該類別的實例,才可以使用物件的屬性與方法。若是以料理來比喻,Comprehensions 的輸出是最後端上桌的菜餚,而生成器的輸出則是菜餚的食譜。
那麼該如何「實例化」生成器類別為資料結構的物件呢?我們可以指定用 list()
函數或 tuple()
函數等將生成器類別化作串列或 Tuple。
primes = [2, 3, 5, 7, 11]
squared_primes = (p**2 for p in primes)
print(list(squared_primes)) # instantiate as a list
squared_primes = (p**2 for p in primes)
print(tuple(squared_primes)) # instantiate as a tuple
生成器類別相較於 Comprehensions 具有儲存的效率優勢,關於這一點或許很抽象,但我們可以從它僅能夠被實例化一次的特性中觀察出這個優勢,嘗試第二次實例化生成器類別就會發現得到一個空的(長度為零)資料結構,表示「資料值生成規則」已經被刪除。
primes = [2, 3, 5, 7, 11]
squared_primes = (p**2 for p in primes)
print(list(squared_primes)) # instantiate as a list
print(tuple(squared_primes)) # empty tuple
具有生成器特性的內建函數
為什麼除了 Comprehensions 以外我們還要認識生成器這個技巧呢?在沒有空間或者時間的資源限制下,是否有必要多暸解這個不是太具體的觀念來混淆自己呢?其實,如果生成器類別並不是那麼常見,我自己認為這個技巧是可以暫時略過的,但是因為有不少方便且好用的內建函數輸出具備了生成器類別特性,認識生成器就變得不可或缺。具體來說有兩類的內建函數:迭代器函數(Iterator functions)與函數型函數(Functional functions),其中迭代器函數包含 enumerate()
函數以及 zip()
函數,函數型函數包含 map()
函數與 filter()
函數。
迭代器函數:
enumerate()
函數,能夠讓迴圈同時走訪序列的「索引值」與「資料值」,回傳會是enumerate
類別的實例。zip()
函數,能夠讓迴圈同時走訪多個序列的「資料值」,回傳會是zip
類別的實例。
primes = [2, 3, 5, 7, 11]
odds = [1, 3, 5, 7, 9]
enum = enumerate(primes)
zipped = zip(primes, odds)
print(enum)
print(list(enum))
print(list(enum)) # empty list
print(zipped)
print(list(zipped))
print(list(zipped)) # empty list
函數型函數:
map()
函數,逐次把序列中的「資料值」當作參數傳給輸入的函數,回傳會是map
類別的實例。filter()
函數,逐次把序列中的「資料值」當作參數傳給輸入的函數,保留函數回傳值為True
的輸出,回傳會是filter
類別的實例。
def squared(x):
return x**2
def is_odd(x):
return x % 2 == 1
primes = [2, 3, 5, 7, 11]
mapped = map(squared, primes)
filtered = filter(is_odd, primes)
print(mapped)
print(list(mapped))
print(list(mapped)) # empty list
print(filtered)
print(list(filtered))
print(list(filtered)) # empty list
前述不論是 enumerate
類別的實例、zip
類別的實例、map
類別的實例或 filter
類別的實例,都是具有生成器特性的類別,意即都需要實例化才能夠成為有儲存資料值的資料結構。
使用函數型函數的時候,假如作為輸入的函數操作是簡單的邏輯,例如前述的 squared()
函數以及 is_odd()
函數,Python 使用者更習慣是將其寫作為 lambda 函數的結構, lambda 函數也稱為「匿名函數」,不同於使用 def
語法定義函數,可以在搭配函數型函數的情況下「同時」定義與使用。
primes = [2, 3, 5, 7, 11]
print(list(map(lambda x: x**2, primes)))
print(list(filter(lambda x: x % 2 == 1, primes)))
讀取檔案
面對副檔名為 txt、csv 或者 json 的純文字檔案,透過 Python 的 with
語法,程式會自動進行檔案相關資源的建立、清理與回收,在 with
語法架構下使用內建函數 open(file_path, “r“)
建立檔案物件,就可以讓使用者輕鬆地在有資源管理的前提下進行檔案的讀取,而不會有檔案被鎖定或者錯誤的情況。
副檔名為 txt 的純文字檔案,可以用檔案物件的 readlines()
方法將檔案內容一列一列(row-by-row)讀入成為串列中的元素。
with open("the_shawshank_redemption_summaries.txt", "r") as file:
the_shawshank_redemption_summaries = file.readlines()
for e in the_shawshank_redemption_summaries:
print(e)
副檔名為 csv 的純文字檔案,可以用標準模組 csv
的 DictReader
類別將 csv 檔案的每一列讀入成為一個以欄位名稱作為鍵(Keys)、觀測值內容作為值(Values)的字典。
import csv
with open("imdb_top_rated_movies.csv", "r") as file:
csv_dict_reader = csv.DictReader(file)
for row in csv_dict_reader:
print(row)
副檔名為 json 的純文字檔案,可以用標準模組 json
的 load()
函數將 json 檔案讀入成為一個字典或者由字典所組合而成的串列。
import json
with open("imdb_top_rated_movies.json", "r") as file:
list_of_dicts = json.load(file)
for row in list_of_dicts:
print(row)
在認識了 Comprehensions、生成器、具有生成器特性的內建函數以及讀取檔案等技巧之後,約維安計畫:第十九週來到尾聲,希望您也和我一樣期待下一篇文章。
延伸閱讀
對於這篇文章有什麼想法呢?喜歡😻、留言🙋♂️或者分享🙌
計畫學員專區
付費訂閱電子報的會員可以點選 nbgitpuller 連結將本篇文章完整的 Jupyter Notebook 以及範例資料(the_shawshank_redemption_summaries.txt、imdb_top_rated_movies.csv、imdb_top_rated_movies.json)複製到自己的 Jupyter Server:
伺服器選項選擇預設的「Data science environment:學習 Python、R 與 Julia 的資料科學環境」就可以順利運作文章中的範例程式。