Python 列表是資料處理的根本,高效的列表操作是提升程式碼效能的關鍵。切片操作能簡潔地擷取子列表,但需要注意淺複製的效能影響,避免多餘的切片操作。生成器表示式是處理大型資料的利器,它能避免一次性載入所有資料到記憶體,有效降低記憶體消耗。相較於 map 和 filter,列表生成式提供更簡潔的語法,但應避免過於複雜的巢狀結構,以保持程式碼的可讀性。enumerate 函式則提供更 Pythonic 的迴圈迭代方式,能同時取得元素索引和值,避免傳統迴圈的冗長和易錯性。熟練運用這些技巧,能讓你寫出更簡潔、高效與易維護的 Python 程式碼。
Python列表切片:提升資料處理效率的關鍵技巧
在資料科學與軟體開發中,列表(List)是最常用的資料結構之一。Python 的列表切片(List Slicing)功能,提供了一種高效與靈活的方式來存取和操作列表中的元素。玄貓將分享如何充分利用列表切片,讓你的程式碼更簡潔、更易讀,並提升效能。
為什麼玄貓推薦使用 Python 列表切片?
列表切片不僅僅是存取列表元素的工具,更是一種程式設計的思維方式。它能幫助我們:
- 提高程式碼可讀性:清晰的切片操作比迴圈更易於理解。
- 簡化程式碼:用一行程式碼完成原本需要多行迴圈才能實作的功能。
- 提升效能:切片操作在底層經過最佳化,通常比手動迴圈更快。
列表切片的基本操作
Python 的列表切片語法非常簡單:list[start:end:step]。
start:起始索引,預設為 0。end:結束索引(不包含),預設為列表長度。step:步長,預設為 1。
讓玄貓來看一些實際的例子:
a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
print(a[:]) # 複製整個列表
# 輸出: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
print(a[:5]) # 從開始到索引 5 (不包含)
# 輸出: ['a', 'b', 'c', 'd', 'e']
print(a[:-1]) # 從開始到倒數第一個元素 (不包含)
# 輸出: ['a', 'b', 'c', 'd', 'e', 'f', 'g']
print(a[4:]) # 從索引 4 到結束
# 輸出: ['e', 'f', 'g', 'h']
print(a[-3:]) # 從倒數第三個元素到結束
# 輸出: ['f', 'g', 'h']
print(a[2:5]) # 從索引 2 到索引 5 (不包含)
# 輸出: ['c', 'd', 'e']
print(a[2:-1]) # 從索引 2 到倒數第一個元素 (不包含)
# 輸出: ['c', 'd', 'e', 'f', 'g']
print(a[-3:-1])# 從倒數第三個元素到倒數第一個元素 (不包含)
# 輸出: ['f', 'g']
程式碼解密:
a[:]:這會建立列表a的一個完整複製。a[:5]:從列表的起始位置到索引 5 之前的元素。a[:-1]:從列表的起始位置到最後一個元素之前的所有元素。a[4:]:從索引 4 開始到列表末尾的所有元素。a[-3:]:從倒數第三個元素到列表末尾的所有元素。a[2:5]:提取列表中索引 2(包括)到索引 5(不包括)的元素。a[2:-1]:提取列表中索引 2(包括)到倒數第一個元素(不包括)之間的元素。a[-3:-1]:提取列表中倒數第三個元素(包括)到倒數第一個元素(不包括)之間的元素。
處理越界索引
列表切片的一個優點是,它可以優雅地處理越界索引。當 start 或 end 超出列表範圍時,Python 不會丟擲錯誤,而是會自動調整切片範圍。
first_twenty_items = a[:20] # 如果列表長度小於 20,則傳回整個列表
last_twenty_items = a[-20:] # 如果列表長度小於 20,則傳回整個列表
玄貓提醒:
直接使用索引存取列表元素時,如果索引越界,會丟擲 IndexError。但切片操作則更安全,不會丟擲錯誤。
切片與列表的複製
切片操作會建立一個新的列表,原始列表的元素會被複製到新列表中。這意味著修改切片後的列表不會影響原始列表。
b = a[4:]
print('Before: ', b) # 輸出: Before: ['e', 'f', 'g', 'h']
b[1] = 99
print('After: ', b) # 輸出: After: ['e', 99, 'g', 'h']
print('No change:', a) # 輸出: No change: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
程式碼解密:
b = a[4:]:這行程式碼會建立一個新的列表b,其中包含a中從索引 4 開始到末尾的所有元素。b[1] = 99:這行程式碼會修改列表b中索引 1 的元素,將其值更改為 99。print('No change:', a):這行程式碼會印出列表a的內容,確認a的內容沒有因為修改b而發生變化。
使用切片進行指定
切片不僅可以讀取列表的子集,還可以進行指定操作,替換原始列表中的一部分元素。
print('Before ', a) # 輸出: Before ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
a[2:7] = [99, 22, 14]
print('After ', a) # 輸出: After ['a', 'b', 99, 22, 14, 'h']
程式碼解密:
a[2:7] = [99, 22, 14]:這行程式碼會將列表a中索引 2(包括)到索引 7(不包括)之間的元素,替換為列表[99, 22, 14]中的元素。
複製列表的另一種方式
如果省略 start 和 end 索引,切片操作會複製整個列表。
b = a[:]
assert b == a and b is not a # 驗證 b 是 a 的一個新複製
替換列表內容
如果使用切片指定,但不指定 start 和 end 索引,則會用新的內容替換原始列表的所有元素,但保持列表物件不變。
b = a
print('Before', a) # 輸出: Before ['a', 'b', 99, 22, 14, 'h']
a[:] = [101, 102, 103]
assert a is b # 驗證 a 和 b 仍然是同一個列表物件
print('After ', a) # 輸出: After [101, 102, 103]
程式碼解密:
a[:] = [101, 102, 103]:這行程式碼會將列表a中的所有元素替換為列表[101, 102, 103]中的元素,但a仍然指向原來的列表物件。
玄貓對列表切片的建議
- 避免冗餘:不要在
start索引上使用 0,也不要在end索引上使用列表長度。 - 靈活處理邊界:切片能優雅處理越界索引,方便處理序列的邊界情況。
- 理解指定行為:切片指定會替換原始序列中的指定範圍,即使長度不同。
進階切片:Stride 的使用
除了基本的切片操作,Python 還支援使用 stride(步長)來進行更精細的切片。語法如下:list[start:end:stride]。
a = ['red', 'orange', 'yellow', 'green', 'blue', 'purple']
odds = a[::2] # 選擇索引為奇數的元素
evens = a[1::2] # 選擇索引為偶數的元素
print(odds) # 輸出: ['red', 'yellow', 'blue']
print(evens) # 輸出: ['orange', 'green', 'purple']
程式碼解密:
odds = a[::2]:這行程式碼會建立一個新的列表odds,其中包含a中索引為偶數的所有元素(即每隔一個元素選擇一個)。evens = a[1::2]:這行程式碼會建立一個新的列表evens,其中包含a中索引為奇數的所有元素(即從索引 1 開始,每隔一個元素選擇一個)。
Stride 的注意事項
stride 雖然強大,但也容易引起混淆。當 stride 為負數時,切片的方向會反轉。
x = b'mongoose'
y = x[::-1] # 反轉位元組字串
print(y) # 輸出: b'esoognom'
但這種方法在處理 UTF-8 編碼的 Unicode 字串時可能會出錯。
w = '你好'
x = w.encode('utf-8')
y = x[::-1]
try:
z = y.decode('utf-8')
except UnicodeDecodeError as e:
print(e)
# 輸出: 'utf-8' codec can't decode byte 0x9d in position 0: invalid start byte
玄貓建議:
- 盡量避免同時使用
start、end和stride。 - 如果必須使用
stride,優先選擇正數,並省略start和end索引。 - 如果需要同時使用
stride和start或end索引,考慮分兩步進行切片操作,以提高程式碼可讀性。
例如,要從列表 a 中,從索引 2 開始,每隔一個元素選擇一個,直到倒數第二個元素,可以這樣做:
temp = a[2:-1]
result = temp[::2]
print(result) # 輸出: ['c', 'e']
為何我不再過度依賴切片:Python 列表操作的藝術
Python 的列表切片功能非常強大,可以輕鬆提取列表的部分元素。不過,切片的使用也隱藏著一些效能上的考量。玄貓在最佳化資料處理流程時,深刻體會到切片操作的細節對效能有顯著影響。
先看一個簡單的例子:
a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
b = a[::2]
c = b[1:-1]
print(c)
內容解密
這段程式碼首先建立一個包含字母的列表 a。
b = a[::2]:這行程式碼使用切片功能,從列表 a 中每隔一個元素取出一個,建立一個新的列表 b。::2 表示從頭到尾,以步長為 2 進行切片。
c = b[1:-1]:這行程式碼再次使用切片,從列表 b 中取出除了第一個和最後一個元素之外的所有元素,建立列表 c。1:-1 表示從索引 1 開始到倒數第二個元素。
print(c):這行程式碼將列表 c 的內容輸出到控制檯。
這個例子中,先切片再取間隔,會額外建立資料的淺複製。最佳的做法是盡可能在第一次操作時就減少結果切片的大小。如果程式對時間和記憶體要求嚴格,可以考慮使用 itertools 模組的 islice 方法,但它不支援負值的 start、end 或 stride。
重點回顧
- 切片中的
start、end和stride可能會造成混淆。 - 盡量在切片中使用正值的
stride,與不帶start或end索引。 - 盡可能避免使用負值的
stride。 - 避免在單一切片中同時使用
start、end和stride。如果需要全部三個引數,考慮分兩次指定(一次切片,另一次取間隔),或使用itertools模組的islice。
列表生成式優於 map 和 filter?玄貓的經驗分享
Python 提供了簡潔的語法,可以從一個列表衍生出另一個列表,這就是列表生成式。例如,計算列表中每個數字的平方:
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
squares = [x**2 for x in a]
print(squares)
內容解密
這段程式碼使用列表生成式計算列表 a 中每個元素的平方。
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:這行程式碼建立一個包含數字 1 到 10 的列表 a。
squares = [x**2 for x in a]:這行程式碼使用列表生成式。對於列表 a 中的每一個元素 x,它計算 x 的平方(x**2),並將結果放入一個新的列表 squares 中。
print(squares):這行程式碼將列表 squares 的內容輸出到控制檯。
除非是應用單引數函式,否則在簡單的情況下,列表生成式比內建函式 map 更清晰。map 需要建立一個 lambda 函式進行計算,這在視覺上顯得很雜亂。
squares = map(lambda x: x ** 2, a)
與 map 不同,列表生成式可以輕鬆地從輸入列表中篩選專案,從結果中移除相應的輸出。例如,只計算可以被 2 整除的數字的平方:
even_squares = [x**2 for x in a if x % 2 == 0]
print(even_squares)
內容解密
這段程式碼使用帶有條件判斷的列表生成式,計算列表 a 中偶數的平方。
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:這行程式碼建立一個包含數字 1 到 10 的列表 a。
even_squares = [x**2 for x in a if x % 2 == 0]:這行程式碼使用列表生成式。對於列表 a 中的每一個元素 x,它首先檢查 x 是否可以被 2 整除(x % 2 == 0)。如果 x 是偶數,則計算 x 的平方(x**2),並將結果放入一個新的列表 even_squares 中。
print(even_squares):這行程式碼將列表 even_squares 的內容輸出到控制檯。
filter 內建函式可以與 map 一起使用以達到相同的結果,但它更難以閱讀。
alt = map(lambda x: x**2, filter(lambda x: x % 2 == 0, a))
assert even_squares == list(alt)
字典和集合也有它們自己的列表生成式等價物。這些使得在編寫演算法時,可以輕鬆建立衍生資料結構。
chile_ranks = {'ghost': 1, 'habanero': 2, 'cayenne': 3}
rank_dict = {rank: name for name, rank in chile_ranks.items()}
chile_len_set = {len(name) for name in rank_dict.values()}
print(rank_dict)
print(chile_len_set)
內容解密
這段程式碼展示瞭如何使用字典和集合生成式。
chile_ranks = {'ghost': 1, 'habanero': 2, 'cayenne': 3}:這行程式碼建立一個字典 chile_ranks,其中鍵是辣椒的名稱,值是它們的辣度排名。
rank_dict = {rank: name for name, rank in chile_ranks.items()}:這行程式碼使用字典生成式,將 chile_ranks 字典的鍵和值互換,建立一個新的字典 rank_dict,其中鍵是辣度排名,值是辣椒的名稱。
chile_len_set = {len(name) for name in rank_dict.values()}:這行程式碼使用集合生成式,計算 rank_dict 字典中每個辣椒名稱的長度,並將這些長度放入一個新的集合 chile_len_set 中。
print(rank_dict):這行程式碼將字典 rank_dict 的內容輸出到控制檯。
print(chile_len_set):這行程式碼將集合 chile_len_set 的內容輸出到控制檯。
重點回顧
- 列表生成式比
map和filter內建函式更清晰,因為它們不需要額外的lambda運算式。 - 列表生成式允許你輕鬆地跳過輸入列表中的專案,這是
map在沒有filter幫助下不支援的行為。 - 字典和集合也支援生成式運算式。
列表生成式:玄貓建議避免超過兩個運算式
除了基本用法,列表生成式還支援多層迴圈。例如,將一個矩陣(包含其他列表的列表)簡化為一個包含所有儲存格的扁平列表。
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]
print(flat)
內容解密
這段程式碼使用列表生成式將一個二維矩陣扁平化為一維列表。
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]:這行程式碼建立一個二維列表(矩陣)matrix。
flat = [x for row in matrix for x in row]:這行程式碼使用列表生成式。外層迴圈 for row in matrix 遍歷矩陣的每一行,內層迴圈 for x in row 遍歷當前行的每一個元素。對於每一個元素 x,它被放入一個新的列表 flat 中。
print(flat):這行程式碼將列表 flat 的內容輸出到控制檯。
上面的例子簡單、可讀,並且是對多個迴圈的合理使用。另一個對多個迴圈的合理使用是複製輸入列表的兩層深度佈局。例如,將二維矩陣中每個儲存格的值平方。由於額外的 [] 字元,這個運算式比較雜亂,但仍然容易閱讀。
squared = [[x**2 for x in row] for row in matrix]
print(squared)
內容解密
這段程式碼使用列表生成式計算一個二維矩陣中每個元素的平方,並保持矩陣的結構。
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]:這行程式碼建立一個二維列表(矩陣)matrix。
squared = [[x**2 for x in row] for row in matrix]:這行程式碼使用巢狀的列表生成式。外層列表生成式 [... for row in matrix] 遍歷矩陣的每一行,內層列表生成式 [x**2 for x in row] 遍歷當前行的每一個元素 x,計算 x 的平方,並將結果放入一個新的列表。最終,外層列表生成式將每一行計算出的平方數列表組合成一個新的二維列表 squared。
print(squared):這行程式碼將列表 squared 的內容輸出到控制檯。
如果這個運算式包含另一個迴圈,列表生成式會變得非常長,你必須將它分割成多行。
my_lists = [
[[1, 2, 3], [4, 5, 6]],
# ...
]
flat = [x
for sublist1 in my_lists
for sublist2 in sublist1
for x in sublist2]
此時,多行生成式並不比替代方案短多少。以下使用一般的迴圈陳述式產生相同的結果。這個版本的縮排使得迴圈比列表生成式更清晰。
flat = []
for sublist1 in my_lists:
for sublist2 in sublist1:
flat.extend(sublist2)
列表生成式也支援多個 if 條件。同一迴圈層級的多個條件是一個隱式的 and 運算式。例如,篩選一個數字列表,只保留大於 4 的偶數值。以下兩個列表生成式是等價的。
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
b = [x for x in a if x > 4 if x % 2 == 0]
c = [x for x in a if x > 4 and x % 2 == 0]
條件可以在 for 運算式之後的每個迴圈層級指定。例如,篩選一個矩陣,使得只剩下可以被 3 整除的儲存格,與這些儲存格所在的列的總和必須大於或等於 10。用列表生成式表達這個邏輯很簡短,但極難閱讀。
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
filtered = [[x for x in row if x % 3 == 0]
for row in matrix if sum(row) >= 10]
print(filtered)
內容解密
這段程式碼展示瞭如何使用帶有複雜條件判斷的列表生成式來過濾一個二維矩陣。
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]:這行程式碼建立一個二維列表(矩陣)matrix。
filtered = [[x for x in row if x % 3 == 0] for row in matrix if sum(row) >= 10]:這行程式碼使用巢狀的列表生成式和條件判斷。
外層列表生成式 [... for row in matrix if sum(row) >= 10] 遍歷矩陣的每一行,但只考慮那些元素總和(sum(row))大於或等於 10 的行。
內層列表生成式 [x for x in row if x % 3 == 0] 遍歷當前行的每一個元素 x,但只保留那些可以被 3 整除的元素(x % 3 == 0)。
最終,外層列表生成式將每一行過濾後的元素組合成一個新的二維列表 filtered。
print(filtered):這行程式碼將列表 filtered 的內容輸出到控制檯。
雖然這個例子有點複雜,但在實踐中,你會遇到一些情況,這樣的運算式看起來很合適。玄貓強烈建議避免使用超過兩個運算式的列表生成式。最好將狀態重構為使用一般的 for 迴圈,並在每次迴圈迭代時使用 if 條件。使用輔助函式通常會讓邏輯更加清晰。相信玄貓,其他開發者(包括未來的你)會感謝你讓程式碼更易於理解,而不是過度追求簡潔。
玄貓認為,過度複雜的列表生成式會降低程式碼的可讀性和可維護性,長期來看弊大於利。
為何我放棄傳統迴圈:Pythonic 的 enumerate 妙用
在 Python 的世界裡,迴圈是家常便飯。但你是否曾想過,某些傳統的迴圈寫法,其實可以更優雅、更 Pythonic 呢?玄貓今天要分享的就是一個小技巧:使用 enumerate 替代 range。
傳統迴圈的痛點
當我們需要同時取得元素及其索引時,傳統的做法是使用 range 搭配索引:
flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
for i in range(len(flavor_list)):
flavor = flavor_list[i]
print('%d: %s' % (i + 1, flavor))
這段程式碼雖然可以達成目的,但存在幾個問題:
- 冗長: 需要先取得列表長度,再使用索引取值,程式碼較為冗長。
- 可讀性差: 程式碼不易閱讀,難以一眼看出迴圈的目的。
- 容易出錯: 索引操作容易出錯,例如索引超出範圍。
enumerate 的優雅解法
Python 內建的 enumerate 函式,可以將任何可迭代物件轉換為一個生成器,每次產生一個包含索引和值的元組。使用 enumerate,上述程式碼可以簡化為:
flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
for i, flavor in enumerate(flavor_list):
print('%d: %s' % (i + 1, flavor))
這段程式碼更加簡潔、易讀,也更不容易出錯。enumerate 讓迴圈邏輯更清晰,將索引和值的取得整合在一起。
從 1 開始計數
enumerate 預設從 0 開始計數,但你可以透過指定第二個引數來改變起始值:
flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
for i, flavor in enumerate(flavor_list, 1):
print('%d: %s' % (i, flavor))
在這個例子中,enumerate 從 1 開始計數,更符合我們對排名的直覺。
enumerate 的應用場景
enumerate 不僅適用於列表,也適用於任何可迭代物件,例如字串、元組、集合等。以下是一些 enumerate 的常見應用場景:
- 遍歷列表並顯示索引: 如上述範例,顯示冰淇淋口味的排名。
- 處理檔案內容: 讀取檔案並顯示行號。
- 遍歷字串並顯示字元位置: 找出字串中特定字元的位置。
玄貓的經驗分享
玄貓在開發過程中,經常使用 enumerate 來簡化迴圈邏輯。例如,在處理使用者輸入時,可以使用 enumerate 來顯示錯誤訊息的行號,方便使用者快速定位問題。
生成器表示式:大型資料的救星
列表生成式(List Comprehension)是 Python 中一個非常方便的語法糖,可以讓我們用簡潔的方式產生列表。但當處理大型資料時,列表生成式可能會消耗大量記憶體,導致程式當機。這時候,生成器表示式(Generator Expression)就派上用場了。
列表生成式的隱憂
列表生成式會一次性產生所有元素,並將其儲存在列表中。當資料量很大時,這個列表會佔用大量記憶體。例如,以下程式碼會讀取一個大型檔案,並將每一行的長度儲存在列表中:
value = [len(x) for x in open('/tmp/my_file.txt')]
print(value)
如果 /tmp/my_file.txt 是一個非常大的檔案,這段程式碼可能會導致記憶體不足。
生成器表示式的優勢
生成器表示式不會一次性產生所有元素,而是產生一個迭代器,每次需要時才產生一個元素。這樣可以節省大量記憶體。生成器表示式的語法與列表生成式類別似,只是將方括號 [] 改為圓括號 ():
it = (len(x) for x in open('/tmp/my_file.txt'))
print(it)
這段程式碼會產生一個生成器物件,但不會立即讀取檔案並計算每一行的長度。只有當我們使用 next() 函式來迭代這個生成器時,才會產生下一個元素:
print(next(it))
print(next(it))
生成器表示式的組合
生成器表示式可以像Pipeline一樣組合在一起,將一個生成器的輸出作為另一個生成器的輸入。例如,以下程式碼將計算每一行長度的平方根:
roots = ((x, x**0.5) for x in it)
print(next(roots))
這種鏈式操作非常高效,因為每個生成器只在需要時才產生元素,避免了不必要的計算和記憶體佔用。
玄貓的經驗分享
玄貓在處理大型日誌檔案時,經常使用生成器表示式來逐行分析資料,避免一次性載入整個檔案。這種方法不僅節省記憶體,還可以提高程式的執行效率。
注意事項
生成器表示式產生的迭代器是有狀態的,只能使用一次。如果需要多次使用,需要重新建立生成器表示式。
列表生成式的進階應用:避免過度複雜
列表生成式是 Python 中一個非常強大的工具,可以讓我們用簡潔的方式產生列表。但過度使用列表生成式,可能會導致程式碼難以閱讀和理解。玄貓建議,適度使用列表生成式,避免過度複雜的表示式。
列表生成式的優點
列表生成式可以將迴圈和條件判斷融合在一起,用一行程式碼產生列表。例如,以下程式碼可以產生一個包含 1 到 10 之間所有偶數的列表:
even_numbers = [x for x in range(1, 11) if x % 2 == 0]
print(even_numbers)
這段程式碼比傳統的迴圈寫法更加簡潔易讀。
列表生成式的陷阱
當列表生成式包含多個迴圈和條件判斷時,程式碼會變得非常複雜,難以閱讀和理解。例如:
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [x for row in matrix for x in row if x % 2 != 0]
print(flattened)
這段程式碼雖然可以將二維矩陣扁平化,並篩選出奇數,但程式碼過於複雜,不易理解。
玄貓的建議
玄貓建議,列表生成式最多隻包含兩個表示式,可以是兩個條件判斷、兩個迴圈,或者一個條件判斷和一個迴圈。如果程式碼超過這個複雜度,應該使用傳統的 if 和 for 陳述式,並將程式碼拆分為多個函式。
例如,上述程式碼可以改寫為:
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
def is_odd(x):
return x % 2 != 0
flattened = []
for row in matrix:
for x in row:
if is_odd(x):
flattened.append(x)
print(flattened)
雖然程式碼變長了,但可讀性大大提高。