python学习之路 一:基础知识

本文最后更新于:2021年12月25日 下午

本文适合有基础c、c++语言知识的人学习,同时也可当作python工具书查阅。

前言

  1. 这里选择的是最新版 Python3
    安装教程这里推荐:http://www.runoob.com/python3/python3-install.html
    win下载地址:https://www.python.org/downloads/windows
    Linux下载地址:https://www.python.org/downloads/source

  2. 可视化开发工具IDE:https://www.jetbrains.com/pycharm/download/

  3. 因国内pip速度较慢,安装完python后建议改为国内清华大学镜像源,以下:

    • 临时使用
      pip install -i https://pypi.tuna.tsinghua.edu.cn/simple 模块名字

    • 设为默认
      升级 pip 到最新的版本 (>=10.0.0) 后进行配置:
      pip install pip -U
      pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
      如果您到 pip 默认源的网络连接较差,临时使用本镜像站来升级 pip:
      pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pip -U

  4. 指定目录 pip下载及离线安装包

    • 将pip安装的包导出至requirements文件列表
      cd进入想要下载的目录。
      pip freeze > requirements.txt
    • 批量下载pip包
      pip download -d d:\0\package -r requirements.txt
    • pip批量安装包及通过列表文件安装(先cd进入requirements所在目录)
      pip install -r requirements.txt

1.1基础语法

1.1.1 输出

打印 print ("Hello, Python!")
print 默认输出是换行的,如果要实现不换行需要在变量末尾加上逗号,
不换行输出 print('*', end=' ')

1
2
3
4
5
# 统计开始时间
startTime = time.time()
# 统计结束时间
endTime = time.time()
print("共花费时间: %d 秒" % (endTime - startTime))

1.1.2 字符串转义

特殊字符会使用反斜杠\来转义。比如\n表示换行,\t表示制表符,字符\本身也要转义,所以\表示的字符就是\
print('Isn\'t, they said.')
如果不希望前置了 \ 的字符转义成特殊字符,而是使用原始字符串方式,在引号前添加 r 即可。
print(r'C:\软件\python')
用三重引号可以跨行连续输入。"""..."""'''...'''
字符串可以用 + 进行连接(粘到一起),也可以用 * 进行重复:3 * 'io' + ' so easy'

1.1.3 脚本式编程

Python 文件将以 .py 为扩展名。
在linux和mac上面编程时,已经设置了PATH变量,头文件加入#!/usr/bin/python,可以直接执行。

1
2
3
#!/usr/bin/python
# -*- coding: utf-8 -*-
print ("Hello, Python!")

这里,假定您的Python解释器在/usr/bin目录中,使用以下命令执行脚本:

1
2
chmod +x test.py     # 脚本文件添加可执行权限
./test.py

1.1.4 行和缩进

Python与其他语言最大的区别就是,Python 的代码块不使用大括号 {} 来控制类,函数以及其他逻辑判断。python 最具特色的就是用缩进来写模块。
缩进的空白数量是可变的,但是所有代码块语句必须包含相同的缩进空白数量(一般是四个空格),这个必须严格执行。

1.1.5 注释

python中单行注释采用 # 开头。
python 中多行注释使用三个单引号(‘’’)或三个双引号(“””)。

1.1.6 同一行显示多条语句

Python可以在同一行中使用多条语句,语句之间使用分号(;)分割。

1.1.7 多个语句构成代码组

缩进相同的一组语句构成一个代码块,我们称之代码组。
像if、while、def和class这样的复合语句,首行以关键字开始,以冒号( : )结束,该行之后的一行或多行代码构成代码组。
我们将首行及后面的代码组称为一个子句(clause)。
如下实例:

1
2
3
4
5
6
if expression :
suite
elif expression :
suite
else :
suite

1.2 变量类型

1.2.1 变量赋值

Python 中的变量赋值不需要类型声明。
Python允许你同时为多个变量赋值。例如:
a, b, c = 1, 2, "john"

1.2.2 标准数据类型

Python 定义了一些标准类型,用于存储各种类型的数据。
Python有五个标准的数据类型:

  • Numbers(数字)
  • String(字符串)
  • List(列表)
  • Tuple(元组)
  • Dictionary(字典)

bool类型有True和False和None(等于NULL)。

1.2.2.1 Python数字

数字数据类型用于存储数值。他们是不可改变的数据类型,这意味着改变数字数据类型会分配一个新的对象。
当你指定一个值时,Number对象就会被创建:
var1 = 1
您也可以使用del语句删除一些对象的引用。del语句的语法是:
del var1[,var2[,var3[....,varN]]]]
您可以通过使用del语句删除单个或多个对象的引用。例如:
del var_a, var_b
Python支持四种不同的数字类型:

  • int(有符号整型,如0112L,0xDEFL,0x69)# long 类型只在Python2.X版本中。在Python3.X版本中 long 类被 int 替代。
  • float(浮点型,-32.54e100
  • complex(复数,4.53e-7j

1.2.2.2 Python字符串

字符串或串(String)是由数字、字母、下划线组成的一串字符。
python的字串列表有2种取值顺序:

  • 从左到右索引默认0开始的,最大范围是字符串长度少1
  • 从右到左索引默认-1开始的,最大范围是字符串开头

如果你要实现从字符串中获取一段子字符串的话,可以使用 [头下标:尾下标] 来截取相应的字符串,其中下标是从 0 开始算起,可以是正数或负数,下标可以为空表示取到头或尾。
[头下标:尾下标] 获取的子字符串包含头下标的字符,但不包含尾下标的字符。

1
2
3
4
5
>>> s = 'abcdef'
>>> print('s[1:5] ',s[1:5])
>>> print('s[-6:-4] ',s[-6:-4])
s[1:5] bcde
s[-6:-4] ab

字符串格式化
在 Python 中,字符串格式化使用与 C 中 sprintf 函数一样的语法。

1
2
>>> print("My name is %s and weight is %d kg!" % ('Zara', 21))
My name is Zara and weight is 21 kg!

1.2.2.3 Python列表

List(列表)相当于c语言中的数组。 是 Python 中使用最频繁的数据类型。
列表可以完成大多数集合类的数据结构实现。它支持字符,数字,字符串甚至可以包含列表(即嵌套)。
列表用 [ ] 标识,是 python 最通用的复合数据类型。
列表中值的切割也可以用到变量 [头下标:尾下标] ,就可以截取相应的列表,从左到右索引默认 0 开始,从右到左索引默认 -1 开始,下标可以为空表示取到头或尾。
加号 + 是列表连接运算符,星号 * 是重复操作。如下实例:

1
2
3
4
5
6
7
8
9
10
>>> list = [ 'runoob', 786 , 2.23, 'john', 70.2 ]
>>> tinylist = [123, 'john']
>>> print ('输出第1个至第三个元素 ', list[1:3])
>>> print ('输出从第2个开始至列表末尾的所有元素 ', list[2:])
>>> print ('输出列表两次 ', tinylist * 2)
>>> print ('打印组合的列表 ', list + tinylist)
输出第1个至第三个元素 [786, 2.23]
输出从第2个开始至列表末尾的所有元素 [2.23, 'john', 70.2]
输出列表两次 [123, 'john', 123, 'john']
打印组合的列表 ['runoob', 786, 2.23, 'john', 70.2, 123, 'john']

每当需要访问最后一个列表元素时,都可使用索引-1,来获取最后一个元素。如list[-1]
squares = [value**2 for value in range(1,11)]此列表解析生成1-10的平方数存入数组。
list[1]=list[:]可以复制列表。

1.2.2.4 Python 元组

元组是另一个数据类型,类似于 List(列表)。
元组用 () 标识。内部元素用逗号隔开。但是元组不能二次赋值,相当于只读列表。
元组是不允许更新的。而列表是允许更新的。

1.2.2.5 Python 字典

字典(dictionary)是除列表以外python之中最灵活的内置数据结构类型。列表是有序的对象集合,字典是无序的对象集合。
两者之间的区别在于:字典当中的元素是通过键来存取的,而不是通过偏移存取。
字典用”{ }”标识。字典由索引(key)和它对应的值value组成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> dict = {}
>>> dict['one'] = "This is one"
>>> dict[2] = "This is two"
>>> tinydict = {'name': 'john','code':6734, 'dept': 'sales'}
>>> print (dict['one']) # 输出键为'one' 的值
>>> print (dict[2]) # 输出键为 2 的值
>>> print (tinydict) # 输出完整的字典
>>> print (tinydict.keys()) # 输出所有键
>>> print (tinydict.values()) # 输出所有值
This is one
This is two
{'name': 'john', 'code': 6734, 'dept': 'sales'}
dict_keys(['name', 'code', 'dept'])
dict_values(['john', 6734, 'sales'])
  1. 修改字典中的值
    dict['color'] = 'yellow'

  2. 删除键-值对
    del dict['color'] # 删除键’color’
    dict.clear() # 清空字典
    del dict # 删除字典

  3. 遍历字典

    1
    2
    3
    for key, value in dict.items():
    print("\nKey: " + key)
    print("Value: " + value)

1.2.3 Python数据类型转换

和c语言一样。当我们需要对数据内置的类型进行转换,数据类型的转换,你只需要将数据类型作为函数名即可。
float(2)

1.2.4 如何查看模块及其方法的使用和它们的源码

  1. 进入cmd或者终端,python交互模式。
  2. import 模块名 # 导入模块
  3. dir(模块名) # 查看模块拥有的方法
  4. help(模块名) # 查看模块介绍及其所含方法、拉到最后file后面查看源码路径
  5. help(方法名) # 查看方法的内置帮助、用法

1.3 运算符

+、-、*、/、%和c语言一样
**是幂,2**3=8
//是取整除,9//2=4
比较运算符和c语言一样
赋值运算符和c语言一样,多了**=,//=
位运算符和c语言一样
逻辑运算符:and等同于c语言中的&&or等于c语言中的||not等于c语言中的!

1.3.1 成员运算符

除了以上的一些运算符之外,Python还支持成员运算符,测试实例中包含了一系列的成员,包括字符串,列表或元组。

  • in
    如果在指定的序列中找到值返回 True,否则返回 False。例子:x in y , 如果 x 在 y 序列中返回 True。
  • not in
    如果在指定的序列中没有找到值返回 True,否则返回 False。例子:x not in y , 如果 x 不在 y 序列中返回 True。

1.3.2 身份运算符

身份运算符用于比较两个对象的存储单元.

  • is
    is 是判断两个标识符是不是引用自一个对象。例子:x is y, 类似 id(x) == id(y)
  • is not
    is not 是判断两个标识符是不是引用自不同对象。例子:x is not y , 类似 id(a) != id(b)

1.4 条件语句

if等用于c语言中ifelse等同于c语言中elseelif等同于c语言中else if
Python程序语言指定任何非0和非空(null)值为True,0 或者 null为False。
Python 编程中 if 语句用于控制程序的执行,基本形式为:

1
2
3
4
5
6
7
8
if 判断条件1:
执行语句1……
elif 判断条件2:
执行语句2……
elif 判断条件3:
执行语句3……
else:
执行语句4……

1.5 循环语句

有while,for循环,循环控制语句有break,continue,pass。

1.5.1 while

while 语句用于循环执行程序,即在某条件下,循环执行某段程序,以处理需要重复处理的相同任务。其基本形式为:

1
2
while 判断条件(condition):
执行语句(statements)……

例子:

1
2
3
4
pets = ['dog', 'cat', 'dog', 'goldfish', 'cat', 'rabbit', 'cat']
while 'cat' in pets:
pets.remove('cat')
print(pets)

1.5.2 for

for循环的语法格式如下:

1
2
for 迭代变量(iterating_var) in 数列、序列(sequence):
执行语句(statements)
1
2
3
4
sum = 0
for x in range(101):
sum = sum + x
print(sum) #1-100的和,等于5050
1
2
3
L = ['Bart', 'Lisa', 'Adam']
for i in L:
print(i)

1.5.3 循环控制语句

break,continue和c语言相同。pass就是一个空语句,不做任何事情,一般用做占位语句。。

1.6 函数

Python内置了很多有用的函数,我们可以直接调用。可以直接从Python的官方网站查看文档:
Python的官方网站查看内置函数
比如求绝对值的函数abs,只有一个参数。
也可以在交互式命令行通过help(abs)查看abs函数的帮助信息。

1.6.1 定义函数

你可以定义一个由自己想要功能的函数,以下是简单的规则:

  1. 函数代码块以 def 关键词开头,后接函数标识符名称和圆括号 ()。
  2. 任何传入参数和自变量必须放在圆括号中间,圆括号之间可以用于定义参数。
  3. 函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。
  4. 函数内容以冒号起始,并且缩进。
  5. return [表达式] 结束函数,选择性地返回一个值给调用方。不带表达式的return相当于返回 None。

一般格式如下:

1
2
def 函数名(参数列表):
函数体

python函数的使用方法基本上和c语言相同,不用指定返回类型,可以返回列表,字典等。

一、python可以给函数指定默认值。
def student(name='Li Ming', age=18):
如果函数有一个实参默认值为空,则必须确保为空的实参在最后一个。如:
def get_name(first_name, last_name, middle_name=''):

二、将列表传递给函数后,函数就可对其进行修改。在函数中对这个列表所做的任何修改都是永久性的,这让你能够高效地处理大量的数据。
有时候,需要禁止函数修改列表。这时候,可向函数传递列表的副本而不是原件。
def stu(name[:], age[:])

三、Python允许函数从调用语句中收集任意数量的实参。
def make_pizza(*toppings):
形参名 *toppings 中的星号让Python创建一个名为 toppings 的空元组,并将收到的所有值都封装到这个元组中。
如果要让函数接受不同类型的实参,必须在函数定义中将接纳任意数量实参的形参放在最后。Python先匹配位置实参和关键字实参,再将余下的实参都收集到最后一个形参中。
def make_pizza(size, *toppings):

四、使用任意数量的关键字实参
def build_profile(first, last, **user_info):

1.6.2 将函数存储在模块中

可以将函数存储在被称为模块的独立文件中,再将模块导入到主程序中。跟c语言差不多,c语言中用头文件,函数文件。

  1. 导入整个模块
    假设xxx.py所在目录内有另外一个pizza.py,添加import pizza语句
    导入名为模块 module_name.py 的的整个模块通用语法:import module_name
    可使用下面的语法来使用其中任何一个函数:module_name.function_name ()
  2. 导入特定的函数
    from module_name import function_0 , function_1 , function_2
    通过用逗号分隔函数名,可根据需要从模块中导入任意数量的函数。
    若使用这种语法,调用函数时就无需使用句点。make_pizza(16, 'pepperoni')
  3. 使用 as 给函数指定别名
    指定别名的通用语法如下:from module_name import function_name as fn
  4. 使用 as 给模块指定别名
    给模块指定别名的通用语法如下:import module_name as mn
  5. 导入模块中的所有函数
    最好只导入你需要用的函数,或者导入整个模块并使用句点表示法。
    导入模块 module_name 中的所有函数通用语法如下:from module_name import *

所有的 import 语句都应放在文件开头,唯一例外的情形是,在文件开头使用了注释来描述整个程序。

1.7 类

1.7.1 创建类

根据约定,在Python中,首字母大写的名称指的是类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Dog():
"""一次模拟小狗的简单尝试"""
def __init__(self, name, age):
"""初始化属性name和age"""
self.name = name
self.age = age
self.son = 'zero'
def sit(self):
"""模拟小狗被命令时蹲下"""
print(self.name.title() + " is now sitting.")
def roll_over(self):
"""模拟小狗被命令时打滚"""
print(self.name.title() + " rolled over!")
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.")
my_dog.sit()
print(my_dog.son)

一 方法 init()
可参考c++中的构造函数。

  1. 我们将方法 init() 定义成了包含三个形参: self 、 name 和 age 。在这个方法的定义中,形参 self 必不可少,还必须位于其他形参的前面,相当于c++中的This指针。
  2. 因为Python调用这个 init() 方法来创建 Dog 实例时,将自动传入实参 self 。每个与类相关联的方法调用都自动传递实参 self ,它是一个指向实例本身的引用,让实例能够访问类中的属性和方法。
  3. 我们将通过实参向 Dog() 传递名字和年龄; self 会自动传递,因此我们不需要传递它。每当我们根据 Dog 类创建实例时,都只需给最后两个形参( name 和 age )提供值。
  4. Dog 类还定义了另外两个方法: sit() 和 roll_over() 。由于这些方法不需要额外的信息,如名字,因此它们只有一个形参 self 。

二 根据类创建实例

  1. 访问属性
    要访问实例的属性,可使用句点表示法。my_dog.name
  2. 调用方法
    用句点表示法来调用 Dog 类中定义的任何方法。my_dog.roll_over()
  3. 给属性指定默认值
    可以在方法 init()中添加默认值
    如果你对某个属性这样做了,就无需包含为它提供初始值的形参。

三 修改属性的值

  1. 直接修改属性的值
    my_dog.son='kuku'
  2. 通过方法修改属性的值
    等于在类里面新建一个方法,来修改类里面属性的值
    def update_name(self, mile):
    self.name = mile
  3. 通过方法对属性的值进行递增
    跟2差不多,用方法里面的实参来修改。

1.7.2 继承

1
2
3
4
5
6
class ElectricCar(Car):
"""电动汽车的独特之处"""
def __init__(self, make, model, year):
"""初始化父类的属性"""
super().__init__(make, model, year)
self.battery_size = 70

一 给子类定义属性和方法
super() 是一个特殊函数,帮助Python将父类和子类关联起来。这行代码让Python调用ElectricCar 的父类的方法 init() ,让 ElectricCar 实例包含父类的所有属性。父类也称为超类(superclass),名称super因此而得名。

二 重写父类的方法

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

1.7.3 导入类

Python允许你将类存储在模块中,然后在主程序中导入所需的模块。

  1. 导入类
    from car import Car
  2. 从一个模块中导入多个类
    from car import Car, ElectricCar
  3. 导入整个模块
    你还可以导入整个模块,再使用句点表示法访问需要的类。import car
  4. 导入模块中的所有类
    from module_name import * #不推荐这种方式
  5. 在一个模块中导入另一个模块

1.7.4 类编码风格

类名应采用驼峰命名法,即将类名中的每个单词的首字母都大写,而不使用下划线。实例名和模块名都采用小写格式,并在单词之间加上下划线。

1.8 文件和异常

1.8.1 从文件中读取数据

1.8.1.1 读取整个文件

open() 函数常用形式是接收两个参数:文件名(file)和模式(mode)。
open(file, mode='r')
完整的语法格式为:
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
参数说明:

  • file: 必需,文件路径(相对或者绝对路径)。
  • mode: 可选,文件打开模式
  • buffering: 设置缓冲
  • encoding: 一般使用utf8
  • errors: 报错级别
  • newline: 区分换行符
  • closefd: 传入的file参数类型
  • opener:
1
2
3
with open('pi_digits.txt') as file_object:
contents = file_object.read()
print(contents)

函数 open() 接受一个参数:要打开的文件的名称。 函数 open()返回一个表示文件的对象。Python将这个对象存储在我们将在后面使用的变量中。
关键字 ==with== 在不再需要访问文件后将其关闭。在这个程序中,我们没有调用 close() ;你只管打开文件,并在需要时使用它,Python自会在合适的时候自动将其关闭。
有了表示 pi_digits.txt 的文件对象后,我们使用方法 read() (前述程序的第2行)读取这个文件的全部内容,并将其作为一个长长的字符串存储在变量 contents 中。这样,通过打印 contents 的值,就可将这个文本文件的全部内容显示出来。
相比于原始文件,该输出唯一不同的地方是末尾多了一个空行。为何会多出这个空行呢?因为 read() 到达文件末尾时返回一个空字符串,而将这个空字符串显示出来时就是一个空行。要删除多出来的空行,可在 print 语句中使用 rstrip() :print(contents.rstrip())

1.8.1.2 文件路径

在Linux和OS X中,你可以这样编写代码:
file_path = '/home/ehmatthes/other_files/text_files/filename.txt'
with open(file_path) as file_object:
Windows系统中,在文件路径中使用反斜杠( \ )而不是斜杠( / ):
file_path = 'C:\Users\ehmatthes\other_files\text_files\filename.txt'
with open(file_path) as file_object:

1.8.1.3 逐行读取

1
2
3
4
filename = 'pi_digits.txt'
with open(filename) as file_object:
for a in file_object:
print(a.rstrip())

1.8.1.4 创建一个包含文件各行内容的列表

1
2
3
4
5
filename = 'pi_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
for line in lines:
print(line.rstrip())

1.8.1.5 使用文件的内容

1
2
3
4
5
6
7
filename = 'pi_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ''
for line in lines:
pi_string += line.strip()
print(pi_string)

输出3.141592653589793238462643383279
在变量 pi_string 存储的字符串中,包含原来位于每行左边的空格,为删除这些空格,可使用 strip() 函数消除空格。
print str.strip( '0' ); # 去除首尾字符 0

1.8.2 写入文件

1.8.2.1 写入空文件

1
2
3
filename = 'a.txt'
with open(filename, 'w') as file_object:
file_object.write("I love programming.\n")

打开文件时,可指定读取模式( ‘r’ )、写入模式( ‘w’ )、附加模式( ‘a’ )或让你能够读取和写入文件的模式( ‘r+’ )。如果
你省略了模式实参,Python将以默认的只读模式打开文件。
Python只能将字符串写入文本文件。要将数值数据存储到文本文件中,必须先使用函数str() 将其转换为字符串格式。

1.8.2.2 写入多行

每句后面加上换行符。

1.8.3 异常

Python使用被称为异常的特殊对象来管理程序执行期间发生的错误。每当发生错误时,它都会创建一个异常对象。如果你编写了处理该异常的代码,程序将继续运行;如果你未对异常进行处理,程序将停止,并显示一个traceback,其中包含有关异常的报告。
异常是使用 try-except 代码块处理的。 try-except 代码块让Python执行指定的操作,同时告诉Python发生异常时怎么办。使用了 try-except 代码块时,即便出现异常,程序也将继续运行:显示你编写的友好的错误消息,而不是令用户迷惑的traceback。

1.8.3.1 使用 try-except-else 代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
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)

当你认为可能发生了错误时,可编写一个 try-except 代码块来处理可能引发的异常。
通过将可能引发错误的代码放在 try-except 代码块中,可提高这个程序抵御错误的能力。这个示例还包含一个 else 代码块;依赖于 try 代码块成功执行的代码都应放到 else 代码块中。
try-except-else 代码块的工作原理大致如下:Python尝试执行 try 代码块中的代码;只有可能引发异常的代码才需要放在 try 语句中。有时候,有一些仅在 try 代码块成功执行时才需要运行的代码;这些代码应放在 else 代码块中。 except 代码块告诉Python,如果它尝试运行 try 代码块中的代码时引发了指定的异常,该怎么办。

1.8.3.2 如何捕获异常

异常的类型太多,是否有方法捕获所有异常呢?

1
2
3
4
try:
print(a)
except Exception as e:
print("发生了异常:{}".format(e))

但是使用这样的方式,我们只知道是报了某个错误,不知道是哪个文件哪个函数的哪一行错误。我们可以使用traceback获取详细的异常信息:

1
2
3
4
5
6
import traceback

try:
print(a)
except Exception as e:
traceback.print_exc()

这样就比较直观,方便我们调试和修复代码。

1.8.3.3 分析多个文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def count_words(filename):
"""计算一个文件大致包含多少个单词"""
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
print("Sorry, the file " + filename + " does not exist.")
else:
# 计算文件大致包含多少个单词
words = contents.split()
num_words = len(words)
print("The file " + filename + " has about " + str(num_words) + " words.")

filenames = ['0.txt', '1.txt', '2.txt', '3.txt']
for filename in filenames:
count_words(filename)

如果你希望失败的时候不提示用户,可以在 except xxxError 后面添加一句pass。

1.8.4 存储数据

模块 json 让你能够将简单的Python数据结构转储到文件中,并在程序再次运行时加载该文件中的数据。还可以使用 json 在Python程序之间分享数据。更重要的是,JSON数据格式并非Python专用的,这让你能够将以JSON格式存储的数据与使用其他编程语言的人分享。

1.8.4.1 使用 json.dump() 和 json.load() 保存和读取用户生成的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 + "!")

先导入模块json,通过方法json.dump() 和 json.load()来保存用户名。
上面的程序,如果第一次执行,保存用户名到username,第二次执行则显示欢迎回来。

1.8.4.2 重构

有时候,我们要保证高内聚,低耦合,需要将各个模块代码分为一系列完成具体工作的函数,这样的过程称为重构。
下面我们来重构上一节的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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()

在这个最终版本中,每个函数都执行单一而清晰的任务。要编写出清晰而易于维护和扩展的代码,划分工作必不可少。

1.9 测试代码

这章中我们学习如何使用Python模块 unittest 中的工具来测试代码。

1.9.1 测试函数

1.9.1.1 单元测试和测试用例

Python标准库中的模块 unittest 提供了代码测试工具。单元测试用于核实函数的某个方面没有问题;测试用例是一组单元测试,这些单元测试一起核实函数在各种情形下的行为都符合要求。良好的测试用例考虑到了函数可能收到的各种输入,包含针对所有这些情形的测试。全覆盖式测试用例包含一整套单元测试,涵盖了各种可能的函数使用方式。对于大型项目,要实现全覆盖可能很难。通常,最初只要针对代码的重要行为编写测试即可,等项目被广泛使用时再考虑全覆盖。

1.9.1.2 如何测试

要为函数编写测试用例,可先导入模块 unittest 以及要测试的函数,再创建一个继承 unittest.TestCase 的类,并编写一系列方法对函数行为的不同方面进行测试。
下面是一个只包含一个方法的测试用例,它检查函数 get_formatted_name() 在给定名和姓时能否正确地工作:

1
2
3
4
5
6
7
8
#文件名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()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import unittest
from name_function import get_formatted_name
#文件名test_name_ function.py
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()

首先,我们导入了模块 unittest 和要测试的函数 get_formatted_ name() 。我们创建了一个名为 NamesTestCase(名字随便) 的类,用于包含一系列针对 get_formatted_name() 的单元测试。这个类必须继承unittest.TestCase 类,这样Python才知道如何运行你编写的测试。
我们运行 test_name_function.py 时,所有以 test_ 打头的方法都将自动运行。
在这个方法中,我们调用了要测试的函数,并存储了要测试的返回值。
最后我们使用了 unittest 类最有用的功能之一:一个断言方法。断言方法用来核实得到的结果是否与期望的结果一致。
我们调用 unittest 的方法 assertEqual() ,并向它传递 formatted_name 和 ‘Janis Joplin’ 。代码行 self.assertEqual(formatted_name, 'Janis Joplin') 的意思是说:“将 formatted_name 的值同字符串 ‘Janis Joplin’ 进行比较,如果它们相等,就万事大吉,如果它们不相等,跟我说一声!”

1.9.2 测试类

1.9.2.1 各种断言方法

Python在 unittest.TestCase 类中提供了很多断言方法。如下:

方 法 用 途
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 中

1.9.2.2 方法 setUp()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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()

使用 setUp() 来创建一个调查对象和一组答案,供方法 test_store_single_response()test_store_three_responses() 使用。
方法 setUp() 做了两件事情:创建一个调查对象(见方法setUp()第4行);创建一个答案列表(见方法setUp()第5行)。
这让两个测试方法都更简单,因为它们都不用创建调查对象和答案。方法 test_store_three_response() 核实 self.responses 中的第一个答案—— self.responses[0] ——被妥善地存储,而方法 test_store_three_response() 核实 self.responses 中的全部三个答案都被妥善地存储。