Effective Python

第 1 章 Pythonic 思維

Speaker: 毛毛 (2016/10/01)

Pythonic

  • 依循特定風格的程式碼
    • 使用者經驗所得出,非編譯器要求
    • 歸納處理常見工作的最佳方式

Outline

  • 01 知道你所用的 Python 版本
  • 02 遵循 PEP8 風格指南
  • 03 搞清楚 bytes、str 與 unicode 之間的差異
  • 04 撰寫輔助函式而非複雜的運算式
  • 05 知道如何切割序列
  • 06 避免在單一切片中使用 start、end 與 stride
  • 07 使用串列概括式而非 map 和 filter
  • 08 在串列概括式中不要使用超過兩個運算式
  • 09 考慮使用產生器運算式取代大型概括式
  • 10 優先選用 enumerate 而非 range
  • 11 使用 zip 來平行處理迭代器
  • 12 避免在 for 或 while 迴圈之後使用 else 區塊
  • 13 善用 try/except/else/finally 中的每個區塊

01 知道你所用的 Python 版本

確認系統命令列底下執行的 Python 是你所預期的版本

  • Python2
    • python
    • python --version
  • Python3
    • python3
    • python3 --version
In [1]:
import sys
print(sys.version)
3.5.0 (default, Sep 23 2015, 04:41:33)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)]

Python2

  • 發展凍結
  • 只會休補臭蟲及改善安全性
  • 提供後向移植,讓 Python 2 容易轉換成 Python 3
    • 2to3
    • six

Python3 (建議使用)

  • 未來開發重點
  • 改善 Python 2 及提供新功能
  • Python 最常見的開源程式庫大多已相容

02 遵循 PEP8 風格指南

Python Enhancement Proposal #8 (PEP8)

Whitespace (空白)

  • 使用空格而非 tab 來做縮排
  • 使用四個空格來做縮排
  • 一行的長度應小於或等於 79 字元
  • 過長的運算式要接續到下一行時,應加上額外的四個空白來縮排
  • 在一個檔案中,函式 (functions) 與類別 (classed) 應以兩個空白行隔開
  • 在一個類別中,方法 (methods) 應以一個空白行隔開
  • 變數指定的前後只放上單一個空格
  • 別在串列索引、函式呼叫或關鍵字指令的周圍放上空格
In [1]:
# good
a = 1
spam(han[1], {egg: 2})
magic(r=real, i=img)

# bad
a =    1
spam( han[ 1 ], { egg: 2 } )
magic(r = real, i = img)

Naming (命名)

  • 函式、變數與屬性 (attributes) 應為 lowercase_underscore
  • 受保護的實體屬性應為 _leading_underscore
  • 私有的實體屬性應為 __double_leading_underscore
  • 類別與例外 (exceptions) 應為 CapitalizedWord
  • 模組層級 (module-level) 的常數 (constants) 應為 ALL_CAPS
  • 類別中的實體方法 (instance methods) 應使用 self 作為第一個參數 (它指向該物件)
  • 類別方法 (class methods) 應使用 cls 作為第一個參數的名稱 (它指向該類別)

Statements (運算式與述句)

  • 避免單行的 if 述句、for 或 while 迴圈,將他們分多行描述以清楚表達
  • 使用行內否定,而非否定正向的運算式
  • 別用查驗長度的方式來檢查空值,因為空值本就會隱含的被估算為 False
  • 呈上,對待非空值的的方式也相同,因為非空值本就會隱含的被估算為 True
In [1]:
if 2 > 1:                  # good
    print('2 > 1')

if 2 > 1: print('2 > 1')   # bad

if a is not b              # good
if not a is b              # bad

if not somelist            # good
if len(somelist) == 0      # bad

if somelist                # good
if len(somelist) > 0       # bad
  • import 一定要放在檔案的前端
  • 匯入的區段順序應為: 標準程式庫模組、第三方模組、你自己的模組
    • 每個子區段應以字母順序來匯入
  • 匯入時永遠用模組的絕對名稱,而非相對於目前模組路徑的名稱 (1)
  • 如果非要用相對匯入,就用明確的語法 (2)
    • 包含相對路徑的 Python 腳本不能直接執行,只能作為 module 被引用
In [1]:
'''
|- bar
|  |- __init__.py
|  |- foo.py
|  |- test1.py
'''

# in test1.py

from bar import foo    # (1) good
import foo             # (1) bad

from . import foo      # (2) good
import foo             # (2) bad

Pylint 工具 (推薦使用)

  • 熱門的 Python 原始碼靜態分析器
    • 自動強制執行 PEP 8
    • 偵測程式中其他類型的常見錯誤
      • 像是有沒有忘記匯入模組

PEP 8 工具

  • pip install pep8
  • usage
    • pep8 -h
    • pep8 --first test.py
    • pep8 --show-source --show-pep8 test.py

03 搞清楚 bytes、str 與 unicode 之間的差異

Python 2

  • str (default)
    • 位元組資料
  • unicode
    • Unicode 字元
In [1]:
print('測試 (str): ', len('測試'))
print('測試 (unicode): ', len(u'測試'))
測試 (str): 6
測試 (unicode): 2

Python 3

  • str (default)
    • Unicode 字元
  • bytes
    • 位元組資料
In [1]:
print('測試 (str): ', len('測試'))
print()

words = '測試'.encode('utf-8')
print(words)
print('測試 (bytes): ', len(words))
測試 (str): 2

b'\xe6\xb8\xac\xe8\xa9\xa6'
測試 (words): 6

Unicode 字元 -> 位元組資料

  • encode (編碼)

位元組資料 -> Unicode 字元

  • decode (解碼)
In [1]:
# Python3

# Unicode 字元 -> 位元組資料
words = '測試'.encode('utf-8')
print(words)

# 位元組資料 -> Unicode 字元
words2 = words.decode('utf-8')
print(words2)
b'\xe6\xb8\xac\xe8\xa9\xa6'
測試

字元轉換輔助函式

  • 確保輸入值的型別符合程式碼的預期
In [1]:
# Python3

# 接受 str (Unicode) 或 bytes,並總是回傳 str
def to_str(bytes_or_str):
    if isinstance(bytes_or_str, bytes):
       value = bytes_or_str.decode('utf-8')
    else:
       value = bytes_or_str
    return value

# 接受 str (Unicode) 或 bytes,並總是回傳 bytes
def to_bytes(bytes_or_str):
    if isinstance(bytes_or_str, str)
       value = bytes_or_str.encode('utf-8')
    else:
       value = bytes_or_str
    return value

注意 1

運算子混用

  • Python2
    • 如果 str 只含有 ASCII 字元,則可與 unicode 物件運算子混用
  • Python3
    • str 和 bytes 無法以運算子混用
    • 兩者即便是空字串,也永遠不相等
In [1]:
# Python2

'123' + u'₡'   # ok
'₡' + u'123'   # UnicodeDecodeError

'a' == u'a'    # True
'₡' == u'₡'    # False, UnicodeWarning

注意 2

讀寫位元組資料 (by open 內建函式)

  • Python2
    • 'r' or 'w': 預設讀寫位元組資料
  • Python3
    • 'r' or 'w': 預設讀寫 Unicode 字元
In [1]:
# os.urandom - a bytes object contains random bytes

# Python2:   ok
with open('/tmp/random.bin', 'w') as f:
    f.write(os.urandom(10))

# Python3:   TypeError: write() argument must be str, not bytes
with open('/tmp/random.bin', 'w') as f:
    f.write(os.urandom(10))

Solution

  • Good for both Python2 and Python3
In [1]:
# Write
with open('/tmp/random.bin', 'wb') as f:
    f.write(os.urandom(10))

# Read
with open('/tmp/random.bin', 'rb') as f:
    data = f.read()

04 撰寫輔助函式而非複雜的運算式

Python 語法讓人能輕易地實作單行運算式

  • 使得簡潔度提升,但易讀性可能下降

我們來看個例子!

解碼 url 的 query string

  • 當參數沒有提供或值是空值時,指定預設值 0 給它
  • 每個參數值最後要轉換為整數來使用

要解碼的 query string

  • 'red=5&blue=0&green='

實際可使用的參數

  • red, blue, green, yellow

1. 解碼 url

In [1]:
from urllib.parse import parse_qs

my_values = parse_qs('red=5&blue=0&green=', keep_blank_values=True)
print(my_values)
{'red': ['5'], 'green': [''], 'blue': ['0']}

2. 取值

In [1]:
# Case 1
red = my_values.get('red')                       # ['5']
blue = my_values.get('blue')                     # ['0']
green = my_values.get('green')                   # ['']
yellow = my_values.get('yellow')                 # None

# Case 2
red = my_values.get('red', 0)                    # ['5']
blue = my_values.get('blue', 0)                  # ['0']
green = my_values.get('green', 0)                # ['']
yellow = my_values.get('yellow', 0)              # 0

# Case 3
red = my_values.get('red', [''])[0]              # '5'
blue = my_values.get('blue', [''])[0]            # '0'
green = my_values.get('green', [''])[0]          # ''
yellow = my_values.get('yellow', [''])[0]        # ''

# Case 4   Why?
red = my_values.get('red', [''])[0] or 0         # '5'
blue = my_values.get('blue', [''])[0] or 0       # '0'
green = my_values.get('green', [''])[0] or 0     # 0
yellow = my_values.get('yellow', [''])[0] or 0   # 0

OR 運算式

In [1]:
# First part is True
a = 1 or 2     # 1
b = 2 or 1     # 2
c = -1 or 1    # -1

# First part is False
a = '' or 0    # 0
b = [] or -1   # -1
c = 0 or 2     # 2

簡潔度 v.s. 易讀性

In [1]:
# 最簡潔!難讀!
red = int(my_values.get('red', [''])[0] or 0)        # 5
blue = int(my_values.get('blue', [''])[0] or 0)      # 0
green = int(my_values.get('green', [''])[0] or 0)    # 0
yellow = int(my_values.get('yellow', [''])[0] or 0)  # 0

# 簡潔!好讀一點!
red = my_values.get('red', [''])[0]                  # '5'
red = int(red) if red else 0                         # 5
blue = my_values.get('blue', [''])[0]                # '0'
blue = int(blue) if red else 0                       # 0
green = my_values.get('green', [''])[0]              # ''
green = int(green) if red else 0                     # 0
yellow = my_values.get('yellow', [''])[0]            # ''
yellow = int(yellow) if red else 0                   # 0

# 最不簡潔!最好讀!
red = my_values.get('red', [''])[0]                  # '5'
if red:
    red = int(red)                                    # 5
else:
    red = 0

blue = my_values.get('blue', [''])[0]                # '0'
if blue:
    blue = int(blue)                                  # 0
else:
    blue = 0

Best solution

In [1]:
# 簡潔!好讀!易修改!
def get_first_int(values, key, default=0):
    found = values.get(key, [''])[0]
    if found:
        found = int(found)
    else:
        found = default
    return found

red = get_first_int(my_values, 'red')        # 5
blue = get_first_int(my_values, 'blue')      # 0
green = get_first_int(my_values, 'green')    # 0
yellow = get_first_int(my_values, 'yellow')  # 0

Summary

  • 避免寫出複雜又難以解讀的單行運算式
  • 將複雜的運算式移到輔助函式內,尤其是在需要重複使用相同的邏輯時

Important

  • 易讀性得到的好處 > 簡潔度得到的好處

05 知道如何切割序列

利用 Python 的切割語法以最少的力氣取用序列的子集

  • 適用型別: list, str and bytes

基本型

somelist[start:end]

  • start 是 inclusive
    • 當從串列的開頭進行切割時,應省略零索引來降低視覺雜訊
  • end 是 exclusive
    • 當切割到串列的尾端時,應省略最後的索引來降低視覺雜訊
  • somelist[:] == somelist
In [1]:
somelist[:2]                 # good
somelist[0:2]                # bad

somelist[2:]                 # good
somelist[2:len(somelist)]    # bad
In [1]:
a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']

print('a[:4] =', a[:4])
print('a[-4:] =', a[-4:])
print('a[3:-3] =', a[3:-3])
print('a[:-1] =', a[:-1])
print('a[2:5] =', a[2:5])
print('a[-3:-1] =', a[-3:-1])
a[:4] = ['a', 'b', 'c', 'd']
a[-4:] = ['e', 'f', 'g', 'h']
a[3:-3] = ['d', 'e']
a[:-1] = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
a[2:5] = ['c', 'd', 'e']
a[-3:-1] = ['f', 'g']

切割語法會適當的處理超出串列邊界的 star 或 end 索引

  • 讓 programmer 可為序列切割設下一個可接受的最大長度
In [1]:
a = [1, 2, 3, 4, 5, 6, 7, 8]

first_three_items = a[:3]  # [1, 2, 3]
first_ten_items = a[:10]   # [1, 2, 3, 4, 5, 6, 7, 8]
last_three_items = a[-3:]  # [6, 7, 8] 
last_ten_items = a[-10:]   # [1, 2, 3, 4, 5, 6, 7, 8]
In [2]:
# 注意 1: 直接指定超出串列邊界的索引會導致錯誤
a[10]
IndexError: list index out of range
In [3]:
# 注意 2: 若 start 或 end 索引設為 -0 時,等同於 0
a = [1, 2, 3, 4, 5, 6, 7, 8]
a[-0:]   # [1, 2, 3, 4, 5, 6, 7, 8]
a[:-0]   # []

切割串列產生的結果會是一個全新的串列

  • 使用 shallow copy 所產生
In [1]:
a = [1, 2, 3]
b = a[:]
c = a
print('a_id:', id(a))
print('b_id:', id(b))
print('c_id:', id(c))
a_id: 140134057715528
b_id: 140134049210056
c_id: 140134057715528

Shallow Copy

In [1]:
# 串列中全是不可變 (immutable) 對象
a = [1, 2, 3]
b = a[:]
b[0] = 4
print('a:', a)
print('b:', b)
a: [1, 2, 3]
b: [4, 2, 3]
In [2]:
# 串列中包含可變 (mutable) 對象
a = [1,2, [3, 4]]
b = a[:]
b[0] = 4       # 修改不可變對象
print('a:', a)
print('b:', b)
print()
b[2][0] = 5    # 修改可變對象
print('a:', a)
print('b:', b)
a: [1, 2, [3, 4]]
b: [4, 2, [3, 4]]

a: [1, 2, [5, 4]]
b: [4, 2, [5, 4]]

串列片段被 assign 值時,值的個數不需與片段長度相等

  • 所給的值會直接取代原串列指定範圍的部分
In [1]:
a = [1, 2, 3, 4, 5, 6, 7, 8]

a[2:6] = ['a', 'b', 'c']   # 前 4 後 3
print(a)
[1, 2, 'a', 'b', 'c', 7, 8]
In [2]:
a[2:4] = ['a', 'b', 'c', 'd', 'e'] # 前 2 後 5
print(a)
[1, 2, 'a', 'b', 'c', 'd', 'e', 5, 6, 7, 8]

06 避免在單一切片中使用 start、end 與 stride

除了基本切割,Python 也有提供 stride (跨步) 的切法

somelist[start:end:stride]

  • 意即每間隔多少就取出一個項目
  • 能輕易地將串列中的偶數索引和奇數索引分組
In [1]:
a = ['a', 'b', 'c', 'd', 'e', 'f']

# 當 stride 是正數,意即從串列的頭開始
a[::2]       # ['a', 'c', 'e']
a[::3]       # ['a', 'd']

# 當 stride 是負數,意即從串列的尾開始
a[::-1]      # ['f', 'e', 'd', 'c', 'b', 'a']
a[::-2]      # ['f', 'd', 'b']

# 當 stride 與 start 或 end 同時使用,容易使人困惑 (stride 為負數時尤勝)
a[2::2]      # ['c', 'e']
a[:-2:2]     # ['a', 'c']
a[2:-2:2]    # ['c']

a[-2::-2]    # ['e', 'c', 'a']
a[:2:-2]     # ['f', 'd']
a[-2:2:-2]   # ['e']
a[4:1:-2]    # ['e', 'c']

問題防範

  • 切割時優先選用 stride 正值,避免 stride 負值,並不要同時搭配 start 或 end
  • 若 stride 一定要搭配 start 或 end 使用,請分成兩個 assignment 來進行
    • 一個 assignment 用於 stride,另一個 assignment 用於 start 和 end
  • 若程式無法負擔兩個 assignment 的時間或記憶體,考慮使用 itertools 內建模組的 islice
    • 它不允許使用負的 start、end 或 stride 值

07 使用串列概括式而非 map 和 filter

我們直接來看個例子!

想要計算一個串列中每個數字的平方

In [1]:
a = [1, 2, 3, 4, 5, 6]

# 使用 map
squares = list(map(lambda x: x**2, a))
print(squares)
[1, 4, 9, 16, 25, 36]
In [2]:
# 使用串列概括式 (list comprehension)
squares = [x**2 for x in a]
print(squares)
[1, 4, 9, 16, 25, 36]

想要計算串列中能被 2 整除的數字的平方

In [1]:
a = [1, 2, 3, 4, 5, 6]

# 使用 map
squares = list(map(lambda x: x**2, filter(lambda x: x%2 == 0, a)))
print(squares)
[4, 16, 36]
In [2]:
# 使用串列概括式 (list comprehension)
squares = [x**2 for x in a if x%2 == 0]
print(squares)
[4, 16, 36]

其他概括式

  • 字典概括式 (dictionary comprehension)
  • 集合概括式 (set comprehension)
In [1]:
# Python3

child_rank = {'Amy': 1, 'Mary': 2, 'Tom': 3}

# dict
rank = {rank: name for name, rank in child_rank.items()}
print(rank)

# set
child_name_length_set = {len(name) for name in rank.values()}
print(child_name_length_set)
{1: 'Amy', 2: 'Mary', 3: 'Tom'}
{3, 4}

08 在串列概括式中不要使用超過兩個運算式

串列概括式支援多層迴圈及每層迴圈中的多個條件

  • 位於同一層迴圈內的多個條件就是一個隱含 and 的運算式
In [1]:
# 想將二維矩陣變成一維陣列
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [item for row in matrix for item in row]
print('flat =', flat)

# 想要計算二維矩陣內每個值的平方
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
squares = [[item**2 for item in row] for row in matrix]
print('squares =', squares)

# 想要過濾出大於 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]
print('b =', b)
print('c =', c)
flat = [1, 2, 3, 4, 5, 6, 7, 8, 9]
squares = [[1, 4, 9], [16, 25, 36], [49, 64, 81]]
b = [6, 8, 10]
c = [6, 8, 10]

帶有兩個以上運算式的串列概括式,難懂!

In [1]:
my_lists = [
   [[1, 2], [3, 4]],
   [[5, 6], [7, 8]],
]
flat = [item for sub_list1 in my_lists for sub_list2 in sub_list1 for item in sub_list2]
print(flat)
[1, 2, 3, 4, 5, 6, 7, 8]
In [2]:
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)
[[6], [9]]

Summary

  • 避免使用帶有兩個以上運算式的概括式
  • 當概括式變複雜時,應改用一般的 if 和 for 述句,並撰寫輔助函式

Important

  • 易讀性得到的好處 > 簡潔度得到的好處

09 考慮使用產生器運算式取代大型概括式

串列概括式會建立出一個新的串列,並可能會為輸入序列的每個值產生一個項目

  • 對大型輸入來說會耗費相當大量的記憶體,使得程式可能當掉!

我們來看個例子!

讀取一個檔案,並記錄每行的字元數

In [1]:
# 若該檔案極端龐大,可能會在 value 實際產生完畢之前就把記憶體塞爆
value = [len(line) for line in open('/tmp/my_file.txt')]
print(value)
[100, 57, 15, 1, 22, 3]

產生器運算式 (generator expressions)

  • 廣義的串列概括式與產生器
  • 不會在執行時具體化整個輸出序列
  • 產生器運算式的結果會是一個迭代器 (iterator),會逐次產出 (yield) 一個項目

串列概括式 v.s. 產生器運算式

In [1]:
# 串列概括式: 檔案過大,記憶體可能爆掉
value = [len(line) for line in open('/tmp/my_file.txt')]
print(value)
print()

# 產生器運算式: 可利用 next 內建函式來一次次讀取輸出值,不用擔心記憶體爆掉
value = (len(line) for line in open('/tmp/my_file.txt'))
print(value)
print(next(value))
print(next(value))
[100, 57, 15, 1, 22, 3]

<generator object <genexpr> at 0x101b81480>
100
57

產生器運算式的另一個強大之處是,可以彼此結合!

  • 串接在一起的產生器運算式執行速度非常快
  • 推進新的迭代器時,會一併推進內部的迭代器
In [2]:
# 利用剛剛的 value 來當作 roots 的輸入
roots = ((x, x**2) for x in value)
print(next(roots))
(15, 225)

10 優先選用 enumerate 而非 range

range 內建函式

  • 適合用來迭代一組整數的迴圈
In [1]:
# 產生 1~10 的串列
a = [i+1 for i in range(10)]
print(a)

# 產生五組介於 1~10 之間的亂數
import random
for i in range(5):
    print(random.randint(1, 10))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[4,3,3,1,7]

enumerate 內建函式

  • 使用惰性產生器包裹任何迭代器
  • 此產生器會產出成對的值: (迴圈索引, next(迭代器))
In [1]:
a = ['red', 'blue', 'green', 'yellow']
for i, color in enumerate(a):
    print(i, color)
0 red
1 blue
2 green
3 yellow

range v.s. enumerate

想要得知每個項目在串列中的順序

In [1]:
# By range
for i in range(len(a)):
    print(i+1, a[i])

print()

# By enumerate
for i, color in enumerate(a, 1):
    print(i, color)
1 red
2 blue
3 green
4 yellow

1 red
2 blue
3 green
4 yellow

11 使用 zip 來平行處理迭代器

直接來看個例子!

找出名字最長的人

In [1]:
names = ['Cecilia', 'Lise', 'Marie']
letters = [len(n) for n in names]
longest_name = None
max_letters = 0

# By range: 有視覺雜訊,相同索引使用了兩次
for i in range(len(names)):
    count = letters[i]
    if count > max_letters:
        max_letters = count
        longest_name = names[i]
print(longest_name)

# By enumerate: 稍微改善,但不夠理想
for i, name in enumerate(names):
    count = letters[i]
    if count > max_letters:
        max_letters = count
        longest_name = name
print(longest_name)
Cecilia
Cecilia

zip 內建函式

  • 使用惰性產生器包裹兩個以上的迭代器
  • 此產生器會產出含有每個迭代器下個值的元組: (next(迭代器1), next(迭代器2), ...)

找出名字最長的人

In [1]:
names = ['Cecilia', 'Lise', 'Marie']
letters = [len(n) for n in names]
longest_name = None
max_letters = 0

for name, count in zip(names, letters):
    if count > max_letters:
        max_letters = count
        longest_name = name
print(longest_name)
Cecilia

注意 1

在 python2 中,zip 不是一個產生器

  • 會完全耗盡提供的迭代器
  • 回傳一個包含全部元組的串列
    • 可能會用去大量的記憶體,導致程式當掉

應改成使用 itertools 內建模組的 izip

注意 2

若輸入的迭代器長度不同,會有非預期的結果

  • 會持續產出元組,直到其中一個迭代器耗盡為止 => 截斷行為
In [1]:
a = [1, 2, 3, 4, 5]
b = ['a', 'b', 'c']

for number, letter in zip(a, b):
    print(number, letter)
1 a
2 b
3 c

若不確定迭代器們的長度是否相等,可使用 itertools 的 zip_longest

  • Python2 中叫做 izip_longest
In [1]:
from itertools import zip_longest

a = [1, 2, 3, 4, 5]
b = ['a', 'b', 'c']

for number, letter in zip_longest(a, b):
    print(number, letter)
1 a
2 b
3 c
4 None
5 None

12 避免在 for 或 while 迴圈之後使用 else 區塊

Python 有特殊語法能在 for 或 while 迴圈後加上 else

  • 當迴圈正常執行結束,才會去執行 else 區塊
In [1]:
# Case 1
for i in range(3):
    print('loop', i)
else:
    print('Else block!')
loop 0
loop 1
loop 2
Else block!
In [2]:
# Case 2
for i in range(3):
    print('loop', i)
    if i == 1:
        break
else:
    print('Else block!')
loop 0
loop 1

注意!

下面這兩種情況也會執行 else 區塊

In [1]:
# Case 3
for x in []:
    print('Never run')
else:
    print('Else block!')
Else block!
In [2]:
# Case 4
while False
    print('Never run')
else:
    print('Else block!')
Else block!

比較其他 else 區塊的用法

if else

  • 前面區塊 (if 區塊) 沒有執行,就執行 else 區塊

try except else

  • 前面區塊 (except 區塊) 沒有執行,就執行 else 區塊

for/while else

  • 前面區塊 (if/while 區塊) 有執行完畢、沒遇到 break,就執行 else 區塊
    • 跟其他種的用法不同,不夠直覺!

for/while else 的使用時機?

判斷兩個數字是否互質

In [1]:
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

實務上的寫法

判斷兩個數字是否互質

In [1]:
# Case 1
def coprime(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

# Case 2
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

Summary

  • 避免使用迴圈後的 else 區塊,因為並不直覺、容易搞不清楚而出錯
  • 實務上不太會使用 for/while else 的寫法

13 善用 try/except/else/finally 中的每個區塊

在 Python 中進行例外處理時,有四個你可能會想要採取行動的時間點

  • 分別由 try、except、else 和 finally 區塊來代表

Finally 區塊

  • 不管有沒有例外發生都會執行
  • 常見用途: 執行清理用的程式碼 (ex: 可靠的關閉檔案)
In [1]:
f = open('/tmp/random_data.txt')     # 這邊可能會有 IOError
try:
    data = f.read()                   # 這邊可能會有 UnicodeDecodeError
finally:
    f.close()

Else 區塊

  • 若 try 區塊沒有遇到例外就會執行
  • 常見用途: 最小化 try 區塊中的程式碼,增進可讀性
In [1]:
def load_json_key(data, key):
    try:
        result_dict = json.loads(data)
    except ValueError as e:
        print(e)
        return None
    else:
        return result_dict[key]         # 這邊可能會有 KeyError

Summary

  • try
    • 發生例外後,想被 except 接收例外去處理的區塊
  • except
    • 處理 try 中發生的例外的區塊
  • else
    • try 成功後,處理剩餘動作的區塊
  • finally
    • 執行清理動作的區塊