自動合並測試LeetCode解題方法
在leetcode.com上答題,Run Code或者Sumbmit通常要Spending一會,如果提交一次就Accepted那還好,如果反復Wrong Answer,很耽誤時間。為了調高效率和減少挫折(來回提交,一直Wrong Answer倍受打擊),我采取在本地Jupyter notebook上coding,測試通過后再提交的方式。本篇主要介紹自動測試的方法。
1. LeetCode模式
打開leetcode一道題后,左側有一個Description選項卡,該選項卡面板內有題目、命題描述、還列舉了多個Example,每個Example都有一個Input和一個Output,有的還會有一個Explanation;右側是coding區,coding區自動生成 class Solution模板。
class Solution: def method(self,**kwargs):
例如,第1071號題Decription區的Example內容如下
1071號題coding區初始化模板如下
2. Jupyter notebook編寫代碼
將LeetCode中coding模板內容復制到jupyter notebook中,編寫解題代碼。假如1071號題的代碼如下。
class Solution:
def gcdOfStrings(self, str1: str, str2: str) -> str:
if len(str1) < len(str2):
tmp, str2, str1 = str2, str1, tmp
len_str2, len_str1 = len(str2), len(str1)
for i in range(1,len_str2+1):
d2,m2 = divmod(len(str2),i)
if m2==0 and str2==str2[:d2]*i:
d1, m1 = divmod(len(str1),d2)
if m1==0 and str1==str2[:d2]*d1:
return str2[:d2]
return ""
3. 手動逐一測試
手動逐一測試,就是編寫好method內容后,實例化Solution,然后復制Decription中每個Example中的Input,傳入實例的方法,執行后觀察結果是否與Example中Output一致。
# 實列化類
s = Solution()
# 測試Example中的用例1
# Input: str1 = "ABCABC", str2 = "ABC"
# Output: "ABC"
str1 = "ABCABC"
str2 = "ABC"
s.gcdOfStrings(str1,str2)
'ABC'
# 測試Example中的用例2
# Input: str1 = "ABABAB", str2 = "ABAB"
# Output: "AB"
str1 = "ABABAB"
str2 = "ABAB"
s.gcdOfStrings(str1,str2)
'AB'
# 測試Example中的用例3
# Input: str1 = "LEET", str2 = "CODE"
# Output: ""
str1 = "LEET"
str2 = "CODE"
s.gcdOfStrings(str1,str2)
''
# 測試Example中的用例4
# Input: str1 = "ABCDEF", str2 = "ABC"
# Output: ""
str1 = "ABCDEF"
str2 = "ABC"
s.gcdOfStrings(str1,str2)
''
4. 自動合並測試
4.1 復制測試用例
從LeetCode問題頁面復制所有Example內容,賦值給str_Examples。這里用3對雙引號,內容可以原封不動。Example描述有規律,而且每個題的Example描述都是同樣的模式,所以我們可以定義一個通用方法來獲取測試用例。
str_Examples ="""Example 1:
Input: str1 = "ABCABC", str2 = "ABC"
Output: "ABC"
Example 2:
Input: str1 = "ABABAB", str2 = "ABAB"
Output: "AB"
Example 3:
Input: str1 = "LEET", str2 = "CODE"
Output: ""
Example 4:
Input: str1 = "ABCDEF", str2 = "ABC"
Output: ""
"""
4.2 定義獲取用例方法
- 目標是把Input、Output提取出來,並成對組織好。
- 通過觀察,可采用正則化split、字符串split、replace、strip等方法提取Input、Output內容
- 輸入參數和輸出存入list,每個用例可能有多個參數,所以用例參數用dict裝載,方便調用測試方法時用**kwargs傳入。
注意:因為輸入用例文檔用的""""",引號里內容還含有雙引號",split后會把里面的引號當做字符內容,導致結果出乎意料。
def get_test_cases(str_examples):
import re
params =re.split('Example.+?:',str_examples.replace('\n','').replace(' ',''))[1:]
cases_arg, cases_ans = [], []
for param in params:
kawargs = {}
args_,ans = param.split('Output:')
args_ = args_.replace('Input:','').strip().split(',')
cases_ans.append(ans)
for arg in args_:
key,val = arg.split("=")
kawargs[key] = val
cases_arg.append(kawargs)
return zip(cases_arg, cases_ans)
4.3 定義自動測試方法
- leetcode每個題生成的默認代碼類名都是Solution,但是函數名根據題目來取,因題而異。dir(Solution())能夠獲取Solution的方法名列表,前面部分是內建方法名,最后一個是自定義的解題方法(這里考慮只有一個自建函數)。用dir(Solution())[-1]獲取函數名,然后使用getattr獲取實例方法f。
- 傳入參數為get_usecases返回的結果——zip打包的測試用例輸入和輸出,for迭代或取每一個測試用例的輸入參數kwargs(Example中的Input)和輸入answer(Example中的Output)。
- 迭代測試用例,斷言 f(**kwargs) == answer 。如果每個斷言成功,返回'Accepted',否則try...except捕獲斷言失敗,返回'Wrong Answer'。
def auto_test(test_cases):
f = getattr(Solution(), dir(Solution())[-1])
try:
for kwargs, answer in test_cases:
print(kawargs,answer)
assert f(**kwargs) == answer
return 'Accepted'
except Exception as e:
print(e)
return 'Wrong Answer'
4.4 what?! 神奇問題bug ?
- 先調用get_test_cases獲取測試用例,然后將結果傳入auto_test進行測試,結果是'Wrong Answer';
- 直接調用auto_test(get_test_cases()),結果也是'Wrong Answer'。
- 然而調用get_test_cases獲取測試用例test_cases后,先迭代一遍test_cases,再將test_cases傳入auto_test,結果為'Accepted'。
test_cases = get_test_cases(str_Examples)
auto_test(test_cases)
name 'kawargs' is not defined
'Wrong Answer'
auto_test(get_test_cases(str_Examples))
name 'kawargs' is not defined
'Wrong Answer'
for input_args,out in test_cases:
print(input_args, out)
auto_test(test_cases)
{'str1': '"ABABAB"', 'str2': '"ABAB"'} "AB"
{'str1': '"LEET"', 'str2': '"CODE"'} ""
{'str1': '"ABCDEF"', 'str2': '"ABC"'} ""
'Accepted'
4.5 編程陷阱
陷阱1:"""doc""",三對雙引號中內容帶引號,沒去除引號被當做字符內容,導致Solution算法“失靈”。
doc = """ABC "efg" HIGK""" # efg帶引號
doc.split() # 分割后'"efg"'而不是'efg',不注意還看不出來
['ABC', '"efg"', 'HIGK']
避坑辦法:.replace('"','')去除內容引號
def get_test_cases(str_examples):
import re
params =re.split('Example.+?:',str_examples.replace('\n','').replace(' ',''))[1:]
cases_arg, cases_ans = [], []
for param in params:
kawargs = {}
args_,ans = param.split('Output:')
args_ = args_.replace('Input:','').strip().split(',')
# ans.replace('"','')
cases_ans.append(ans.replace('"',''))
for arg in args_:
key,val = arg.split("=")
# val.replace('"','')
kawargs[key] = val.replace('"','')
cases_arg.append(kawargs)
return zip(cases_arg, cases_ans)
陷阱2:try...except不僅捕獲assert斷言成功失敗,在try中print(kawargs,answer),‘kawargs’寫錯也導致進入except塊。
避坑辦法:try...except Exception as e打印異常,debug錯誤
# 捕獲except異常並不等於assert斷言失敗
try:
print(what)
assert 1==1
except Exception as e:
print(e)
name 'what' is not defined
def auto_test(test_cases):
f = getattr(Solution(), dir(Solution())[-1])
try:
for kwargs, answer in test_cases:
print(kwargs,answer)
assert f(**kwargs) == answer
return 'Accepted'
except Exception as e:
print(e)
return 'Wrong Answer'
陷阱2:zip只能迭代一次,looping后被釋放,外部迭代一次后再傳入auto_test,不進入for迭代,造成外部迭代后返回正確'Accepted'的假象。
val =[1,2,3]
key = ['a','b','c']
ziped = zip(key,val)
print('----first iter zip---------')
for (key,val) in ziped:
print(key,val)
print('----second iter zip---------')
for (key,val) in ziped:
print(key,val)
----first iter zip---------
a 1
b 2
c 3
----second iter zip---------
避坑辦法:頭一次掉這坑,刷新zip認知,記住它。
4.6 再次測試
test_cases = get_test_cases(str_Examples)
auto_test(test_cases)
{'str1': 'ABCABC', 'str2': 'ABC'} ABC
{'str1': 'ABABAB', 'str2': 'ABAB'} AB
{'str1': 'LEET', 'str2': 'CODE'}
{'str1': 'ABCDEF', 'str2': 'ABC'}
'Accepted'
auto_test(get_test_cases(str_Examples))
{'str1': 'ABCABC', 'str2': 'ABC'} ABC
{'str1': 'ABABAB', 'str2': 'ABAB'} AB
{'str1': 'LEET', 'str2': 'CODE'}
{'str1': 'ABCDEF', 'str2': 'ABC'}
'Accepted'
5. 總結
step1: 在Jupyter notebook中編寫LeetCode問題解答代碼
class Solution: def method(self,**kwargs):
step2: 復制LeetCode中Example內容復制給變量str_Examples
step3: 使用get_test_cases獲取測試用例
step4: 調用auto_test進行自動測試,返回'Accepted'所有測試用例通過,'Wrong Answer'表示存在沒能通過的用例。
LeetCode問題Description中所有Example測試通過,即Run Code結果為'Accepted',並不代表Submit后的結果一定會為'Accepted'。相反,Run Code結果為'Wrong Answer',那么Submit后結果一定是'Wrong Answer'。因為Description中的Example只是部分示例,是Submit后驗證的測試用例集合的子集。
def get_test_cases(str_examples):
import re
params =re.split('Example.+?:',str_examples.replace('\n','').replace(' ',''))[1:]
cases_arg, cases_ans = [], []
for param in params:
kawargs = {}
args_,ans = param.split('Output:')
args_ = args_.replace('Input:','').strip().split(',')
# ans.replace('"','')
cases_ans.append(ans.replace('"',''))
for arg in args_:
key,val = arg.split("=")
# val.replace('"','')
kawargs[key] = val.replace('"','')
cases_arg.append(kawargs)
return zip(cases_arg, cases_ans)
def auto_test(test_cases):
f = getattr(Solution(), dir(Solution())[-1])
try:
for kwargs, answer in test_cases:
print(kwargs,answer)
assert f(**kwargs) == answer
return 'Accepted'
except Exception as e:
print(e)
return 'Wrong Answer'