《Python入门与实践》 PART1 基础知识 要点记录


笔者最近需要写一个爬虫程序,用于爬取文献,于是准备重新捡起半年没用的 Python3,回顾所学的一些基本的语法和函数,并对其中有必要注意的作记录,以便以后在这种情况下能快速查阅(当然用不上最好)。

下面是我用的 Python3 版本、解释器以及集成开发环境:

Version:Python 3.6.4

Interpreter: Anaconda Python 3.7

IDE: PyCharm 2019.3.3 (项目用)& IDLE Python 3.6 (语言学习用)



2020-11-18 星期三 晴

第二章 变量与简单数据类型

  1. title() → \rightarrow 字符串的所有单词首字母变为大写,其余字母小写。这里注意的是,空格以及其他一切非字母字符一律视为 ⌈ \lceil 空格 ⌋ \rfloor ,即单词的分隔符,正因如此下面示例中最后一个 m 大写了。

    >>> "I HAVE a DREA234m".title()
    'I Have A Drea234M'
    

    类似功能的函数还有:upper()lower()

  2. 删除空白 → \rightarrow rstrip() 右边;②lstrip() 左边;③strip() 两边。

  3. 转换为字符串 → \rightarrow str(),经常用在 print() 函数中,将非字符串类型转换成字符串类型。

  4. python之禅 → \rightarrow import this

第三章 列表简介

列表的简单应用:

>>> fruit = ['apple', 'pear', 'peach', 'orange']  
>>> print (fruit)
['apple', 'pear', 'peach', 'orange']
>>> print (fruit[-1])
orange

说明:

  1. 索引 -1 表示列表最后一个元素,-2 表示倒数第二个元素,以此类推。注:列表为空时会越界。

  2. 索引从 0 开始,可能越界。

  3. 增删插改操作:

    1. 修改第 i 个元素 → \rightarrow fruit[i] = 'banana'
    2. 添加元素于末尾 → \rightarrow fruit.append('lemon')
    3. 于位置 i 插入元素 → \rightarrow fruit.insert(i, 'pineapple')
    4. 删除位置 i 的元素 → \rightarrow del fruit[i]
    5. 弹出末尾的元素 → \rightarrow poped_fruit = fruit.pop();弹出位置 i 的元素 → \rightarrow poped_fruit = fruit.pop(i)
    6. 删除列表中第一个值为 orange 的元素 → \rightarrow fruit.remove('orange')
  4. 排序:

    1. 按顺序(字典序、数值大小等)永久性排序 → \rightarrow fruit.sort();逆序 → \rightarrow fruit.sort(reverse = True),返回 None
    2. 临时排序 → \rightarrow sorted(fruit),返回临时排序列表。
    3. 注意:大写字母在不同的排序函数中可能存在不同的解读,笔者用的版本中大写字母排在小写字母前面。
    4. 反转列表 → \rightarrow fruit.reverse()
    5. 列表长度 → \rightarrow len(fruit),返回长度


2020-11-19 星期四 晴

第四章 操作列表

  1. 循环 → \rightarrow for item in fruit:

  2. 注意缩进

  3. 创建数字列表 → \rightarrow range(1, 9, 2) → \rightarrow [1, 3, 5, 7]

  4. 创建并转换为数字列表 → \rightarrow list(range(1, 6)) → \rightarrow [1, 2, 3, 4, 5]

  5. 乘方运算符:**

  6. 一些函数:min()max()sum()

  7. 列表解析:将 for 循环和创建新元素的代码合并成一行,并自动附加新元素 → \rightarrow list1 = [expression(x) for x in range(...)],注意循环无冒号。具体实例:

    >>> squares = [y**2 for y in range(1, 10, 2)]
    >>> print (squares)
    [1, 9, 25, 49, 81]
    
  8. 切片

    1. 切片形式:① squares[0: 3];② squares[: 2];③squares[1: ];④squares[ -3:] → \rightarrow [9, 25, 49, 81]
    2. 遍历切片 → \rightarrow for square in squares[-3:]:
    3. 复制列表 → \rightarrow squares_two = squares[:]
  9. 元组:元素不可变的列表 → \rightarrow rect = (50, 50)

  10. 代码规范:尽量不要用tab进行缩进

第五章 if 语句

给个例子即可:

available_toppings = ['mushrooms', 'olives', 'green peppers',
'pepperoni', 'pineapple', 'extra cheese']

requested_toppings = ['mushrooms', 'french fries', 'extra cheese']

for requested_topping in requested_toppings:
	if requested_topping in available_toppings:
		print("Adding " + requested_topping + ".")
	else:
		print("Sorry, we don't have " + requested_topping + ".")

另外,注意:if-elif-else

第六章 字典

>>> colors = {
    
    'cucumber': 'green', 'apple':'red', 'banana': 'yellow'}
>>> print (colors['apple'])
red
  1. 键-值对的排列顺序与添加顺序不同。Python不关心键值对的添加顺序,而只关心键与值的关联关系。

  2. 添加键值对 → \rightarrow colors['orange'] = 'orange' ;删除键值对 → \rightarrow del colors['cucumber']; 修改键值对:同添加

  3. 创建空字典 → \rightarrow dict = {}

  4. 遍历字典。键值对遍历顺序也不是确定的。

    1. 遍历所有键值对

      user_0 = {
              
              
      'username': 'efermi',
      'first': 'enrico',
      'last': 'fermi',
      }
      for key, value in user_0.items():
      	print("\nKey: " + key)
      	print("Value: " + value)
      
    2. 遍历所有键 → \rightarrow items() 改为 keys();遍历所有值 → \rightarrow 改为 values()

    3. 根据列表创建数学意义上的集合 → \rightarrow set(user_0.values())

  5. 字典嵌套字典

    alien_0 = {
          
          'color': 'green', 'points': 5}
    alien_1 = {
          
          'color': 'yellow', 'points': 10}
    alien_2 = {
          
          'color': 'red', 'points': 15}
    aliens = [alien_0, alien_1, alien_2]
    
  6. 字典存储列表

    pizza = {
          
          
    'crust': 'thick',
    'toppings': ['mushrooms', 'extra cheese'],
    }
    
  7. 列表和字典的嵌套层级不应太多

  8. 字典中存储字典:字典中的字典结构要尽量一致,方便处理

users = {
    
    
	'aeinstein': {
    
    
	'first': 'albert',
	'last': 'einstein',
	'location': 'princeton',
	},
	'mcurie': {
    
    
	'first': 'marie',
	'last': 'curie',
	'location': 'paris',
	}
}
for username, user_info in users.items():
	print("\nUsername: " + username)
	full_name = user_info['first'] + " " + user_info['last']
	location = user_info['location']
	print("\tFull name: " + full_name.title())
	print("\tLocation: " + location.title())


2020-11-20 星期五 阴/雨

第七章 用户输入与 while 循环

  1. 输入函数 input() → \rightarrow name = input("Please enter your name")
  2. 类型转换函数 → \rightarrow int()float()str()
  3. while 循环 → \rightarrow while <expression>:
  4. continuebreakpass → \rightarrow 跳过本次循环、退出循环、什么都不做。
  5. 陷入死循环 → \rightarrow Ctrl+C 或 关闭终端窗口。
  6. for 循环中不要轻易修改列表,否则导致 Python 难以跟踪其中的元素,如果要则用 while 循环。

第八章 函数

  1. 传递实参的方式 以 def fun (a, b): 为例

    1. 位置实参 → \rightarrow fun("Happy", "Gay")

    2. 关键字实参 → \rightarrow fun(a="Happy", b="Gay") ⇔ \Leftrightarrow fun(b="Gay", a="Happy")

    3. 默认参数 → \rightarrow def fun (a, b="Gay"): → \rightarrow fun(a="Hello")

    注:基本类型的变量是按值传递的,但列表等(目前只验证了列表,验证其他的?)高级数据结构是按引用传递的。

  2. 返回值 → \rightarrow return

  3. 禁止修改列表 → \rightarrow function_name(list_name[:])

  4. 传递任意数量的实参 → \rightarrow def butFruit (*lists): → \rightarrow 形参名 *lists 中的星号让Python创建一个名为 lists 的空元组,并将收到的所有值都封装到这个元组中。

  5. 传递任意数量的关键字实参,看下面的例子:

    def build_profile(first, last, **user_info):
    	"""创建一个字典,其中包含我们知道的有关用户的一切"""
    	profile = {
          
          }
    	profile['first_name'] = first
    	profile['last_name'] = last
    	for key, value in user_info.items():
    		profile[key] = value
    	return profile
    user_profile = build_profile('albert', 'einstein', 
    				location='princeton', field='physics')
    print(user_profile)
    

    Output:

    {'first_name': 'albert', 'last_name': 'einstein',
    'location': 'princeton', 'field': 'physics'}
    

    说明:形参 **user_info 中的两个星号让 Python 创建一个名为 user_info 的空字典,并将收到的所有 名称-值对 都封装到这个字典中。

  6. 导入模块

    1. 方法 → \rightarrow 只需编写一条 import 语句并在其中指定模块名,就可在程序中使用该模块中的所有函数。如果你使用这种 import 语句导入了名为 module_name.py 的整个模块,就可使用下面的语法来使用其中任何一个函数: module_name.function_name ()
    2. 导入特定函数 → \rightarrow from module_name import function_0 , function_1 , function_2。使用这种语法无需使用句点即可使用函数。
    3. 使用 as 给函数指定别名 → \rightarrow from module_name import function_name as fn → \rightarrow 调用 function_namefn(...)
    4. 导入模块中所有函数 → \rightarrow from module_name import *
  7. 函数命名规范

    1. 给函数指定描述性名称,且只在其中使用小写字母下划线
    2. 如果形参很多,导致函数定义的长度超过了79字符,可在函数定义中输入左括号后按回车键,并在下一行按两次Tab键,从而将形参列表和只缩进一层的函数体区分开来。
    3. 如果程序或模块包含多个函数,可使用两个空行将相邻的函数分开,这样将更容易知道前一个函数在什么地方结束,下一个函数从什么地方开始。
    4. 所有的 import 语句都应放在文件开头,唯一例外的情形是,在文件开头使用了注释来描述整个程序。

第九章 类

★ 类的创建和使用:

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!")

my_dog = Dog('piupiu', 6)  # 创建类实例

Output:

My dog's name is piupiu.
My dog is 6 years old.

与 C++、C# 对比:

  1. __init__(...) → \rightarrow 构造函数,实例创建时自动运行
  2. self → \rightarrow 类似 *this(C++)、this(C#),指向实例本身
  3. 属性可在方法中添加,而 C++、C# 不行

★ 类继承实例:

class Car():
	"""一次模拟汽车的简单尝试"""
	def __init__(self, make, model, year):
		self.make = make
		self.model = model
		self.year = year
		self.odometer_reading = 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
		
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()

Output:

2016 Tesla Model S
This car has a 70-kWh battery.

说明:

  1. 创建子类时,父类必须包含在当前文件中,且位于子类前面。
  2. 定义子类时,必须在括号内指定父类的名称。
  3. super() 是一个特殊函数,帮助 Python 将父类和子类关联起来。它让 Python 调用 ElectricCar 的父类的方法 __init__() ,让 ElectricCar 实例包含父类的所有属性。父类也称为超类(superclass),名称 super 因此而得名。

★ 重写类方法 → \rightarrow 可在子类中定义一个这样的方法,即它与要重写的父类方法同名。这样,Python 将不会考虑这个父类方法,而只关注你在子类中定义的相应方法。示例:

def ElectricCar(Car):
	-- snip --
	def fill_gas_tank():
		"""电动汽车没有油箱"""
		print("This car doesn't need a gas tank!")

使用继承时,可让子类保留从父类那里继承而来的精华,并剔除不需要的糟粕。

★ 将实例作为属性

class Car():
	-- snip --
	
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.")
		
class ElectricCar(Car):
	"""电动汽车的独特之处"""
	def __init__(self, make, model, year):
		"""
		初始化父类的属性,再初始化电动汽车特有的属性
		"""
		super().__init__(make, model, year)
		self.battery = Battery()
		
my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
my_tesla.battery.describe_battery()

Output:

2016 Tesla Model S
This car has a 70-kWh battery.

★ 类的导入

  1. 导入一个类 → \rightarrow from module_name import class_name

  2. 导入多个类 → \rightarrow from module_name import class_name01, class_name02

  3. 导入整个模块 → \rightarrow import module_name → \rightarrow 使用: module_name.class_name。需要从一个模块中导入很多类时,最好导入整个模块。

  4. 导入模块中所有类 → \rightarrow from module_name import *
    不推荐这种方式,其原因有二。① 如果只要看一下文件开头的 import 语句,就能清楚地知道程序使用了哪些类,将大有裨益;② 这种导入方式还可能引发名称方面的困惑。如果你不小心导入了一个与程序文件中其他东西同名的类,将引发难以诊断的错误。

  5. 在一个模块中导入另一个模块,看示例就好了:
    ① car.py

    class Car():
    	-- snip --
    

    ② electric_car.py

    from car import Car
    
    class Battery():
    	-- snip --
    	
    class ElectricCar(Car):
    	-- snip --
    

    ③ my_cars.py

    from car import Car
    from electric_car import ElectricCar
    
    my_beetle = Car('volkswagen', 'beetle', 2016)
    print(my_beetle.get_descriptive_name())
    
    my_tesla = ElectricCar('tesla', 'roadster', 2016)
    print(my_tesla.get_descriptive_name())
    

    Output:

    2016 Volkswagen Beetle
    2016 Tesla Roadster
    

★ Python 标准库

OrderedDict → \rightarrow OrderedDict 实例的行为几乎与字典相同,区别只在于记录了键-值对的添加顺序。

from collections import OrderedDict

favorite_languages = OrderedDict()
favorite_languages['jen'] = 'python'
favorite_languages['sarah'] = 'c'
favorite_languages['edward'] = 'ruby'
favorite_languages['phil'] = 'python'

for name, language in favorite_languages.items():
	print(name.title() + "'s favorite language is " +
			language.title() + ".")

请注意,这里没有使用花括号,而是调用 OrderedDict() 来创建一个空的有序字典,并将其存储在 favorite_languages 中。

★ 类编码风格

  1. 类名应采用驼峰命名法,即将类名中的每个单词的首字母都大写,而不使用下划线。实例名和模块名都采用小写格式,并在单词之间加上下划线。
  2. 对于每个类,都应紧跟在类定义后面包含一个文档字符串。这种文档字符串简要地描述类的功能,并遵循编写函数的文档字符串时采用的格式约定。
  3. 可使用空行来组织代码,但不要滥用。在类中,可使用一个空行来分隔方法;而在模块中,可使用两个空行来分隔类。
  4. 需要同时导入标准库中的模块和你编写的模块时,先编写导入标准库模块的 import 语句,再添加一个空行,然后编写导入你自己编写的模块的 import 语句。在包含多条 import 语句的程序中,这种做法让人更容易明白程序使用的各个模块都来自何方。

第十章 文件和异常

文件

★ 读取文件

file_object = open('data.txt')
contents = file_object.read()
print(contents)

说明: 相比于原始文件,该输出唯一不同的地方是末尾多了一个空行。 这是因为 read() 到达文件末尾时返回一个空字符串,而将这个空字符串显示出来时就是一个空行要删除多出来的空行,可在 print 语句中使用 rstrip()

另外,使用关键字 with 时, open() 返回的文件对象只在 with 代码块内可用。如果要在 with 代码块外访问文件的内容,可在 with 代码块内将文件的各行存储在一个列表中,并在 with 代码块外使用该列表:你可以立即处理文件的各个部分,也可推迟到程序后面再处理。如下例子所示:

with open(filename) as file_object:
	lines = file_object.readlines()
for line in lines:
	print(line.rstrip())

★ 文件路径

相对路径和绝对路径不多说,这里提一句:Windows 下层级表示用的是反斜杠 \LinuxOS X 下使用斜杠 /

★ 写入文件

filename = 'programming.txt'
with open(filename, 'w') as file_object:
	file_object.write("I love programming.")

注:

  1. 打开文件时,可指定读取模式( ‘r’ )、写入模式( ‘w’ )、附加模式( ‘a’ )或让你能够读取和写入文件的模式( ‘r+’ )。
  2. Python 只能将字符串写入文本文件。要将数值数据存储到文本文件中,必须先使用函数 str() 将其转换为字符串格式。
  3. 函数 write() 不会在你写入的文本末尾添加换行符,要自己添加。
  4. 附加模式 a 下直接在原文件后面追加内容。

异常

只要程序依赖于外部因素,如用户输入、存在指定的文件、有网络链接,就有可能出现异常。

try-except 捕获并处理异常

try:
	print(5/0)
except ZeroDivisionError:
	print("You can't divide by zero!")

注:使用 try-except 代码块提供了两个重要的优点:① 避免让用户看到traceback;② 让程序能继续分析能够找到的其他文件。

try-except-else 遇到错误时捕获并处理异常,否则执行 else 块的正常代码

try:
	answer = int(first_number) / int(second_number)
except ZeroDivisionError:
	print("You can't divide by 0!")
else:
	print(answer)

★ 常见异常类型
1. ZeroDivisionError
2. FileNotFoundError

存储数据

★ JSON 格式

  1. JSON(JavaScript Object Notation)格式最初是为 JavaScript 开发的,但随后成了一种常见格式,被包括 Python 在内的众多语言采用。JSON数据格式并非 Python 专用的,这让你能够将以JSON格式存储的数据与使用其他编程语言的人分享。这是一种轻便格式,很有用,也易于学习。

  2. 存储数据为 JSON 格式 → \rightarrow 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)
    

    numbers.json 内容:[2, 3, 5, 7, 11, 13]

  3. 读取 JSON 格式文件到内存 → \rightarrow json.load(),接受一个实参,即文件对象。示例:

    import json
    filename = 'numbers.json'
    with open(filename) as f_obj:
    	numbers = json.load(f_obj)
    print(numbers)
    

本章综合示例:

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 + "!")

★ 重构

代码能够正确地运行,但可做进一步的改进——将代码划分为一系列完成具体工作的函数。这样的过程被称为重构。重构让代码更清晰、更易于理解、更容易扩展。

将上个代码进行重构如下:

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 usernam

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()

要编写出清晰而易于维护和扩展的代码,以上划分工作必不可少。

第十一章 测试代码

本章用到了 Python 标准库中的 unittest 模块。在此之前先熟悉测试的相关概念:

  1. 单元测试:核实函数的某个方面没有问题。

  2. 测试用例:一组单元测试,这些单元测试一起核实函数在各种情形下的行为都符合要求。良好的测试用例考虑到了函数可能收到的各种输入,包含针对所有这些情形的测试。

  3. 全覆盖测试用例:包含一整套单元测试,涵盖了各种可能的函数使用方式。对于大型项目,要实现全覆盖可能很难。通常,最初只要针对代码的重要行为编写测试即可,等项目被广泛使用时再考虑全覆盖。

PART 1 测试函数

★ 创建测试用例的过程

要为函数编写测试用例,可先导入模块 unittest 以及要测试的函数,再创建一个继承 unittest.TestCase 的类,并编写一系列方法对函数行为的不同方面进行测试。

首先是准备好待测试函数:
name_function.py

def get_formatted_name(first, last):
    """Generate a neatly formatted full name."""
    full_name = first + ' ' + last
    return full_name.title()

然后为函数创建测试用例:

test_name_function.py

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.030s

OK

我们修改 get_formatted_name(),添加一个中间名,故意使测试不通过,修改如下:

def get_formatted_name(first, middle, last):
    """Generate a neatly formatted full name."""
    full_name = first + ' ' + middle + ' ' + last
    return full_name.title()

然后再执行 test_name_function.py,运行结果如下:

E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
能够正确地处理像Janis Joplin这样的姓名吗?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "...\test_name_function.py", line 10, 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.010s

FAILED (errors=1)

我们先不要想着修改测试用例,而应该先考虑如何修改待测函数以使得原测试用例能通过,然后在此基础上再考虑添加新的测试用例来测试修改后函数的新特性,这里我们对 get_formatted_name() 作进一步修改:

def get_formatted_name(first, last, middle=''):
    """Generate a neatly formatted full name."""
    if middle:
        full_name = first + ' ' + middle + ' ' + last
    else:
        full_name = first + ' ' + last
    return full_name.title()

然后再运行一次,通过了。

.
----------------------------------------------------------------------
Ran 1 test in 0.015s

OK

还没结束,因为刚才只表示函数在原有用例上表现没问题,我们还需要构建新的测试用例来确保修改引入的新特性没有问题。为此,在类 NamesTestCase 中新添加一个测试:

def test_first_middle_last_name(self):
    """能够正确地处理像Wolfgang Amadeus Mozart这样的姓名吗?"""
    formatted_name = get_formatted_name(
        'wolfgang', 'mozart', 'amadeus')
    self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')

说明:

  1. 方法名必须以test_打头,这样它才会在我们运行 test_name_function.py 时自动运行。
  2. 在 TestCase 类中使用很长的方法名是可以的;这些方法的名称必须是描述性的,这才能让你明白测试未通过时的输出;这些方法由 Python 自动调用,你根本不用编写调用它们的代码。

看看测试结果,通过了:

..
----------------------------------------------------------------------
Ran 2 tests in 0.010s

OK

至此演示了一个函数的测试流程。

PART 2 测试类

在进入测试过程前,先盘点一下 unittest 模块中的断言方法:

  • assertEqual(a, b)
  • assertNotEqual(a, b)
  • assertTrue(x)
  • assertFalse(x)
  • assertIn(item, list)
  • assertNotIn(item, list)

★ 测试类的过程

首先,编写一个待测试的类:

class AnonymousSurvey():
    """collect the responses of anonymous questionnaire"""

    def __init__(self, question):
        """store a question and get ready for the storage of responses"""
        self.question = question
        self.responses = []

    def show_question(self):
        """show the questionnaire"""
        print (question)

    def store_response(self, new_response):
        """store the response of a questionnaire"""
        self.responses.append(new_response)

    def show_results(self):
        """show all the collected responses"""
        print ("Survey results:")
        for response in responses:
            print ('- ' + 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)

unittest.main()

测试运行结果:

.
----------------------------------------------------------------------
Ran 1 test in 0.050s

OK

接着我们验证 <多个答案的收集工作是否正常> ,为此添加新的测试用例:

def test_store_three_response(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)

运行通过:

..
----------------------------------------------------------------------
Ran 2 tests in 0.082s

OK

到此基本测试流程走完了,但是效率上存在优化的空间。因为每一个类成员函数的测试都要创建一个实例,效率会打折扣,我们希望能让所有测试只用一个类实例,这是可以用 unittest.TestCase 类中的 setUp() 函数,它能让我们只需创建这些对象一次,并在每个测试方法中使用它们。

如果在 TestCase 类中包含了方法 setUp() ,Python 将先运行它,再运行各个以test_打头的方法。这样,在你编写的每个测试方法中都可使用在方法 setUp() 中创建的对象了。

修改 test_survey.py 如下:

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('English')
        self.assertIn('English', 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()

上述代码的主要改动为:

  1. 添加了 setUp()
  2. 很多变量变为了实例的类属性,故要需要用 self 进行引用

可在 setUp() 方法中创建一系列实例并设置它们的属性,再在测试方法中直接使用这些实例。相比于在每个测试方法中都创建实例并设置其属性,这要容易得多。

补充知识

  1. 不换行输出 → \rightarrow print (str, end="");换行输出 → \rightarrow print (str, end="\n")
  2. 注释 → \rightarrow ①单行:# -content- ;②多行:""" -content- """
  3. 拆分字符串 → \rightarrow .split()

笔者水平有限,上述总结难免存在问题,欢迎各位大佬指正~

猜你喜欢

转载自blog.csdn.net/weixin_42430021/article/details/109789940