PythonChapter9~11笔记_2019-05-02
2019-05-02 本文已影响0人
AJohn11
Chapter9 类
- 面向对象编程时最有效的软件编写方法之一。
- 定义一大类对象都有的通用行为,基于类创建对象时,每个对象都自动具备这种通用行为,然后可根据需要赋予每个对象独特的个性。
- 实例化:根据类来创建对象被称为实例化,这让你能够使用类的实例。
1. 创建和使用类
- 使用类几乎可以模拟任何东西。
1. 创建Dog类
- 根据约定,在Python中,首字母大写的名称指的是类。
class Dog():#首字母大写--类
"""一次模拟小狗的简单尝试"""
def __init__(self, name, age):
"""初始化属性name和age"""
self.name=name
self.age=age
def sit(self):
"""模拟小狗被命令蹲下"""
print(self.name.title()+" is now sitting.")
def roll_over(self):
"""模拟小狗被命令打滚"""
print(self.name.title()+" rolled over!")
- 方法__init__()
- 类中的函数称--方法。
- 前面学的有关函数的一切都适用于方法,目前唯一重要的差别是调用方法的方式。
- __init__()是特殊方法每当根据Dog类创建新实例,Python都会自动运行它。
- __init__()前后各有两个下划线--约定,旨在避免Python默认方法与普通方法名称冲突。
- 方法__init__()定义包含3个形参:self, name, age。形参self必不可少,还必须位于其他形参前面。
- Python调用__init__()方法来创建Dog实例时,将自动传入实参self。每个与类相关联的方法调用都自动传递实参self,它是一个指向实例本身的引用,让实例能够访问类中的属性和方法。
- 创建Dog实例时,Python将调用Dog类的方法__init__()。我们将通过实参像Dog类传递name和age;self会自动传递,无需传递它。
-
self.name=name
、self.age=age
都有前缀self。以self为前缀的变量都可供类中的所有方法使用,还可通过类的任何实例来访问这些变量。 -
self.name = name
获取存储在形参name中的值,并将其存储到变量name 中,然后该变量被关联到当前创建的实例。 - 上面这种可以通过实例访问的变量称为属性。
- 在Python 2.7中创建类--括号内包含object
class ClassName(object):
--snip--
2. 根据类创建实例
- 可以将类视为如何创建实例的说明。
my_dog = Dog('willie', 6)
print("My dog's name is " + my_dog.name.title() + ".")
print("My dog is " + str(my_dog.age) + " years old.")
- Python使用实参'willie'和6调用Dog类中的方法__init__() 。方法__init__() 创建一个表示特定小狗的示例,并使用我们提供的值来设置属性name和age 。
- 方法__init__()未显式地包含return语句,但Python自动返回一个表示这条小狗的实例,我们将这个实例存储在变量my_dog中。
- 首字母大写--类,小写--实例。
- 访问属性
- 访问实例的属性--句点表示法。
实例名.属性名
,如my_dog.name
。
- 访问方法
- 调用类中定义的方法--句点表示法。
实例名.方法名
,如my_dog.sit()
。
- 创建多个实例
- 就算我们给第二条小狗指定同样的名字和年龄,Python依然会根据Dog类创建另一个实例。
- 你可按需求根据一个类创建任意数量的实例,条件是将每个实例都存储在不同的变量中,或占用列表或字典的不同位置。
2. 使用类和实例
1. Car类
- 为更有趣,添加随时间变化的属性--汽车行驶总里程。
2. 给属性指定默认值
- 类中的每个属性都必须有初始值,哪怕这个值是0或空字符串。在有些情况下,如设置默认值时,在方法__init__()内指定这种初始值是可行的;如果你对某个属性这样做了,就无需包含为它提供初始值的形参。
class Car():
"""一次模拟汽车的简单尝试"""
def __init__(self, make, model, year):
"""初始化描述汽车的属性"""
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0#默认值是0
def get_descriptive_name(self):
"""返回整洁的描述性信息"""
long_name = str(self.year) + ' ' + self.make + ' ' + self.model
return long_name.title()
def read_odometer(self):
"""打印一条指出汽车里程的消息"""
print("This car has " + str(self.odometer_reading) + " miles on it.")
my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())
my_new_car.read_odometer()
输出:
2016 Audi A4
This car has 0 miles on it.
3. 修改属性的值
三种方式:
- 直接通过实例修改
- 通过方法进行设置
- 通过方法进行递增
- 直接修改属性的值
- 通过实例直接访问并修改。
- 通过方法修改属性的值
- 有更新属性的方法大有裨益。
- 通过方法对属性值进行递增
- 有时候需要将属性值递增特定的量,而不是将其设置为全新的值。
- 注意:你可以使用类似于上面的方法来控制用户修改属性值(如里程表读数)的方式,但能够访问程序的人都可以通过直接访问属性来将里程表修改为任何值。要确保安全,除了进行类似于前面的基本检查外,还需特别注意细节。
总程序示例:
class Car():
"""一次模拟汽车的简单尝试"""
def __init__(self, make, model, year):
"""初始化描述汽车的属性"""
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0#默认值是0
def get_descriptive_name(self):
"""返回整洁的描述性信息"""
long_name = str(self.year) + ' ' + self.make + ' ' + self.model
return long_name.title()
def read_odometer(self):
"""打印一条指出汽车里程的消息"""
print("This car has " + str(self.odometer_reading) + " miles on it.")
def update_odometer(self, mileage):
"""将里程表读数设置为指定的值 禁止将里程表读数往回调"""
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")
def increment_odometer(self, miles):
"""将里程表读数增加指定的量"""
self.odometer_reading += miles
my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())
my_new_car.read_odometer()
my_new_car.odometer_reading = 23
my_new_car.read_odometer()
my_new_car.update_odometer(25)
my_new_car.read_odometer()
my_new_car.update_odometer(22)
my_new_car.read_odometer()
my_used_car = Car('subaru', 'outback', 2013)
print(my_used_car.get_descriptive_name())
my_used_car.update_odometer(23500)
my_used_car.read_odometer()
my_used_car.increment_odometer(100)
my_used_car.read_odometer()
输出:
2016 Audi A4
This car has 0 miles on it.
This car has 23 miles on it.
This car has 25 miles on it.
You can't roll back an odometer!
This car has 25 miles on it.
2013 Subaru Outback
This car has 23500 miles on it.
This car has 23600 miles on it.
3. 继承
- 编写类时,并非总是要从空白开始。
- 如果你要编写的类是另一个现成类的特殊版本,可使用继承--子类自动获得父类的所有属性和方法.
1.子类的方法__init()__
- 创建子类的实例时,Python首先需要完成的任务是给父类的所有属性赋值。子类的方法__init()__ 需要父类施以援手。
- 创建子类时,父类必须包含在当前文件中,且位于子类前面。
- 创建子类时,必须在括号内指定父类的名称。
- super()是一个特殊函数--帮助Python将父类和子类关联起来。
super().__init__(make, model, year)
让子类ElectricCar调用父类的方法__init()__ (父类也称超类superclass,super()由此得名)
class ElectricCar(Car):
"""电动汽车的独特之处"""
def __init__(self, make, model, year):
"""初始化父类的属性"""
super().__init__(make, model, year)
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
输出:
2016 Tesla Model S
2. Python 2.7中的继承
class Car(object):
def __init__(self, make, model, year):
--snip--
class ElectricCar(Car):
def __init__(self, make, model, year):
super(ElectricCar, self).__init__(make, model, year)
--snip--
- 函数super() 需要两个实参: 子类名和对象self
- Python 2.7中使用继承时,务必在定义父类时在括号内指定object
3. 给子类定义属性和方法
- 让一个类继承另一个类后,可添加区分子类和父类所需的新属性和方法。
例:
class ElectricCar(Car):
"""电动汽车的独特之处"""
def __init__(self, make, model, year):
"""电动汽车的独特之处
初始化父类的属性,再初始化电动汽车特有的属性 """
super().__init__(make, model, year)
self.battery_size = 70
def describe_battery(self):
"""打印一条描述电瓶容量的消息"""
print("This car has a " + str(self.battery_size) + "-kWh battery.")
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.describe_battery()
输出:
2016 Tesla Model S
This car has a 70-kWh battery.
4. 重写父类的方法
- 对于父类的方法,只要它不符合子类模拟的实物的行为,都可对其进行重写。
- 可在子类中定义一个与要重写的父类方法同名的方法。
- 此时调用时将忽略父类中的方法,转而调用子类中的同名方法。
5. 将实例用作属性
class Battery():
"""一次模拟电动汽车电瓶的简单尝试"""
def __init__(self, battery_size=70):
"""初始化电瓶的属性"""
self.battery_size = battery_size
def describe_battery(self):
"""打印一条描述电瓶容量的消息"""
print("This car has a " + str(self.battery_size) + "-kWh battery.")
def get_range(self):
"""打印一条消息,指出电瓶的续航里程"""
if self.battery_size == 70:
range = 240
elif self.battery_size == 85:
range = 270
message = "This car can go approximately " + str(range)
message += " miles on a full charge."
print(message)
class ElectricCar(Car):
"""电动汽车的独特之处"""
def __init__(self, make, model, year):
"""电动汽车的独特之处
初始化父类的属性,再初始化电动汽车特有的属性 """
super().__init__(make, model, year)
self.battery_size = 70
self.battery = Battery()#用实例做属性
def describe_battery(self):
"""打印一条描述电瓶容量的消息"""
print("This car has a " + str(self.battery_size) + "-kWh battery.")
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()
my_tesla.battery.get_range()
输出:
2016 Tesla Model S
This car has a 70-kWh battery.
This car can go approximately 240 miles on a full charge.
6. 模拟实物
- 模拟复杂物件会碰到有趣的问题--续航里程算是车的属性还是电池的属性?
- 进入了另一个境界--更高的逻辑层面,考虑的不是Python,而是如何用代码表示实物。
4. 导入类
- 添加功能 -> 文件变长 -> Python允许将类存储在模块中,然后在主程序中导入模块。
1. 导入单个类
from car import Car
- 注意文件的命名问题
- 应为自己创建的每个模块都编写文档字符串,对该模块的内容做了简要描述。
2. 在一个模块中存储多个类
- 同一模块中的类应有某种相关性。
- 可根据需要在一个模块中存储任意数量的类。
3. 从一个模块中导入多个类
from car import Car, ElectricCar
- 逗号分隔多个类
4. 导入整个模块
- 使用句点表示法表示需要访问的类:
import module_name
module_name.class_name
5. 导入模块中的所有类
from module_name import *
不推荐:
- 没有明确指出使用了哪些类
- 可能引发同名导致的错误。
- 需要从一个模块导入很多类时,推荐使用:
import module_name
和module_name.class_name
6. 在一个模块中导入另一个模块
可以在一个模块中导入另一个模块中的类。
7. 自定义工作流程
- 在组织大型项目的代码方面,Python提供了很多选项。
- 一开始应让代码结构尽可能简单。先尽可能在一个文件中完成所有的工作,确定一切都能正确运行后,再将类移到独立的模块中,尝试让代码更为组织有序。
5. Python标准库
- Python标准库是一组模块,安装的Python都包含它。
- 可使用标准库中的任何函数和类,为此 只需在程序开头包含一条简单的import 语句。
6. 类编码风格
- 应采用驼峰命名法:类名中的每个单词的首字母都大写,而不使用下划线;实例名和模块名都采用小写格式,并在单词之间加上下划线。
- 对于每个类,都应紧跟在类定义后面包含一个文档字符串:简要地描述类的功能,并遵循编写函数的文档字符串时采用的格式约定。
- 每个模块也都应包含一个文档字符串:对其中的类可用于做什么进行描述。
- 空行:可使用空行来组织代码,但不要滥用。类中--使用一个空行来分隔方法;模块中--使用两个空行来分隔类。
- import不同来源的模块:同时导入标准库中的模块和你编写的模块时,先编写导入标准库模块的import 语句,再添加一个空行,然后编写导入你自己编写的模块的import 语句。
Chapter10 文件和异常
- 处理文件,让程序能够快速地分析大量的数据。
- 错误处理,避免程序在面对意外情形时崩溃。
- 异常,它们是Python创建的特殊对象,用于管理程序运行时出现的错误。
- 模块json ,它让你能够保存用户数据,以免在程序停止运行后丢失。
- 提高程序的适用性、可用性和稳定性。
1. 从文件中读取数据
- 文本文件可存储的信息量--多!
- 使用文本文件中信息,首先--读取到内存中。
- 读取整个文件
例:
with open('pi_digits.txt') as file_object:
contents = file_object.read()
print(contents)
输出:
3.1415926535
8979323846
2643383279
- 函数open()--打开文件,这样才能访问。
- 函数open() 接受一个参数:要打开的文件的名称,返回一个表示文件的对象。
- 关键字with--在不再需要访问文件后将其关闭。未妥善地关闭文件可能会导致数据丢失或受损,让Python去确定关闭文件的恰当时机。(也可以调用open() 和close() 来打开和关闭文件,但是关闭的时机不好控制)
- 方法read()--读取这个文件的全部内容,并将其作为一个长长的字符串存储在变量中。
- 末尾多了一个空行--read() 到达文件末尾时返回一个空字符串,而将这个空字符串显示出来时就是一个空行。(print 语句中使用rstrip()可以删除空行)
- 文件路径
要让Python打开不与程序文件位于同一个目录中的文件 -- 需要提供文件路径。
- 位置是相对于当前运行的程序所在目录的--可使用相对文件路径来打开该文件夹中的文件。如:
with open('text_files/filename.txt') as file_object:
- 将文件准确位置告诉Python,这样就不用关心当前运行的程序存储在什么地方了,称为绝对文件路径。如:
file_path = '/home/ehmatthes/other_files/text_files/filename.txt'
with open(file_path) as file_object:
- 在相对路径行不通时,可使用绝对路径。
- 注意:Windows系统有时能够正确地解读文件路径中的斜杠,有时候要换成反斜杠。
- 逐行读取
filename='pi_digits.txt'
with open(filename) as file_object:
for line in file_object:#通过**执行循环**来**遍历文件中的每一行**
print(line)
输出:
3.1415926535
8979323846
2643383279
- 常常需要检查其中的每一行:你可能要在文件中查找特定的信息,或者要以某种方式修改文件中的文本。
- 可以通过执行循环来遍历文件中的每一行。
- 每行末尾都有两个换行符:一个来自文件,另一个来自print 语句。
- 创建一个包含文件各行内容的列表
- 注意:使用关键字with 时,open() 返回的文件对象只在with代码块内可用。
- 要在with 代码块外访问文件的内容:可在with代码块内--将文件的各行存储在一个列表中,并在with代码块外--使用该列表。
- 可以立即处理文件的各个部分,也可推迟到程序后面再处理。
-
方法readlines() 从文件中读取每一行,并将其存储在一个列表中。
例:
filename = 'pi_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
for line in lines:
print(line.rstrip())
输出:
3.1415926535
8979323846
2643383279
- 使用文件的内容
- 文件读取到内存中后--可以以任何方式使用这些数据了。
- 注意:读取文本文件时,Python将其中的所有文本都解读为字符串。读取是数字并将其作为数值使用 -- 必须使用函数int()转换为整数,或函数float()转换为浮点数。
- 包含100万位的大型文件
- 代码示例也可处理大得多的文件。
- Python没有任何限制可处理的数据量 -- 只要系统的内存足够多,你想处理多少数据都可以。
- 圆周率值中包含你的生日吗
例:
filename = '/Users/ligao/Desktop/python_work/《Python编程》源代码文件-更新/chapter_10/pi_million_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ''
for line in lines:
pi_string += line.rstrip()
birthday = input("Enter your birthday, in the form mmddyy: ")
if birthday in pi_string:#**in**用法也可以是 字符串 in 字符串,即后一个字符串中是否包含前一个字符串
print("Your birthday appears in the first million digits of pi!")
else:
print("Your birthday does not appear in the first million digits of pi.")
输出:
Enter your birthday, in the form mmddyy: 031897
Your birthday appears in the first million digits of pi!
-
注意:in用法也可以是
字符串 in 字符串
,即后一个字符串中是否包含前一个字符串中。
2. 写入文件
保存数据的最简单的方式 -- 写入到文件中。通过将输出写入文件,可以:
- 在程序结束运行后查看这些输出;
- 可与别人分享输出文件;
- 可编写程序来将这些输出读取到内存中并进行处理。
- 写入空文件
例:
filename = 'programming.txt'
with open(filename, 'w') as file_object:
file_object.write("I love programming.")
输出文件:
I love programming.
- 调用open() 时提供了两个实参--
第一个实参filename->打开的文件的名称;
第二个实参('w' )->写入模式 打开这个文件。 - 打开文件时,可指定读取模式 ('r' )、写入模式 ('w' )、附加模式 ('a' )或让你能够读取和写入文件的模式('r+' )。
- 省略模式实参 -- 默认的只读模式打开文件。
- 如果你要写入的文件不存在 -- 函数open() 会自动创建。
- 写入('w' )模式打开文件时千万要小心:因为如果指定的文件已经存在,Python将在返回文件对象前 清空 该文件。
- 文件对象的方法write() 将一个字符串写入文件。
-
注意:Python只能将字符串写入文本文件。
要将数值数据存储到文本文件中,必须先使用函数str() 将其转换为字符串格式。
- 写入多行
- 函数write() 不会在你写入的文本末尾添加换行符。
- 需要时可以使用**空格、制表符\t 和 换行符\n **来设置输出格式。
- 附加到文件
- 添加内容而非覆盖原 -- 附加模式 打开文件。
- 附加模式打开文件,不会在返回文件对象前清空文件,写入到文件的行都将添加到文件末尾。
- 指定的文件不存在 -- Python将创建一个空文件。
3. 异常
- Python使用被称为异常 的特殊对象来管理程序执行期间发生的错误。
- 每当发生让Python不知所措的错误时,它都会创建一个异常对象。如果你编写了处理该异常的代码,程序将继续运行;
如果你未对异常进行处理,程序将停止,并显示一个traceback,其中包含有关异常的报告。 - 使用try-except 代码块处理异常。
try-except 代码块让Python执行指定的操作,同时告诉Python发生异常时怎么办。 - 使用了try-except 代码块时,即便出现异常,程序也将继续运行:显示你编写的友好的错误消息,而不是令用户迷惑的traceback。
- 处理ZeroDivisionError 异常(除数是0)
- 错误ZeroDivisionError--一个异常对象
- Python将停止运行程序,并指出引发了哪种异常,而我们可根据这些信息对程序进行修改。
print(5/0)
输出:
Traceback (most recent call last):
File "/Users/ligao/Desktop/python_work/Chapter10.py", line 38, in <module>
print(5/0)
ZeroDivisionError: division by zero
- 使用try-except代码块
try:
print(5/0)
except ZeroDivisionError:
print("You can't divide by zero!")
输出:
You can't divide by zero!
- try 代码块中的代码运行起来没有问题,Python将跳过except 代码块;
- try 代码块中的代码导致了错误,Python将查找指定的错误与引发的错误相同的except 代码块,并运行其中的代码。(友好的错误消息)
- try-except 代码块后面其他代码 -- 接着运行
(因为已经告诉了Python如何处理这种错误)
- 使用异常避免崩溃
- 发生错误时,如果程序还有工作没有完成,妥善地处理错误就尤其重要。
- 这种情况经常会出现在要求用户提供输入的程序中:如果程序能够妥善地处理无效输入,就能再提示用户提供有效输入,而不至于崩溃。
- 程序崩溃可不好,但让用户看到traceback也不好。
不懂技术的用户会 -- 被搞糊涂;怀有恶意用户 -- 通过traceback获悉你不希望他知道的信息(可能方便他攻击你)
- else代码块
- else 代码块:依赖于try 代码块成功执行的代码都应放到else 代码块。
try-except-else 代码块的工作原理:
- try 代码块:可能引发异常的代码;
- else 代码块:仅在try 代码块成功执行时才需要运行的代码
- except 代码块:尝试运行try 代码块中的代码时引发了指定的异常该怎么办。
- 预测可能发生错误的代码 -- 健壮的程序。
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)
输出:
Enter 'q' to quit.
First number: 1
Second number: 2
0.5
First number: 3
Second number: 0
You can't divide by 0!
First number: q
- 处理FileNotFoundError 异常
- 使用文件常见问题 -- 找不到文件:文件在其他地方、文件名不正确 或者 文件根本就不存在。
- 可使用try-except 代码块以直观的方式进行处理。
- 分析文本
- 可以尝试计算文本包含多少个单词
- 方法split():以空格为分隔符将字符串分拆成多个部分,存储到一个列表中。结果是包含字符串中所有单词的列表,有些单词可能包含标点。
- 使用多个文件
- 将这个程序的大部分代码移到名为count_words() 的函数中,这样对多本书进行分析时将更容易。
- 修改程序的同时更新注释。
try-except 代码块提供了两个重要的优点:
- 避免让用户看到traceback
- 让程序能够继续执行下去
- 失败时一声不吭
- 并非每次捕获到异常时都需要告诉用户,有时候你希望程序在发生异常时一声不吭、继续运行。
- pass 语句:可在代码块中使用它来让Python 什么都不要做。
- pass 语句还充当了占位符,提醒你在程序的某个地方什么都没有做,并且以后也许要在这里做些什么。
pass、continue、break、exit()的区别:
- pass :不做任何事情,只起到占位的作用
- continue: 跳出本次循环
- break:结束循环
- exit():结束整个程序
- 决定报告哪些错误
- Python的错误 处理结构让你能够细致地控制与用户分享错误信息的程度,分享多少信息由你决定。
- 编写得很好且经过详尽测试的代码--不容易出现内部错误,如语法或逻辑错误;但只要程序依赖于外部因素,如用户输入、存在指定的文件、有网络链接,就有可能出现异常。
- 凭借经验可判断该在程序的什么地方包含异常处理块,以及出现错误时该向用户提供多少相关的信息。
4. 存储数据
- 程序把用户提供的信息存储在列表和字典等数据结构中。
- 用户关闭程序时,保存信息:简单的方式--使用模块json 来存储数据。
- 模块json 让你能够将简单的Python数据结构转储到文件中,并在程序再次运行时加载该文件中的数据。而且json并非Python专用的,易于分享。
- 使用json.dump() 和json.load()
- 导入模块json -> 再创建一个数字列表 -> 指定文件名称(通常使用扩展名.json指出文件存储的数据为JSON格式 -> 写入模式打开文件 -> 写入数据。
json.dump(要写入的内容, 写入文件的打开对象)
- 读取方式打开文件 -> 使用函数json.load() 加载文件中的信息 -> 存储到变量中
json.load(要读取的文件的打开对象)
- 这是在 程序之间共享数据 的简单方式。
json.dump:
import json
numbers = [2, 3, 5, 7, 11, 13]
filename = 'numbers.json'
with open(filename, 'w') as f_obj:#写入模式打开文件
json.dump(numbers, f_obj)
json.load:
import json
filename = 'numbers.json'
with open(filename) as f_obj:
numbers = json.load(f_obj)
print(numbers)
- 保存和读取用户生成的数据
- 对于用户生成的数据,如果不存,等程序停止时用户的信息将丢失!-- 使用json 保存大有裨益
import json
# 如果以前存储了用户名,就加载它
# 否则,就提示用户输入用户名并存储它
filename = 'username.json'
try:
with open(filename) as f_obj: username = json.load(f_obj)
except FileNotFoundError:
username = input("What is your name? ")
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)#保存用户输入
print("We'll remember you when you come back, " + username + "!")
else:
print("Welcome back, " + username + "!")
输出:
(base) Johnson-MacBook-Pro:python_work ligao$ python Chapter10_jasonloadtest.py
What is your name? Eric
We'll remember you when you come back, Eric!
(base) Johnson-MacBook-Pro:python_work ligao$ python Chapter10_jasonloadtest.py
Welcome back, Eric!
- 重构
经常有这样的情况:
- 代码能够正确地运行,但可进一步改进——将代码划分为一系列完成具体工作的函数。这样的过程被称为重构。
- 重构让代码更清晰、更易于理解、更容易扩展。
例(重构后每个函数执行单一而清晰地任务):
import json
def get_stored_username():
"""如果存储了用户名,就获取它"""
filename = 'username.json'
try:
with open(filename) as f_obj:
username = json.load(f_obj)
except FileNotFoundError:
return None
else:
return username
def get_new_username():
"""提示用户输入用户名"""
username = input("What is your name? ")
filename = 'username.json'
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
return username
def greet_user():
"""问候用户,并指出其名字"""
username = get_stored_username()
if username:
print("Welcome back, " + username + "!")
else:
username = get_new_username()
print("We'll remember you when you come back, " + username + "!")
greet_user()
None
- 是python中的一个特殊的常量,表示一个空的对象,空值是python中的一个特殊值。数据为空并不代表是空对象,[],''等都不是None。
- None,False,0,[],"",{},()都相当于False
- None在Python里是个单例对象,一个变量如果是None,它一定和None指向同一个内存地址。Python里和None比较时,是is None而不是== None。
is 和 ==
- is表示的是对象标识符,用来检查对象的标识符是否一致,即两个对象在内存中的地址是否一致。在使用 a is b 的时候,相当于id(a)==id(b)。
- ==表示两个对象是否相等,相当于调用eq()方法,即'a==b' ==> a.eq(b)。
Chapter11 测试代码
- 编写函数或类时,还可为其编写测试 -- 可确定代码面对各种输入都能够按要求的那样工作。
- 在程序中添加新代码时 -- 可以对其进行测试,确认它们不会破坏程序既有的行为。
- 如果和其他程序员开发的项目共享代码,就必须证明你编写的代码通过了既有测试,通常还需要为你添加的新行为编写测试。
1. 测试函数
- 每次自己输入测试--太繁琐。
- Python提供了一种自动测试函数输出的方式。
- 单元测试和测试用例(TestCase())
- Python标准库中的模块unittest 提供了代码测试工具。
- 单元测试 用于核实函数的某个方面没有问题。
- 测试用例 是一组单元测试,这些单元测试一起核实函数在各种情形下的行为都符合要求。良好的测试用例 -- 考虑到了函数可能收到的各种输入。
- 全覆盖式测试 用例包含一整套单元测试,涵盖了各种可能的函数使用方式。
- 大型项目,要实现全覆盖可能很难。最初--对重要行为编写测试,等项目被广泛使用时再考虑全覆盖。
- 可通过的测试
- 创建测试用例的语法需要一段时间才能习惯,但测试用例创建后,再添加针对函数的单元测试就很简单了。
- 导入了模块unittest 和要测试的函数get_formatted_name() -> 创建名为NamesTestCase 的类, 用于包含一系列针对get_formatted_name() 的单元测试。类可以随便命名,但是看起来与要测试的函数相关,并包含字样Test。
- 我们运行testname_function.py时,所有以test_ 打头的方法都将自动运行。
- unittest 类最有用的功能之一:一个断言 方法--核实得到的结果是否与期望的结果一致。
- 调用unittest 的方法
assertEqual(得到结果, 预期结果)
--如果它们相等,就万事大吉,如果它们不相等,跟我说一声!
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')
unittest.main()
输出:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
- 不能通过的测试
例:
def get_formatted_name(first, middle, last):
"""生成整洁的姓名"""
full_name = first + ' ' + middle + ' ' + last
return full_name.title()
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')
unittest.main()
输出:
E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
\u80fd\u591f\u6b63\u786e\u5730\u5904\u7406\u50cfJanis Joplin\u8fd9\u6837\u7684\u59d3\u540d\u5417?
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/ligao/Desktop/python_work/test_name_function.py", line 19, in test_first_last_name
formatted_name = get_formatted_name('janis', 'joplin')
TypeError: get_formatted_name() missing 1 required positional argument: 'last'
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
阅读输出的方式:
-
E
->指出测试用例中有一个单元测试导致了错误 -
ERROR: test_first_last_name (__main__.NamesTestCase)
->NamesTestCase 中的test_first_last_name() 导致了错误--(包含众多测试时可以确定是哪个测试未通过) -
Traceback (most recent call last): ... TypeError: get_formatted_name() missing 1 required positional argument: 'last'
标准的traceback -> 指出函数调用get_formatted_name('janis', 'joplin')有问题,缺少一个必不可少的位置实参 -
Ran 1 test in 0.000s
-> 运行了1个单元测试 -
FAILED (errors=1)
-> 整个测试用例都未通过,运行该测试用例时发生了1个错误
- 测试为通过时怎么办
- 如果你检查的条件没错,测试通过--函数的行为是对的,而测试未通过--编写的新代码有错。
- 测试未通过时,不要修改测试,而应修复导致测试不能通过的代码:检查修改,找出不符合预期的修改。
修改以下函数后,测试通过了。
def get_formatted_name(first, last, middle=''):
"""生成整洁的姓名"""
if middle:
full_name = first + ' ' + middle + ' ' + last
else:
full_name = first + ' ' + last
return full_name.title()
- 添加新测试
- 在TestCase 类中使用很长的方法名是可以的,必须以test_打头。
- 方法的名称--必须是描述性的,这才能让你明白测试未通过时的输出。
- 这些方法由Python自动调用,你根本不用编写调用它们的代码。
添加的新测试:
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')
2. 测试类
- 前面--测试函数;后面--测试类。
- 对类的测试通过了--对类的改进没有破坏原有行为。
- 各种断言方法
- Python在unittest.TestCase(翻译成汉语就是单元测试模块.测试用例类,也是描述性函数名称) 类中提供了很多断言方法。
- 断言方法检查你认为应该满足的条件是否确实满足。
满足--对程序行为的假设就得到了确认;
不满足--Python将引发异常。
6个常用的断言方法:
方法 | 用途 |
---|---|
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 中 |
- 一个要测试的类
-
类的测试与函数的测试相似——你所做的大部分工作都是测试类中方法(其实就是类中的函数)的行为,但存在一些不同之处。
例:
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 类
- 导入模块unittest 和要测试的类 -> 测试用例命名TestAnonymousSurvey(继承了unittest.TestCase) -> 测试方法描述性名称是test_store_single_response() -> 若未通过--通过输出中的方法名得知,在存储单个调查答案方面存在问题。
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(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)
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()
输出结果:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
- 方法setUp()
- 前面测试方法:每个测试方法中都创建了一个AnonymousSurvey 实例,并在每个方法中都创建了答案。
- unittest.TestCase 类包含方法setUp():只需创建这些对象一次,并在每个测试方法中使用它们。
- 在TestCase 类中包含了方法setUp() ,Python将先运行它,再运行各个以test_打头的方法。
-
方法setUp():可在setUp() 方法中创建一系列实例并设置它们的属性,再在测试方法中直接使用这些实例。
例:
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']
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()
输出结果:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
- 方法setUp() 做两件事:创建一个调查对象;创建一个答案列表。
- 存储这两样东西的变量名包含前缀self (即存储在属性中),因此可在这个类的任何地方使用。
测试用例输出的解读
运行测试用例时,每完成一个单元测试,Python都打印一个字符:
测试通过 -- 打印一个句点;
测试引发错误 -- 打印一个E ;
测试导致断言失败 -- 打印一个F 。