Effictive Python:編寫高質量Python代碼的59個有效方法(2)

第二章 函數

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)
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容