第二章 函數
14. 盡量用異常來表示特殊情況,而不要返回None。
- 用None這個返回值來表示特殊意義的函數,很容易使調用者犯錯,因為None和0及空字符串之類的值,在條件表達式里都會評估為False。
- 函數在遇到特殊情況時,應該拋出異常,而不要返回None。調用者看到該函數的文檔中所描述的異常之后,應該就會編寫相應的代碼來處理它們了。
示例
編寫工具函數(utility function)時,程序員喜歡給None這個返回值賦予特殊含義。例如,兩數相除,除數為0時返回None表示除數為0,似乎是合理的。
def divide(a, b):
try:
return a/b
except ZeroDivisionError:
return None
#在調用該函數時,調用者會對這個特殊含義做出解讀。
result = divide(x, y)
if result is None:
print('Invalid inputs')
如果分子是0,那么結果為0 。但是對于if result is None
這句來說,會出問題。因為我們可能不會專門去判斷函數的返回值是否為None,而是會假設如果返回結果與False等效的結果,就說明函數出錯了。(如空值,0,空列表等)
x, y = 0, 5
result = divide(x, y)
if not result:
print('Invalid inputs') #這段代碼是錯誤的
讓函數返回None,可能會使調用它的人寫出錯誤的代碼。解決辦法是不返回None,而是把異常拋向上一級,讓調用者必須面對它。
def divide(a, b):
try:
return a/b
except ZeroDivisionError as e:
raise ValueError('Invalid inputs') from e
這樣調用者需要處理因輸入值引起的異常了。
x, y = 5, 2
try:
result = divide(x, y)
except ValueError:
print('Invalid inputs')
else:
print('Result is %.1f' % result)
15.了解如何在閉包里使用外圍作用域中的變量。
- 對于定義在某作用域內的閉包來說,它可以引用這些作用域中的變量。
- 使用默認方式對閉包內的變量賦值,不會影響外圍作用域中的同名變量。
- 在Python3中,程序可以在閉包內用nonlocal語句來修飾某個名稱,使該閉包能夠修改外圍作用域中的同名變量。
16. 考慮用生成器來改寫直接返回列表的函數
- 使用生成器比把收集到的結果放入列表里返回給調用者更加清晰。
- 由生成器函數所返回的那個迭代器,可以把生成器函數體中傳給yield表達式的那些值依次產生出來。
- 無論輸入量多大,生成器都能產生一系列輸出,因為這些輸入量和輸出量都不會影響它在執行時所耗的內存。
#找到字符串中每個單詞的首字母在整個字符串的位置
def index_words_iter(text):
if text:
yield 0
for index, letter in enumerate(text):
if letter == ' ':
yield index+1
調用該生成器函數返回的迭代器,可以傳給內置的list函數,將其轉換為列表,避免了占用大量內存。
address = 'Four score and seven years ago...'
result = list(index_words_iter(address))
17.在參數上面迭代時,要多加小心。
- 函數在輸入的參數上面多次迭代時要當心,如果參數是迭代器,可能會出現奇怪的行為并錯失某些值。因為迭代器有狀態,只能產生一輪結果,并且在用完的(exhausted,又稱耗盡的)迭代器上面繼續迭代時不會報錯。
- python的迭代器協議,描述了容器和迭代器應該如何與iter和next內置函數、for循環及相關表達式相互配合。
- 把iter方法實現為生成器,即可定義自己的容器類型。
- 想判斷某個值是迭代器還是容器,可以拿該值為參數,兩次調用iter函數,若結果相同,則是迭代器,調用內置的next函數即可令該迭代器前進一步。
18.用數量可變的位置參數減少視覺干擾(visual nosie)。
- 在def語句中使用*args,即可令函數接受數量可變的位置參數。
- 調用函數時,可以采用*操作符,把序列中的元素當成位置參數,傳給該函數。
- 對生成器使用*操作符,可能導致程序耗盡內存并崩潰。
- 在已經接受*args參數的函數上面繼續添加位置參數,可能會產生難以排查的bug。
#定義log函數以便打印調試函數,參數固定,即使沒有值要打印也要傳遞空列表
def log(message, values):
if not values:
print(message)
else:
values_str = ', '.join(str(x) for x in values)
print('%s: %s' % (message, values_str)
#改為可選的位置參數(星號參數)能更清晰,減少干擾。
def log(message, *values):
if not values:
print(message)
else:
values_str = ', '.join(str(x) for x in values)
print('%s: %s' % (message, values_str)
#如果傳遞列表給可變長的函數,調用時可以在列表前加上*,這樣python會把列表元素視為位置參數。
favorites = [7, 33, 99]
log('Favorite colors', *favorites)
#采用數量可變的位置參數會引起兩個問題。第一,變長參數在傳遞時先轉化成元組。
#如果把(*生成器)作為參數,python必須迭代一輪,并放入元組,可能會消耗大量內存。
def my_generator():
for i in range(10):
yield id
def my_func(*args):
print(args)
it = my_generator()
my_func(it)
#第二個問題是在已經接受*args參數的函數上面繼續添加位置參數,可能會產生難以排查的bug。
def log(sequence, message, *values):
if not values:
print('%s: %s' % (sequence, message)
else:
values_str = ', '.join(str(x) for x in values)
print('%s: %s: %s' % (sequence, message, values_str)
log(1, 'Favorites', 7, 33)#New usage is OK
log('Favorite numbers', 7, 33)#Old usage breaks
>>>
1: 'Favorites': 7, 33
'Favorite numbers': 7: 33
#這里的問題在于第二條log語句是以前寫好的,當時的log函數還沒有sequence參數,
#現在多了這個參數,使7變成了message參數的值,這種bug很難追蹤,因為代碼仍然運行且不拋出異常。
#為了避免這種問題,我們應該使用只能以關鍵字形式指定的參數,來擴展這種接收*args的函數(21條)
19.用關鍵字參數來表達可選的行為。
- 函數參數可以按位置或關鍵字來指定。
- 只使用位置參數來調用函數,可能會導致這些參數值的含義不夠明確,而關鍵字參數則能夠闡明每個參數的意圖。
- 給函數添加新的行為時,可以使用帶默認值的關鍵字參數,以便與原有的函數調用代碼保持兼容。
- 可選的關鍵字參數總是應該以關鍵字形式來指定,而不應該以位置參數的形式來指定。
20.用None和文檔字符串來描述具有動態默認值的參數。
- 參數的默認值,只會在程序加載模塊并讀到本函數的定義時評估一次。對于{}或[]等動態的值,可能會導致奇怪的行為。
- 對于以動態值作為實際默認值的關鍵字參數來說,應該把形式上的默認值寫為None,并在函數的文檔字符串里面描述該默認值所對應的實際行為。
def log(message, when=datetime.now()):
print('%s: %s' % (when, message))
#參數的默認值會在模塊加載進來的時候求出,一旦加載進來,默認值就固定不變了,程序不會再次執行datetime.now()
>>> log('Hi there!')
2019-03-19 11:16:00.139197: Hi there!
>>> log('Hi, again!')
2019-03-19 11:16:00.139197: Hi, again!
#若要實現動態默認值,習慣上是把默認值設為None,并在文檔字符串中把None對應的實際行為描述出來
def log(message, when=None):
'''Log a message with a timestamp.
Args:
message: Message to print.
when: datetime of when the message occured.
Defaults to the present time.
'''
when = datetime.now() if when is None else when
print('%s: %s' % (when, message))
>>> log('Hi there!')
2019-03-19 11:23:34.145581: Hi there!
>>> log('Hi, again!')
2019-03-19 11:23:41.577505: Hi, again!
##注意下面
Note = '如果參數的實際默認值是可變類型(mutable),如{},[],則一定要用None作為形式上的默認值。'
21. 用只能以關鍵字形式指定的參數來確保代碼清晰。
- 關鍵字參數能夠使函數調用的意圖更加明確。
- 對于各參數之間很容易混淆的函數,可以聲明只能以關鍵字形式指定的參數,以確保調用者必須通過關鍵字來指定它們。對于接受多個Boolen標志的函數,更應該這么做。
- 在編寫函數時,Python3有明確的語法來定義這種只能以關鍵字形式指定的參數。
#要計算兩數相除,同時忽略OverflowError異常并返回0,忽略ZeroDivisionError,返回無窮。
def safe_division(number, divisor, ignore_overflow, ignore_zero_division):
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
#調用函數
result = safe_division(1.0, 10**500, True, False)
print(result) >>>0.0
result = safe_division(1, 0, False, True)
print(result)>>>inf
#但是調用者調用代碼時可能分不清這兩個參數。
#提升代碼可讀性的方法是采用關鍵字參數。
def safe_division(number, divisor, ignore_overflow=False, ignore_zero_division=Flase):
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
result = safe_division(1.0, 10**500, ignore_overflow=True)
result = safe_division(1, 0, ignore_zero_division=True)
#但這種方式也有缺陷,因為關鍵字參數是可選的,所以沒辦法確保函數調用者一定會使用關鍵字來指定這些參數的值,依然可以使用位置參數的形式調用它。
result = safe_division(1.0, 10**500, True, False)
#對于這種復雜的函數來說,最好保證調用者必須以清晰的調用代碼來闡明調用該函數的意圖。
#python3可定義必須以關鍵字形式指定參數。
#*標志位置參數就此終結,之后的參數只能以關鍵字形式指定。
def safe_division(number, divisor, * ignore_overflow=False, ignore_zero_division=Flase):
#這樣不能以位置參數的形式來指定關鍵字參數了。
result = safe_division(1.0, 10**500,True,False)#錯誤
#但依然可以用關鍵字的形式指定,如果不指定,依然會采用默認值。
result = safe_division(1.0, 10**500, ignore_overflow=True)