[python] [VI coding] 第十九章 一些好東西與除錯技巧 - 教學區

[python] [VI coding] 第十九章 一些好東西與除錯技巧

文章瀏覽次數 1176

特種兵

特種兵圖像(預設)

2021-11-26 18:41:42

From:211.23.21.202

第十九章 一些好東西與除錯技巧

這份講義的目標是儘量使用最少語法來完成 python 程式的撰寫。遇到有數種方法可以解決問題時,我們會選擇其中一個,並且先不提另外一個,以完成任務為優先考量。

但學習多種處理問題的方式,就像手上擁有不同工具,遇到不同的問題能選擇更適合的方式,有助於寫出更高效率的程式。

本章將介紹一些 python 的特性或語法,讓你更了解 python,並且細說除錯的細節。

19-1 生成器表達式

還記得列表的表達式嗎?以下範例產生一個 1 到 10 的平方列表:

lst = [x**2 for x in range(1, 11)]
print(lst)
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

語法上,生成器表達式很像列表表達式,只是把中括號換成小括號而已。改寫上面的範例為生成器表達式:

g = (x**2 for x in range(1, 11))
print(g)
# <generator object <genexpr> at 0x00000204E01E9430>

這裡的小括號不是數組,而是生成器表達式的語法。從顯示結果可以看到 generator object (生成器物件) 關鍵字與十六進位的記憶體位置。

生成器表達式可以被遍歷,可以使用 next 函數來取值:

g = (x**2 for x in range(1, 11))
x = next(g)
# x = 1
x = next(g)
# x = 4
for i in g:
	print(i)
# 9
# 25
# ...
# 100
next(g)
#Traceback (most recent call last):
#  File "D:\test.py", line 12, in <module>
#    next(g)
#StopIteration

它會記住移動元素的位置,從下一個元素開始操作。已經到最後一個元素再繼續 next 取值,就會發生錯誤。

以下是應用在 sum 累加函數的例子:

>>> sum(x**2 for x in range(5))
30

也可將生成器表達式轉成其他內建資料型態,如使用 list() 轉成列表。

19-2 any 與 all 函數

介紹這兩個內建函數給大家。any 接受一系列布林值,只要其中有 True 就回傳 True,參數不管是列表或生成器都可以,如下範例:

>>> any([False, False, True])
True
>>> any(letter == 't' for letter in 'monty')
True

看起來雖然與 in 運算子的功能差不多,改寫一個之前自訂的函數比較看得出差異:

def avoids(word, forbidden):
	return not any(letter in forbidden for letter in word)

word = ['yes', 'no', 'good']
forbidden = ['no', 'j']
print(avoids(word, forbidden))
# False

搭配生成器表達式來使用時,遇到 True 就會停止,才不會浪費資源來計算整個序列。

all 函數跟 any 不同的地方,在於所有的元素皆為 True 才回傳 True。

def hasSameElement(word1, word2):
	return all(letter in word2 for letter in word1)

word1 = ['yes', 'no', 'good']
word2 = ['no', 'yes', 'good', 'baby']
print(hasSameElement(word1, word2))
# True

上例中,如果 word1 的所有單字都在 word2 裡面,那就回傳 True。

19-3 Counters

Counters 類似 set (集合),每個相同的元素只會有一個,它會計算每個元素出現的次數,被定義在 collections 模組中,可以使用字串或列表來對其初始化:

>>> from collections import Counter
>>> count = Counter('parrot')
>>> count
Counter({'r': 2, 't': 1, 'o': 1, 'p': 1, 'a': 1})

結果看起來很像字典,鍵值對就是字母與出現次數。和字典不同的地方在於使用不存在的元素不會報錯,而是回傳 0:

>>> count['d']
0

改寫之前的函數來更快速地比較字母出現的次數:

def isAnagram(word1, word2):
	return Counter(word1) == Counter(word2)

如果這兩個參數互為字迷 (tide 與 edit),字元個數一定是一樣的,所以結果會相同。

19-4 語法錯誤

語法錯誤是最常發生的,在熟悉 python 一陣子後,這樣的錯誤就會愈來愈少。出現錯誤時,直譯器給我們的訊息,通常不利於除錯,例如:

  • SyntaxError: invalid syntax
  • SyntaxError: invalid token

需要檢查的地方,除了給定的錯誤行號外,它的前一兩行也該確認一下,幸好語法錯誤是比較容易發現的。

從網頁上或檔案裡複製程式碼時,必須確認縮排、空格、單字是否拼錯、符號是否打錯等。

以下提出一些建議,來避免語法錯誤的發生:

  • 確認自己沒有使用到 python 的關鍵字來當作變數名稱。
  • 確認語句的末尾是否漏掉冒號,像是 if, for, while, def 等敘述。
  • 確認所有的字串外都有使用成對的引號包住,並且是正引號而非反引號。
  • 如果使用了多行字串,確認三個引號的數量是否正確,有無遺漏結束的三個引號。
  • 確認成對的括號有沒有結束在正確的位置,如 ( ), [ ], { } 等。
  • 檢查比較運算子的 == 是否少了一個等號,這是個典型錯誤。
  • 確認縮排的符號,tab 與空白不可混用,建議使用支援 python 縮排的編輯器,有助於發現此類錯誤。
  • 如果使用了非 ascii 字元,像是中文等,不管是字串或註解,都有可能產生問題,雖然 python 3 已經完整支援,還是要注意。

當你一直修改程式,但還是得到一樣的錯誤時,可以注意以下幾點:

  • 可能執行的程式與正在修改的程式,不是同一個。
  • 撰寫新程式碼沒有存檔就執行直譯器,這時候直譯器載入的程式碼是舊的。
  • 已經更改新檔名,但直譯器還在執行舊程式。
  • 開發環境的某些設定不正確。請參考相關手冊或說明。
  • 撰寫自己的模組或函數時,避免與內建函數名稱相同。
  • 使用 import 載入模組時,記得重新執行程式才會生效。

試著在檔案開頭列印一些文字或故意產生新的語法錯誤,看看直譯器是否能發現。若沒有,則確定正在執行不同的程式。

19-5 執行錯誤

當程式語法都正確時,才能被執行。如果發生語法錯誤,就不會出現執行錯誤,兩種錯誤不會同時存在。

  • 如果程式好像什麼事都沒做,可能是因為沒有實際調用類別或函數。若只有定義,程式不會有任何作用。
  • 當程式執行到一半卡住時,可能有無窮迴圈或無限遞迴函數存在。試著在懷疑區塊的開始與結束加入 print 語句來確認。
# 測試無窮迴圈
while x > 0 and y < 0 :
	# 做某些事
	print('x:', x)
	print('y:', y)
	print('while 運算式:', (x > 0 and y < 0))

通常無限遞迴在執行一段時間後會因為遞迴深度超出而終止程式,並且顯示錯誤訊息。

一樣可以使用 print 語句來輸出,確認是否正確,並找出有問題的演算法,重新思考與測試相關敘述。

  • 當程式執行時發生異常,python 會顯示包含錯誤行號、訊息及回溯調用函數的過程。試看看能否根據這些資訊來發現問題,可能的錯誤有:
    1. NameError: 名稱錯誤,正在使用不存在的變數,確認是否有拼錯字的問題,還是使用了已經不存在的區域變數(無法跨區)。
    2. TypeError: 型態錯誤,有一些可能的原因:
      • 使用不當的值,像是索引只能是整數。
      • 使用格式化指定的型態與給定的不符,如 %s 卻給了數字變數。
      • 給定的參數數量與原始定義不符,或者忘記自訂方法的第一個參數是 self,需檢查函數或方法的定義。
    3. KeyError: 使用了不存在字典裡的鍵來取值。
    4. AttributeError: 使用了不存在的屬性或方法,檢查是否有拼錯字,可以使用 vars 內建函數來列出類別內已存在的屬性。
    5. IndexError: 使用超過範圍的索引操作列表或字串等型態。
  • 如果使用太多 print 來輸出而混亂,通常是因測試程式時到處散落的 print 所致。有兩種方法來改善:
    1. 簡化輸出:刪除一些 print,或者合併多個 print 為一個,將 print 的輸出資訊優化成比較完整的顯示才不會錯亂,像是帶入變數名稱等。
    2. 簡化程式:縮小正在處理的問題,將程式簡單化,像是本來在遍歷列表,就減少列表內容,原本讓使用者的輸出改成可能出現問題的固定資料,整理程式,刪除沒必要的程式碼,重新架構程式邏輯。例如多層迴圈可能有問題,那就改寫它,將大函數再拆成數個小函數。

19-6 語意錯誤

這是最難除錯的錯誤類型,因為 python 直譯器不會給出任何錯誤訊息,而且只有開發者知道程式應該做什麼才是正確的。

設法讓程式行為與內容關聯,需要假設程式應該做哪些事,程式執行的速度通常快於人類,應該設法讓它慢下來與我們對話,便於除錯。

除了設定與使用除錯工具外,在適當的位置插入 print 來顯示資訊,可能是最快的方法。

  • 當程式不運作時,問自己以下問題:
    1. 找到應該執行而未執行的程式片段,檢查問題並修復,讓它在該被執行時執行。
    2. 檢查不該被執行卻執行的程式片段,是否在什麼條件下不小心讓它執行了。
    3. 程式執行結果不如預期,仔細檢查程式的敘述和我們想要的結果是否一致,搭配小測試會更實際。寫程式不能靠想像,而是要腳踏實地地確認每個敘述。如果引用了 python 模組或函數,需要仔細閱讀文件避免誤用;若是自己撰寫的程式,應該把它分成不同的小區塊進行單元測試;如果舊的程式碼落實了檢測,那麼只可能在新撰寫的程式上發生問題,如此需要檢查的範圍會小很多。
  • 遇到較複雜的運算式發生錯誤:把它拆成數個小段,每一段都印出結果,看看得到的值是不是原本期待的,如下範例:
self.hands[i].addCard(self.hands[self.findNeighbor(i)].popCard())
# 改寫成
neighbor = self.findNeighbor(i)
pickedCard = self.hands[neighbor].popCard()
self.hands[i].addCard(pickedCard)

追蹤上面改寫的三個變數,就能發現問題點。

複雜運算式也可能遇到運算子執行順序與期待不同的狀況,如下範例:

y = x / 2 * math.pi
# 乘與除的優先順序相同,所以會由左至右執行,結果會是 xpi /2
# 解決方式是加小括號,調整優先順序
y = x / (2 * math.pi)
# 這樣對於不確定執行順序的情況很有幫助,可讀性也大大提升了
  • 有個返回了非預期結果的函數:那就在 return 之前印出結果來確認,例如:
return self.hands[i].removeMatches()
# 改寫成
count = self.hands[i].removeMatches()
print(count)
# 這樣就可以在回傳之前知道結果正不正確
  • 如果經過上述的測試,還找不出問題,可能會覺得沮喪、憤怒、電腦討厭我,或認為要在桌上放乖乖,程式才能正常。這時可以離開電腦幾分鐘,反向思考為何程式會有這樣的反應。有時除錯的靈感不一定會在電腦前出現,可能上完廁所、喝個下午茶,或一覺醒來就產生靈感。
  • 承上,最強的程式設計師也可能遇到困境,一直卡在同一個程式裡,可能讓你產生盲點,需要一雙新鮮的眼睛來幫忙。但在請求他人協助前,需要做好以下準備:
    1. 已經加上程式註解。
    2. 有充份的 print 顯示語句。
    3. 足夠了解問題與程式架構,以便於提供幫助者相關訊息。
    4. 如果有錯誤訊息,應當顯示出來。
    5. 發生錯誤前,你做了什麼事,修改或增加了哪些程式碼?
    6. 發生錯誤後,你做了哪些努力、學習或嘗試了什麼?

程式設計師的目標不只是讓程式運作,而是學習怎麼讓程式運作。

動動腦

  • 選擇檔案的方法
import wx

app = wx.App()
print(app)
# wx.core.App object
filename = wx.FileSelector('請選擇檔案')
print(filename)

習題

練習 1 檔名: 19-1.py

仔細檢查下列程式,它們的錯誤類型和問題是什麼?該怎麼修正?

(1)

x = input('x = ', x)
y = input('y = ', y)
if x > 3 and y < 10
	print(x, y)
else
	print('ending')

(2)

while x < 100:
	x = input('請輸入:')
print('ending')

(3)

lst = [10, 20, 30, 40, 50]
lst.pop(min(lst))
lst.pop(max(lst))
avg = sum(lst) / len(lst)
print('avg =', avg)
練習 2 檔名: 19-2.py

以生成器表達式改寫下列程式:

ans = []
qtn = [1, 3, 5]
for x in qtn:
	x *= 10
	ans.append(x+1)
練習 3 檔名: 19-3.py

請使用更簡化的寫法來改寫以下函數:

def getSmallNumberCount(lst):
	count = 0
	lst.sort()
	small = lst[0]
	for i in lst:
		if small == i:
			count = count + 1
	return count

number_list = [10, 30, 6, 7, 50, 6, 7, 8]
print(getSmallNumberCount(number_list))

影片

第十九章 一些好東西與除錯技巧 part one

第十九章 一些好東西與除錯技巧 part two

最後更新:2021-11-26 22:39:14

From: 111.249.153.227

By: 特種兵