在 Python 開發中,處理多個列表的迭代是常見的需求。zip 函式提供了一種優雅的平行迭代方式,避免了繁瑣的索引操作。然而,zip 也有一些需要注意的點,例如 Python 2 中的記憶體問題,以及不同長度列表的截斷行為。對於這些情況,itertools 模組中的 izip 和 zip_longest 提供了更安全的替代方案。另一方面,Python 迴圈的 else 區塊提供了一種在迴圈正常結束後執行特定程式碼的機制。雖然看似方便,但它很容易造成程式碼理解上的困擾。例如,在判斷兩數是否互質的場景中,使用 for...else 結構並不直觀。更好的做法是使用輔助函式,並在找到目標條件時提前傳回,或者使用布林變數追蹤迴圈結果,以提升程式碼可讀性。
Python高效迭代技巧:玄貓解密 zip 與 Else 的妙用
在 Python 的世界裡,我們經常需要處理多個相關的列表。列表生成式讓我們能輕鬆地從一個列表轉換出另一個(參見Item 7:“使用列表生成式取代 map 和 filter”)。這些衍生列表中的元素,通常會與原始列表中的元素,透過索引產生關聯。
為何我放棄傳統索引:zip 的平行迭代魅力
假設我們有姓名和對應字母數量的兩個列表,想要找出名字最長的人。傳統的做法是透過索引來遍歷:
names = ['Cecilia', 'Lise', 'Marie']
letters = [len(n) for n in names]
longest_name = None
max_letters = 0
for i in range(len(names)):
count = letters[i]
if count > max_letters:
longest_name = names[i]
max_letters = count
print(longest_name)
這段程式碼的問題在於,它看起來相當雜亂。names 和 letters 的索引讓程式碼難以閱讀。迴圈索引 i 被重複使用來存取陣列。雖然使用 enumerate (參見Item 10:“優先使用 enumerate 取代 range”)可以稍微改善,但仍然不夠理想。
for i, name in enumerate(names):
count = letters[i]
if count > max_letters:
longest_name = name
max_letters = count
為了讓程式碼更清晰,Python 提供了內建函式 zip。zip 接受兩個或多個迭代器,並傳回一個惰性生成器。這個生成器會產生元組,其中包含來自每個迭代器的下一個值。使用 zip,程式碼會變得更簡潔:
for name, count in zip(names, letters):
if count > max_letters:
longest_name = name
max_letters = count
zip 的雙面刃:記憶體與長度陷阱
zip 雖然方便,但也存在兩個潛在問題。
首先,在 Python 2 中,zip 並不是生成器;它會耗盡所有提供的迭代器,並傳回一個包含所有已建立元組的列表。這可能會佔用大量記憶體,導致程式當機。如果需要在 Python 2 中處理非常大的迭代器,建議使用 itertools 模組中的 izip (參見Item 46:“考慮使用生成器取代傳回列表”)。
其次,如果輸入迭代器的長度不同,zip 的行為可能會讓人困惑。例如,假設我們在上面的列表中新增了一個名字,但忘記更新字母計數。在兩個輸入列表上執行 zip 會產生意想不到的結果。
names.append('Rosalind')
for name, count in zip(names, letters):
print(name)
結果會是:
Cecilia
Lise
Marie
‘Rosalind’ 的新專案並未出現。這是 zip 的運作方式:它會持續產生元組,直到其中一個迭代器耗盡為止。當我們確定迭代器的長度相同時(例如由列表生成式建立的衍生列表),這種方法很有效。但在其他情況下,zip 的截斷行為可能會讓人感到意外。如果不確定要壓縮的列表長度是否相等,可以考慮使用 itertools 模組中的 zip_longest 函式(在 Python 2 中也稱為 izip_longest)。
for 與 while 迴圈後的 else:被誤解的語法糖?
Python 的迴圈有一個在其他程式語言中不常見的額外功能:你可以在迴圈的重複內部區塊之後,緊接著放置一個 else 區塊。
for i in range(3):
print('Loop %d' % i)
else:
print('Else block!')
結果如下:
Loop 0
Loop 1
Loop 2
Else block!
令人驚訝的是,else 區塊會在迴圈完成後立即執行。為什麼這個子句被稱為 “else”?為什麼不是 “and”?在 if/else 陳述式中,else 的意思是 “如果之前的區塊沒有發生,則執行此操作”。在 try/except 陳述式中,except 具有相同的定義:“如果嘗試之前的區塊失敗,則執行此操作”。
類別似地,try/except/else 中的 else 也遵循這種模式(參見Item 13:“善用 try/except/else/finally 中每個區塊”),因為它的意思是 “如果之前的區塊沒有失敗,則執行此操作”。try/finally 也很直觀,因為它的意思是 “在嘗試之前的區塊之後,始終執行最終操作”。
鑑於 else、except 和 finally 在 Python 中的所有用法,新的程式設計師可能會認為 for/else 中的 else 部分意味著 “如果迴圈沒有完成,則執行此操作”。但實際上,它的作用恰恰相反。在迴圈中使用 break 陳述式實際上會跳過 else 區塊。
for i in range(3):
print('Loop %d' % i)
if i == 1:
break
else:
print('Else block!')
結果如下:
Loop 0
Loop 1
另一個讓人意外的是,如果迴圈遍歷一個空序列,else 區塊會立即執行。
for x in []:
print('Never runs')
else:
print('For Else block!')
結果如下:
For Else block!
當 while 迴圈最初為 false 時,else 區塊也會執行。
while False:
print('Never runs')
else:
print('While Else block!')
結果如下:
While Else block!
這些行為的理由是,當你使用迴圈來搜尋某些東西時,迴圈後的 else 區塊非常有用。例如,假設你想確定兩個數字是否互質(它們唯一的公約數是 1)。在這裡,我遍歷每個可能的公約數並測試這些數字。在嘗試了每個選項之後,迴圈結束。當數字互質時,else 區塊會執行,因為迴圈沒有遇到 break。
玄貓的程式碼解密:互質判斷的 else 妙用
以下程式碼示範瞭如何使用 for...else 判斷兩數是否互質:
def is_coprime(a, b):
for i in range(2, min(a, b) + 1):
if a % i == 0 and b % i == 0:
print(f"{a} 和 {b} 有共同的因數 {i},所以不是互質")
break # 找到共同因數,跳出迴圈
else:
print(f"{a} 和 {b} 沒有共同的因數,所以是互質")
# 測試案例
is_coprime(12, 17) # 12 和 17 沒有共同的因數,所以是互質
is_coprime(12, 18) # 12 和 18 有共同的因數 2,所以不是互質
程式碼解密
is_coprime(a, b)函式- 這個函式接受兩個整數
a和b作為輸入,用於判斷這兩個數是否互質。
- 這個函式接受兩個整數
for i in range(2, min(a, b) + 1):迴圈- 這個迴圈從 2 開始,遍歷到
a和b中較小的一個數。我們從 2 開始是因為 1 是所有數字的因數,而我們關心的是除了 1 之外是否有其他共同的因數。 min(a, b) + 1確保迴圈包含較小數字本身,因為range函式不包含上限。
- 這個迴圈從 2 開始,遍歷到
if a % i == 0 and b % i == 0:條件判斷- 這個條件判斷檢查當前的數字
i是否同時是a和b的因數。a % i == 0檢查a除以i的餘數是否為 0,如果是,則表示i是a的因數。同樣,b % i == 0檢查i是否是b的因數。 - 如果
i同時是a和b的因數,那麼這兩個數就不是互質的。
- 這個條件判斷檢查當前的數字
print(f"{a} 和 {b} 有共同的因數 {i},所以不是互質")- 如果找到共同的因數,則印出相應的訊息,告知使用者這兩個數字不是互質的,並顯示找到的共同因數。
break陳述式- 一旦找到共同的因數,
break陳述式會立即終止迴圈。因為我們只需要找到一個共同的因數就可以確定這兩個數不是互質的,所以沒有必要繼續遍歷剩餘的數字。
- 一旦找到共同的因數,
else:區塊- 這個
else區塊會執行,只有在迴圈正常結束時,也就是說,當迴圈完整地遍歷了所有可能的因數,而沒有找到任何共同的因數時。 - 換句話說,如果迴圈因為
break陳述式而提前終止,else區塊就不會執行。
- 這個
print(f"{a} 和 {b} 沒有共同的因數,所以是互質")- 如果在迴圈結束後,
else區塊被執行,那麼印出相應的訊息,告知使用者這兩個數字是互質的。
- 如果在迴圈結束後,
測試案例說明
is_coprime(12, 17)- 迴圈遍歷從 2 到 12 的數字,但沒有找到任何數字同時是 12 和 17 的因數。
- 因此,迴圈正常結束,
else區塊被執行,印出 “12 和 17 沒有共同的因數,所以是互質”。
is_coprime(12, 18)- 迴圈從 2 開始,當
i為 2 時,發現 2 同時是 12 和 18 的因數。 - 因此,印出 “12 和 18 有共同的因數 2,所以不是互質”,然後
break陳述式終止迴圈。 else區塊不會被執行。
- 迴圈從 2 開始,當
重點總結
zip函式可以平行迭代多個迭代器,讓程式碼更簡潔。- 在 Python 2 中,
zip會一次性產生所有元組,可能導致記憶體問題。 zip會在最短的迭代器耗盡時停止,可能導致資料遺漏。for和while迴圈後的else區塊,只有在迴圈沒有被break中斷時才會執行。for...else結構適合用於搜尋,當找到目標時用break跳出,否則執行else區塊。
玄貓提醒: 掌握 zip 和 for...else 的特性,能讓你的 Python 程式碼更簡潔、更有效率,也能避免一些潛在的陷阱。
Python 迴圈後面的 else:你可能不知道的秘密
在 Python 中,for 和 while 迴圈可以搭配 else 區塊,這是一個鮮為人知但有時很有用的特性。但玄貓認為,這種語法糖帶來的可讀性問題,往往大於它所能提供的便利。
else 的運作方式
當迴圈正常結束,也就是說,不是因為 break 陳述式提前跳出時,else 區塊就會被執行。這聽起來很直觀,但實際應用中,可能會讓人感到困惑。
舉例來說,判斷兩個數字是否互質(coprime)的程式碼:
a = 4
b = 9
for i in range(2, min(a, b) + 1):
print('Testing', i)
if a % i == 0 and b % i == 0:
print('Not coprime')
break
else:
print('Coprime')
這段程式碼的輸出結果如下:
Testing 2
Testing 3
Testing 4
Coprime
在這個例子中,迴圈檢查了 2、3、4 是否同時為 a 和 b 的因數。因為迴圈沒有被 break 中斷,所以執行了 else 區塊,印出了 “Coprime”。
更好的替代方案
玄貓通常不建議這樣寫程式碼。更清晰的做法是使用輔助函式(helper function),並在找到目標條件時提前傳回。
第一種方式,找到符合條件的情況就提前傳回:
def coprime(a, b):
for i in range(2, min(a, b) + 1):
if a % i == 0 and b % i == 0:
return False
return True
第二種方式,使用一個變數來追蹤是否在迴圈中找到目標:
def coprime2(a, b):
is_coprime = True
for i in range(2, min(a, b) + 1):
if a % i == 0 and b % i == 0:
is_coprime = False
break
return is_coprime
這兩種寫法都比使用 else 區塊更易於理解。else 區塊所能提供的簡潔性,並不能彌補它對程式碼可讀性造成的負擔。
善用 try/except/else/finally 區塊:提升程式碼健壯性
在 Python 中,例外處理機制提供了 try、except、else 和 finally 四個區塊,它們各自扮演不同的角色,組合使用可以讓程式碼更具備健壯性和可讀性。
finally 區塊:確保資源釋放
當你希望無論是否發生異常,都必須執行某些清理程式碼時,可以使用 try/finally 結構。一個常見的例子是關閉檔案控制程式碼:
handle = open('/tmp/random_data.txt') # 可能丟擲 IOError
try:
data = handle.read() # 可能丟擲 UnicodeDecodeError
finally:
handle.close() # 確保在 try 區塊後執行
即使 read 方法丟擲異常,finally 區塊中的 handle.close() 也一定會被執行,確保檔案資源被正確釋放。
else 區塊:區分成功與失敗
try/except/else 結構可以清晰地劃分程式碼中哪些異常需要處理,哪些異常應該向上層傳播。當 try 區塊沒有丟擲異常時,else 區塊會被執行。
例如,從 JSON 字串中載入字典資料,並傳回指定鍵的值:
import json
def load_json_key(data, key):
try:
result_dict = json.loads(data) # 可能丟擲 ValueError
except ValueError as e:
raise KeyError from e
else:
return result_dict[key] # 可能丟擲 KeyError
如果 data 不是有效的 JSON 格式,json.loads 會丟擲 ValueError,並在 except 區塊中被處理。如果解碼成功,則在 else 區塊中進行鍵查詢。任何鍵查詢可能拋出的異常,都會向上層傳播。
整合應用:try/except/else/finally
當你需要在一個複合陳述式中完成所有事情時,可以使用 try/except/else/finally 結構。例如,從檔案中讀取工作描述,處理它,然後更新檔案:
import json
UNDEFINED = object()
def divide_json(path):
handle = open(path, 'r+') # 可能丟擲 IOError
try:
data = handle.read() # 可能丟擲 UnicodeDecodeError
op = json.loads(data) # 可能丟擲 ValueError
value = (
op['numerator'] /
op['denominator']) # 可能丟擲 ZeroDivisionError
except ZeroDivisionError as e:
return UNDEFINED
else:
op['result'] = value
result = json.dumps(op)
handle.seek(0)
handle.write(result) # 可能丟擲 IOError
return value
finally:
handle.close() # 確保執行
在這個例子中,try 區塊用於讀取和處理檔案,except 區塊用於處理預期的異常,else 區塊用於更新檔案,finally 區塊用於清理檔案控制程式碼。