调试
1.print()
用print()把可能有问题的变量打印出来,但是用print()最大的坏处是将来还得删掉它,想想程序里到处都是print(),运行结果也会包含很多垃圾信息
2.断言
凡是可能有问题的变量,都可以用断言(assert)来替代:
def foo(s):
n = int(s)
assert n != 0, 'n is zero!'
return 10 / n
def main():
foo('0')
assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。
如果断言失败,assert语句本身就会抛出AssertionError:
Traceback (most recent call last):
...
AssertionError: n is zero!
程序中如果到处充斥着assert,和print()相比也好不到哪去。
3.logging
和assert比,logging不会抛出错误,而且可以输出到文件:
import logging
logging.basicConfig(level=logging.INFO)
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)
输出
$ python3 err.py
INFO:root:n = 0
Traceback (most recent call last):
File "err.py", line 8, in <module>
print(10 / n)
ZeroDivisionError: division by zero
logging允许指定记录信息的级别,有debug,info,warning,error等几个级别,当指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。这样就可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方
4.pdb
启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。程序:# err.py
s = '0'
n = int(s)
print(10 / n)
以参数-m pdb启动后,pdb定位到下一步要执行的代码-> s = '0'。
$ python -m pdb err.py
> /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>()
-> s = '0'
输入命令l
来查看代码, 输入命令n
可以单步执行代码;任何时候都可以输入命令p 变量名
来查看变量;输入命令q
结束调试,退出程序:
(Pdb) l
1 # err.py
2 -> s = '0'
3 n = int(s)
4 print(10 / n)
(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(3)<module>()
-> n = int(s)
(Pdb) p s
'0'
(Pdb) q
通过pdb在命令行调试的方法理论上是万能的,但太麻烦了。
5.pdb.set_trace()
只需要import pdb,然后,在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点:# err.py
import pdb
s = '0'
n = int(s)
pdb.set_trace() # 运行到这里会自动暂停
print(10 / n)
运行代码,程序会自动在pdb.set_trace()暂停并进入pdb调试环境,可以用命令p查看变量,或者用命令c继续运行
6.IDE集成开发环境目前比较好的Python IDE有PyCharm。虽然用IDE调试起来比较方便,但是最后你会发现,logging才是终极武器。
异常
Python使用被称为异常 的特殊对象来管理程序执行期间发生的错误。每当发生让Python不知所措的错误时,它都会创建一个异常对象。如果你编写了处理该异常的代码,程序将继续运行;如果你未对异常进行处理,程序将停止,并显示一个traceback,其中包含有关异常的报告。异常是使用try-except 代码块处理的。try-except 代码块让Python执行指定的操作,同时告诉Python发生异常时怎么办。使用了try-except 代码块时,即便出现异常,程序也将继续运行:显示你编写的友好的错误消息,而不是令用户迷惑的traceback。
1.使用try-except 代码块
当你认为可能发生了错误时,可编写一个try-except 代码块来处理可能引发的异常。你让Python尝试运行一些代码,并告诉它如果这些代码引发了指定的异常,该怎么办
处理ZeroDivisionError 异常的try-except 代码块类似于下面这样:
try:
print(5/0)
except ZeroDivisionError:
print("You can't divide by zero!")
1.1try-except-else 代码块
print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")
while True:
first_number = input("\nFirst number: ")
if first_number == 'q':
break
second_number = input("Second number: ")
try:
answer = int(first_number) / int(second_number)
except ZeroDivisionError:
print("You can't divide by 0!")
else:
print(answer)
1.2FileNotFoundError 异常
filename = 'alice.txt'
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " + filename + " does not exist."
print(msg)
1.3分析文本
>>> title = "Alice in Wonderland"
>>> title.split()
['Alice', 'in', 'Wonderland']
方法split() 以空格为分隔符将字符串分拆成多个部分,并将这些部分都存储到一个列表中。结果是一个包含字符串中所有单词的列表,虽然有些单词可能包含标点。为计算 Alice in Wonderland 包含多少个单词,我们将对整篇小说调用split() ,再计算得到的列表包含多少个元素,从而确定整篇童话大致包含多少个单词:
filename = 'alice.txt'
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " + filename + " does not exist."
print(msg)
else:
# 计算文件大致包含多少个单词
words = contents.split()
num_words = len(words)
print("The file " + filename + " has about " + str(num_words) + " words.")
1.4使用多个文件
def count_words(filename):
"""计算一个文件大致包含多少个单词"""
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " + filename + " does not exist."
print(msg)
else:
# 计算文件大致包含多少个单词
words = contents.split()
num_words = len(words)
print("The file " + filename + " has about " + str(num_words) + " words.")
filename = 'alice.txt'
count_words(filename)
filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']
for filename in filenames:
count_words(filename)
The file alice.txt has about 29461 words.
Sorry, the file siddhartha.txt does not exist.
The file moby_dick.txt has about 215136 words.
The file little_women.txt has about 189079 words.
使用try-except 代码块提供了两个重要的优点:避免让用户看到traceback;让程序能够继续分析能够找到的其他文件。如果不捕获因找不到siddhartha.txt而引发的FileNotFoundError 异常,用户将看到完整的traceback,而程序将在尝试分析 Siddhartha 后停止运行——根本不分析 Moby Dick 和 Little Women 。
def count_words(filename):
"""计算一个文件大致包含多少个单词"""
try:
--snip--
except FileNotFoundError:
❶ pass
else:
--snip--
出现FileNotFoundError 异常时,将执行except 代码块中的代码,但什么都不会发生。这种错误发生时,不会出现traceback,也没有任何输出。pass 语句还可以充当占位符,它提醒你在程序的某个地方什么都没有做,并且以后也许要在这里做些什么
测试函数
使用Python模块unittest 中的工具来测试代码。学习编写测试用例,核实一系列输入都将得到预期的输出。你将看到测试通过了是什么样子,测试未通过又是什么样子,还将知道测试未通过如何有助于改进代码。你将学习如何测试函数和类,并将知道该为项目编写多少个测试。
1单元测试和测试用例
单元测试 用于核实函数的某个方面没有问题;测试用例 是一组单元测试,这些单元测试一起核实函数在各种情形下的行为都符合要求。良好的测试用例考虑到了函数可能收到的各种输入,包含针对所有这些情形的测试。全覆盖式测试 用例包含一整套单元测试,涵盖了各种可能的函数使用方式。对于大型项目,要实现全覆盖可能很难。通常,最初只要针对代码的重要行为编写测试即可,等项目被广泛使用时再考虑全覆盖。
要学习测试,得有要测试的代码。下面是一个简单的函数,它接受名和姓并返回整洁的姓名:
name_function.py
def get_formatted_name(first, last):
"""Generate a neatly formatted full name."""
full_name = first + ' ' + last
return full_name.title()
1能通过的测试
要为函数编写测试用例,可先导入模块unittest 以及要测试的函数,再创建一个继承unittest.TestCase 的类,并编写一系列方法对函数行为的不同方面进行测试。下面是一个只包含一个方法的测试用例,它检查函数get_formatted_name() 在给定名和姓时能否正确地工作:方法名必须以test_打头,这样它才会在我们运行test_name_function.py时自动运行
test_name_function.py
import unittest
from name_function import get_formatted_name
❶ class NamesTestCase(unittest.TestCase): #创建类NamesTestCase继承于unittest.TestCase 类,用于包含针对get_formatted_name()的单元测试
"""测试name_function.py"""
def test_first_last_name(self):
"""能够正确地处理像Janis Joplin这样的姓名吗?"""
❷ formatted_name = get_formatted_name('janis', 'joplin')
#将formatted_name 的值同字符串'Janis Joplin' 进行比较,如果相等,通过;如果不相等,则不通过
❸ self.assertEqual(formatted_name, 'Janis Joplin')
unittest.main()
unittest 类最有用的方法assertEqual() 之一:一个断言 方法。断言方法用来核实得到的结果是否与期望的结果一致。代码行unittest.main() 让Python运行这个文件中的测试。运行test_name_function.py时,得到的输出如下:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
第1行的句点表明有一个测试通过了。接下来的一行指出Python运行了一个测试,消耗的时间不到0.001秒。最后的OK 表明该测试用例中的所有单元测试都通过了。
2不能通过的测试
修改get_formatted_name() ,使其能够处理中间名,但这样做让这个函数无法正确地处理像Janis Joplin只有名和姓的姓名。name_function.py
def get_formatted_name(first, middle, last):
"""生成整洁的姓名"""
full_name = first + ' ' + middle + ' ' + last
return full_name.title()
运行程序test_name_function.py时,输出如下:
❶ E #指出测试用例中有一个单元测试导致了错误
======================================================================
❷ ERROR: test_first_last_name (__main__.NamesTestCase) #NamesTestCase 中的test_first_last_name() 导致了错误
----------------------------------------------------------------------
❸ Traceback (most recent call last):
File "test_name_function.py", line 8, in test_first_last_name
formatted_name = get_formatted_name('janis', 'joplin')
TypeError: get_formatted_name() missing 1 required positional argument: 'last'
#标准的traceback,它指出函数调用get_formatted_name('janis', 'joplin') 有问题,缺少一个必不可少的位置实参。
----------------------------------------------------------------------
❹ Ran 1 test in 0.000s
❺ FAILED (errors=1) #指出整个测试用例都未通过,因为运行该测试用例时发生了一个错误
测试未通过时怎么办
测试未通过时,如果你检查的条件没错,测试通过了意味着函数的行为是对的,而测试未通过意味着你编写的新代码有错。因此,测试未通过时,不要修改测试,而应修复导致测试不能通过的代码:检查刚对函数所做的修改,找出导致函数行为不符合预期的修改。
将中间名设置为可选的,可在函数定义中将形参middle 移到形参列表末尾,并将其默认值指定为一个空字符串。我们还要添加一个if 测试,以便根据是否提供了中间名相应地创建姓名:
name_function.py
def get_formatted_name(first, last, middle=''):
"""生成整洁的姓名"""
if middle:
full_name = first + ' ' + middle + ' ' + last
else:
full_name = first + ' ' + last
return full_name.title()
再次运行test_name_function.py:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
添加新测试
确定get_formatted_name() 又能正确地处理简单的姓名后,我们再编写一个测试,用于测试包含中间名的姓名。为此,我们在NamesTestCase 类中再添加一个方法:
import unittest
from name_function import get_formatted_name
class NamesTestCase(unittest.TestCase):
"""测试name_function.py """
def test_first_last_name(self):
"""能够正确地处理像Janis Joplin这样的姓名吗?"""
formatted_name = get_formatted_name('janis', 'joplin')
self.assertEqual(formatted_name, 'Janis Joplin')
def test_first_last_middle_name(self):
"""能够正确地处理像Wolfgang Amadeus Mozart这样的姓名吗?"""
❶ formatted_name = get_formatted_name(
'wolfgang', 'mozart', 'amadeus')
self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')
unittest.main()
再次运行test_name_function.py时,两个测试都通过了:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
测试类
1各种断言方法
Python在unittest.TestCase 类中提供了很多断言方法。断言方法检查你认为应该满足的条件是否确实满足。如果该条件确实满足,你对程序行为的假设就得到了确认,你就可以确信其中没有错误。如果你认为应该满足的条件实际上并不满足,Python将引发异常。
类的测试与函数的测试相似——你所做的大部分工作都是测试类中方法的行为,但存在一些不同之处,下面来编写一个类进行测试。来看一个帮助管理匿名调查的类:
survey.py
class AnonymousSurvey():
"""收集匿名调查问卷的答案"""
❶ def __init__(self, question):
"""存储一个问题,并为存储答案做准备"""
self.question = question
self.responses = []
❷ def show_question(self):
"""显示调查问卷"""
print(question)
❸ def store_response(self, new_response):
"""存储单份调查答卷"""
self.responses.append(new_response)
❹ def show_results(self):
"""显示收集到的所有答卷"""
print("Survey results:")
for response in responses:
print('- ' + response)
为证明AnonymousSurvey 类能够正确地工作,我们来编写一个使用它的程序:
from survey import AnonymousSurvey
#定义一个问题,并创建一个表示调查的AnonymousSurvey对象
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)
#显示问题并存储答案
my_survey.show_question()
print("Enter 'q' at any time to quit.\n")
while True:
response = input("Language: ")
if response == 'q':
break
my_survey.store_response(response)
# 显示调查结果
print("\nThank you to everyone who participated in the survey!")
my_survey.show_results()
输出:
What language did you first learn to speak?
Enter 'q' at any time to quit.
Language: English
Language: Spanish
Language: English
Language: q
Thank you to everyone who participated in the survey!
Survey results:
- English
- Spanish
- English
AnonymousSurvey 类可用于进行简单的匿名调查。假设我们将它放在了模块survey 中,并想进行改进:让每位用户都可输入多个答案;编写一个方法,它只列出不同的答案,并指出每个答案出现了多少次;再编写一个类,用于管理非匿名调查。
进行上述修改存在风险,可能会影响AnonymousSurvey 类的当前行为。例如,允许每位用户输入多个答案时,可能不小心修改了处理单个答案的方式。要确认在开发这个模块时没有破坏既有行为,可以编写针对这个类的测试。
test_survey.py
import unittest
from survey import AnonymousSurvey
❶ class TestAnonmyousSurvey(unittest.TestCase):
"""针对AnonymousSurvey类的测试"""
❷ def test_store_single_response(self):
"""测试单个答案会被妥善地存储"""
question = "What language did you first learn to speak?"
❸ my_survey = AnonymousSurvey(question)
my_survey.store_response('English')
❹ self.assertIn('English', my_survey.responses)
unittest.main()
运行test_survey.py时,测试通过了:
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
只能收集一个答案的调查用途不大。下面来核实用户提供三个答案时,它们也将被妥善地存储。为此,我们在TestAnonymousSurvey 中再添加一个方法:
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
"""针对AnonymousSurvey类的测试"""
def test_store_single_response(self):
"""测试单个答案会被妥善地存储"""
--snip--
def test_store_three_responses(self):
"""测试三个答案会被妥善地存储"""
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)
❶ responses = ['English', 'Spanish', 'Mandarin']
for response in responses:
my_survey.store_response(response)
❷ for response in responses:
self.assertIn(response, my_survey.responses)
unittest.main()
运行test_survey.py时,两个测试(针对单个答案的测试和针对三个答案的测试)都通过了:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
方法setUp()与tearDown()
可以在单元测试中编写两个特殊的setUp()
和tearDown()
方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。setUp()
和tearDown()
方法有什么用呢?设想你的测试需要启动一个数据库,这时,就可以在setUp()
方法中连接数据库,在tearDown()
方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码.
unittest.TestCase 类包含方法setUp() ,让我们只需创建一次实例对象,并在每个测试方法中使用它们。如果你在TestCase 类中包含了方法setUp() ,Python将先运行它,再运行各个以test_打头的方法。这样,在你编写的每个测试方法中都可使用在方法setUp() 中创建的对象了。
使用setUp() 来创建一个调查对象和一组答案,供方法test_store_single_response() 和test_store_three_responses() 使用:
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
"""针对AnonymousSurvey类的测试"""
def setUp(self):
"""
创建一个调查对象和一组答案,供使用的测试方法使用
"""
question = "What language did you first learn to speak?"
❶ self.my_survey = AnonymousSurvey(question)
❷ self.responses = ['English', 'Spanish', 'Mandarin'] #存储这两样东西的变量名包含前缀self,因此可在这个类的任何地方使用
def test_store_single_response(self):
"""测试单个答案会被妥善地存储"""
self.my_survey.store_response(self.responses[0])
self.assertIn(self.responses[0], self.my_survey.responses)
def test_store_three_responses(self):
"""测试三个答案会被妥善地存储"""
for response in self.responses:
self.my_survey.store_response(response)
for response in self.responses:
self.assertIn(response, self.my_survey.responses)
unittest.main()
再次运行test_survey.py时,这两个测试也都通过了。如果要扩展AnonymousSurvey ,使其允许每位用户输入多个答案,这些测试将很有用。修改代码以接受多个答案后,可运行这些测试,确认存储单个答案或一系列答案的行为未受影响。
测试自己编写的类时,方法setUp() 让测试方法编写起来更容易:可在setUp() 方法中创建一系列实例并设置它们的属性,再在测试方法中直接使用这些实例。相比于在每个测试方法中都创建实例并设置其属性,这要容易得多。
注意
运行测试用例时,每完成一个单元测试,Python都打印一个字符:测试通过时打印一个句点;测试引发错误时打印一个E ;测试导致断言失败时打印一个F。
参与工作量较大的项目时,你应对自己编写的函数和类的重要行为进行测试。这样你就能够更加确定自己所做的工作不会破坏项目的其他部分,你就能够随心所欲地改进既有代码了。如果不小心破坏了原来的功能,你马上就会知道,从而能够轻松地修复问题。相比于等到不满意的用户报告bug后再采取措施,在测试未通过时采取措施要容易得多。
练习:对Student类编写单元测试,结果发现测试不通过,请修改Student类,让测试通过:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def get_grade(self):
if self.score >= 80 and self.score <=100:
return 'A'
elif self.score >= 60 and self.score<80:
return 'B'
elif self.score>=0 and self.score<60:
return 'C'
raise ValueError(r"score must be between 0~100 '%s'" % self.score)
class TestStudent(unittest.TestCase):
def test_80_to_100(self):
s1 = Student('Bart', 80)
s2 = Student('Lisa', 100)
self.assertEqual(s1.get_grade(), 'A')
self.assertEqual(s2.get_grade(), 'A')
def test_60_to_80(self):
s1 = Student('Bart', 60)
s2 = Student('Lisa', 79)
self.assertEqual(s1.get_grade(), 'B')
self.assertEqual(s2.get_grade(), 'B')
def test_0_to_60(self):
s1 = Student('Bart', 0)
s2 = Student('Lisa', 59)
self.assertEqual(s1.get_grade(), 'C')
self.assertEqual(s2.get_grade(), 'C')
def test_invalid(self):
s1 = Student('Bart', -1)
s2 = Student('Lisa', 101)
with self.assertRaises(ValueError):
s1.get_grade()
with self.assertRaises(ValueError):
s2.get_grade()
if __name__ == '__main__':
unittest.main()
练习
class Dict(dict):
def __init__(self, **kw):
super().__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
import unittest
from mydict import Dict
class TestDict(unittest.TestCase):
def test_init(self):
d = Dict(a=1, b='test')
self.assertEqual(d.a, 1)
self.assertEqual(d.b, 'test')
self.assertTrue(isinstance(d, dict))
def test_key(self):
d = Dict()
d['key'] = 'value'
self.assertEqual(d.key, 'value')
def test_attr(self):
d = Dict()
d.key = 'value'
self.assertTrue('key' in d)
self.assertEqual(d['key'], 'value')
def test_keyerror(self):
d = Dict()
with self.assertRaises(KeyError):
value = d['empty']
def test_attrerror(self):
d = Dict()
with self.assertRaises(AttributeError):
value = d.empty
另一种重要的断言就是期待抛出指定类型的Error,比如通过d['empty']
访问不存在的key时,断言会抛出KeyError
:
with self.assertRaises(KeyError):
value = d['empty']
而通过d.empty
访问不存在的key时,我们期待抛出AttributeError
:
with self.assertRaises(AttributeError):
value = d.empty
小结:
单元测试可以有效地测试某个程序模块的行为,是未来重构代码的信心保证。
单元测试的测试用例要覆盖常用的输入组合、边界条件和异常。
单元测试代码要非常简单,如果测试代码太复杂,那么测试代码本身就可能有bug。
单元测试通过了并不意味着程序就没有bug了,但是不通过程序肯定有bug。
文件测试Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用...
表示中间一大段烦人的输出。
让我们用doctest来测试上次编写的Dict
类:
# mydict2.py
class Dict(dict):
'''
Simple dict but also support access as x.y style.
>>> d1 = Dict()
>>> d1['x'] = 100
>>> d1.x
100
>>> d1.y = 200
>>> d1['y']
200
>>> d2 = Dict(a=1, b=2, c='3')
>>> d2.c
'3'
>>> d2['empty']
Traceback (most recent call last):
...
KeyError: 'empty'
>>> d2.empty
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'empty'
'''
def __init__(self, **kw):
super(Dict, self).__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
if __name__=='__main__':
import doctest
doctest.testmod()
运行
python mydict2.py
:
$ python mydict2.py
什么输出也没有。这说明我们编写的doctest运行都是正确的。如果程序有问题,比如把
__getattr__()
方法注释掉,再运行就会报错:
$ python mydict2.py
**********************************************************************
File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 10, in __main__.Dict
Failed example:
d1.x
Exception raised:
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'x'
**********************************************************************
File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 16, in __main__.Dict
Failed example:
d2.c
Exception raised:
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'c'
**********************************************************************
1 items had failures:
2 of 9 in __main__.Dict
***Test Failed*** 2 failures.
练习:
def fact(n):
'''
Calculate 1*2*...*n
>>> fact(1)
1
>>> fact(10)
3628800
>>> fact(-1)
Traceback (most recent call last):
...
ValueError
'''
if n < 1:
raise ValueError()
if n == 1:
return 1
return n * fact(n - 1)
if __name__ == '__main__':
import doctest
doctest.testmod()