月度归档:2022年09月

第1章NumPy基础

请参考《机器学习的数学》中NumPy基础

NumPy基础

第2章 PyTorch基础

PyTorch是Facebook团队于2017年1月发布的一个深度学习框架,虽然晚于TensorFlow、Keras等框架,但自发布之日起,其关注度就在不断上升,目前在GitHub上的热度已超过Theano、Caffe、MXNet等框架。
与PyTorch 1.0之前的版本相比,PyTorch 1.0版本增加了很多新功能,对原有内容进行了优化,并整合了caffe2,使用更方便,也大大增强其生产性,所以其热度在迅速上升。
PyTorch采用Python语言接口来实现编程,非常容易上手。它就像带GPU的NumPy,而且与Python一样都属于动态框架。PyTorch继承了Torch灵活、动态的编程环境和用户友好等特点,支持以快速和灵活的方式构建动态神经网络,还允许在训练过程中快速更改代码而不妨碍其性能,支持动态图形等尖端AI模型的能力,是快速实验的理想选择。本章主要介绍PyTorch的一些基础且常用的概念和模块,具体包括如下内容:
 为何选择PyTorch
 PyTorch环境的安装与配置
 NumPy与Tensor
 Tensor与Autograd
 使用NumPy实现机器学习
 使用Tensor及antograd实现机器学习
 使用优化器自动微分等实现机器学习
 使用TensorFlow2架构实现机器学习

2.1 为何选择PyTorch

PyTorch是一个建立在Torch库之上的Python包,旨在加速深度学习应用。它提供一种类似NumPy的抽象方法来表征张量(或多维数组),可以利用GPU来加速训练。由于 PyTorch 采用了动态计算图(Dynamic Computational Graph)结构,且基于tape的autograd 系统的深度神经网络。其他很多框架,比如 TensorFlow(TensorFlow2.0也加入了动态网络的支持)、Caffe、CNTK、Theano 等,采用静态计算图。通过PyTorch一种称之为反向模式自动微分(Reverse-mode auto-differentiation)的技术,你可以非常方便地构建网络。
torch是PyTorch中的一个重要包,它包含了多维张量的数据结构以及基于其上的多种数学操作。
自2015 年谷歌开源 TensorFlow以来,深度学习框架之争越来越激烈,全球多个看重 AI 研究与应用的科技巨头均在加大这方面的投入。从 2017 年年初发布以来,PyTorch 可谓是异军突起,在短时间内就取得了一系列成果,成为其中的明星框架。之后PyTorch进行了一些较大的版本更新,如0.4版本把Varable与Tensor进行了合并,增加了Windows的支持;1.0版本增加了JIT(全称Just-in-time compilation,即时编译,它弥补了研究与生产的部署的差距)、更快的分布式、C++扩展等。
目前PyTorch 1.0 稳定版已发布,它 从 Caffe2 和 ONNX 移植了模块化和产品导向的功能,并将它们和 PyTorch 已有的灵活、专注研究的特性相结合。PyTorch 1.0 中的技术已经让很多 Facebook 的产品和服务变得更强大,包括每天执行 60 亿次文本翻译。
PyTorch由4个主要包组成,具体如下。
 torch:类似于NumPy的通用数组库,可将张量类型转换为torch.cuda.TensorFloat,并在GPU上进行计算。
 torch.autograd:用于构建计算图形并自动获取梯度的包。
 torch.nn:具有共享层和损失函数的神经网络库。
 torch.optim:具有通用优化算法(如SGD,Adam等)的优化包。

2.2 安装配置

在安装PyTorch时,请先核查当前环境是否有GPU,如果没有,则安装CPU版PyTorch;如果有,则安装GPU版PyTorch。

2.2.1 安装CPU版PyTorch

安装CPU版PyTorch的方法比较简单。PyTorch是基于Python开发的,所以如果没有安装Python则需要先安装Python,再安装PyTorch。具体步骤如下。
1. 下载Python
安装Python建议采用anaconda方式安装,先从Anaconda的官网:https://www.anaconda.com/distribution, 如图2-1 所示。

图2-1 下载Anaconda界面
下载Anaconda3的最新版本,如Anaconda3-2021.11-Linux-x86_64.sh,建议使用3系列,3系列代表未来发展。另外,下载时根据自己环境,选择操作系统等。
2. 安装Python
在命令行,执行如下命令,开始安装Python:
Anaconda3-2021.11-Linux-x86_64.sh
根据安装提示,直接按回车即可。其间会提示选择安装路径,如果没有特殊要求,可以按回车使用默认路径(~/ anaconda3),然后就开始安装。安装完成后,程序提示是否把anaconda3的binary路径加入到当前用户的.bashrc配置文件中,建议添加。添加以后,就可以使用python、ipython命令时自动使用Anaconda3的python环境。
3. 安装PyTorch
登录PyTorch官网(https://pytorch.org/),登录后,可看到如图2-2 所示界面,然后选择对应项。

图2-2 安装CPU版PyTorch

把第⑥项内容复制到命令行,执行即可。
conda install pytorch-cpu torchvision-cpu -c pytorch
(6)验证安装是否成功
启动Python,然后执行如下命令,如果没有报错,说明安装成功!

2.2.2 安装GPU版PyTorch

安装GPU版本的PyTorch稍微复杂一点,除需要安装Python、PyTorch,还需要安装GPU的驱动(如英伟达的Nvidia)及cuda、cuDNN计算框架,主要步骤如下。
1. 安装NVIDIA驱动
下载地址为https://www.nvidia.cn/Download/index.aspx?lang=cn。 登录可以看到如图2-3所示的界面。

图2-3 NVIDIA的下载界面
选择产品类型、操作系统等,然后点击搜索按钮,进入下载界面。
安装完成后,在命令行输入nvidia-smi,用来显示GPU卡的基本信息,如果出现如图2-4所示信息,则说明安装成功。如果报错,则说明安装失败,请搜索其他安装驱动的方法。

图2-4 显示GPU卡的基本信息
2. 安装CUDA
CUDA(Compute Unified Device Architecture,统一计算设备架构),是英伟达公司推出的一种基于新的并行编程模型和指令集架构的通用计算架构,它能利用英伟达GPU的并行计算引擎,比CPU更高效地解决许多复杂计算任务。安装CUDA 驱动时,需保证该驱动与NVIDIA GPU 驱动的版本一致,这样CUDA才能找到显卡。
3. 安装cuDNN
NVIDIA cuDNN是用于深度神经网络的GPU加速库。注册NVIDIA并下载cuDNN包,地址为https://developer.nvidia.com/rdp/cudnn-archive。
4. 安装Python及PyTorch
这步与2.2.1节安装CPU版PyTorch的步骤相同,只是选择CUDA时,不是选择None,而是选择对应CUDA的版本号,如图2-5所示。

图2-5 安装GPU版PyTorch
5. 验证
验证PyTorch安装是否成功的方法与2.2.1节一样,如果想进一步验证PyTorch是否在使用GPU,可以运行以下这段测试GPU的程序test_gpu.py。

在命令行运行以下脚本:
python test_gpu.py
如果可以看到如图2-6所示的结果,说明安装GPU版PyTorch成功!

图2-6 运行test_gpu.py的结果
在命令行运行nvidia-smi,可以看到如图2-7所示界面。

图2-7 含GPU进程的显卡信息

2.3 Jupyter Notebook环境配置

Jupyter Notebook是目前Python比较流行的开发、调试环境,此前被称为 IPython Notebook。它以网页的形式打开,可以在网页页面中直接编写代码和运行代码,代码的运行结果(包括图形)也会直接显示,如在编程过程中添加注释、目录、图像或公式等内容。Jupyter Notebook具有以下特点。
 编程时具有语法高亮、缩进、tab补全的功能。
 可直接通过浏览器运行代码,同时在代码块下方展示运行结果。
 以富媒体格式展示计算结果。富媒体格式包括:HTML,LaTeX,PNG,SVG等。
 对代码编写说明文档或语句时,支持Markdown语法。
 支持使用LaTeX编写数学性说明。
接下来介绍配置Jupyter Notebook的主要步骤。
1)生成配置文件。

将在当前用户目录下生成文件:.jupyter/jupyter_notebook_config.py
2)生成当前用户登录jupyter密码。打开ipython, 创建一个密文密码:

3)修改配置文件。

进行如下修改:

4)启动Jupyter Notebook。

在浏览器上,输入IP:port,即可看到如图2-8所示界面。

图2-8 Jupyter notebook主页界面
接下来就可以在浏览器进行开发调试PyTorch、Python等任务了。

2.4 NumPy与Tensor

第1章我们介绍了NumPy,知道其读取数据非常方便,而且还拥有大量的函数,所以深得数据处理、机器学习者喜爱。这节我们将介绍PyTorch的Tensor,它可以是零维(又称为标量或一个数)、一维、二维及多维的数组。其自称为神经网络界的NumPy, 它与NumPy相似,它们共享内存,它们之间的转换非常方便和高效。不过它们也有不同之处,最大的区别就是NumPy 会把 ndarray 放在 CPU 中加速运算,而由Torch 产生的 Tensor 会放在 GPU 中进行加速运算(假设当前环境有GPU)。

2.4.1 Tensor概述

对Tensor的操作很多,从接口的角度来划分,可以分为两类:
1)torch.function,如torch.sum、torch.add等,
2)tensor.function,如tensor.view、tensor.add等。
这些操作对大部分Tensor都是等价的,如torch.add(x,y)与x.add(y)等价。在实际使用时,可以根据个人爱好选择。
如果从修改方式的角度,可以分为以下两类。
1)不修改自身数据,如x.add(y),x的数据不变,返回一个新的tensor。
2)修改自身数据,如x.add_(y)(运行符带下划线后缀),运算结果存在x中,x被修改。
以下代码说明add与add_的区别。

运行结果如下:
tensor([4, 6])
tensor([1, 2])
tensor([4, 6])

2.4.2 创建Tensor

新建Tensor的方法很多,可以把列表或ndarray等数据对象直接转换为Tensor,也可以根据指定的形状构建。常见的构建Tensor的方法,可参考表2-1。
表2-1 常见的新建Tensor方法

函数 功能
Tensor(*size) 直接从参数构造一个的张量,支持list、numpy数组
eye(row, column) 创建指定行数,列数的二维单位tensor
linspace(start,end,steps) 从step到end,均匀切分成steps份
logspace(start,end,steps) 从10^step, 到10^end,均匀切分成steps份
rand/randn(*size) 生成[0,1)均匀分布/标准正态分布数据
ones(*size) 返回指定shape的张量,元素初始为1
zeros(*size) 返回指定shape的张量,元素初始为0
ones_like(t) 返回与t的shape相同的张量,且元素初始为1
zeros_like(t) 返回与t的shape相同的张量,且元素初始为0
arange(start,end,step) 在区间[start,end)上以间隔step生成一个序列张量
from_numpy(ndarray) 从ndarray创建一个tensor

下面举例说明。

【说明】注意torch.Tensor与torch.tensor的几点区别
1)torch.Tensor是torch.empty和torch.tensor之间的一种混合,但是,当传入数据时,torch.Tensor使用全局默认dtype(FloatTensor),torch.tensor从数据中推断数据类型。
2)torch.tensor(1)返回一个固定值1,而torch.Tensor(1)返回一个大小为1的张量,它是随机初始化的值。
举例如下。

运行结果如下:
t1的值tensor([3.5731e-20]),t1的数据类型torch.FloatTensor
t2的值1,t2的数据类型torch.LongTensor

下面来看一些根据一定规则,自动生成tensor的例子。

2.4.3 改变Tensor形状

在处理数据、构建网络层等过程中,我们经常需要了解Tensor的形状、改变Tensor的形状。与改变NumPy的形状类似,改变tenor的形状也有很多类似函数,具体可参考表2-2。 表2-2 为tensor常用修改形状的函数。

函数 说明
size() 返回张量的shape属性值,与函数shape(0.4版新增)等价
numel(input) 计算tensor的元素个数
view(*shape) 修改tensor的shape,与reshape(0.4版新增)类似,但view返回的对象与源tensor共享内存,修改一个另一个同时修改。Reshape将生成新的tensor,而且不要求源tensor是连续的。View(-1)展平数组。
resize 类似于view,但在size超出时会重新分配内存空间
item 若tensor为单元素,则返回pyton的标量
unsqueeze 在指定维度增加一个"1"
squeeze 在指定维度压缩一个"1"

下面来看一些实例。

【说明】torch.view与torch.reshape的异同。
1)reshape()可以由torch.reshape(),也可由torch.Tensor.reshape()调用。view()只可由torch.Tensor.view()来调用。
2)对于一个将要被view的Tensor,新的size必须与原来的size与stride兼容。否则,在view之前必须调用contiguous()方法。
3)同样也是返回与input数据量相同,但形状不同的tensor。若满足view的条件,则不会copy,若不满足,则会copy。
4)如果你只想重塑张量,请使用torch.reshape。 如果您还关注内存使用情况并希望确保两个张量共享相同的数据,请使用torch.view。

2.4.4 索引操作

Tensor的索引操作与NumPy类似,一般情况下索引结果与源数据共享内存。从tensor获取元素除了可以通过索引,也可借助一些函数,常用的选择函数可参考表2-3。
表2-3 常用选择操作函数

函数 说明
index_select(input,dim,index) 在指定维度上选择一些行或列
nonzero(input) 获取非0元素的下标
masked_select(input,mask) 使用二元值进行选择
gather(input,dim,index) 在指定维度上选择数据,输出的形状与index(index的类型必须是LongTensor类型的)一致
scatter_( input, dim, index, src) 为gather的反操作,根据指定索引补充数据

以下为部分函数的实现代码:

2.4.5 广播机制

前文1.8节介绍了NumPy的广播机制,它是向量运算的重要技巧。PyTorch也支持广播规则,下面通过几个示例进行说明。

2.4.6 逐元素操作

与NumPy一样,tensor也有逐元素操作,操作内容相似,但使用函数可能不尽相同。大部分数学运算都属于逐元操作,逐元素操作输入与输出的形状相同。,常见的逐元素操作,可参考表2-4。
表2-4常见逐元素操作

函数 说明
abs/add 绝对值/加法
addcdiv(t,t1,t2,value=1) t1与t2的按元素除后,乘value加t
addcmul(t,t1,t2, value=1) t1与t2的按元素乘后,乘value加t
ceil/floor 向上取整/向下取整
clamp(t, min, max) 将张量元素限制在指定区间
exp/log/pow 指数/对数/幂
mul(或*)/neg 逐元素乘法/取反
sigmoid/tanh/softmax 激活函数
sign/sqrt 取符号/开根号

【说明】这些操作均创建新的tensor,如果需要就地操作,可以使用这些方法的下划线版本,例如abs_。
以下为部分逐元素操作代码实例。

2.4.7 归并操作

归并操作,顾名思义,就是对输入进行归并或合计等操作,这类操作的输入输出形状一般不相同,而且往往是输入大于输出形状。归并操作可以对整个tensor进行归并,也可以沿着某个维度进行归并。常见的归并操作可参考表2-5。
表2-5 常见的归并操作

函数 说明
cumprod(t, axis) 在指定维度对t进行累积
cumsum 在指定维度对t进行累加
dist(a,b,p=2) 返回a,b之间的p阶范数
mean/median 均值/中位数
std/var 标准差/方差
norm(t,p=2) 返回t的p阶范数
prod(t)/sum(t) 返回t所有元素的积/和

【说明】
归并操作一般涉及一个dim参数,指定沿哪个维进行归并。另一个参数是keepdim,说明输出结果中是否保留维度1,默认情况是False,即不保留。
以下为归并操作的部分代码。

2.4.8 比较操作

比较操作一般进行逐元素比较,有些是按指定方向比较。常用的比较函数可参考表2-6。
表2-6 常用的比较函数

函数 说明
eq 比较tensor是否相等,支持broadcast
equal 比较tensor是否有相同的shape与值
ge/le/gt/lt 大于/小于比较/大于等于/小于等于比较
max/min(t,axis) 返回最值,若指定axis,则额外返回下标
topk(t,k,axis) 在指定的axis维上取最高的K个值

以下是部分函数的代码实现。

2.4.9 矩阵操作

机器学习和深度学习中存在大量的矩阵运算,用的比较多的有两种,一种是逐元素乘法,另外一种是点积乘法。PyTorch中常用的矩阵函数可参考表2-7。
表2-7 常用矩阵函数

函数 说明
dot(t1, t2) 计算张量(1D)的内积或点积
mm(mat1, mat2)/bmm(batch1,batch2) 计算矩阵乘法/含batch的3D矩阵乘法
mv(t1, v1) 计算矩阵与向量乘法
t 转置
svd(t) 计算t的SVD分解

【说明】
1)torch的dot与NumPy的dot有点不同,torch中dot对两个为1维张量进行点积运算,NumPy中的dot无此限制。
2)mm是对2维矩阵进行点积运算,bmm对含batch的3维矩阵进行点积运算。
3)转置运算会导致存储空间不连续,需要调用contiguous方法转为连续。

2.4.10 PyTorch与NumPy比较

PyTorch与NumPy有很多类似的地方,并且有很多相同的操作函数名称,或虽然函数名称不同但含义相同;当然也有一些虽然函数名称相同,但含义不尽相同。对此,有时很容易混淆,下面我们把一些主要的区别进行汇总,具体可参考表2-8。
表2-8 PyTorch与NumPy函数对照表

操作类别 NumPy PyTorch
数据类型 np.ndarray torch.Tensor
np.float32 torch.float32; torch.float
np.float64 torch.float64; torch.double
np.int64 torch.int64; torch.long
从已有数据构建 np.array([3.2, 4.3], dtype=np.float16) torch.tensor([3.2, 4.3],
dtype=torch.float16)
x.copy() x.clone()
np.concatenate torch.cat
线性代数 np.dot torch.mm
属性 x.ndim x.dim()
x.size x.nelement()
形状操作 x.reshape x.reshape; x.view
x.flatten x.view(-1)
类型转换 np.floor(x) torch.floor(x); x.floor()
比较 np.less x.lt
np.less_equal/np.greater x.le/x.gt
np.greater_equal/np.equal/np.not_equal x.ge/x.eq/x.ne
随机种子 np.random.seed torch.manual_seed

2.5 Tensor与Autograd

在神经网络中,一个重要内容就是进行参数学习,而参数学习离不开求导,PyTorch是如何进行求导的呢?
现在大部分深度学习架构都有自动求导的功能,PyTorch也不列外,torch.autograd包就是用来自动求导的。autograd包为张量上所有的操作提供了自动求导功能,而torch.Tensor和torch.Function为autograd包的两个核心类,它们相互连接并生成一个有向非循环图。接下来我们先简单介绍tensor如何实现自动求导,然后介绍计算图,最后用代码实现这些功能。

2.5.1 自动求导要点

autograd包为对tensor进行自动求导,为实现对tensor自动求导,需考虑如下事项。
1)创建叶子节点(leaf node)的tensor,使用requires_grad参数指定是否记录对其的操作,以便之后利用backward()方法进行梯度求解。requires_grad参数默认值为False,如果要对其求导需设置为True,与之有依赖关系的节点自动变为True。
2)可利用requires_grad_()方法修改tensor的requires_grad属性。可以调用.detach()或with torch.no_grad():将不再计算张量的梯度,跟踪张量的历史记录。这点在评估模型、测试模型阶段常常使用。
3)通过运算创建的tensor(即非叶子节点),会自动被赋于grad_fn属性。该属性表示梯度函数。叶子节点的grad_fn为None。
4)最后得到的tensor执行backward()函数,此时自动计算各变在量的梯度,并将累加结果保存grad属性中。计算完成后,非叶子节点的梯度自动释放。
5)backward()函数接受参数,该参数应和调用backward()函数的Tensor的维度相同,或者是可broadcast的维度。如果求导的tensor为标量(即一个数字),backward中参数可省略。
6)反向传播的中间缓存会被清空,如果需要进行多次反向传播,需要指定backward中的参数retain_graph=True。多次反向传播时,梯度是累加的。
7)非叶子节点的梯度backward调用后即被清空。
8)可以通过用torch.no_grad()包裹代码块来阻止autograd去跟踪那些标记为.requesgrad=True的张量的历史记录。这步在测试阶段经常使用。
在整个过程中,PyTorch采用计算图的形式进行组织,该计算图为动态图,它的计算图在每次正向传播时,将重新构建。其他深度学习架构,如TensorFlow、Keras一般为静态图。接下来我们介绍计算图,用图的形式来描述就更直观了,该计算图为有向无环图(DAG)。

2.5.2计算图

计算图是一种有向无环图像,用图形方式表示算子与变量之间的关系,直观高效。如图2-9所示,圆形表示变量,矩形表示算子。如表达式z=wx+b可写成两个表示式:如果y=wx,则z=y+b。其中x、w、b为变量,是用户创建的变量,不依赖于其他变量,故又称为叶子节点。为计算各叶子节点的梯度,需要把对应的张量参数requires_grad属性设置为True,这样就可自动跟踪其历史记录。y、z是计算得到的变量,非叶子节点,z为根节点。mul和add是算子(或操作或函数)。这些变量及算子就构成一个完整的计算过程(或正向传播过程)。

图2-9正向传播计算图
我们的目标是更新各叶子节点的梯度,根据复合函数导数的链式法则,不难算出各叶子节点的梯度。
\frac{\partial z}{\partial x}=\frac{\partial z}{\partial y}\frac{\partial y}{\partial x}=w \tag{2.1}
 \frac{\partial z}{\partial w}=\frac{\partial z}{\partial y}\frac{\partial y}{\partial w}=x \tag{2.2}
 \frac{\partial z}{\partial b}=b \tag{2.3}
PyTorch调用backward(),将自动计算各节点的梯度,这是一个反向传播过程,这个过程可用图2-9表示。在反向传播过程中,autograd沿着图2-10,从当前根节点z反向溯源,利用导数链式法则,计算所有叶子节点的梯度,并梯度值将累加到grad属性中。对非叶子节点的计算操作(或function)记录在grad_fn属性中,叶子节点的grad_fn值为None。

图2-10 梯度反向传播计算图
下面我们用代码实现这个计算图。

2.5.3 标量反向传播

PyTorch使用torch.autograd.backward来实现反向传播,backward函数的具体格式如下:

参数说明如下。
 tensor: 用于计算梯度的tensor。
 grad_tensors: 在计算非标量的梯度时会用到。其形状一般需要和前面的tensor保持一致。
 retain_graph: 通常在调用一次backward后,pytorch会自动把计算图销毁,如果要想对某个变量重复调用backward,则需要将该参数设置为True
 create_graph: 当设置为True的时候可以用来计算更高阶的梯度
 grad_variables:这个参数后面版本中应该会丢弃,直接使用grad_tensors就好了。
假设x、w、b都是标量,z=wx+b,对标量z调用backward(),我们无须对backward()传入参数。以下是实现自动求导的主要步骤。
1)定义叶子节点及算子节点。

运行结果如下:
x,w,b的require_grad属性分别为:False,True,True
2)查看叶子节点、非叶子节点的其他属性。

3)自动求导,实现梯度方向传播,即梯度的反向传播。

2.5.4 非标量反向传播

2.5.3小节我们介绍了当目标张量为标量时,调用backward()无须传入参数。目标张量一般是标量,如我们经常使用的损失值Loss,一般都是一个标量。但也有非标量的情况,后面我们介绍的Deep Dream的目标值就是一个含多个元素的张量。如何对非标量进行反向传播呢?PyTorch有个简单的原则,不让张量对张量求导,只允许标量对张量求导,因此,如果目标张量对一个非标量调用backward(),需要传入一个gradient参数,该参数也是张量,而且其形状需要与调用backward()的张量形状相同。
为什么要传入一个张量gradient?这是为了把张量对张量求导转换为标量对张量求导。这有点拗口,我们举一个例子来说,假设目标值为loss=(y_1,y_2,\cdots,y_m)传入的参数为v=(v_1,v_2,\cdots,v_m),那么就可把对loss的求导,转换为对loss*v^T标量的求导。即把原来\frac {\partial {loss}}{\partial X}得到雅可比矩阵(Jacobian)乘以张量v^T,便可得到我们需要的梯度矩阵。
1、 非标量简单示例
我们先看目标张量为非标量的简单实例。

运行后会报错:RuntimeError: grad can be implicitly created only for scalar outputs。这是因为张量y为非标量所致。
如何避免类似错误呢?我们手工计算Y的导数。已知:
X=[x_1,x_2]
Y=[x_1^2+3,x_2^2+3]
如何求\frac {\partial Y}{\partial X}呢?
Y为一个向量,如果我们想办法把这个向量转变成一个标量不就好了?比如我们可以对Y求和,然后用求和得到的标量在对X求导,这样不会对结果有影响,例如:
Y_{sum}=\sum y_i =x_1^2+x_2^2+6
\frac {\partial Y_{sum}}{\partial x_1}=2x_1,\frac {\partial Y_{sum}}{\partial x_2}=2x_2
这个过程可写成如下代码。

可以看到对y求和后再计算梯度没有报错,结果也与预期一样。
实际上,对Y求和就是等价于Y点积一个的全为1的向量或矩阵。即,而这个向量矩阵V也就是我们需要传入的grad_tensors参数。(点积只是相对于一维向量而言的,对于矩阵或更高为的张量,可以看做是对每一个维度做点积。)
2.非标量复杂实例
(1)定义叶子叶子节点及计算节点

(2)手工计算y对x的梯度
我们先手工计算一下y对x的梯度,为了验证PyTorch的backward的结果是否正确。
y对x的梯度是一个雅可比矩阵,各项的值,我们可通过以下方法进行计算。
假设x=(x_1=2,x_2=3),y=(y_1=x_1^2+3x_2,y_2=x_2^2+2x_1),不难得到:

x_1=2,x_2=3时,
(3)调用backward获取y对x的梯度
这里我们可以分成两步的计算。首先让v=(1,0)得到y_1对x的梯度,然后使v=(0,1),得到y_2对x的梯度。这里因需要重复使用backward(),需要使参数retain_graph=True,具体代码如下:

运行结果如下:
tensor([[4., 3.],[2., 6.]])
这个结果与手工运行的式(2.5)结果一致。
(4)如果V值不对,将导致错误结果。
如果取v=[1,1]将导致错误结果,代码示例如下:

这个结果与我们手工运算的不符,显然这个结果是错误的,错在哪里呢?这个结果的计算过程是:
J^T\cdot v^T=\left(\begin{matrix} 4 & 2\cr 3 & 6 \end{matrix}\right)\left(\begin{matrix} 1\cr 1\end{matrix}\right)=\left(\begin{matrix} 6\cr 9\end{matrix}\right)\tag{2.7}
由此,错在v的取值错误,通过这种方式得的到并不是y对x的梯度。
3.小结
1)PyTorch不允许张量对张量求导,只允许标量对张量求导,求导结果是和自变量同型的张量。
2)为避免直接对张量求导,可以利用torch.autograd.backward()函数中的参数grad_tensors, 把它转换标量来求导。 y.backward(v) 的含义是:先计算 loss = torch.sum(y * v),然后求 loss 对(能够影响到 y 的)所有变量 x 的导数。这里,y和 v是同型 Tensor。也就是说,可以理解成先按照 v对y的各个分量加权,加权求和之后得到真正的 loss,再计算这个 loss 对于所有相关变量的导数。
3)PyTorch中的计算图是动态计算图,动态计算图有两个特点:正向传播是立即执行的;反向传播后计算图立即销毁。我们把PyTorch使用自动微分的计算图的生命周期用图2-11来表示。
图2-11 PyTorch计算图的生命周期

2.5.5切断一些分支的反向传播

训练网络时,有时候我们希望保持一部分的网络参数不变,只对其中一部分的参数进行调整;或者只训练部分分支网络,并不让其梯度对主网络的梯度造成影响,这时候可以使用detach()函数来切断一些分支的反向传播。
detach_()将张量从创建它的计算图(Graph)中分离,把它作为叶子节点,其grad_fn=None且requires_grad=False。
假设y是作为x的函数,而z则是y和x的函数。如果我们想计算z关于x的梯度,但由于某种原因,我们希望将y视为一个常数。为此,我们可以分离y来返回一个新变量c,c变量与y具有相同的值, 但丢弃计算图中如何计算y的任何信息。 换句话说,梯度不会向后流经c到x。 因此,下面的反向传播函数计算z=c*x关于x的偏导数,同时将c作为常数处理,即有\frac {\partial z}{\partial X}=c,而不是把z=x^3+3关于x的偏导数,\frac {\partial z}{\partial X}\neq 3x^2

由于变量c记录了y的计算结果,在y上调用反向传播, 将得到y= x**2+3关于的x的导数,即2*x。

2.6 使用NumPy实现机器学习

前面我们介绍了NumPy、Tensor的基础内容,对如何用NumPy、Tensor操作数组有了一定认识。为了加深大家对PyTorch的谅解,本章剩余章节将分别用NumPy、Tensor、autograd、nn及optimal实现同一个机器学习任务,比较它们的异同及优缺点,从而加深对PyTorch的理解。
首先,我们用最原始的NumPy实现一个有关回归的机器学习任务,不用PyTorch中的包或类。这种方法的代码可能会多一点,但每一步都是透明的,有利于理解每步的工作原理。
主要步骤分析如下。
首先,是给出一个数组x,然后基于表达式:y=3x^2+2,加上一些噪声数据到达另一组数据y。
然后,构建一个机器学习模型,学习表达式y=wx^2+b的两个参数w,b。利用数组x,y的数据训练模型。
最后,采用梯度下降法,通过多次迭代,学习到w、b的值。
1)导入需要的库。

2)生成输入数据x及目标数据y。设置随机数种子,生成同一个份数据,以便用多种方法进行比较。

3)查看x,y数据分布情况。

运行结果如图2-12所示。

图2-12 NumPy实现的源数据
4)初始化权重参数。

5)训练模型。
定义损失函数,假设批量大小为100:
用代码实现上面这些表达式:

6)查看可视化结果。

运行结果如图2-13所示。

图2-13 可视化NumPy学习结果
[[2.98927619]] [[2.09818307]]
从结果看来,学习效果还是比较理想的。

2.7 使用Tensor及Autograd实现机器学习

2.6节可以说是纯手工完成一个机器学习任务,数据用NumPy表示,梯度学习是自己定义并构建学习模型。这种方法适合于比较简单的情况, 如果稍微复杂一些, 代码量将几何级增加。 是否有更方便的方法呢? 这节我们将使用PyTorch的自动求导的一个
包——autograd,利用这个包及对应的Tensor, 便可利用自动反向传播来求梯度,无须手工计算梯度。以下是具体实现代码。
1)导入需要的库。

2)生成训练数据,并可视化数据分布情况。

运行结果如图2-14所示。

图2-14 可视化输入数据
3)初始化权重参数。

4)训练模型。

5)查看可视化训练结果。

运行结果如图2-15所示。

图2-15 使用 autograd的结果
tensor([[2.9645]], requires_grad=True) tensor([[2.1146]], requires_grad=True)。
这个结果与使用NumPy机器学习的差不多。

2.8 使用优化器及自动微分

使用PyTorch内置的损失函数、优化器和自动微分机制等,可大大简化整个机器学习过程。梯度更新可简化为optimizer.step(),梯度清零可使用optimizer.zero_grad()。详细代码如下。导入模块与生成数据代码与2.7小节的基本相同,只需添加导入nn模块(这个模块第3章将介绍),这里就重写了。
1)定义损失函数及优化器。

2)训练模型。

3)查看可视化运行结果。

运行结果如图2-16所示。

图2-16 使用优化器及自动微分(autograd)的结果
tensor([[2.6369]], requires_grad=True) tensor([[2.2360]], requires_grad=True)
由此可知,使用内置损失函数、优化器及自动微分实现机器学习比较简洁,这也是深度学习普遍采用的方式。

2.9 把数据集转换带批量的迭代器

把数据集转换为带批量的迭代器,这样训练时就可进行批量处理。如果数据量比较大,采用批量处理可提升训练模型的效率及性能。
1)构建数据迭代器。

2)训练模型。

3)查看可视化运行结果。

运行结果如图2-17所示。

图2-17 使用数据迭代器、优化器和自动微分(autograd)的结果
tensor([[2.6370]], requires_grad=True) tensor([[2.2360]], requires_grad=True)

2.10 使用TensorFlow2架构实现机器学习

2.6节用NumPy实现了回归分析,2.7节用PyTorch的autograd及Tensor实现了这个任务。这节我们用深度学习的另一个框架TensorFlow实现该回归分析任务,大家可比较一下不同架构之间的区别。为便于比较,这里使用TensorFlow 2实现这个任务。
1)导入库及生成训练数据。

2)生成训练数据,并初始化参数。

3)构建模型。

4)训练模型。

5)查看可视化运行结果。

运行结果如图2-18所示。

图2-18 使用Tensorflow的结果

2.11 小结

本章主要介绍PyTorch的基础知识,这些内容是后续章节的重要支撑。首先介绍了PyTorch的安装配置,然后介绍了PyTorch的重要数据结构Tensor。Tensor类似于NumPy的数据结构,但Tensor提供GPU加速及自动求导等技术。最后分别用NumPy、Tensor、autograd、Optimizer和TensorFlow2等技术分别实现同一个机器学习任务。

第10章 可视化

俗话说得好,“一图胜千言”,可见图像给我们带来的震撼效果。生活如此,机器学习也如此,图的直观、简单明了同样给我不一样的感觉和理解。那么,如何把数据变成图?如何把一些比较隐含的规则通过图像展示出来呢?
本章主要介绍几个基于Python、TensorFlow开发的可视化的强大工具,具体包括:
 matplotlib
 pyecharts

10.1 matplotlib

matplotlib 是 Python 中最著名的2D绘图库,它提供了与 matlab 相似的 API,十分适合交互式绘图,简单明了,功能强大,而且可以方便地作为绘图控件,嵌入 GUI 应用程序中。下面我们进入matplotlib的世界,开始我们的数据可视化之旅。

10.1.1 matplotlib的基本概念

在介绍matplotlib前,首先要保证环境中安装了Python。建议使用Anaconda安装,因为Anaconda安装包中包含很多常用的工具包,如matplotlib、NumPy、Pandas、Sklearn等,并且后续的更新维护也非常方便。
在绘制我们的第一个图形之前,我们先来了解几个matplotlib的非常重要的概念,以帮助我们更快地理解matplotlib的各种API,以及能让你和你的同事使用一种大家都能听得懂的语言以及术语进行沟通。
matplotlib设置坐标主要参数配置详细说明及示例说明如下。
1)导入绘图相关模块;
2)生成数据;
3)plot绘制图形,(选 - 线条设置)设置线linestyle或标记marker;
4)(选 - 坐标轴设置 - 添加坐标标签)给x轴添加标签xlabel和y轴添加标签ylabel;
5)(选 - 坐标轴设置 - 添加坐标刻度)设置x轴的刻度xlim()和y轴的刻度ylim();
6)(选 - 图例设置label)设置图例legend();
7)输出图形show()。
下面来看一个使用matplotlib绘图的实例,具体如下:

使用matplotlib对数据进行可视化的示例的运行结果如图10-1所示。

图10-1 使用matplotlib对数据进行可视化
也可以把图10-1拆成两个图,代码如下。

把图10-1拆成两个图的运行结果如图10-2所示。

图10-2 把图5-1拆成两个图

10.1.2 使用matplotlib绘制图表

matplotlib能绘制出各种各样的图表,所以开发人员可根据需要展示的数据格式、内容以及要用图表来达到的效果来选择合适的图形种类。下面我们通过日常工作中最常用的4种图表来做一个演示。
1.柱状图
柱状图是指用一系列高度不等的纵向条纹或者线段直观地显示统计报告来帮助人们理解数据的分布情况。在绘制柱状图时,我们可以使用plt.bar(x,y,tick_label),给出x,y坐标值,同时给出x坐标轴上对应刻度的含义等,示例如下。

绘制出的柱状图如图10-3所示。

图10-3 柱状图
2. 折线图
折线图通常用来显示随时间变化而变化的连续的数据,它非常适用于展示在相等的时间间隔下的数据的变化趋势。比如,使用折线图展示一个系统从2010年到2020年的每年的注册人数。在绘制折线图时,我们可以使用plt.plot()。 下面我们用折线图来显示系统注册人数的变化情况。

绘制出的折线图如图10-4所示。

图10-4 折线图
从图10-4中我们可以直观地看到,系统的注册人数在2011年进入了一个谷值,而2014是峰值。
3. 饼图
饼图常常用来显示一个数据系列中各项的大小及其在整体中的占比。比如我们可以用下面的饼图来展示每个人的月收入,并显示他们的月收入占总体收入的比例。

绘制出的饼图如图10-5所示。

图10-5 饼图
4. 散点图
散点图是指在回归分析中数据点在坐标系平面上的分布图,用于表示因变量随自变量变化而变化的大致趋势,从而帮助我们根据其中的关系选择合适的函数对数据点进行拟合。下面我们绘制一张身高和体重关系的散点图。

绘制出的散点图如图10-6所示。

图10-6 散点图
除了上述介绍的4种图形,matplotlib还可以绘制其他图形,比如线箱图、极限图、气泡图等。感兴趣的读者可以自行查阅matplotlib的网站或者源代码,以了解更多内容。

10.1.3 使用rcParams

rcParams用于存放matplotlib的图表全局变量,我们可以用它来设置全局的图表属性,当然在进行具体图表绘制的时候,我们也可以对全局变量进行覆盖。下面介绍几个常用的全局变量。注意,如果想在图表中显示中文内容,比如显示中文标题,则需要在matplotlib的全局变量rcParams里进行设置。
1)没设置rcParams属性。

运行结果如图10-7所示。

图10-7 没有设置rcParams属性的情况
如图10-7所示,中文标题没有正确显示,而是随机变成几个方框。此时,通过rcParams设置文字属性即可使标题正确显示。

运行结果如图10-8所示。

图10-8 设置rcParams属性的情况
更多关于rcParams的设置问题,请参照matplotlib官网(https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)。

10.2 pyecharts

我们接下来要介绍的pyecharts正是Python版本的eCharts。
相较于经典的matplotlib,pyecharts可以在保证易用、简洁、交互性的基础上让开发人员绘制出种类更加丰富(比如3D,和地图模块的集成)、样式更加新颖的图表。下面我们先来看如何安装pyecharts 。

10.2.1 pyecharts安装

pyecharts 是一个用于生成 ECharts 图表的类库,官网为https://pyecharts.org/。
pyecharts有两个大的版本,v0.5.x 以及 v1.x。其中, v0.5.x 支持 Python 2.7 以及Python 3.4, v1.x 支持Python 3.6及以上版本。考虑到v0.5版本已经不再维护,而且大多数公司已经升级到Python 3.7及以上版本,所以本节只介绍1.x版本,并且以最新版v1.9为基础进行讲解。pyecharts安装主要有两种方式,通过源码或者pip安装,这里以pip安装为例进行讲解:

【说明】安装pyecharts时,可改用国内的安装源,如清华安装源,以提高下载速度,具体代码如下:

10.2.2 使用pyecharts绘制图表

我们先来用一个简单的例子直观地了解如何使用pyecharts绘图,体会它的便利性和优雅。

绘制出的pyecharts的柱状图如图10-9所示。
图10-9 pyecharts的柱状图
上述代码显示了苏州XX渔具店在2020年和2021年各种子品类的销售金额。 首先我们创建了一个Bar类型的图表,添加了X轴(add_xaxis)来代表各种品类,之后添加了两个Y轴的数据(add_yaxis)来代表2020年以及2021年的业绩。为了让图表更加容易理解,我们增加了标题以及副标题(title以及subtitle)。
用Pyecharts画的柱状图非常优雅,当然,用它画其他图形同样如此。绘制出的图形如下:
1. 仪表盘(Gauge)
我们第一个例子来模拟汽车的仪表盘,仪表盘上显示这辆汽车的最高时速,以及当前行驶速度,汽车仪表盘还会使用醒目的红色提醒驾驶员不要超速行驶,我们把这些信息一并添加到我们需要绘制的图形里面。

运行结果:

图10-10 仪表盘
大家可以从上图看到,仪表盘图形(Gauge)非常适合展示进度或者占比信息,通常我们会把几个仪表盘图形组合成一个组合图表进行展示,这样能让使用者对全局的信息有个快速的了解。不如,我们可以用几个仪表盘图形展示我们集群里面各个节点的健康状态,它们的CPU的使用率,IO的吞吐是不是在一个可承受的范围内等等。
2、地理坐标系(Geo)
这几年,各大app推出一个显示用户出行轨迹的应用广受各位旅游达人以及飞人的喜欢,在一张中国地图或者世界地图上,用箭头代表自己的飞行路径,线段的粗细代表了飞行这条航线的频率,让用户对自己过去一年的行踪有个直观的认识,也当做是在朋友圈凡尔赛的资料。接下来,我们用pyecharts来大概模拟这个功能。

pyecharts内嵌了中国以及各个省份的矢量图,可以方便的绘制出你想要的区域,使用者可以通过使用坐标或者城市名称的形式标定出具体的位置,进而用不同的颜色代表特殊的含义。

10.2.3 从上海出发的航线图

用于带有起点和终点信息的线数据的绘制,主要用于地图上的航线、路线的可视化。

打开shanghai-out.html

图10-11 从上海到各城市的航线图

10.3 实例:词云图

词云图又叫文字云,是对文本数据中出现频率较高的关键词予以视觉上的突出,形成"关键词的渲染"就类似云一样的彩色图片,从而过滤掉大量次要信息,使人一眼就可以领略文章的核心要义。

10.3.1 实例概况

实例环境:windows或linux,Python3.6+,jieba (中文分词),PIL(图像处理),wordcloud (词云表现)、matplotlib(图像显示)等。其中jieba、wordcloud需要用pip安装。具体安装方法如下:

文本信息:使用环球时报上一篇文章,题为《“中国芯”亟待顶层设计》

10.3.2 代码实现

【说明】大家可以从网络上随便采取一段文章作为输入文本(本例的输入文本为:chinese-core.txt),背景图片可以从网上随便下载一个作为词云背景图(本例的背景图像:back.jpg)。

图10-12 词云图

10.4 练习

1、尝试用其他主题文章进行词云展示。
2、尝试使用pyecharts的WordCloud画一下词云图,然后比较一下与有何区别。

第8章 文件处理和异常处理

8.1 问题:Python如何获取文件数据?

Python处理文件的步骤包括打开、读写、关闭。第一步当然就是先要打开文件。要以读文件的模式打开一个文件对象,使用Python内置的open()函数,传入文件名和其他参数。open() 函数常用形式是接收两个参数:文件名(file)和模式(mode),如:

完整的语法格式为:

其中:
 file: 必需,文件路径(相对或者绝对路径)。如果是linux环境,路径一般表示为'./data/file_name',如果是windows环境,一般表示为'.\data\file_name',因反斜杠"\"在Python中被视为转义字符,为确保正确,应以原字符串的方式指定路径,即在开头的单引号前加上r。
 mode: 可选,文件打开模式
 buffering: 可选,设置缓冲
 encoding: 可选,一般使用utf8
 errors: 可选,报错级别
 newline: 可选,区分换行符,如\n,\r\n等
 closefd: 可选,传入的file参数类型
 opener: 可选,可以通过调用*opener*来自定义opener。
用open()函数打开文件具体代码如下:

运行结果:
Python,java
PyTorch,TensorFlow,Keras
在操作系统中对文件的操作划分了很多权限,比如读权限、写权限、追加方式写和覆盖方式写等等。Python打开文件的常用语法格式是open(file,mode=’r’),第二个参数是字符,其值有规定的内容和含义,如表9-1所示:
表9-1 mode参数取值和含义

参数值 含义
‘r’ 以只读方式打开已存在的文件
‘w’ 以写入方式打开文件,如不存在则自动创建
‘x’ 以可写入方式打开文件
‘a’ 以追加方式打开文件,新写入的内容会附加在文件末尾
‘b’ 以二进制方式打开文件
‘t’ 以文本方式打开文件
‘+’ 以读写方式打开文件
‘U’ 通用换行符模式(不建议使用)

上面的参数值可以配合使用,比如open(file,’ab’)就是以追加方式打开二进制文件。如果open()方法不写mode参数,mode的默认值是’rt’,即只读方式打开文本文件。
如果要打开的文件并不存在,open方法会报错。如下所示:

这种报错信息叫做异常,如何捕捉异常、如何处理异常等9.4章节将介绍。

8.2基本文件操作

对文件的常用操作包括读取文件,写入文件。读取文件又可以根据文件的大小选择不同的读取方式,如按字节读取、逐行读取、读取整个文件等方式。

8.2.1 读取文件

打开文件后,读取文件使用read()方法。一个文本文件由多行字符串组成,而一行字符串又由多个字符组成。read(size)方法是以字节为单位读取文件内容。比如read(1)就是从当前文件指针位置开始,读取1个字节的内容。如果read()括号中没有数字或是负数,则读取整个文件内容。
(1)按字节读取
下面代码每次从文件中读取固定的1个字节。每次读完后,文件指针会指向下一个字节的位置,就好比用瓢从水缸中舀水,每次都盛出相同的水量。

(2)读取整个文件
不指定read()括号中的参数,会读取整个文件内容。

8.2.2读取文件使用with语句

无论使用哪种高级语言来读取文件,都是先打开磁盘上的一个物理文件,获得一个文件句柄,通过这个句柄(或称作文件对象)来读取,最后再关闭。如果代码中忘记了关闭文件对象,这个文件对象会一直存在于内存中,除非使用close()方法来释放这个文件对象所占用的空间。Python语言为了避免忘记关闭文件,提供了with关键字来自动关闭文件。即使用with格式,就不需要再写close语句了。

8.2.3 逐行读取文件

使用read()方法要么读取整个文件,要么读取固定字节数,总归不太方便。文本文件都是由多行字符串组成,Python也可以逐行读取文件,使用readline()方法。

(1)逐行读取文件内容并打印

运行结果如下:
no,name,age,gender

01,李康,15,M

02,张平,14,F

03,刘畅,16,M
(2)从上面的打印结果可以看出,行之间多了一个空行。为何出现这种情况?这是因为在文件中,每行的末尾都有一个不可见的换行符(如\n),print语句会加上这个换行符。如何去掉这些空行?只要在print中使用rstrip()或strip()即可:

运行结果:
no,name,age,gender
01,李康,15,M
02,张平,14,F
03,刘畅,16,M
(3)使用readline()可以每次读取一行
使用readline()也会把文件中每行末尾的回车符读进来,如果需要去掉这些空行,同样可以使用rstrip或strip函数。

8.2.4 读取文件所有内容

使用readline()方法虽然可以一次读一行,比使用read(size)方法一次读一个字节方便了不少,但每次运行readline()方法后,文件指针会自动指向下一行,仍然要再调用一次readline()方法,才能读取下一行内容。还是不方便。
(1)使用readlines()读取文件所有内容
Python还提供了readlines()方法一次把文件所有行都读出来,放入到一个列表中。

下面定义一个类Stu,该类实现利用readlines()函数返回的列表中,并处理每行的每列数据,并打印出每列属性值。
(2)定义类Stu

(3)处理文件中每列数据

#上面代码运行的结果如下:
学号:01,姓名:李康, 年龄:15,性别:M
学号:02,姓名:张平, 年龄:14,性别:F
学号:03,姓名:刘畅, 年龄:16,性别:M
上面介绍的三种读文件的方法,都是从文件头开始读,直到遇到文件结束符(EOF)。这种读取方式称作顺序读取。如果一个文件有几个G大小,我想读出其中的一小部分内容,可以采取随机读取方式,使用seek()或tell()方法,有兴趣的读者可以参考Python文档资料。

8.2.5写入文件

write(str)方法把str字符串写入文件,返回值是str字符串的长度。写文件前要先使用追加或写入模式打开文件。

上面代码中的文件名newfile,如果不存在将自动创建。写入的方式是"a",即追加的方式,如果存在将往里追加记录没有指定扩展名。。但写入的是字符串,仍然是一个文本文件,可以使用记事本查看。write方法写入的字符串最后不会加上回车键\n。
如果要把多行内容写文件,可以每行都调用write方法,Python也提供了writelines(seq)方法一次性写入多行内容。参数seq是一个列表或元祖。

以'w'模式写入文件时,如果文件已存在,会直接覆盖(相当于删掉后新写入一个文件)。
上面写入文件的字符串要加入回车键,否则即使调用多次writelines()方法,Python执行时也不会自动加上回车。
【说明】
要读取非UTF-8编码的文本文件,需要给open()函数传入encoding参数,例如,读取GBK编码的文件:

更多信息可参考:
https://blog.csdn.net/xrinosvip/article/details/82019844

8.3 异常处理

8.3.1 如何使你的程序更可靠?

写出程序能运行不是我们的目的,写出能运行且不出错的程序才是本事。代码的健壮性和稳定性是衡量一个软件好坏的指标之一。大多数高级语言都提供了异常处理机制来确保代码的健壮性。Python的异常处理语法简单且功能实用,是必须要掌握的要点。

8.3.2 捕获异常

异常处理有两个关键字:try和except。这两个关键字把程序分成两个代码块。try中放置程序正常运行代码,except中是处理程序出错后的代码。其语句结构如下:

try..except代码执行过程类似于 if...else,但后者仅限于可以预知的错误,而使用except是来捕获隐藏的错误。下面代码演示除数为零的异常。

在进行文件操作时,也会出各种异常情况,同样适用try..except语法格式。以下代码中要打开的文件并不存在,程序捕捉到这种异常后,会进入except模块

8.3.3 捕获多种异常

异常的种类有多种,针对不同类型的异常可以做区别处理。Python中定义的异常类型有很多种,常见的几种类型可参考表9-3:
表9-3 常见异常种类

异常类名 含义
AttributeError 对象缺少属性
IOError 输入/输出操作失败
ImportError 导入模块/对象失败
KeyError 集合中缺少键值错误
NameError 未声明或初始化变量
OSError 操作系统错误
StopIteration 迭代器没有更多的值
ZeroDivisionError 除数为0或用0取模
Exception 常规异常的基类

捕获多种异常的语法格式为:

我们把上一节的两种异常代码合并处理:

多个except并列时,try中的代码最先遇到哪个异常种类,就会进入对应的except代码块,而忽略其他的异常种类。except..as 后面的变量名e是为该异常类创建的实例,可以拿到具体的异常信息。

8.3.4 捕获所有异常

既然有那么多的异常种类,我需要每个都捕获么?那样代码写起来太冗长了。Python的每个常规异常类型被定义成了一个类,这些类都有一个共同的父类,就是Exception类。在不需要区分异常类型的情况下,把所有异常都归入Exception类也是通用的做法。

另外需要注意,如果多个except并列出现,要把Exception基类放在最下面,否则会出现某个异常种类捕捉不到的情况。以下代码是错误的:

8.3.5 清理操作

异常处理中还有一个关键字是finally。final是最终的意思,finally代码块放在所有except代码的后面,无论是否执行了异常代码,finally中的代码都会被执行。

finally关键字只能出现一次,里面的代码主要完成清理工作。比如关闭文件、关闭数据库链接、记录运行日志等。如下代码把关闭文件放在finally中。

由于try、except、finally分属三个代码块,myfile变量需要定义在外面,以便在代码块中可以引用。

8.3.6 练习

编写一个脚本,实现以下功能:
(1)把用户名、用户登录密码写人文件,至少3条记录,文件名为login.txt
(2)文件login.txt列之间用逗号分割。
(3)用input函数作为一个登录界面,输入用户名、用户密码
(4)用input输入中的用户名及用户密码与文件login.txt中的用户名及密码进行匹对,如果两项都对,提示登录成功,否则提示具体错误,如用户名不存在或密码错误等。

第9章 正则化

正则表达式是处理字符串的强大工具,它有自己特定的语法结构,有了它,实现字符串的检索、替换、匹配验证都不在话下。当然,对于爬虫来说,有了它,从HTML里提取想要的信息就非常方便了。

9.1 简单实例

打开开源中国提供的正则表达式测试工具http://tool.oschina.net/regex/,输入待匹配的文本,然后选择常用的正则表达式,就可以得出相应的匹配结果了。例如,这里输入待匹配的文本如下:
my email is wumg3000 and my website is http://feiguyunai.com
在下图的输入框输入以上语句,然后点击测试匹配,则可得到匹配结果。

【结果解释】

a-z代表匹配任意的小写字母,\s表示匹配任意的空白字符(等价于\t \n \r\f),[^\s]表示不是非空白字符,*就代表匹配前面的字符任意多个,这一长串的正则表达式就是这么多匹配规则的组合。
[a-zA-Z]+://[^\s]* 结果为:
.*[a-zA-Z]+://[^\s]* 结果为:

9.2 常用匹配规则

以下是常用匹配规则

模式 描述
\w 匹配字母、数字及下划线, 注意 Unicode 正则表达式会匹配中文字符.
\W 匹配不是字母、数字及下划线的字符
\s 匹配任意空白字符,包括空格,制表符等,价于[ \t\n\r\f]  \r回车,\f换页
\S 匹配任意非空字符,等价于[^\f\n\r\t]
\d 匹配任意数字,等价于[0-9]
\D 匹配任意非数字的字符
\A 匹配字符串开头
\Z 匹配字符串结尾,如果存在换行,只匹配到换行前的结束字符串
\z 匹配字符串结尾,如果存在换行,同时还会匹配换行符
\G 匹配最后匹配完成的位置
\n 匹配一个换行符
\t 匹配一个制表符
^ 匹配一行字符串的开头
$ 匹配一行字符串的结尾
. 匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符
[...] 用来表示一组字符,单独列出,比如[amk]匹配a、m或k
[^...] 不在[]中的字符,比如[^abc]匹配除了a、b、c之外的字符
* 匹配前一个字符0个或多个
+ 匹配前一个字符1个或多个
? 匹配0个或1个前面的正则表达式定义的字符,非贪婪方式
{n} 精确匹配n个前面的表达式
{n, m} 匹配n到m次由前面正则表达式定义的片段,贪婪方式
a|b 匹配a或b
( ) 匹配括号内的表达式,也表示一个组

说明:
*、+、? {n}、{n,m}等为数量限定。

正则表达式特殊字符优先级:

优先级 符号
最高 \
“()” “(?:)” “(?=)” “[]”
中(数量限定) “*”“+” “?”“{n}” “{n,}” “{n,m}”
“^” “$” “中介字符”
次最低 串接,即相邻字符连接在一起
最低 “|”

9.3 常用函数

正则表达式常用函数

match() 决定正则表达式对象是否在字符串最开始的位置匹配。注意:该方法不是完全匹配。当模式结束时若 原字符串还有剩余字符,仍然视为成功。想要完全匹配,可以在表达式末尾加上边界匹配符“$”
search() 在字符串内查找模式匹配,只要找到第一个匹配然后返回,如果字符串没有匹配,则返回“None”
findall() 遍历匹配,可以获取字符串中所有匹配的字符串,返回一个列表
sub() 替换原字符串中每一个匹配的子串后返回替换后的字符串

9.3.1match()

这里首先介绍re的第一个常用的匹配方法——match(),向它传入要匹配的字符串以及正则表达式,就可以检测这个正则表达式是否匹配字符串。其格式为:
re.match(pattern, string, flags=0)
参数说明:
 Pattern:匹配的正则表达式
 String:匹配的字符串
 Flags:标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。
match()方法会尝试从字符串的起始位置匹配正则表达式,如果匹配,就返回匹配成功的结果;如果不匹配,就返回None。示例如下:

运行结果
1

Hello 123 4567 World_This
(0, 25)
【结果说明】
用它来匹配这个长字符串。
开头的^是匹配字符串的开头,也就是以Hello开头;
然后\s匹配空白字符,用来匹配目标字符串的空格;
\d匹配数字,3个\d匹配123;
然后再写1个\s匹配空格;
后面还有4567,我们其实可以依然用4个\d来匹配,但是这么写比较烦琐,所以后面可以跟{4}以代表匹配前面的规则4次,也就是匹配4个数字;
然后后面再紧接1个空白字符,
最后\w{10}匹配10个字母及下划线。
而在match()方法中,第一个参数传入了正则表达式,第二个参数传入了要匹配的字符串。
打印输出结果,可以看到结果是SRE_Match对象,这证明成功匹配。该对象有两个方法:group()方法可以输出匹配到的内容,结果是Hello 123 4567 World_This,这恰好是正则表达式规则所匹配的内容;span()方法可以输出匹配的范围,结果是(0, 25),这就是匹配到的结果字符串在原字符串中的位置范围。
如果想从字符串中提取一部分内容,该怎么办呢?
可以使用()括号将想提取的子字符串括起来。()实际上标记了一个子表达式的开始和结束位置,被标记的每个子表达式会依次对应每一个分组,调用group()方法传入分组的索引即可获取提取的结果。示例如下:

运行结果

Hello 1234567 World
1234567
(0, 19)
【结果说明】
可以看到,我们成功得到了1234567。这里用的是group(1),它与group()有所不同,后者会输出完整的匹配结果,group(1)输出第一个被()包围的匹配结果。假如正则表达式后面还有()包括的内容,那么可以依次用group(2)、group(3)等来获取,如下例

运行结果为

Hello 1234567 World_This is
1234567
is
(0, 27)
【练习】
1、用简单的正则表达式,输出如下结果:
Hello 1234567 World_This is
2、用简单的正则表达式,输出如下结果:
Hello 1234567 World_This

1、通配符
刚才我们写的正则表达式其实比较复杂,出现空白字符我们就写\s匹配,出现数字我们就用\d匹配,这样的工作量非常大。其实完全没必要这么做,因为还有一个万能匹配可以用,那就是.*(点星)。其中.(点)可以匹配任意字符(除换行符),*(星)代表匹配前面的字符无限次,所以它们组合在一起就可以匹配任意字符了。有了它,我们就不用挨个字符地匹配了。

接着上面的例子,我们可以改写一下正则表达式:

运行结果

Hello 1234567 World_This is a Regex Demo
7
(0, 40)
【结果说明】
这里为何是7而不是1234567?
这里就涉及一个贪婪匹配与非贪婪匹配的问题了。在贪婪匹配下,.*会匹配尽可能多的字符。正则表达式中.*后面是\d+,也就是至少一个数字,并没有指定具体多少个数字,因此,.*从开始处抓取满足模式的最长字符,这里就把123456匹配了,给\d+留下一个可满足条件的数字7,最后得到的内容就只有数字7了。
2、贪婪与非贪婪
为了达到我们预期的效果,我们可以采用非贪婪的方式。
非贪婪匹配的写法是.*?,多了一个?,那么它可以达到怎样的效果?我们再用实例看一下:

运行结果

Hello 1234567 World_This is a Regex Demo
1234567
(0, 40)

【结果说明】
此时就可以成功获取1234567了。原因可想而知,贪婪匹配是尽可能匹配多的字符,非贪婪匹配就是尽可能匹配少的字符。当.*?匹配到Hello后面的空白字符时,再往后的字符就是数字了,而\d+恰好可以匹配,那么这里.*?就不再进行匹配,;留给\d+去匹配后面的数字。所以这样.*?匹配了尽可能少的字符,\d+的结果就是1234567了。
所以说,在做匹配的时候,字符串中间尽量使用非贪婪匹配,也就是用.*?来代替.*,以免出现匹配结果缺失的情况。
但这里需要注意,如果匹配的结果在字符串结尾,.*?就有可能匹配不到任何内容了,因为它会匹配尽可能少的字符。例如:

运行结果
result1匹配结果:
result2匹配结果: kEraCN
【结果说明】
因.*?为非贪婪模式---即匹配尽可能少的字符,故.*?没有匹配到任何结果,而.*则尽量匹配多的内容,成功得到了匹配结果
3、标志符
在re.match函数中,有一个flags参数,缺省值为0,如果不为0,它有哪些作用呢?

运行结果:
报错,报错信息如下:
AttributeError Traceback (most recent call last)
in ()
5 '''
6 result = re.match('^He.*?(\d+).*?Demo$', content)
----> 7 print(result.group(1))

AttributeError: 'NoneType' object has no attribute 'group'
【结果说明】
运行直接报错,也就是说正则表达式没有匹配到这个字符串,返回结果为None,而我们又调用了group()方法导致AttributeError。
那么,为什么加了一个换行符,就匹配不到了呢?这是因为\.匹配的是除换行符之外的任意字符,当遇到换行符时,.*?就不能匹配了,所以导致匹配失败。这里只需加一个修饰符re.S,即可修正这个错误:

运行结果
1234567

这个re.S在网页匹配中经常用到。因为HTML节点经常会有换行,加上它,就可以匹配节点与节点之间的换行了。另外,还有一些标识符,在必要的情况下也可以使用,如下表。

标识符 描述
re.I 使匹配对大小写不敏感
re.L 做本地化识别(locale-aware)匹配
re.M 多行匹配,影响^和$
re.S 使.匹配包括换行在内的所有字符
re.U 根据Unicode字符集解析字符。这个标志影响\w、\W、 \b和\B
re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解

4、转义字符
我们知道正则表达式定义了许多匹配模式,如.匹配除换行符以外的任意字符,但是如果目标字符串里面就包含.,那该怎么办呢?
这里就需要用到转义匹配了,示例如下:

运作结果

当遇到用于正则匹配模式的特殊字符时,在前面加反斜线(\)转义一下即可。

9.3.2 search()

match()方法是从字符串的开头开始匹配的,一旦开头不匹配,那么整个匹配就失败了。我们看下面的例子:

运算结果
None

【结果说明】
在匹配时,search()方法扫描整个字符串,返回第一个匹配字符串,如果搜索完了还没有找到,就返回None。

因此,为了匹配方便,我们可以尽量使用search()方法。下面再用几个实例来看看search()方法的用法。
首先,这里有一段待匹配的HTML文本,接下来写几个正则表达式实例来实现相应信息的提取

从以上HTML文件可知,ul节点里有许多li节点,其中li节点中有的包含a节点,有的不包含a节点,a节点还有一些相应的属性——超链接和歌手名、歌曲名。

以下我们从这个HTML文件中提取歌手名和歌名,正则表达式该如何写呢?
首先,我们尝试提取class为active的li节点内部的超链接包含的歌手名和歌名,此时需要提取第三个li节点下a节点的singer属性和文本。

此时正则表达式可以以li开头,然后寻找一个data-view为7,中间的部分可以用.*?来匹配。接下来,要提取singer这个属性值,所以还需要写入singer="(.*?)",这里需要提取的部分用小括号括起来,以便用group()方法提取出来,它的两侧边界是双引号。
然后还需要匹配a节点的文本,其中它的左边界是>,右边界是。然后目标内容依然用(.*?)来匹配,所以最后的正则表达式就变成了:

然后再调用search()方法,它会搜索整个HTML文本,找到符合正则表达式的第一个内容返回。另外,由于代码有换行,所以这里第三个参数需要传入re.S。整个匹配代码如下

运行结果
任贤齐沧海一声笑
【注意】
由于绝大部分的HTML文本都包含了换行符,所以尽量都需要加上re.S修饰符,以免出现匹配不到的问题。

9.3.3findall()

前面我们介绍了match()、search()方法,
match()从字符串的起始位置匹配正则表达式,如果匹配,就成功返回;如果不匹配,就返回None。
search()方法可以返回匹配正则表达式的第一个内容,如果还有匹配内容,不会返回。
如果想要获取匹配正则表达式的所有内容,那该怎么办呢?这时就要借助findall()方法了。该方法会搜索整个字符串,然后返回匹配正则表达式的所有内容。
还是上面的HTML文本,如果想获取所有a节点的超链接、歌手和歌名,就可以将search()方法换成findall()方法。如果有返回结果的话,就是列表类型,所以需要遍历一下来依次获取每组内容。代码如下:

运行结果
('/2.mp3', '任贤齐', '沧海一声笑')
/2.mp3 任贤齐沧海一声笑
('/3.mp3', '齐秦', '往事随风')
/3.mp3 齐秦往事随风
('/4.mp3', 'beyond', '光辉岁月')
/4.mp3 beyond 光辉岁月
('/5.mp3', '陈慧琳', '记事本')
/5.mp3 陈慧琳记事本
('/6.mp3', '邓丽君', '但愿人长久')
/6.mp3 邓丽君但愿人长久

这个结果不是很完美,其中还带有 * 的内容,我们有方法去除这些内容吗?有的。使用sub()函数就可简单实现。

9.3.4 sub()

除了使用正则表达式提取信息外,有时候还需要借助它来修改文本。比如,想要把一串文本中的所有数字都去掉,可以借助sub()方法。示例如下:

运行结果
aKyroiRixLg
如果我们要去除<i.*?>,可采用如下方法:

运行结果

正则表达式如果比较长,如果要多次引用,就比较繁琐,有更简洁的方法吗?我们可以采用compile()的方法,通过这个方法把正则表达式编译为一个正则表达式对象,以后引用这个对象即可,这样写起来就简洁多了,如代码:

运行结果
2018-09-15 2018-09-17 2018-09-22

第7章 面向对象编程

7.1 问题:如何实现不重复造轮子?

7.2 类与实例

在面向对象编程中,首先编写类,然后,基于类创建实例对象,并根据需要给每个对象一些其它特性。

7.2.1 创建类

创建类的格式如下:
class class_name:
'''类的帮助信息''' #类文档字符串
statement #类体
定义类无需def关键字,类名后也无需小括号(),如果要继承其它类,要添加小括号,类的继承后面将介绍。
下面以创建表示人的类,它保存人的基本信息及使用这些信息的方法。

创建类要注意的几个问题:
①按约定,在Python中,类的首字母一般大写
②方法__init__()
类中的函数称为方法,__init__()是一个特殊方法,init的前后都是两个下划线,被称为类的构造函数或初始化方法,实例化类时将自动调用该方法。
在方法__init__()中,有三个形参,分别是self、name、age,其中self表示实例本身,而且必须放在其它形参的前面,调用方法时,该参数将自动传入,所以调用方法时,无需写这个实参。self与实例的关系,如图7-1所示。

图7-1 self表示实例本身
③形参name、age
把这两个形参,分别赋给两个带self前缀的两个变量,即self.name、self.age。带self前缀的变量,将与实例绑定,类中的所有方法都可调用它们。这样的变量又称为实例属性。
④方法display()
方法display()只有一个self形参,它引用了两个实例属性。

7.2.2 创建类的实例

其它编程语言实例化类一般用关键字 new,但在 Python 中无需这个关键字,类的实例化类似函数调用方式。以下将类Person实例化,并通过 __init__() 方法接收参数、初始化参数。

根据类Person创建实例p1,使用实参"李白",28调用方法__init__()。访问实例中的方法或属性,使用实例名加句点的方法即可,比如方法name属性及display()方法。

根据实参可以创建不同的实例

7.2.3 访问属性

属性根据在类中定义的位置,又可分为类的属性和实例属性。类属性是定义在类中,但在各方法外的属性。实例属性是方法内,带self前缀的属性。
(1)创建类
在类Person定义一个类属性percount,如下列代码。

(2)实例化并访问类属性和实例属性

类属性可以通过类名或实例名访问。

7.2.4 访问限制

类Person中pernum是类的属性,因各实例都可访问,又称为类的公有属性,公有属性各实例可以访问,也可以修改。如下例

这样对一些属性就不安全了,为了提高一些类属性或实例属性的安全级别,可以设置私有属性,只要命名时,加上两个下划线为前缀即可,如:__percount。私有属性只能在类内部访问,实例不能访问。

类的私有属性__percount、实例的私有属性__pwd只能在类的内部使用,实例及类的外部不能访问。

7.2.5类的专有方法

__init__ : 构造函数,在生成对象时调用
__del__ : 析构函数,释放对象时使用

7.3 继承

继承是面向对象的重要特征之一,继承是两个类或者多个类之间的父子关系,子进程继承了父进程的所有公有实例变量和方法。继承实现了代码的重用。重用已经存在的数据和行为,减少代码的重新编写,python在类名后用一对圆括号表示继承关系, 括号中的类表示父类,如果父类定义了__init__方法。带双下划线 __ 的方法都是特殊方法,除了 __init__ 还有很多,几乎所有的特殊方法(包括 __init__)都是隐式调用的(不直接调用)。则子类必须显示地调用父类的__init__方法,如果子类需要扩展父类的行为,可以添加__init__方法的参数。下面演示继承的实现

运行结果:
fruit's color: red
apple's color: red
grow ...
fruit's color: yellow
banana's color: yellow
banana grow...

7.4 调用父类的init方法

子类(派生类)并不会自动调用父类(基类)的init方法,需要在子类中调用父类的init函数。
(1)如果子类没有定义自己的初始化函数,父类的初始化函数会被默认调用;但是如果要实例化子类的对象,则只能传入父类的初始化函数对应的参数,否则会出错。

(2)如果子类定义了自己的初始化函数,而在子类中没有显示调用父类的初始化函数,则父类的属性不会被初始化

在子类中没有显示调用父类的初始化函数,则父类的属性不会被初始化,因而此时调用子类中name属性不存在:
AttributeError: ‘Child’ object has no attribute ‘name’

(3)如果子类定义了自己的初始化函数,在子类中显示调用父类,子类和父类的属性都会被初始化。

子类定义了自己的初始化函数,显示调用父类,子类和父类的属性都会被初始化的输出结果:
create an instance of: Parent
name attribute is: tom
call __init__ from Child class
create an instance of: Child
name attribute is: data from Child
data from Child

(4) 调用父类的init方法
方法1,父类名硬编码到子类

方法2,利用super调用

运行结果
Parent
Child
HelloWorld from Parent
Child bar fuction
I'm the parent.

7.5 把类放在模块中

为了永久保存函数,需要把函数存放在模块中。同样,要保存类,也需要把定义类的脚本保存到模块中,使用时,根据需要导入相关内容。

7.5.1 导入类

把定义类Person及Student的代码,保存在当前目录的文件名为class_person的py文件中。通过import语句可以导入我们需要的类或方法或属性等。

7.5.2 在一模块中导入另一个模块

创建名为train_class.py的主程序,存放在当前目录下,在主程序中导入模块class_person中的Student类,具体代码如下:

在命令行运行该主程序:

输入一所大学名称: 清华大学
Student(姓名:张华,年龄:21,所在大学:清华大学)

7.6 实例1:使用类和包

这节通过几个实例来加深大家对Python相关概念的理解和使用。

7.6.1 概述

创建一个Person父类,两个继承这个父类的子类:Student和Tencher,它们之间的关系如图7-3 所示。

图7-3 类之间的继承关系

7.6.2 实例功能介绍

(1)创建Person类
属性有姓名、年龄、性别,创建方法displayinfo,打印这个人的信息。
(2)创建Student类
继承Person类,属性所在大学college,专业profession,重写父类displayinfo方法,调用父类方法打印个人信息外,将学生的学院、专业信息也打印出来。
(3)创建Teacher类
继承Person类,属性所在学院college,专业profession,重写父类displayinfo方法,调用父类方法打印个人信息外,将老师的学院、专业信息也打印出来。
(4)创建二个学生对象,分别打印其详细信息
(5)创建一个老师对象,打印其详细信息

7.6.3 代码实现

代码放在当前目录的createclasses,具体包括存放__init__.py和classes.py。另外,在
当前目录存放主程序run_inst.py。以下是各模块的详细实现。
(1)模块classes.py的代码

(2)主程序run_inst.py代码

7.9 练习

(1)高铁售票系统
高铁某车厢有13行、每行有5列,每个座位初始显示“有票”,用户输入座位(如9,1)后,按回车,对应座位显示为“已售”。
(2)创建一个由有序数值对(x, y) 组成的 Point 类,它代表某个点的 X 坐标和 Y 坐标。X 坐标和 Y 坐标在实例化时被传递给构造函数,如果没有给出它们的值,则默认为坐标的原点。
(3)创建一个名为User的类,其中包含属性first_name和last_name,还有用户简介通常会存储的其他几个属性。在类User中定义一个名为describe_user()的方法,它打印用户信息摘要;再定义一个名为greet_user()的方法,它向用户发出个性化的问候。
创建用户实例,调用上述两个方法。

第6章 函数

6.1 问题:如何实现代码共享

这个这个代码块取一个名称,可以直接分享给其他人使用,如果在这个代码块中加上一些功能说明就完美。下节将介绍该功能的代码实现。

6.2 创建和调用函数

根据上节的一个具体需求,我们用一个函数来完成。具体代码如下:
(1)创建函数

定义函数,要主要以下几点:
①定义函数的关键是def
②def 空格后 是函数名称,函数的命名规则与变量的规则一样。
③函数名后紧跟着是一对小括号(),这个不能少,小括号后面是冒号:
④冒号下面的语句将统一缩进4格
⑤最后用return语句 返回这个函数的执行结果,return一般是这个函数最后执行的语句,一般放在最后。当然,还有特殊情况,后续将介绍。
(2)调用这个函数,就可得到结果

(3)修改这个函数
如果把这个自然数固定为10,就失去灵活性了。如果把截止的这个自然数,作为参数传给函数,这样这个函数就可实现累加的任何一个自然数了。为此,我们稍加修改即可。

调用这个函数

(4)加上函数的帮助信息
这个函数到底起啥作用呢?我们可以在函数定义后,加上加一句功能说明或帮助信息,使用这样函数的人,一看这个说明就知道这个函数的功能,功能说明内容放在三个双引号""" """里。查看这个函数功能说明或帮助信息,无需打开这个函数,只要函数名.__doc__便可看到,非常方便。

函数的功能说明或帮助信息,需放在函数的第一句。
查看函数功能说明或其帮助信息。

(5)优化函数
我们可以进一步优化这个函数,为便于大家的理解,使用了for循环。实际上,实现累加可以直接使用Python的内置函数sum即可,优化后的代码如下:

6.3 传递参数

在调用函数sum_1n(n)时,传入一个参数n,这是传入单个参数。Python支持更多格式的传入方式,可以传入多个参数、传入任意个参数等。接下来将介绍函数参数的一些定义及传入方式。

6.3.1 形参与实参

在定义函数时,如果需要传入参数,在括号里需要指明,如sum_1n(n)中n,这类参数就称为形式参数,简称为形参。
在调用函数或执行函数时,函数括号里的参数,如sum_1n(100)中的100,就是实际参数,简称为实参。
在具体使用时,有时人们为简便起见,不分形参和实参,有些参考资料上统称为参数。
函数定义中可以没有参数、一个参数或多个参数。如果有多个参数,在调用函数时也可能要多个实参。向函数传入实参的方式有很多,可以依据函数定义时的位置和顺序确定的位置参数;可以使用关键字实参;也可以使用列表和字典作为实参等。接下来将介绍这些内容。

6.3.2 位置参数

位置参数顾名思义就是跟函数定义时参数位置有关的参数,位置参数必须按定义函数时形参的顺序保持一致。
位置参数是必备参数,调用函数时根据函数定义的形参位置来传递实参。为了更好说明这个原理,还是以函数sum_1n为例。
假设现在修改一下要求,把从1开始累积,改为任何小于n的一个数(如m<n)累积,那么,m也需要作为参数。为此,修改一些函数sum_1n。

定义函数sum_1n时,指明了两个参数:m和n(如果多个参数,需要用逗号分隔),在调用函数sum_1n时,实参需要输入两个,而且这两个实参的位置及顺序必须与形参保持一致,如:

其中1,10或10,20就是位置实参。位置实参的顺序很重要,如果顺序不正确,可能报错或出现异常情况。

6.3.3 关键字参数

为此,我们把函数sum_1n的形参改成有一定含义的单词,调用是直接给这些单词赋值即可。

调用函数时,说明参数名并赋给对应值即可,无需考虑它们的位置或次序。当然,实参名称必须与形参名称一致,否则将报错。

6.3.4 默认值

使用默认值,修改一下函数sum_1n(start,end):

调用函数

6.4 返回值

在Python中,在函数体内用return语句为函数指定返回值,返回值可以是一个或多个,类型可以是任何类型。如果没有return语句,则返回None值,即返回空值。不管return语句在函数体的什么位置,执行完return语句后,就会立即结束函数的执行。下面介绍函数返回值的情况。
(1)返回一个值
上节介绍的函数sum_1n,只返回一个值。如:

(2)返回多个值
把函数sum_1n的返回值改一下,同时返回所有数的累加、偶数累加。

调用函数

(3)返回空值
在函数体中不使用return语句,不过可以用print语句把结果显示出来。

调用函数

6.5 传递任意数量的参数

前面我们介绍了Python支持的几种参数类型,如位置参数、关键字参数和默认值参数,适当使用这些参数,可大大提高Python函数的灵活性。此外,Python还支持列表或元组参数,即用列表或元组作为实参传入函数。例如:

6.5.1 传递任意数量的实参

要实现输入任意数量的实参,只要在形参前加一个*号即可,比如函数calc_sum(lst)改为calc_sum(*lst)。形参加上*号后,在函数内部,把任意多个参数封装在lst这个元组中,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数,示例如下:

通过这种方式传入任意多个实参,就和我们的预期一样了。

6.5.2 传递位置参数及任意数量的实参

位置参数可以和支持任意数量的实参一起使用,不过,如果遇到不同类型的实参时,必须在函数定义中,将接纳任意数量的实参的形参放在最后。Python先匹配位置实参和关键字实参,然后再将剩下的实参归为最后的实参里。例如:
#定义一个函数,把位置参数b和任意数量参数w累加。

根据函数calc_add的定义,Python把调用函数的实参中,把第一个数4存储在size中,把剩下的所有值存储在元组numb中。

6.5.3 传递任务数量的关键字实参

Python支持采用关键字实参,而且也可以是任意数量,只要在定义函数时在对应的形参前加上两个*号即可(如**user_info)。把任意多个关键字参数封装在user_info这个字典中。对应的形参在函数内部将以字典的方式存储,调用函数需要采用 arg1=value1,arg2=value2 的形式。例如:

调用函数

运行结果
height is 180
weight is 70
age is 30
如果出现多种类型参数的情况,如既有位置形参,又有表示任意数量参数和任意数量的关键字形参的函数。如格式为customer(fargs, *args, **kwargs) 的函数。其中*args与**kwargs的都是python中的可变参数。 *args表示可传入任何多个无名实参, **kwargs表示可传入任意多个关键字实参,它本质上是一个dict。示例如下:

调用函数

当函数中有多种类型参数时,需注意以下问题:
①注意顺序,如果同时出现,fargs在前,*args必在**args之前。
②*args 相当于一个不定长的元组。
③**args 相当于一个不定长的字典。

6.6 函数装饰器

python装饰器不过就是一个针对嵌套函数的语法糖,它的作用就是在函数调用方法不变的情况下,增强原函数的功能,就像一根信号线加上(或装饰)一个插头就可充电一样。
装饰器何时使用?它有哪些神奇功能呢?
我们来看这样一个场景:
假如我原来写好一个函数fun(),这个函数已部署到生产环境,后来,客户说,需要监控该函数的运行时间,对此,如何处理呢?首先想到的可能是修改原函数,但有一点风险,尤其当该函数涉及面较广时。哪是否有不修改原函数,仍可达到目的方法呢?有的!我们可以添加一个函数,然后采用装饰器的方法就可达到目的,详细情况,请看下例:

显示如下结果,这个结果正好是testdeco.pyz文件中的三个引号部分'''
示例: 使用装饰器(decorator)示例
装饰函数的参数是被装饰的函数对象,返回原函数对象
装饰的实质语句: myfunc = deco(myfunc)
在函数func外增加一个函数,统计运行函数func所耗时间的函数timelong,然后采用函数装饰器,这样我们就看不改变原函数func的情况下,增加函数func的功能,详细请看如下示例:

运行结果:"It's used : 0.002424 ."

6.7 属性装饰器

上节介绍了函数装饰器(decorator),其作用可以给函数动态加上功能。对于类的方法,装饰器一样起作用。Python内置的@property装饰器就是负责把一个方法变成属性调用的。
使用场景:当一个类中,含有一些属性,这些属性不想被直接操作,这时可以考虑属性装饰器,即@property。

不希望对初始化变量直接操作,使用了两个函数;不过这样写,有点麻烦,每次给变量赋值需要采用函数格式。

可以通过在函数前,添加@property,使函数或方法变为属性来操作。

调用函数

这样通过在方法前加上@property,就把方法变成了属性,操作属性比操作函数就简单一些,这或许就是属性特征的来由吧。

6.8 内置函数

内置函数可理解为开发语言自带的函数,Java有很多内置函数、MySQL也有很多自带的函数,有效利用这些函数能大大提高我们的开发效率。
Python有哪些内置函数呢?如何使用内置函数法?查看内置函数可用通过以下命令:

查看这些内置函数的使用方法,可以用help(内置函数)方法或?内置函数。

将显示该函数的语法及使用方法等:
Init signature: map(self, /, *args, **kwargs)
Docstring:
map(func, *iterables) --> map object

Make an iterator that computes the function using arguments from
each of the iterables. Stops when the shortest iterable is exhausted.
Type: type
Subclasses:

以下介绍几种常用的内置函数,这些函数后续将大量使用,而且scala中也有类型函数。Hadoop的计算架构为MapReduce,实际上是由Map和Reduce两部分组成的,map和reduce
在Python也类似函数。
1、映射(map)
map(function,seq1,,,)
map()函数接收两个参数,一个是函数,一个是序列,map将传入的函数依次作用到序列的每个元素,并把结果作为新的list返回。函数遍历1个(或多个)序列(或迭代器)的每个元素,映射到另一个列表。

2、filter(过滤)
filter(function, seq1)
把函数应用于序列(或迭代器)的每个元素,取出满足函数条件的元素,构成一个新序列,等价于[ item for item in iterable if function(item)]

运行结果:
[6, 8]
3、foreach
foreach(function, iterator) ##这个是Python3才有。
foreach的作用是在不改变迭代器中的值的前提下(单纯依靠函数的副作用),将函数应用到迭代器中的各个元素上,主要是用作输出和调试作用,它一般不返回值。
map和foreach类似,都是将一个函数应用到迭代器的所有值中,但map返回一个新的列表作为结果,而foreach不返回值。
4、range([lower,]stop[,step])
xrange 用法与 range 完全相同,所不同的是生成的不是一个list对象,而是一个生成器。要生成很大的数字序列的时候,用xrange会比range性能优很多,因为不需要一上来就开辟一块很大的内存空间,用于大数据迭代时xrange优于range。
注:Python 3系列只有range,它就相当于xrange。

运行结果:
[0, 2, 4, 6, 8]
4
Numpy.random模块也提供了一些用于高效生成多种随机数的函数,如normal可产生一个标准正态分布的样本。

运行结果:
array([[-2.60662221, 0.41874463],
[ 0.64875586, -0.7013413 ],
[ 2.08334769, -1.41301304]])

6.9 lambda函数

lambda函数又称为匿名函数,使用lambda函数可以返回一个运算结果,其格式如下:
result=lambda[arg1,[arg2,...,]]:express
参数说明
①result就是表示式express的结果。
②关键字lambda是必须的
③参数可以一个,也可多个,多个参数以逗号分隔
④lambda也是函数,是一行函数,参数后需要一个冒号:
⑤express只能有一个表达式,无需return语句,表达式本身的结果就是返回值。
lambda函数非常简洁,它通常作为参数传递给函数,以下是些应用实例。

可以把lambda函数作为参数传递给其它函数,例如:

6.10 装饰器

装饰器本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

现在有一个新的需求,希望可以记录下函数的执行日志,于是在代码中添加日志代码:

如果函数 bar()、bar2() 也有类似的需求,怎么做?再写一个 logging 在 bar 函数里?这样就造成大量雷同的代码,为了减少重复写代码,我们可以这样做,重新定义一个新的函数:专门处理日志 ,日志处理完之后再执行真正的业务代码。

WARNING:root:foo is running
i am foo

这样做逻辑上是没问题的,功能是实现了,但是我们调用的时候不再是调用真正的业务逻辑 foo 函数,而是换成了 use_logging 函数,这就破坏了原有的代码结构, 现在我们不得不每次都要把原来的那个 foo 函数作为参数传递给 use_logging 函数,那么有没有更好的方式的呢?当然有,答案就是装饰器。

WARNING:root:foo is running
i am foo
use_logging 就是一个装饰器,它一个普通的函数,它把执行真正业务逻辑的函数 func 包裹在其中,看起来像 foo 被 use_logging 装饰了一样,use_logging 返回的也是一个函数,这个函数的名字叫 wrapper。在这个例子中,函数进入和退出时 ,被称为一个横切面,这种编程方式被称为面向切面的编程。
@ 语法糖
如果你接触 Python 有一段时间了的话,想必你对 @ 符号一定不陌生了,没错 @ 符号就是装饰器的语法糖,它放在函数开始定义的地方,这样就可以省略最后一步再次赋值的操作。

如上所示,有了 @ ,我们就可以省去foo = use_logging(foo)这一句了,直接调用 foo() 即可得到想要的结果。你们看到了没有,foo() 函数不需要做任何修改,只需在定义的地方加上装饰器,调用的时候还是和以前一样,如果我们有其他的类似函数,我们可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。

装饰器在 Python 使用如此方便都要归因于 Python 的函数能像普通的对象一样能作为参数传递给其他函数,可以被赋值给其他变量,可以作为返回值,可以被定义在另外一个函数内。
可能有人问,如果我的业务逻辑函数 foo 需要参数怎么办?比如:

WARNING:root:foo is running
i am apple

6.11 生成器函数

前面我们介绍了函数的返回值,可以一个或多个。如果返回百万个或更多值时,将消耗很大一部分资源,为解决这一问题,人们想到用生成器。具体方法很简单,就是把函数中return 语句换成yield语句即可,示例如下:

遍历函数生成器gen61(10)

6.12 把函数放在模块中

前面我们介绍了函数及函数参数等,函数定义好之后,我们可以调用,无需重写代码。不过这些函数如果仅停留在开发环境,环境一旦关闭函数也不存在了,那么,如何永久保存定义好的函数?
很简单,只要把这些函数放在模块中即可。所谓模块实际上就是扩展名为.py的文件。
如果当前运行的程序需要使用定义好的函数,只要导入对应的模块即可,导入模块的方式有多种,下面将介绍每种方式。

6.12.1 导入整个模块

假设我们已生成一个模块,模块对应的文件名为:func_op.py,文件存在当前目录下,当前目录可以通过以下命令查看:

当然也可放在其它Python能找到的目录(sys.path)下。Python首先查找当前目录,然后查找Python的lib目录、site-packages目录和环境变量PYTHONPATH设置的目录。
(1)创建.py文件
创建.py文件,可以使用pycharm或一般文本编辑器,如NotePad或UE都可。
创建文件后,把该文件放在jupyter notebook当前目录下。
#cat func_op.py具体内容如下:

(2)导入模块

导入模块,就import 对应的模块名称。导入模块实际上就是让当前程序或会话打开对应的文件,并将文件中的所有函数都复制过来,当然,复制过程都是Python在幕后操作,我们不必关心。
(3)调用函数
导入模块func_op.py后,在jupyter notebook界面,通过模块名.+tab键 就可看到图6-1的内容。

图6-1 查看导入模块中的函数或变量等
调用函数,使用模块名.函数名,中间用句点.

6.12.2 导入需要的函数

有时一个模块中有很多函数,其中很多函数暂时用不上或对应程序不需要这些函数,那么我们导入模块时,为节省资源,就可导入需要的函数,不需要的就不导入。导入需要函数的格式为:

如果需要导入模块中的多个函数,可以用逗号分隔这些函数。

这种情况,调用函数时,不需要使用句点,直接使用函数名即可。

有时函数名比较长,我们可以用取别名的方式,简化函数名称,调用时,直接使用别名就可。

6.12.3 导入所有函数

如果模块中函数较多,或我们不想一个个写需要导入的函数,也可导入所有函数。导入所有函数使用如下格式

调用函数时也无需使用句点,直接调用函数名即可。示例如下:

使用这种导入方式简单,但存在一定风险。用这种方式导入的函数或变量将覆盖当前程序或环境已有的函数或变量。所以,一般不建议使用,尤其对导入的模块不熟悉时。比较理想的方法就是按需导入,导入我们需要的函数,或采用句点的方式导入,这样可以更好地避免覆盖函数或变量的风险。

6.12.4 主程序

在编写众多Python程序中,通常至少一个会使用main(),根据不成为文的约定,带有main()函数的程序,被认为是主程序,它是程序运行的起点。主程序可以导入其它模块,然后使用这些模块中的函数、变量等。例如,创建一个名为train_sum.py的主程序,该程序作为执行起点。

假设这个主程序放在Jupyter notebook的当前目录,运行该主程序,可以在命令行执行或Jupyter notebook界面执行。具体执行格式如下:

在主程序中,因加了if __name__=='__main__'语句,所以如果导入主程序将不会运行。
其中参数是通过语句input获取,也可以通过命令行运行程序时直接给定。把train_sum.py稍微修改一下。

如果在命令行输入更多参数,或希望得到更强的表现力,可以使用argparse模块,argparse的使用可参考Python官网

6.13 练习

(1)简述形参、实参、位置参数、默认参数、动态参数的区别。
(2)写函数,检查传入列表的长度,如果大于4,那么仅保留前4个长度的内容,并将新内容返回给调用者;否则,返回原列表。
(3)有一个字典dic = {"k1":"ok!","k2":[1,2,3,4],"k3":[10,20]},写函数,遍历字典的每一个value的长度,如果大于2,那么仅仅保留前两个长度的内容,并将返回修改后的字典。

第5章 字典和集合

5.1 问题:当索引不能满足需求时

为了满足英文字典如英文:中文,或新华字典,如拼音:汉字等类似需求,Python提供了字典这种数据结构,这种结构类似于字典,它由一系列的键-值对构成,根据键查询或获取对应的值,这就非常方便了。

5.2 一个简单字典实例

字典由一系列键-值对构成,这些键-值对包含在一对花括号{}里,键-值对用逗号分隔,键与值用冒号分隔。在一个字典中,键是唯一的,值可以不唯一,键必须是不可变的,不能是列表、字典。因键-值对在字典中存储的位置是根据键计算得到的,如果修改键将修改其位置,这就可能导致键-值对丢失或无法找到。
不过键-值对中的值,既可重复,也可修改。
用字典表示类别与标签,键-值对中,键表示类别,值表示标签值。示例如下:
dict51={'小猫':1,'小狗':2,'黄牛':3,'水牛':3 ,'羊':4}
在字典dict51中,动物类别为键,值为对应的标签值,其中值3重复2次。

5.3 创建和维护字典

字典是Python中的重要数据结构,它是一系列的键-值对,通过键来找值。键必须是不可变的,如数字、字符串、元组等,不能是可变的对象,如列表、字典等。但值可以是Python的任何对象。

5.3.1 创建字典

创建字典有多种方法,如直接创建一个含键-值对的字典(先创建一个空字典,然后往空字典添加键-值对),通过函数dict创建字典等方法。字典对键的限制包括:键唯一、必须是不可变的,如元组、字符串等,不能是列表、字典等。
(1)直接创建含键-值对的字典

(2)创建一个空字典

5.3.2 添加键-值对

字典是可修改的,所以,创建字典后,可以往里添加键-值对。

用这些方法添加的字典,Python不关心其添加顺序,如果要关注添加顺序,可以使用OrderedDict()函数,用这个函数创建的字典,将按输入的先后顺序排序,具体使用方法后续将介绍。

5.3.3 修改字典中值

修改字典中值,可根据字典名及对应键来修改。

修改字典指修改字典中键-值对中值。

5.3.4 删除字典中的键-值对

删除字典中的键-值对,需指明字典名及对应键,可以使用Python的内置函数del,这个函数将永久删除。
(1)删除一个键-值对

(2)删除所有键-值对
删除字典中所有键-值对,也可以使用字典函数clear(),它清除所有键-值对,但会保留字典结构。del 字典名将删除整个字典,包括字典中所有键-值对和字典定义。

5.4 遍历字典

我们可以用for循环遍历列表、元组,同样,也可以遍历字典。不过遍历字典有点特别,因字典的元素是键-值对,遍历字典可以遍历所有的键-值对、键或值。

5.4.1 遍历字典所有的键-值对

利用字典函数items()可以遍历所有的键-值对。

运行结果
字典值 :dict_items([('Google', 'www.google.com'), ('baidu', 'www.baidu.com'), ('taobao', 'www.taobao.com')])
Google www.google.com
baidu www.baidu.com
taobao www.taobao.com

5.4.2 遍历字典中所有的键

根据需要,也可以只遍历字典的所有键,遍历字典名或遍历字典函数keys()的值。

5.4.3 遍历字典中所有的值

遍历字典的所有键,使用字典函数keys(),遍历字典的所有值,使用字典函数values()。

这节我们用到了很多字典函数,如items()、keys()、values()、clear()等,字典函数还有很多,你可以在交互式命令中调用dir(dict),可用的字典函数还有很多,这里就不一一介绍了。

5.5 集合

5.5.1 创建集合

创建集合可用直接使用一对花括号来创建,也可使用set函数把序列转换为集合。
(1)直接用{}创建集合

(2)使用set()函数创建集合
使用set()函数创建集合,可以把列表、字符串、元组等转换为集合,同时自动去重。

5.5.2 集合的添加和删除

集合是可变的,所以可以添加元素、删除元素。添加使用集合函数add()、删除使用集合函数remove()或pop()或clear()等。
(1)添加元素

(2)删除元素

5.6 字符串、列表、元组、字典和集合的异同

本书第3章、本章介绍了列表、元组、字典和集合等数据结构,下面通过表5-1比较这些数据结构的异同。
表5-1 列表、元组、字典及集合的异同

5.7 列表、集合及字典的推导式

在4.3小节我们简单介绍了列表推导式,这里我们介绍字典、集合推导式。什么叫推导式?它有哪些特点?如果觉得概念不好理解没关系,先来理解它的本质,推导式简单理解为把循环语句与判断语句或表达式放在一起作为一个句子。这个Python非常强大也是非常最受欢迎的特点之一,这个特点不但是程序简洁、而且逻辑更清晰和直观。
(1)列表的推导式:
如:[expr for val in collection [if condition]],这条语句转换为我们熟悉的方式就是;
result=[]
for val in collection:
[if condition:] ###条件不是必须的
result.append(expr)

运行结果:
[4, 8, 16]
(2)字典的推导式:
{key_expr:value_expr for val in collection [if condition]]}

运行结果:
{3, 4}
[(0, 'python'), (1, 'scala'), (2, 'hadoop'), (3, 'sparkml')]
(3)集合的推导式:
{expr for val in collection [if condition]]}

运行结果:
{1, 2, 3, 5, 8}
{11, 12, 13, 15, 18}

5.8迭代器和生成器

当列表、元组、字典、集合中的元素很多时,如几百万、几亿甚至更多,那么这些元素一次全面放在内存里,它们将占据大量的内存资源。是否有更好、更高效的存储方式呢?迭代器和生成器就为解决这一问题而提出的。采用迭代器和生成器,不会一次性把所有元素加载到内存,而是需要的时候才生成返回结果。它们既可存储很大数据、甚至无限数据,又无需多少资源。利用生成器或迭代器来存储数据的方式,在大数据处理、机器学习中经常使用。
我们前面介绍的序列、元组、字典及集合都是可迭代对象,用在for,while等语句中。
这些数据结构又称为容器,在容器上使用iter()就得到迭代器,利用next()函数就可持续取值,直到取完为止。生成器是迭代器,生成器我们后续将介绍,图5-1说明了生成器、迭代器、可迭代对象之间的关系。


图5-1 Python可迭代对象、迭代器和生成器的关系图
(1)容器是一系列元素的集合,str、list、set、dict、file、sockets对象都可以看作是容器,容器都可以被迭代(用在for,while等语句中),因此它们被称为可迭代对象。
(2)可迭代对象实现了__iter__方法,该方法返回一个迭代器对象。
(3)迭代器持有一个内部状态的字段,用于记录下次迭代返回值,它实现了__next__和__iter__方法,迭代器不会一次性把所有元素加载到内存,而是需要的时候才生成返回结果。
(4)生成器是一种特殊的迭代器,它的返回值不是通过return而是用yield。

5.8.1 迭代器

用函数iter()可以把列表、元组、字典集合等对象转换为迭代器。迭代器是Python最强大的功能之一,是访问集合元素的一种方式。迭代器是一个可以记住遍历的位置的对象,迭代器对象使用next()函数,从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。
(1)定义一个列表

(2)生成迭代器
对列表、元组、字典和集合,使用函数iter(),即可把转换为迭代器。

(3)从迭代器中取元素

这里是用来异常处理except,后续将介绍。其中使用next()从迭代器取数,直到没有数据(即StopIteration)触发语句break。

5.8.2 生成器

从图5-1可知,生成器可分为生成器函数、生成器表达式。生成器函数第6章将介绍,这里主要介绍生成器表达式。生成器表达式是列表推倒式的生成器版本,看起来像列表推导式,但是它返回的是一个生成器对象而不是列表对象。
生成器表示式与列表推导式相似,列表推导式是在中括号[]里,把中括号改为小括号()变成生成器。

或用next()函数,从生成器next()逐一取数据,与for循环取数效果一样。

5.9 练习

(1)创建一个字典,字典中包括3中动物名称,3种植物名称,以这些名称为键,动物对应的值都为1,植物对应的值都为2。遍历这个字典,把动物名称放在一个列表中,植物名称放在另一个列表中。
(2)编写一个Python脚本来生成一个字典,其中键是1到10之间的数字(都包括在内),值是键的平方。
(3)现有一个列表li = [1,3,'a','c'],有一个字典(此字典是动态生成的,可用dic={}模拟字典)
现在需要完成如下操作:
①如果该字典没有"k1"这个键,那就创建
这个"k1"键和对应的值(该键对应的值为空列表),并将列表li中的索引位为奇数对应的元素,添加到
"k1"这个键对应的空列表中。
②如果该字典中有"k1"这个键,且k1对应的value是列表类型,则不做任何操作。

第4章 if语句与循环语句

4.1 问题:Python控制语句有何特点?

在Python中,可以把循环语句、if语句放在一行里,而这就是Python控制语句的特点之一。把if与循环语句组合在一起,既增量了Python代码的可读性,也使Python代码更加简洁、高效。例如,选出列表lst7=[98,97,87,88,76,68,80,100,65,87,84,92,95,87]中大于90的元素,Python只要一句即可完成。

由此,可见Python语言的简洁和高效。Python的控制语句可以有机组合在一行,Python的控制语句还可与表达式写在一行,如下语句:

类似的特点还有很多,后续章节我们会经常看到。

4.2 if语句

在实际生活中,我们经常遇到分类统计的情况。对不同分类或不同状态,做不同处理,如果用程序实现的话,可以用if语句来实现。

4.2.1 if语句格式

if语句用于检测某个条件是否满足。满足,执行一个逻辑块;不满足则执行另一个逻辑块。if语句的一般格式为:
if 条件 :
代码块1
else:
代码块2
Rrrr
这是只有两种情况,如果情况更多,可以使用更多分支的if语句,如下代码样例。
if 条件1 :
elif 条件2 :
else:
代码块3
if语句以关键字if开头,然后跟一个布尔表达式或if条件,if条件后面是一个冒号(:),代码块1、代码块2等都缩进4格,以相同缩进作为代码块的标志,同级代码块必须用相同的缩进格数。多一个或少一个都会报错,这条规则必须严格遵守。Python将冒号:作为if、循环语句、函数定义一行的结束标记。

4.2.2 if语句

给出一个年龄值,利用if-elif-else结构,判断该年龄值属于哪个年龄段。以下是实现代码。

4.2.3 使用and连接条件语句

根据给定年龄,判断属于哪个年龄段的问题,if的条件也可用and连接。

4.2.4 元素是否在列表中

如果要判断一个元素是否在一个列表或元组中,可以使用in或not in 的方法。当列表非常大时,这种方法效率非常高。例如,判断"keras"是否在列表lst42=["Pythoon","Numpy","Matplotlib","OpenCV","Sklearn","Pytorch","Keras","TensorFlow"],假设列表lst42表示目前环境已安装的软件。我们用if语句中带not的条件即可,具体实现如下:

结果:keras不在列表中
这个结果乍一看,与我们的期望不一样,keras应该在lst42中,不过仔细再看一下,问题在大小上,lst42中是"Keras",第一个字母是大写,而我们使用的keras为小写。为此,我们可以把lst42的字符全变成小写,然后再进行比较,修改后的代码如下;

结果:keras在列表中

4.2.5 缩进易出现的问题

Python是通过缩进来判断是否属于一个代码块,而不是通过显式的{}或[]等来说明。所以出现缩进问题不易发现,与缩进有关还一个冒号,在Python中冒号往往表示Python一个语句的结束,一个逻辑块的开始(这句话不一定很准确)。我们先看一些易疏忽的问题。
(1)忘记缩进

这个if语句将报错,因 if a (2)忘记加上冒号

这个语句将会在else报错,因else后没有冒号。
(3)有缩进,但缩进的格数不同

这个if语句也会报错,因这两个print语句属于同一级的逻辑块,但缩进的格数不一致。
为尽量避免类似问题,大家编写代码时,尽量使用一些工具,如PyCharm,或Jupyter等,使用这些工具,遇到冒号回车将自动缩进,而且报错后,出错的地方会高亮或被标注。

4.3 循环语句

循环语句用来重复执行一些代码块,通常用来遍历序列、字符串、字典、迭代器等,然后对其中每个元素做相同或类似处理,字典、迭代器后续将介绍。Python有两种循环:for循环和while循环。for循环通常用于已知对象,while循环基于某个条件,满足这个条件循环执行,否则结束循环。

4.3.1 for循环

我们先看for循环的一个简单实例,从range(10)中每次读取一个数,然后打印这个数。

for循环的关键字为for,接下来是循环变量(这里为i,当然也可是其它变量),然后是关键字in,关键字in后是序列、字符串、字典等可迭代对象,最后以冒号结束。每次循环结束时,循环变量就被设置成下一个值,直到获取最后一个值为止。
for循环与if语句一起使用,可以产生各种各样的数据,比如,利用for循环及if语句可以统计列表lst41=["a","b","a","a","b"]中的a和b各出现多少次。
列表lst41的分类统计,用代码实现如下:

4.3.2 while 循环

while循环的执行过程:首先检查循环条件为True或False,如果为True,就执行循环体;如果为False,就跳出循环,执行后面的语句。我们用while循环实现上节for循环的内容。

使用while循环时,要避免出现死循环问题,如果这个while 循环少了i=i+1这个条件,,那么这个循环将一直执行下去,除非强制结束循环或按ctrl+c停止执行当前任务。

4.3.3 嵌套循环

Python中for循环和while循环都可以进行循环嵌套,for循环中又有for循环或while循环;while循环中有while循环或for循环。
这里我们看一个for循环中又有for循环的情况。比如要累加列表lst42=[[1,2,3],[4,5,6],[7,8,9]]中这9个数据,可以先用一个for循环里面的每个列表,然后,再用一个for循环累加取出的每个列表的元素。具体实现如下;

循环是很耗资源的,实现编程中要尽量避免使用循环,尤其是循环嵌套,因循环嵌套可读性较差,更重要的是耗资源又慢。后续我们将介绍不使用循环,直接利用矩阵进行计算,其性能是使用循环的几倍甚至几十、几百倍。

4.3.4 break跳出循环

在for循环、while循环都可以使用break跳出整个循环,不再执行剩下的循环语句。如果break在循环嵌套里,break将跳出所在或当前循环。
比如,在一个列表中,查找一个单词,如果没有找到继续查询,一旦找到,就停止查找,退出循环。

结果:
找了3次,终于找到了!
总的查询次数:3次
从总的查询次数是3次,可以看出,一旦找到就停止循环,不再查找了。

4.3.5 continue加快循环

与break跳出循环不同,continue不是立即跳出整个循环,而是立即返回循环开头,继续循环,直到循环结束。
上面这个查找例子,如果把break,换成continue,会是什么情况呢?

结果:
找了3次,终于找到了!
总的查询次数:6次
说明找到白骨精后,循环还继续,直到找遍列表中所有元素为止。
break结束循环,continue继续循环,这就是两种最大的区别。

4.3.6 列表推导式

这节主要介绍列表推导式,列表推导式提供了一种简单明了的方法来创建列表。
它的结构是在一个中括号里包含一个表达式,然后是一个for语句,后面再接0个或多个for或者if语句。那个表达式可以是任意的,意味着你可以在列表中放入任意类型的对象。返回结果将是一个新的列表。以下通过实例来说明。
假设我们要把从1到100这100个自然数中的偶数取出来,为实现这个需求,我们采用两种方法,一种是普通方法,另一种是采用列表推导式,然后,比较两种方法。
(1)使用普通方法
使用普通方法就是先创建一个空列表,执行一个循环语句,在循环语句里加上if语句,判断是否为偶数,是偶数则追加到这个列表中。

(2)使用列表推导式
使用列表推导式,就是把for循环和if语句在一行来完成整个逻辑,具体代码如下:

一句话就搞定了,简洁明了,还高效!

4.4 练习

(1)求1到100连续自然数中偶数的和
(2)列表[2,4,-1,0,10,0,-2,9]按升序排序。
(3)编写一个脚本,对任意一个列表进行升序排序
(4)过滤第(2)题的列表中小于等于0的值。
(5)假设x=[1,2,3],y=[4,5,6],求两点x,y之间的距离。

(6)编写一个程序,统计从1到99共有多少个6字。

第3章 列表和元组

3.1问题:如何存取更多数据?

Python的列表、元组、字典等就是解决类似问题的有效方法。这些数据结构既可以存放单个数据,也可存放几十个、成千上万个数据,而且操作、维护里面的元素也非常方便。

3.2 列表概述

在Python中,一个列表中的数据类型可以相同,也可以各不相同。数据类型包括整数、实数、字符串等基本类型,也包括列表、元组、字典、集合以及其他自定义类型的对象,可以说包罗万象。

3.3 如何访问列表元素

列表是有序的,列表中每个元素都有唯一标号(即对应的索引),不过要要注意一点,索引是以0开始的,这或许与很多度量工具的起始值一致,米尺也是从0开始的。不过列表的索引除了可以从左到右标识,也可以从右到左标识,此时,索引就为负数。列表中各元素与对应索引的关系,可参考图3-1。图3-1 列表a的索引从左到右算起,第一个索引为0,第二个为1,依次类推。

图3-1 列表a中元素与正索引的对应关系
对列表a的元素,也可以从右到左算起,最右这个元素的索引是-1(注意不是0,否则,将与从左到右的第一个索引冲突!),依次类推,具体可参考图3-2。

图3-2 列表a中元素与负索引的对应关系
了解了列表中元素与对应索引的关系,获取列表中的元素就非常简单了。

3.3.1 获取一个元素

从列表中,提取单个元素,直接指定对应索引即可,示例如下:

3.3.2 获取连续多个元素

一次从列表中提取多个连续元素,可以采用冒号,具体示例如下:

打印结果
[2, 3, 4]
[4, 2, 6]
[4, 2, 6, 7]

3.3.3 遍历列表

以上介绍了如何查看列表的部分元素,如果需要遍历所有元素或同时获取列表的元素及对应索引,该如何处理呢?这样的场景,在数据分析、数据处理中经常会遇到。要遍历所有元素,可以使用for循环(for循环第4章将介绍),同时查看列表的索引和对应元素,可以使用enumerate()函数。以下是实现遍历列表的具体代码:

北京|上海|广州|深圳|
====遍历索引及对应元素====
0 北京
1 上海
2 广州
3 深圳

3.3.4 访问列表经常出现的一个问题

我们在访问列表经常遇到list index out of range这个问题,出现这个问题,主要是访问的索引超出列表范围,比如,访问一个只有4个元素的列表,但索引却大于4;访问一个空列表也会报这个错误,具体可参考如下代码:

为有效避免这类问题,可以用len函数得到列表的元素个数n,然后用索引小于n去访问列表。

3.4 对列表进行增、删、改

列表是序列,而且是可以修改的序列,列表对象自身带有很多操作函数,使用这些函数就可对列表进行增加、删除、修改等操作。

3.4.1 添加新元素到列表

往列表中添加新元素方法很多,如以追加的方式加入、以插入的方式加入、还可以拼接两个列表的加入等等。对应的列表函数有append、insert、extend等,具体请参考表3-1。
表3-1 添加元素的列表函数

列表函数 返回值
lst.append(x) 在列表lst末尾添加元素x
lst.insert(i, x) 将元素x插入到索引为i指定的位置,相当于lst[i]=x
lst.extend(lst2) 将列表lst2的所有元素追加到列表lst末尾

以下是往列表添加新元素的示例代码。

['Python', 'Java', 'C++', 'keras']
['Python', 'Java', 'C++', 'Pytorch', 'keras']
['Python', 'Java', 'C++', 'Pytorch', 'keras', 'TensorFlow', 'Caffe2']

3.4.2 从列表中删除元素

删除列表中的元素,你可以根据位置或值来删除列表的元素。
(1)创建列表
先用for循环及append创建一个列表lst4,具体步骤是先创建一个空列表,然后,用for循环从一个已知列表中获取元素i,把i*2+1放入列表lst4中。

(2)根据位置删除列表元素
如果知道要删除的元素索引或位置,你可以使用del、pop(i)、pop()方法。

打印结果
[3, 5, 7, 3, 9, 11]
[3, 7, 3, 9, 11]
[3, 3, 9, 11]
7
[3, 3, 9]
(3)根据值删除元素
有时要删除明确值,对位置或其索引不关心,这种情况下,可以用remove(x)函数。
接下来我们从lst4= [3, 3, 9]删除3,这个列表中有两个3,remove(x)只会删除第一个匹配的值。
如果要删除列表指定值,该值有多次重复,那么就需要使用循环语句。第4章我们将介绍类似场景的实例。

3.4.3 修改列表中的元素

修改列表中元素,可以先通过索引定位该元素,然后再给赋值。

['欲穷千里目', '更上一层楼', '王之涣']

3.5 统计分析列表

如果列表中元素都是数字,统计分析列表的元素,Python提供很多内置函数,如求最大(小)值、统计某个值的总数、列表各元素之和、获取某个值的索引等。

3.5.1 求列表最大(小)值

统计列表最大(小)值,使用内置函数max或min即可。

3.5.2 求列表总和或平均值

利用sum内置函数求列表总和,再除以元素个数便可得到平均值。

3.5.3 求列表元素出现次数及对应索引

3.5.4 求列表元素总数

用内置函数len可以得到列表元素个数,注意,列表的元素可以是字符串、数字、列表、字典、元组等。如果列表中还有列表,或其它对象,通过len得到的元素个数是如何统计的呢?这个问题很重要,以后与多维数据打交道时,经常会遇到类似问题。接下来还是以实例来说吧。

3.6 组织列表

对列表各元素进行排序是经常遇到的问题,Python提供了几种方法:永久修改列表排序,使用列表函数sort(),使用这种方法不保留底稿;使用内置函数sorted()临时修改列表排序,原列表的次序不变;把列表颠倒过来,使用reverse()函数。

3.6.1 使用sort()函数

sort()将永久修改列表的排序,如要恢复列表原来的次序就不方便。

3.6.2 使用sorted()函数

内置函数sorted()只是临时改变列表的次序,原列表次序不变。

3.6.3 使用reverse()函数

reverse()函数与排序函数不同,只是把列表的排列次数倒过来。

列表函数reverse()也是永久修改列表次数,不过只要再次使用该函数就可复原列表。

3.7 生成列表

前面介绍的列表基本都是手工创建的,用这种方法如果元素不多还可接受,如果要生成几百个、上万个元素就不方便了。这里我们介绍几种生成列表的简单方法,使用这些方法,你可以生成任意多的整数、小数都很方便。

3.7.1range()函数

内置函数range()可以自动生成数据,如果再结合for循环,几乎可以生成任何数据集。range()函数的格式为:
range ([start], stop[, step])
range的功能就是生成整数序列,共有3个参数,其中start,step参数是可选的。start表示序列的初始值,缺省为0。参数step表示步长,为整数,缺省值为1。stop为序列的上限,序列元素不包括该值,range()函数的参数具体含义,可参考图3-3。

图3-3 range函数示例
在图3-3 中,range(5),只使用了一个stop参数,stop=5,但生成的序列不包括5。参数start、step都取缺省值,分别为0,1。range函数各种情况的具体使用,请看如下代码。

3.7.2用range()创建列表

用range()函数创建列表非常方便,除了使用for循环,还可以用list()函数,直接把range结果转换为列表。
(1)使用range函数及for循环生成列表

(2)使用range()及list()函数生成列表

3.8 元组

前面我们介绍了列表,列表是可修改的序列,其应用比较广泛。有时,我们又希望生成后的列表不能修改,只能读,就像一些只能读的文件一样,用元组就可满足这需求,元组就是不可修改的序列。

3.8.1 定义元组

定义列表用方括号[],定义元组用圆括号()。定义元组后,就可以使用索引访问元素,这个与列表类似。

由此可知,定义只有一个元素的元组时,不能简单把该元素加圆括号,否则,把一个数字用圆括号括起只是一个数字,而不是元组。
那一个元素的元组如何定义呢?在元素后加上一个逗号即可,如:

3.8.2 查看元组中元素

查看元组中的元素,与查看列表中元素一样,通过索引就可。

3.8.3 使用tuple()生成元组

用list()函数可以把range生成的序列转换为列表,与此类似可以用tuple()函数把range生成的序列转换为元组,也可用tuple()函数把列表转换为元组。

3.9练习

(1)生成一个至少含5个元素的列表,打印各元素的和。
(2)把(1)中得到的列表中倒序,即如果由(1)得到的列表为[3,4,5,8,2],其倒序为[2,8,5,4,3]。
(3)使用range函数,求1到1000的连续自然数的平方和。