在本节我们将学习完毕Python的基础语法。
面向对象编程是最有效的编程方法之一,在面向对象编程时,可以编写表示现实世界中的实物和情境的类,并给予这些类来创建对象。在编写类时,我们往往会定义一大类对象通有的行为。在基于类创建对象时,每个对象自动具备这种通用行为,然后再根据需要赋予每个对象独特的个性。
根据类来创建对象被称为实例化,这让我们能够使用类的实例。
首先我们来创建一个Dog类:
class Dog():
"""依次模拟小狗的简单尝试"""
def __init__(self, name, age):
"""初始化姓名,年龄"""
self.name = name
self.age = age
def sit(self):
"""模拟小狗被命令时蹲下"""
print(self.name.titile() + " is now sitting")
def eat(self):
"""模拟小狗吃东西"""
print(self.name.titile() + " is now eating")
在第一行,我们定义了一个名为Dog的类,在Python中类的名字的首字母大写。后边跟上括号和冒号,冒号后边具有缩进的被称为类的内容。
类中的函数被称为方法,我们学过的有关函数的都适用于方法,目前唯一有区别的地方在于函数和方法的调用形式不同。方法__init__()
是一个特殊的方法,每当我们创建Dog类的一个实例时,Python都会自动运行他它。这个方法名称开头和结尾各有两个下划线,意在和其他方法进行区分。
方法__init__()
定义成了包含三个形参:self、name和age,在这个方法定义中self必不可少,且必须放在所有形参的前边,Python在调用方法__init__()
来创建实例时,会自动传入参数self,每个与类相关联的方法在调用时也会自动传递实参self,它能够让实例访问类中的属性和方法。
就用法来说,感觉于Java的构造方法非常类似
第五句和第六句定义的两个变量都有前缀self,以self为前缀的变量可供类中的所有方法使用,我们还可以通过类的任何实例来访问这些变量。self.name = name
获取到存储在形参中的值,然后将其保存在name中。
此外该类还定义了两个方法,sit()
和eat()
,这些方法都不需要额外的信息,因为它只有一个形参self。我们后面将创建的实例能访问这些方法。
我们使用上述的类来讲解如何创建实例:
"""
在下边的代码中我们创建了一个名字为willie,年龄为6的小狗,遇到这个代码,Python使用实参willie和6调用Dog类中的方法__init__(),这个方法创建了一个特定小狗的示例,并使用我们提供的属性值来设置name和age。
"""
my_dog = Dog("willie", 6)
#我们可以通过类名.属性名的方式来访问实例中的属性、
print(my_dog.name) #willie
print(my_dog.age) #6
#我们同样可以使用句点表示法来调用Dog类中定义的任何方法
my_dog.sit() #Willie is now sitting
my_dog_eat() #Willie is now eating
#可以按照需求创建任意数量的实例,下边再创建一个your_dog的实例,这个实例能像my_dog一样使用属性方法。
your_dog = Dog("lucy", 5)
下边先编写一个汽车类,其中包含了汽车的有关信息
class Car:
"""一个汽车类"""
def __init__(self, make, module, year):
"""描述汽车的初始属性"""
self.make = make
self.module = module
self.year = year
self.odometer_reading = 0
def get_descriptive_name(self):
"""返回汽车的描述信息"""
name = str(self.year) + " " + self.make + " " + self.module
return name
def read_odometer(self):
print(self.odometer_reading)
类中的每个属性都必须由初始值,哪怕这个值是0或者是空字符串,如果想要给某个属性设置默认值,可以在__init__
方法中设置,这个时候我们就不需要为这个属性提供为其提供初始值的形参了。如上属性odometer(里程),我们设置其属性为0,就没有在__init__
方法中为其设置形参。
这时候我们可以先尝试创建一个汽车的实例,并且使用各个属性和方法:
my_car = Car("Audi", "A4", 2016)
print(my_car.get_descriptive_name()) #2016 Audi A4
my_car.read_odometer() #0
可以使用三种方法修改属性的值:直接通过实例修改;通过方法进行设置;通过方法增加特定的值。
要修改属性的值,最好的办法是通过实例直接访问它,下边的代码将里程(odometer)属性的值设置为50:
my_car = Car("Audi", "A4", 2016)
my_car.odometer_reading = 50
my_car.read_odometer() #50
如果我们设置了修改属性的方法,那么我们就不需要通过实例俩访问属性,直接将值传递给一个方法,并在内部对方法进行更新,我们在上边的Car类中增加新的update_odometer函数来实现更新里程的值,同时我们对该方法进行扩展来禁止有人将里程表的数回调:
def update_odometer(self, mileage):
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("你不能回调里程数")
my_car = Car("Audi", "A4", 2016)
my_car.update_odometer(50)
my_car.read_odometer() #50
有时候需要将属性值递增特定的量,比如我们买了一辆二手车,从购买到登记增加了100公里的里程,我们可以使用increase_odometer函数,来实现里程数的递增:
def increase_odometer(self, miles):
self.odometer_reading += miles
my_car = Car("Audi", "A4", 2016)
my_car.update_odometer(10000)
my_car.read_odometer() #10000
my_car.increase_odometer(100)
my_car.read_odometer() #10100
编写类时,我们并非总要从空白开始,如果要编写的类于另一个已经有的类很相似,我们可以采用继承的方法。一个类继承另一个类时,他会获得另一个类的所有属性和方法,原有的类被称为父类,而新类被称为子类。子类继承其父类的所有属性和方法,同时还可以编写自己的属性和方法。
在上边的Car类的基础上,我们创建子类电动汽车Electric_Car,它具备Car类的所有属性和方法:
class Car:
"""一个汽车类"""
def __init__(self, make, module, year):
"""描述汽车的初始属性"""
self.make = make
self.module = module
self.year = year
self.odometer_reading = 0
def get_descriptive_name(self):
"""返回汽车的描述信息"""
name = str(self.year) + " " + self.make + " " + self.module
return name
def read_odometer(self):
print(self.odometer_reading)
def update_odometer(self, mileage):
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("你不能回调里程数")
def increase_odometer(self, miles):
self.odometer_reading += miles
class Electric_Car(Car):
"""电动汽车类,继承了Car类"""
def __init__(self, make, module, year):
"""c初始化父类属性"""
super().__init__(make, module, year)
my_Electric_Car = Electric_Car("tesla", "s", "2018")
print(my_Electric_Car.get_descriptive_name()) #2018 tesla s
在创建子类时,父类必须包含在当前文件中,并且要位于子类的前边在第29句,我们定义子类时一定要在括号里边指明父类的名称,表示继承关系。
第33句的super()
是一个特殊函数,帮助Python将子类和父类关联起来,这行代码让Electric_Car调用父类的__init__
方法,能够保证Electric_Car的实例包含父类的全部属性。
在后边我们对继承结果进行了测试,可以发现,我们能够使用父类的属性,并且使用其中的方法。
在一个类继承另一个类后,我们可以在子类中添加属性和方法以此来和父类进行区分,如下,我们在子类中创建了电动汽车的特有属性,battery(电量),并且增加了特有方法打印当前的电量。
class Electric_Car(Car):
"""电动汽车类,继承了Car类"""
def __init__(self, make, module, year):
"""c初始化父类属性"""
super().__init__(make, module, year)
self.battery = 80
def describe_battery(self):
print("当前的电量是: " + self.battery)
my_Electric_Car = Electric_Car("tesla", "s", "2018")
print(my_Electric_Car.get_descriptive_name())
my_Electric_Car.describe_battery() #当前的电量是: 80
在创建电动汽车子类时,我们可以根据需要添加任意数量的属性和方法,但是如果有属性或者方法是父类或者子类共有的时候,我们就用该将其添加到父类中。
对于父类的方法,只要不符合子类的行为,都可以对其进行重写。为此,可以在子类中定义一个方法,它要与重写的父类方法同名,这样在调用时,Python不会考虑这个父类方法,而只关注在子类中定义的相应的方法。比如在下边我们重写Car类的get_descriptive_name()
方法:
def get_descriptive_name(self):
name="制造时间是:"+self.year+"\t制造商是:"+self.make+"\t型号是:"+self.module
return name
my_Electric_Car = Electric_Car("tesla", "s", "2018")
print(my_Electric_Car.get_descriptive_name())
#制造时间是:2018 制造商是:tesla 型号是:s
如果有人对电动汽车调用get_descriptive_name()
方法,Python会忽略Car类的get_descriptive_name()
方法,转而使用Electric_Car类中的get_descriptive_name()
方法。
在用代码模拟实物的时候,我们会发现自己给类添加的细节越来越多,属性和方法也越来越多,在这种情况下,可以将类的一部分作为一个独立的类提取出来。
例如在电动汽车类中,我们将描述电瓶的类提取出来,放到名为Battery的类中,并将其实例作为Electric_Car类的一个属性。
class Battery:
"""模拟电动车电瓶"""
def __init__(self, battery_size=70):
self.battery_size = battery_size
def describe_battery(self):
"""打印电瓶的描述信息"""
print("电瓶的容量是:" + str(self.battery_size))
class Electric_Car(Car):
"""电动汽车类,继承了Car类"""
def __init__(self, make, module, year):
"""c初始化父类属性"""
super().__init__(make, module, year)
self.battery = Battery()
my_Electric_Car = Electric_Car("tesla", "s", "2018")
print(my_Electric_Car.get_descriptive_name()) #2018 tesla s
my_Electric_Car.battery.describe_battery() #电瓶的容量是:70
在这里我们定义了一个名为Battery的新类,__init__
方法包括了设置默认值的形参battery_size,如果没有给他提供值他就会使用默认值70。
在Electric_Car类中,我们定义了self.battery
的属性,这个代码让Python创建一个新的Battery实例,并将实例存储在属性self.battery
中。从最后一个输出语句我们可以看出,我们使用的是my_Electric_Car.battery.describe_battery()
这个句点表示法语句进行输出,先使用Electric_Car的属性battery调用Battery类的实例,再通过实例调用Battery类的方法,这也证明了我们将实例用作属性。
Python允许将类存储在模块,然后再主程序中导入模块。
首先先创建一个只包含单个类的模块,将其命名为car.py
,下边是该模块中的主要代码:
class Car:
"""一个汽车类"""
def __init__(self, make, module, year):
"""描述汽车的初始属性"""
self.make = make
self.module = module
self.year = year
self.odometer_reading = 0
def get_descriptive_name(self):
"""返回汽车的描述信息"""
name = str(self.year) + " " + self.make + " " + self.module
return name
def read_odometer(self):
print(self.odometer_reading)
def update_odometer(self, mileage):
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("你不能回调里程数")
def increase_odometer(self, miles):
self.odometer_reading += miles
class Electric_Car(Car):
"""电动汽车类,继承了Car类"""
def __init__(self, make, module, year):
"""c初始化父类属性"""
super().__init__(make, module, year)
self.battery = 80
def describe_battery(self):
print("当前的电量是: " + self.battery)
之后我们创建一个新的文件,命名为my_car.py
:
from car import Car
my_new_car = Car("audi", "a4", 2016)
print(my_new_car.get_descriptive_name()) #2016 audi a4
my_new_car.odometer_reading = 23
my_new_car.read_odometer() #23
上述第一句话,我们从car.py
中导入Car类,之后就可以使用Car类了,就像再这个文件中定义一样。
可以根据需要在程序文件中导入任意数量的类,我们在car.py
中存储了两个类,一个是Car类,一个是Electric_Car类,现在我们可以将两个类导出:
from Variable import Car, Electric_Car
my_new_car = Car("audi", "a4", 2016)
print(my_new_car.get_descriptive_name()) #2016 audi a4
my_new_car.odometer_reading = 23 #23
my_new_car.read_odometer()
my_battery_car = Electric_Car("tesla", "s", "2017")
print(my_battery_car.get_descriptive_name()) #2017 tesla s
my_battery_car.describe_battery() #当前的电量是: 80
还可以直接导入整个模块,再使用句点表示法访问需要的类,这种导入方式很简单,代码也容易阅读。
import Variable
my_new_car = Variable.Car("Audi", "A4", "2016")
print(my_new_car.get_descriptive_name()) #2016 Audi A4
my_new_car.odometer_reading = 23
my_new_car.read_odometer() #23
导入模块中的所有类:
from car import *
不推荐使用上述代码导入,因为我们在导入时,往往希望知道程序具体使用了哪些类,这种方式不仅没有指出导入了哪些类,还可能引起类名称的困惑。
Python标准库是一组模块,在安装Python时都会包含。现在简单介绍模块collections中的一个类——OrderedDict,该类的实例与字典几乎相同,区别在于该类记录了键值对的添加顺序,确保输出时是按照键值对的添加顺序进行输出:
from collections import OrderedDict
favorite_language = OrderedDict()
favorite_language["jane"] = "C"
favorite_language["jack"] = "Java"
favorite_language["TOM"] = "Python"
favorite_language["Maria"] = "C"
for name, language in favorite_language.items():
print(name.title() + " " + language)
'''
Jane C
Jack Java
Tom Python
Maria C
'''
需要注意的是,这里没有使用花括号,而是使用OrderedDict()
来创建一个空白字典。
类名应采用驼峰命名法,即类名中的每个单词的首字母大写,而不使用下划线。
对于每个类,都应该在后边加上文档注释,说明类的作用,每个模块也都应该包含一个文档字符串,对其中的类的作用进行描述。
在类中往往使用一个空行分隔方法,而在模块中用两个空行分隔类。
需要同时导入标准库和自己的模块时,先编写标准库的import语句,再添加一个空行,编写自己导入的import语句。
文本文件可以存储大量的信息,我们可以通过读取文件来分析和修改存储在文件中的信息,也可以重新重新设置数据的格式并将其写入文件。
如果要使用文本文件中的信息,首先要将信息读取在内存中,为此我们既可以一次性读取文件的全部内容,也可以按照一次一行的方式读取。
要读取文件,首先需要一个包含几行文本的文件。首先来创建一个文件,它包含精确到小数点后30位的圆周率,且在小数点后每10位出换行。
3.1415926535
8979323846
2643383279
接下来编写代码,打开并读取文件,将其内容显示到屏幕上。
with open("pi.txt") as file_object:
contents = file_object.read()
print(contents.rstrip())
'''
3.1415926535
8979323846
2643383279
'''
如果想要以任何方式使用文件,都必须先打开文件。函数open()
接收一个参数:要打开的文件的名称,Python会在当前执行的文件的的目录中查找指定文件(运行open函数的代码再哪个文件夹,就会在哪个文件夹搜索指定文件)。。函数open()
会返回一个表示文件的对象,Python将这个对象存储在后边的变量中。
关键字with在我们不需要访问文件后将其关闭,在有了pi.txt
文件的对象后,我们就可以使用方法read()
读取文件的全部内容,并将其作为一个长长的字符串存在contents中。
上述代码在输出时使用了contents.rstrip()
,这是因为read()
函数在读到文件末尾时会返回一个空字符串,而将这个空字符串显示出来就是一个空格。
上边我们使用ope()
函数时,打开的是当前执行文件所在目录的文件,但是有时候,我们可能要打开不再当前目录中的文件,这个时候我们推荐使用绝对路径来传递文件的路径,在windows环境下,如下:
with open("E:\\PythonLearning\\pi.txt") as file_object:
contents = file_object.read()
print(contents)
这里推荐用两个反斜杠来分隔路径,因为一个反斜杠可能会和后边的路径或者文件名组成转义字符,导致程序出现错误。
读取文件时,常常需要检查其中一行,比如在其中查找特定的信息,或者修改其中的文本。要以按照每一行的方式进行检查,可以对文件对象使用for循环:
with open("E:\\PythonLearning\\pi.txt") as file_object:
for line in file_object:
print(line)
'''
3.1415926535
8979323846
2643383279
''''''
我们可以看出每一行输出之后都会再多出来一个空行,这是因为每行的末尾都有一个看不见的换行符,而print语句会加上这个换行符,因此每行末尾都有两个换行符,一个来自print语句,一个来自文件本身,要消除空白行,再输出时可以使用rstrip()
with open("E:\\PythonLearning\\pi.txt") as file_object:
for line in file_object:
print(line.rstrip())
'''
3.1415926535
8979323846
2643383279
'''
在使用关键字with时,open()
返回的文件对象只能在with代码块中使用,如果要在with代码块外访问文件内容,可以将文件内容存储在列表中:
with open("E:\\PythonLearning\\pi.txt") as file_object:
lines = file_object.readlines()
for line in lines:
print(line.rstrip())
方法readlines()
从文件中读取每一行并将其存储在列表中,之后该列表被存储到变量lines中。
保存数据最简单的方式之一是将其写入文件,通过将输出写入文件,即便关闭程序终端窗口,这些输出也依然存在。
要将文本写入文件,在调用open()
时提供另一个实参,告诉Python你要打开的文件。
#注意修改文件路径
with open("E:\\PythonLearning\\pi.txt", "w") as file_object:
file_object.write("I love Python")
在这里调用open时提供了两个实参,第一个实参是需要写入的文件的名称;第二个实参"w"告诉Python我们要以写入的方式打开文件。打开文件时可以指定读取模式(“r”),写入模式(“w”),附加模式(“a”),以及能够读取和写入文件的模式(“r+”),如果我们省略了模式实参,Python将以只读模式打开文件。
如果要写入的文件不存在,函数open()
将自动创建它,然而,以(“w”)模式打开文件时,如果指定的文件已经存在,Python将在返回文件对象前清空该文件。
Python只能将字符串写入文本文件,如果想要将数值数据存储到文本文件,需要先使用
str()
函数将其转换为字符串格式
函数write()
不会再写入的文本末尾添加换行符,因此如果写入多行时没有指定换行符,文件可能和我们预想的排版不太一样。如果要让字符串单独占一行,需要在write()
语句中包含换行符。
#注意修改文件路径
with open("E:\\PythonLearning\\pi.txt", "w") as file_object:
file_object.write("I love Python\n")
file_object.write("I also love Java\n")
我们还可以使用空格,制表符和空行来设置不同的输出样式。
如果要给文件添加内容,而不是覆盖原有的内容,可以使用附加模式打开文件,Python不会再返回对象前清空文件,而我们写入到文件的内容都会添加到文件末尾,如果指定的文件不存在,Python会为你创建一个空文件。
#注意修改文件路径
with open("E:\\PythonLearning\\pi.txt", "a") as file_object:
file_object.write("I love Php\n")
file_object.write("I also love go\n")
Python使用称为异常的特殊对象来管理程序执行期间发生的错误,每当程序出现错误时,它都会创建一个异常对象,如果我们编写了处理异常的代码,那么程序会继续运行,否则程序就会停止,返回一个traceback。
ZeroDivisionError
异常我们知道一个数字不能除以0,但是我们如果还要让Python这么做呢:
print(5 / 0)
'''
Traceback (most recent call last):
File "E:/PythonLearning/Test.py", line 1, in
print(5 / 0)
ZeroDivisionError: division by zero
'''
在上述的traceback中,第七行指出了错误是ZeroDivisionError
是一个异常对象,Python无法按照我们的要求做时,就会创建这种对象,并指出发生了哪种异常,这时候我们就可以根据反馈修改代码。
使用try-except代码块
处理ZeroDivisionError
异常的try-except代码类似于下边这样:
try:
print(5 / 0)
except ZeroDivisionError:
print("0不能作为除数")
我们将导致错误的代码放在一个try的代码块中,如果try中的代码运行起来没有问题,Python将会跳过except代码块,如果try中的代码块导致了错误,并且错误的类型和except声明的错误类型相同,那么就会运行except中的代码。
依赖try代码成功执行的代码都应该放在else代码中:
print("请以此输入两个数,并对他们执行除法")
print("输入q来退出程序")
while True:
first_number = input("请输入被除数:")
if first_number == "q":
break
second_number = input("请输入除数:")
if second_number == "q":
break
try:
answer = int(first_number) / int(second_number)
except ZeroDivisionError:
print("你不能除以0")
else:
print(answer)
"""
请以此输入两个数,并对他们执行除法
输入q来退出程序
请输入被除数:9
请输入除数:0
你不能除以0
请输入被除数:9
请输入除数:3
3.0
请输入被除数:q
"""
try-except-else代码块的工作原理大致如下:Python尝试执行try中的代码块,只有可能引发异常的语句才会放在try语句中,有一些仅在try代码块成功运行时才需要运行的代码应放在else代码块中。except代码块告诉Python,如果运行时出现了指定的异常应该怎么办。
FileNotFoundError
异常使用文件时,一种常见的问题就是找不到文件,你要使用或者查找的文件可能在其他地方,文件名不正确,或者文件根本不存在,这种情况下我们也可以使用和try-exception解决。
我们并非每次捕获到异常时都要告诉用户,有时候我们希望出现错误时,程序会像什么都没有发生一样继续运行。可以正常编写try代码块,但是在except代码块中明确的告诉Python什么都不要做。Python中有一个pass语句,可以在代码块中使用它告诉Python什么也不做。
try:
answer = 5 / 2
answer_1 = 5 / 0
except ZeroDivisionError:
pass
如上,这时候程序在出现异常时不会有任何提示信息,在有时候pass会很有用。
pass语句还充当占位符的作用,它会提醒你在程序的某个地方什么没有做,并且以后需要在这里做些什么。
我们在多数情况下都会需要用户输入某些信息,然后我们再将用户的信息存储在列表和字典等数据结构中,用户关闭程序时,我们几乎总是要保存他们提供的信息,一种简单的方式是通过使用模块json来保存信息:
模块json可以将简单的Python数据结构存储到文件中,并在程序再次运行时加载文件中的数据。
json.dump()
和json.load()
我们将编写一个存储一组数字的简短程序,并在程序再次运行时加载该文件中的数据。
import json
numbers = [2, 4, 6, 8, 10]
file_name = "number.json"
with open(file_name, "w") as f_obj:
json.dump(numbers, f_obj)
通常使用文件扩展名.json
来指出文件存储的数据位json格式。接下来我们以写入模式打开文件,让json能够将数据写入其中,再第七行,我们使用函数json.dump()
将数字列表存储到文件中。
这个程序没有输出,但是我们可以打开文件number.json
来查看文件中的内容。
import json
file_name = "number.json"
with open(file_name) as f_obj:
numbers = json.load(f_obj)
print(numbers) #[2, 4, 6, 8, 10]
这里我们需要确保我们读取的是之前写入的文件,我们使用函数json.load()
加载存储在numbers.json
中的信息,并将其存储在numbers,然后对numbers进行打印。
有时候代码能够正确的运行,但是我们能够对代码做进一步的改进——将代码分为一些列完成具体工作的函数,这的过程被称为重构,重构可以让代码更加清晰,易于扩展。
具体来说可以将同时实现多个功能的程序分割为不同的函数,每个函数实现一个功能,最终将函数合并实现原有的功能。
编写函数或者类时,还可以为其编写测试。通过测试,可以确定代码面对各种输入都能够按要求进行工作。
Python标准库中的模块unittest
提供了代码测试的工具。单元测试用于核实函数的某个方面没有问题,测试用例是一组单元测试,用于核实函数在一系列下的行为都符合要求。全覆盖测试用例包括一整套单元测试,涵盖了各种可能的函数使用方式。
首先我们指定需要测试的函数,并且将该函数保存在name_function.py
中:
def get_formatted_name(first, last):
"""格式化的输出姓名"""
full_name = first + " " + last
return full_name.title()
接下来编写测试函数:
import unittest
from name_function import get_formatted_name
class NameTestCase(unittest.TestCase):
"""测试name_function.py"""
def test_first_last_name(self):
"""能正确处理类似Talor Swift这样的名字吗"""
formatted_name = get_formatted_name("talor", "Swift")
self.assertEqual(formatted_name, " Talor Swift")
unittest.main()
'''
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
'''
首先我们导入了模块unittest
和要测试的函数,在第五行,我们创建了一个类,用于包含一系列针对get_formatted_name()
的测试,这个类可以随意命名,但是最好让他看起来和要测试的函数有关,并包含Test字样,同时,该类必须继承unittest.TestCase
,这样Python才知道如何运行编写的测试。
NameTestCase
只有一个方法,用于测试get_formatted_name()
,我们将这个方法命名为test_first_last_name()
,在运行这个Python文件时,所有以test开头的方法都会自动运行,在这个方法中,我们调用了要测试的函数,并存储了要测试的返回值。在实例中,我们用 formatted_name
的值和Talor Swift
进行比较。
在第十一行,我们使用了unittest
类最重要的功能之一,断言方法。断言方法是用来核实得到的结果与期望是否一致,并返回比较结果。
代码行unittest.main()
让Python运行这个文件中的测试。
接下来我们修改get_formatted_name()
使其能够处理中间名:
def get_formatted_name(first, middle, last):
"""格式化的输出姓名"""
full_name = first + " " + middle + " " + last
return full_name.title()
'''
E
======================================================================
ERROR: test_first_last_name (__main__.NameTestCase)
能正确处理类似Talor Swift这样的名字吗
----------------------------------------------------------------------
Traceback (most recent call last):
File "E:/PythonLearning/Test.py", line 10, in test_first_last_name
formatted_name = get_formatted_name("talor", "Swift")
TypeError: get_formatted_name() missing 1 required positional argument: 'last'
'''
但是修改完我们再对代码进行测试时,会发现代码出现错误了。
在上述的第8行,E提示测试用例中有一个单元测试出现了错误。
第10行看到NameTestCase
中的test_first_last_name
导致了错误,测试用例包含众多测试单元时,知道哪个测试未通过至关重要。
接下来我们看到了标准的traceback,告诉我们调用get_formatted_name()
有问题,因为缺少一个实参。
如果测试通过了,意味着函数的行为是正确的。但是当测试没有通过时,意味着我们编写的新的代码有问题,我们应该修复导致测试不通过的代码,而不是去修改测试。
Python在unittest
中提供了很多断言方法,断言方法用于检测你认为应该满足的条件是否满足。如果确实满足了,我们就可以认为程序没有错误,如果没有满足,程序就会发生异常。
下表给出了常用的6个断言方法,使用这些方法可以核实返回的值是否等于或者不等于预期的值,返回的值为True或False,返回的值是否在列表中:
方法 | 用途 |
---|---|
assertEqual(a, b) |
核实 a == b |
assertNotEqual(a, b) |
核实 a != b |
assertTrue(x) |
核实x为True |
assertFalse(x) |
核实x为False |
assertIn(item, list) |
核实item在list中 |
assertNotIn(item, list) |
核实item不再list中 |
首先我们创建一个帮助管理匿名调查的类,并将类存储在模块survey中:
class AnonymousSurvey:
"""收集匿名调查的问卷"""
def __init__(self, question):
"""存储一个问题,并未存储答案做准备"""
self.question = question
self.responses = []
def show_question(self):
"""显示调查问卷"""
print(self.question)
def store_response(self, new_response):
"""收集单份问卷"""
self.responses.append(new_response)
def show_results(self):
"""显示收集到的所有问卷"""
print("Survey results: ")
for response in self.responses:
print("- " + response)
接下来验证该类能正确运行:
from survey import AnonymousSurvey
question = "你喜欢说什么语言"
my_survey = AnonymousSurvey(question)
my_survey.show_question()
print("输入Q来退出")
while True:
response = input("请输入喜欢的语言")
if response == "Q":
break
my_survey.store_response(response)
print("感谢调查")
my_survey.show_results()
'''
你喜欢说什么语言
输入Q来退出
请输入喜欢的语言汉语
请输入喜欢的语言英语
请输入喜欢的语言Q
感谢调查
Survey results:
- 汉语
- 英语
'''
接下来白那些一个测试,对AnonymousSurvey
类进行测试,首先我们验证只存储一个答案时,能正确进行输出。
import unittest
from Variable import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
"""针对AnonymousSurvey进行测试"""
def test_store_single_response(self):
"""测试单个答案会被妥善存储"""
question = "你喜欢什么语言"
my_survey = AnonymousSurvey(question)
my_survey.store_response("汉语")
self.assertIn("汉语", my_survey.responses)
unittest.main()
'''
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
'''
从上可以看出,我们的测试通过了。此外,再提醒一下,测试的方法必须以test开头,否则就在测试时不会自动运行。
接下来我们验证存储多个答案时也能正确通过测试:
import unittest
from Variable import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
"""针对AnonymousSurvey进行测试"""
def test_store_three_response(self):
"""测试三个答案会被妥善存储"""
question = "你喜欢什么语言"
my_survey = AnonymousSurvey(question)
responses = ["汉语", "英语", "日语"]
for response in responses:
my_survey.store_response(response)
for response in responses:
self.assertIn(response, my_survey.responses)
unittest.main()
'''
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
'''
setUp()
方法在前边的测试中,我们为每个测试都创建了一个AnonymousSurvey
类的实例,并在每个方法都创建了答案。unittest.TestCase
类中包含的方法setUp()
,让我们可以只用创建这些对象一次,并在每个测试方法中使用它们。如果TestCase
类中包含方法setUp()
,Python会先运行它,再运行以test开头的方法,下边使用setUp()
来创建一个调查对象和一组答案,供方法test_store_single_response()
和test_store_three_response()
使用。
import unittest
from Variable import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
"""针对AnonymousSurvey进行测试"""
def setUp(self):
"""创建一个调查对象和一组答案,共使用的测试方法使用"""
question = "你喜欢什么语言"
self.my_survey = AnonymousSurvey(question)
#加上self是为了保证在整个类中都可以使用变量
self.responses = ["汉语", "英语", "日语"]
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_response(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()
'''
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
'''
测试自己编写的类时,方法setUp()
让测试方法编写起来更容易,可以在setUp()
方法中创建一系列实例并设置它们的属性,再在测试方法中使用这些实例,这种方式相比再每个测试中都创建实例要容易的多。
运行测试用例的时候,每完成一个单元测试,Python都会打印一个字符,测试通过打印一个句点,测试引发错误打印一个E,测试导致断言失败打印一个F。如果测试中包含很多单元测试,需要运行很久,可以通过观察这些结果来了解有多少测试通过了。