作者归档:feiguyun

第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的连续自然数的平方和。

第2章 变量和数据类型

2.1 问题:Python是如何定义变量?

Python最核心理念是:简单好用,少用一个单词就少一份风险!这一理念在Python中处处都有体现。
(1)Java定义变量

(2)Python定义变量

Python无需指明变量类型,也无需用分号结束。
以上是Python理念在定义变量上的一个体现,后续还将介绍Python在逻辑结构方面的独特语法。

2.2 变量

变量是Python最重要的概念之一,变量可以保存任何数,也可修改它的值。

2.2.1 给变量赋值

Python中的变量不需要声明其类型,直接使用=(等号)即可,等号是赋值运算符,例如:

2.2.2变量的命名规则

变量名必须遵循以下几条规则:
(1)变量名只能包含字母、数字和下划线(_),不能使用空格,不能以数字开头。
(2)变量名区分大小写,a和A是两个变量。
(3)不能以Python的关键字(如if、while、for、and等)、内置函数(如abs、int、len等)做变量名,如果用这些做变量名将导致错误。
表2-1 合法和非法的变量名

if=10 #将报错SyntaxError: invalid syntax

2.2.3多重赋值

一般一次给一个变量赋值,Python也可一次给多个变量赋值。

2.3 字符串

字符串是一系列字符,字符包括字母、数字、标点符号及其他可打印或不可打印的字符。

2.3.1 字符串的多种表示

可以使用三种方式来表示字符串,单引号、双引号、三引号(切记,这些引号都是指英文格式的,如果用中文格式将报错!),单引号与双引号作用相似。不过,有些情况用单引号就不合适,有些用双引号不好表示或表示比较麻烦的,用三引号却迎刃而解。
(1)单引号
如'ok!','I like Python','house'
如果字符串中有',如let's go等,如果把这句话用单引号表示,'let's go',将报错。这时就需要使用双引号表示。
(2)双引号
let's go用双引号表示就没问题,"let's go"。当然,一般用单引号表示,也可用双引号表示,如"ok!","house"等。
如果我们希望表示windows下的路径,该如何表示呢?比如表示路径:C:\Users\lenovo\logs,我们首先想到的可能" C:\Users\lenovo\logs",这个表示是否正确?
我们运行以下语句

结果报错一个SyntaxError错误。错误的原因就是这个字符串中含有一个特殊字符“\”
遇到一些特殊字符(如\,'),Python与C、Java一样,可以在这些特殊字符前加反斜杠($$即可,紧跟\后面的这个字符就成了一般字符。把字符串" C:\Users\lenovo\logs"改为:
" C:\\Users\\wumg\\jupyter-ipynb\\logs "后,第2个\就变成一般字符了。

【说明】或改成如下形式也可,效果一样。

(3)三引号
三引号(''' a ''',或"""a""")除了具有一般双引号、单引号功能外,还有一些特殊用法,
如表示多行

此外,还经常用在对一些函数、类的功能注释,及函数或类等帮助信息上。Python的每个对象都有一个属性__doc__,这个属性的内容就用于描述该对象的作用,这些描述都放在三引号内。如:

【练习】
打印let's go 这句话。

2.3.2 字符串的长度

统计一个字符串的长度或元素个数,是经常遇到的问题,如何计算字符串的长度?很简单,只要用Python的内置函数len即可(可以使用命令dir(__builtins__)来查看Python的内置函数清单)。如:

2.3.3 拼接字符串

可以使用加号(+)来拼接或合并字符串,比如:

【注意】如果把"3.7"改为:3.7,情况将如何?

2.3.4 字符串常用方法

对字符串有一些常用方法,如删除空白、转变字符串大小写等,比如:
(1)删除空格
删除字符串中,首尾多余空格,可以使用rstrip()、lstrip()、strip()等函数。

(2)修改大小写
修改字符串中字母的大小写,可以使用lower()、upper()等。

(3)分割单词
可以把字符串按指定字符分割,缺省是按空格分割。

【练习】
统计这句话的单词数:str1="NumPy is the fundamental package for scientific computing with Python. NumPy can also be used as an efficient multi-dimensional container of generic data"

2.3.5 输出到屏幕

Python3使用print()函数把数据打印在屏幕上,打印内容必须放在括号()内。如果是Python2版本的,打印内容无需放在括号内。print可以打印任何数据,且打印还可以按指定格式打印。print有两种格式化输出:一种是str%,另一种是str.format
(1)print函数的格式

其中
 value: 打印的值,可多个
 file: 输出流,默认是 sys.stdout
 sep: 多个值之间的分隔符
 end: 结束符,默认是换行符 \n
 flush: 是否强制刷新到输出流,默认否
(2)打印字符串

Python
Pytorch
(3)不换行打印
默认情况下,print打印完内容后添加一个换行符:\n,即打印后光标移动下一行。要是内容打印在同一行,可以加上参数end=',',说明把结束符改为逗号。例如:

打印结果
Python,Pytorch
(4)格式打印,传统方法print(str%())

运行结果
今年国庆出游人数:7.84 亿,去年国庆出游人数:7
(5)format格式,print(str.format())

带编号

带编号及格式

2.4 数字与运算符

Python3的数字类型包括整型、浮点型、布尔型等,声明变量时无需说明数字类型。由Python内置的基本数据类型管理变量,在程序的后台负责数值与类型的关联,以及类型的转换等操作。
运算符是对数字的操作,包括算术运算符、关系运算符和逻辑运算符等,以下是具体运算符及实例。

2.4.1 算术运算符

表2-2 算术运算符

2.4.2 关系运算符

表2-3 关系运算符,(假设a=2,b=3)

2.4.3 逻辑运算符

表2-4 逻辑算法符

2.4.4 赋值运算符

表2-5 赋值运算符

2.5数据类型转换

转换数据类型是经常遇到的问题,如"python"+3将报错。因3是整数,不是字符串,故不能相加。若要相加,需要先把3转换为字符型。实现类型转换,Python提供了很多内置函数,接下来我们将介绍这些内置函数。

2.5.1 将整数和浮点数转换为字符串

函数str(x)将数x转换为字符串,例如:

2.5.2 把整数转换为浮点数

把整数转换为浮点数,可以使用float(x)函数,例如:

2.5.3 把浮点数转换为整数

把浮点数转换为整数,情况比较复杂,涉及如何对待小数部分。如果只是简单的想去掉小数部分,可以使用int(x)函数即可;如果需要考虑向下取整或向上取整,就需要使用round(x)。另外Python的math模块也提供了很多函数,如math.ceil(x)、math.trunc(x)等,以下通过实例来说明。
(1)直接删除小数部分,可以int(x)函数。

(2)使用round(x),一般采用四舍五入的规则,但如果x小数部分为.5时,将取x最接近的偶数。

2.5.4 把字符串转换为数字

把字符串转换为数字比较简单,使用内置函数int(x)或float(x)即可,例如:

2.5.5 使用input函数

input()函数用于接收用户的输入,它的返回值是字符串,其格式为:

input()函数使用实例
(1)输入一个字符串

【练习】输入两个整数,打印它们的和。

2.6 注释

注释用于说明代码的功能、使用的算法、注意事项等等。代码越复杂,注释就越重要,否则,将给后续代码维护、分享带来极大不便。
注释很简单,只要在需要说明的语句前加上#号即可。Python编译器,遇到带#号的行将忽略。
注释的文字,可以是英文或中文等,注释尽量做到言简意赅。以下为一段代码的注释。

2.7 练习

(1)用input函数输入任意两个数字,打印它们的平方和。
(2)把(1)写成一个脚本,然后,在命令行或Jupyer notebook上执行这个脚本。

第1章 Python安装配置

1.1 问题:Python能带来哪些优势?

我们认为主要有以下原因。
(1)简单易学
(2)简单高效
(3)生态完备
Python的生态非常完备,目前已广泛应用于人工智能、云计算开发、大数据开发、数据分析、科学运算、网站开发、爬虫、自动化运维、自动化测试、游戏开发等领域,而且领域在成千上万的无私奉献者努力下,还在不断开疆拓土。

1.2安装Python

Python使用平台包括Linux、macOS以及Windows,其编写的代码在不同平台上运行时,几乎不需要做较大的改动,非常方便。在各平台上的安装和配置也很简单,Python的安装方法有很多,本书建议使用Aanaconda这个发行版。该发现版包括Conda, NumPy, Scipy, Ipython Notebook、Matplotlib等超过180个科学包及其依赖项,大小约600M左右。

1.2.1在Linux系统上安装

在Linux环境下安装Python,具体步骤如下:
(1)下载Python
安装Python建议采用Anaconda方式安装,先从Anaconda的官网:https://www.anaconda.com/distribution, 如图1-1 所示。

图1-1 下载Anaconda界面
下载Anaconda3的最新版本,如Anaconda3-5.0.1-Linux-x86_64.sh,建议使用3系列,3系列代表未来发展。另外,下载时根据自己环境,选择操作系统等。
(2)在命令行,执行如下命令,开始安装Python。
把下载的这个sh文件放在某个目录下(如用户当前目录),然后执行如下命令。

(3)根据安装提示,按回车即可。其间会提示选择安装路径,如果没有特殊要求,可以按回车使用默认路径(~/ anaconda3),然后就开始安装。
(4)安装完成后,程序提示我们是否把anaconda3的binary路径加入到当前用户的.bashrc配置文件中,建议添加。添加后就可以在任意目录下执行python、ipython命令。
(5)验证安装
安装完成后,运行Python命令,看是否成功,如果不报错,说明安装成功。退出Python编译环境,执行exit()即可。
(6)安装第三方包

(7)卸载第三方包

(8)查看已安装包

1.2.2在Windows系统上安装

在windows下安装与Linux环境下安装类似,不同之处:
(1)选择windows操作系统
(2)下载的文件名称不是sh文件,而是一个exe文件,如:
Anaconda3-2021.05-Windows-x86_64
(3)安装时,双击这个执行文件,后续步骤与Linux环境安装类似。

1.2.3 在macOS系统上安装

在mac OS下安装与Linux环境下安装类似,不同之处:
(1)选择mac OS操作系统
(2)下载的文件名称不是sh文件,而是一个pkg文件,如:
Anaconda3-2019.03-MacOSX-x86_64.pkg
(3)安装时,双击这个pkg文件,后续步骤与Linux环境安装类似。

1.3 配置开发环境

Python开发环境的配置比较简单,Python可以在Windows、Linux、macOS等环境开发。

1.3.1 自带开发环境IDLE

在安装Python后,将自动安装一个IDLE(在安装目录的Scripts目录下),它是Python软件包自带的一个集成开发环境,它是一个Python shell,是入门级的开发环境,程序员可以利用它创建、运行、调试Python程序。运行Python程序有两种方式,交互式和文件式,交互式指Python解释器即时响应用户输入的每条代码,立即给出结果。文件式指用户将python程序写在一个或多个文件中, 然后启动Python解释器批量执行文件中的代码。交互式般用于调试少量代码,文件式则是最常用的编程方式。windows下使用IDLE步骤如下:
(1)找到idle.bat文件,双击该文件,进入交互式编辑界面,如图1-2所示:

图1-2 IDLE界面
(2)运行Python语句

图1-3 交互式运行Python语句
(3)文件式界面
打开IDLE,按快捷键Ctrl+N打开一个新窗口,或在菜单中选择File- +New File选项。这个新窗口不是交互模式,它是一个具备Python语法高亮辅助的编辑器,可以进行代码编辑。在其中输入Python代码,例如,输入Hello World程序并保存为hello.py文件
图1-4 编写一个简单Python代码
(4)运行脚本
按快捷键F5,或在菜单中选择Run->RunModule选项运行该文件,在IDLE交互界面输出图1-5的结果。

图1-5 运行结果

1.3.2 安装配置Pycharm

PyCharm是一种Python IDE,带有一整套可以帮助用户在使用Python语言开发时提高其效率的工具。PyCharm在敲代码时会有纠错,提示的功能,用起来非常方便,比较适合开发Python项目。

1.3.3 Linux下配置Jupyter book

Jupyter Notebook非常好用,配置也简单,主要步骤如下。
(1)生成配置文件

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

(3)修改配置文件
vim ~/.jupyter/jupyter_notebook_config.py
进行如下修改:
c.NotebookApp.ip='*' # 就是设置所有ip皆可访问
c.NotebookApp.password = u'sha:ce...刚才复制的那个密文'
c.NotebookApp.open_browser = False # 禁止自动打开浏览器
c.NotebookApp.port =8888 #这是缺省端口,也可指定其他端口
(4)启动Jupyter notebook

在浏览器上,输入IP:port,即可看到图1-6所示的界面。

图1-6 登录Jupyter的界面
接下来就可以在浏览器进行开发调试Pytorch、Python等任务了。

1.3.4 Windows下配置Jupyter book

(1)生成配置文件
在cmd或Anaconda Prompt 下运行以下命令:
jupyter notebook --generate-config
将在当前用户目录下生成文件:.jupyter/jupyter_notebook_config.py
(2)打开文件jupyter_notebook_config.py,找到含有
#c.NotebookApp.notebook_dir=' '的行
把该行改为:
c.NotebookApp.notebook_dir ='D:\python-script\py'
这里假设希望在'D:\python-script\py' 目录下启动jupyter notebook,并在这个目录自动生成ipyb文件。当然,这个目录可以根据实际环境进行修改。
(3)找到jupyer notebook命令的快捷方式,点击右键,进人属性页面,如下图1-7所示:

图1-7 修改Jupyter Notebook启动目录界面
(4)最后,启动Jupyter Notebook之后,自动弹出一个网页(网址为:localhost:8888),点击其中的new下拉菜单,选择pyhton3,就可进行编写代码、运行代码。

1.4 试运行Python

以Windows环境为例(对Linux中使用shell而不是windows的dos命令)。
(1)如何执行cell中的代码
同时按Shift键和Enter键即可。以下为代码示例。

运行结果
开发语言:python

(3)导入该脚本(或模块),并查看该模块的功能简介

图1-8 运行Python命令
(4)执行Python脚本

图1-9 运行Python脚本

【说明】
①为了使该脚本有更好的移植性,可在第一行加上一句#!/usr/bin/python
②运行.py文件时,python自动创建相应的.pyc文件,.pyc文件包含目标代码(编译后的代码),它是一种python专用的语言,以计算机能够高效运行的方式表示的python源代码。这种代码无法阅读,故可以不管这个文件。

(5)修改文件名称
Jupyter的文件自动保存,名称也是自动生成的,对自动生成的文件名称,我们也可重命名,具体步骤如下;
点击目前文件名称:

图1-10 修改文件名称
然后重命名,并点击rename即可,具体可参考下图:

图1-11 点击修改名称按钮
(6)画图
以下是画一条抛物线的代码,在Jupyter显示图形,需要加上一句:%matplotlib inline,具体代码如下:

 

图1-12显示图像
(7)查看帮助信息
在Jupyter 里查看函数、模块的帮助信息也很方便。
在函数或模块后加点(.) 然后按tab键 可查看所有的函数。
在函数或模块后加问号(?) 回车,可查看对应命令的帮助信息。

查看a1数组,可以使用的函数

图1-13 显示可以的函数
要查看argmax函数的具体使用方法,只要在函数后加上一个问号(?),然后运行,就会弹出一个详细帮助信息的界面,具体可参考图1-14:

图1-14 显示帮助信息

1.5 练习

(1)尝试写一个py脚本,输出你的姓名,分别在命令行和Jupyter notebook上运行。
(2)尝试在Jupyter notebook中,查看print命令的使用方法。

线性代数

机器学习、深度学习的基础除了编程语言外,还有一个就是应用数学。它一般包括线性代数、概率与信息论、概率图、数值计算与最优化等。其中线性代数又是基础的基础。线性代数是数学的一个重要分支,广泛应用于科学和工程领域。大数据、人工智能的源数据在模型训练前,都需要转换为向量或矩阵,而这些运算正是线性代数的主要内容。
如在深度学习的图像处理中,如果1张图由28*28像素点构成,那这28*28就是一个矩阵。在深度学习的神经网络中,权重一般都是矩阵,我们经常把权重矩阵W与输入X相乘,输入X一般是向量,这就涉及矩阵与向量相乘的问题。诸如此类,向量或矩阵之间的运算在深度学习中非常普遍,也非常重要。
本章主要介绍如下内容:
- 自动求解线性方程组
- 标量、向量、矩阵和张量
- 矩阵和向量运算
- 特殊矩阵与向量
- 特征值分解
- 奇异值分解
- 迹运算
- 实例

第1章 线性代数基础

1.1自动求解线性方程组

假设有如下线性方程组:
\begin{cases} x+y+z=6 \\ 2y+5z=-4 \\ 2x+5y-z=27 \end{cases}\tag{2.1}
这个线性方程组,如果用高中知识,手工很快能求得结果。如何用Python实现该方程组的自动求解呢?要实现Python求解,先要解决哪些问题?
要用Python处理这个问题,首先,需要用Python的方式表示这个方程组。
用矩阵、向量的方式表示这个线性方程组,如图2-1所示:
图2-1 用矩阵表示方程组
要使用Python实现式(2.1)的求解,首先需要把式(2.1)用矩阵和向量的方式表示,然后利用相关线性代数相关知识进行求解。具体步骤如下:
1)用矩阵、向量表示式(2.1)或图2-1中关系。
假设系数矩阵为A,变量构成的向量为X,右边的值构成的向量为B,则他们之间的关系,可用下式表示:
AX=B (2.2)
其中A与X是点积(dot)的关系。
2)用NumPy表示矩阵、向量。

3) 因行列式|A|≠0(可计算:np.linalg.det(A) ≠0),由此可知,矩阵A可逆,
即存在A的可逆矩阵A^{-1} ,使得A^{-1}A=E (单位矩阵),对式(2.2)两边同时乘以矩阵A^{-1} ,可得:
A^{-1}AX=A^{-1}B
EX=A^{-1}B
由此可得:X=A^{-1}B
4)求线性方程组的解
A的逆矩阵(用A1表示)可表示为:

所以

可得线性方程组的解为:
array([ 5., 3., -2.])

1.2标量、向量、矩阵和张量

在机器学习、深度学习中,首先遇到的就是数据,如果按类别来划分,我们通常会遇到以下4种类型的数据。

1.2.1 标量(scalar)

一个标量就是一个单独的数,一般用小写的变量名称表示,如a,x等。

1.2.2 向量(vector)

向量就是一列数或一个一维数组,这些数是有序排列的。通过次序中的索引,我们可以确定向量中每个单独的数。通常我们赋予向量粗体的小写变量名称,如x、y等。一个向量一般有很多元素,这些元素如何表示?我们一般通过带脚标的斜体表示,如x_1 表示向量x中的第一个元素,x_2 表示第二元素,依次类推。向量元素的个数称为向量的维数。
当需要明确表示向量中的元素时,我们一般将元素排列成一个方括号包围的一行,如下式:
X=[ x_1,x_2,\ldots,x_n] \tag {2.3}
X称为行向量。
X也可表示为列向量:
\left[\begin{matrix} x_1\cr x_2 \cr\vdots \cr x_n\end{matrix}\right]\tag{2.4}
我们可以把向量看作空间中的点,每个元素是不同的坐标轴上的坐标。
向量可以这样表示,那我们如何用编程语言如python来实现呢?如何表示一个向量?如何获取向量中每个元素呢?请看如下实例:

打印结果如下:
5
1 2 4 8

这说明向量元素个数为5,向量中索引一般从0开始,如a[0]表示第一个元素1,a[1]
表示第二个元素2,a[2]表示第三个元素4,依次类推。这是从左到右的排列顺序,如果从右到左,我们可用负数来表示,如a[-1]表示第1个元素(注:从右到左),a[-2]表示第2个元素,依次类推。

1.2.3 矩阵(matrix)

矩阵是二维数组,其中的每一个元素被两个索引而非一个所确定。我们通常会赋予矩阵粗体的大写变量名称,比如A。如果一个实数矩阵高度为m,宽度为n,那么我们说 A\in R^{m\times n}
与向量类似,可以通过给定行和列的下标表示矩阵中元素,下标用逗号分隔,如A_{1,1}表示A左上的元素,A_{1,2}表示第一行第二列对应的元素,依次类推;这是表示单个元素,如果我们想表示1列或1行,该如何表示呢?我们可以引入冒号":"来表示,如第1行,可用A1,:表示,第2行,用A2,:表示,第1列用A:,1表示,第n列用A:,n表示。
如何用Python来表示或创建矩阵呢?如果希望获取其中某个元素,该如何实现呢?请看如下实例:

打印结果:
[[1 2 3]
[4 5 6]]
6
(2, 3)
1 2 5
[4 5 6]

矩阵可以用嵌套向量生成,和向量一样,在Numpy中,矩阵元素的下标索引也是从0开始的。

1.2.4张量(tensor)

几何代数中定义的张量是向量和矩阵的推广或更通用的称呼,我们可以将标量视为零阶张量,向量视为一阶张量,那么矩阵就是二阶张量,三阶的就称为三阶张量,以此类推。在机器学习、深度学习中经常遇到多维矩阵,如一张彩色图片就是一个三阶张量,三个维度分别是图片的高度、宽度和色彩数据。
张量(tensor)也是深度学习框架TensorFlow、PyTorch的重要概念。TensorFlow由tensor(张量)+flow(流)构成。
同样我们可以用Python来生成张量及获取其中某个元素或部分元素,请看实例:

打印结果如下:
[[[ 0 1 2 3]
[ 4 5 6 7]]

[[ 8 9 10 11]
[12 13 14 15]]]
16
(2, 2, 4)
0 1 5
[4 5 6 7]

第1章 NumPy基础

为何第1章介绍NumPy基础?在机器学习和深度学习中,图像、声音、文本等首先要数字化,如何实现数字化?数字化后如何处理?这些都涉及NumPy。NumPy是数据科学的通用语言,它是科学计算、矩阵运算、深度学习的基石。PyTorch中的重要概念张量(Tensor)与NumPy非常相似,它们之间可以方便地进行转换,掌握NumPy是学好PyTorch的重要基础,故我们把它列为全书第1章。
基于NumPy的运算有哪些优势?实际上Python本身含有列表(list)和数组(array),但对于大数据来说,这些结构有很多不足。因列表的元素可以是任何对象,因此列表中所保存的是对象的指针。例如为了保存一个简单的[1,2,3],都需要有3个指针和三个整数对象。对于数值运算来说这种结构显然比较浪费内存和CPU等宝贵资源。 至于array对象,它直接保存数值,和C语言的一维数组比较类似。但是由于它不支持多维,建立在上面的函数也不多,因此也不适合做数值运算。
NumPy(Numerical Python 的简称)的诞生弥补了这些不足,NumPy提供了两种基本的对象:ndarray(N-dimensional array object)和 ufunc(universal function object)。ndarray是存储单一数据类型的多维数组,而ufunc则为数组进行和处理提供了丰富的函数。
NumPy的主要特点:
1) ndarray,快速和节省空间的多维数组,提供数组化的算术运算和高级的广播功能。
2) 使用标准数学函数对整个数组的数据进行快速运算,而不需要编写循环。
3) 读取/写入磁盘上的阵列数据和操作存储器映像文件的工具。
4) 线性代数,随机数生成,和傅里叶变换的能力。
5) 集成C,C++,Fortran代码的工具。
本章主要内容如下:
♦ 把图像数字化
♦ 存取元素
♦ NumPy的算术运算
♦ 数组变形
♦ 批量处理
♦ 节省内存
♦ 通用函数
♦ 广播机制

1.1生成NumPy数组

NumPy是Python的第三方库,若要使用它,需要先导入NumPy。

导入NumPy后,可通过np.+Tab键查看可使用的函数,如图1-1所示。如果对其中一些函数的使用不很清楚,想看对应函数的帮助信息,可以在对应函数+?,再运行,就可很方便地看到使用函数的帮助信息。

图1-1 通过np.+Tab键查看可用函数
运行如下命令,便可查看函数abs的详细帮助信息。

NumPy不但强大,而且非常友好。接下来我们将介绍NumPy的一些常用方法,尤其是与机器学习、深度学习相关的一些内容。
NumPy封装了一个新的数据类型ndarray(n-dimensional array,n维数组),它是一个多维数组对象。该对象封装了许多常用的数学运算函数,方便我们做数据处理、数据分析等。如何生成ndarray呢?这里我们介绍生成ndarray的几种方式,如从已有数据中创建、利用random创建、创建特殊多维数组、使用arange函数等。
机器学习中图像、自然语言、语音等在输入模型之前,都需要数字化。这里我们用cv2把一个汽车图像(如图1-2所示)转换为NumPy多维数组,然后查看该多维数组的基本属性,具体代码如下:

运行结果如下:
数据类型:<class 'numpy.ndarray'>,形状:(675, 1200, 3)

图1-2 把轿车图像转换为NumPy

1.1.1 数组属性

在NumPy中,维度被称为轴,比如把轿车图像转换为一个NumPy之后的数组是1个三维数组,这个数组中有3个轴,这3个轴的长度分别为675、1200、3。
NumPy的ndarray对象有3个重要的属性。
 ndarray.ndim:数组的维度(轴)的个数。
 ndarray.shap:数组的维度,值是一个整数元祖,元祖的值代表其所对应的轴的长度。 比如对于二维数组,它用来表达这是个几行几列的矩阵,值为(x, y),其中x代表这个数组中有几行, y代表有几列。
 ndarray.dtype:数据类型,描述数组中元素的类型。
比如上面的img数组:

为更好地理解ndarray对象的3个重要属性,我们把一维数组、二维数组、三维数组进行可视化,如图1-3所示。

图1-3 多维数组的可视化表示。

1.1.2从已有数据中创建数组

直接对 Python 的基础数据类型(如列表、元组等) 进行转换来生成 ndarray:
1)将列表转换成 ndarray。

2)嵌套列表可以转换成多维 ndarray。

如果把上面示例中的列表换成元组,上述方法也同样适合。

1.1.3利用 random 模块生成数组

在深度学习中,我们经常需要对一些参数进行初始化。为了更有效地训练模型,提高模型的性能,有些初始化还需要满足一定条件,如满足正态分布或均匀分布等。这里我们介绍几种常用的方法,表1-1列举了 np.random 模块常用的函数。
1-1 np.random模块常用函数

下面我们来看看这些函数的具体使用方法:

运行结果如下:
生成形状(4, 4),值在0-1之间的随机数:
[[0.32033334 0.46896779 0.35755437 0.93218211]
[0.83150807 0.34724136 0.38684007 0.80832335]
[0.17085778 0.60505495 0.85251224 0.66465297]
[0.5351041 0.59959828 0.59819534 0.36759263]]

生成形状(3, 3),值在low-high之间的随机整数::
[[29 23 49]
[44 10 30]
[29 20 48]]

产生的数组元素是均匀分布的随机数:
[[2.16986668 1.43805178 2.84650421]
[2.59609848 1.96242833 1.02203859]
[2.64679581 1.30636158 1.42474749]]

生成满足正态分布的形状为(3, 3)的矩阵:
[[-0.26958446 -0.04919047 -0.86747396]
[-0.16477117 0.39098747 1.97640843]
[ 0.73003926 -1.03079529 -0.1624292 ]]
用以上方法生成的随机数是无法重现的,比如调用两次np.random.randn(3, 3), 输出结果一样的概率极低。如果我们想要多次生成同一份数据怎么办?我们可以使用np.random.seed函数设置种子。设置一个种子,然后调用随机函数产生一个数组,如果想要再次得到一个一模一样的数组,只要再次设置同样的种子就可以。

运行结果如下:
按指定随机种子,第1次生成随机数:
[[2 2]
[1 4]]
按相同随机种子,第2次生成的数据:
[[2 2]
[1 4]]

1.1.4创建特定形状的多维数组

在对参数进行初始化时,有时需要生成一些特殊矩阵,如全是0或1的数组或矩阵,这时我们可以利用np.zeros、np.ones、np.diag来实现,如表1-2所示。
表1-2 创建特定形状多维数组的函数

下面我们通过几个示例来说明:

运行结果如下:
******nd5******
[[0. 0. 0.]
[0. 0. 0.]
[0. 0. 0.]]
******nd6******
[[1. 1. 1.]
[1. 1. 1.]
[1. 1. 1.]]
******nd7******
[[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]
******nd8******
[[1 0 0]
[0 2 0]
[0 0 3]]
有时我们可能需要把生成的数据暂时保存起来,以备后续使用。

运行结果如下:
[[0.41092437 0.5796943 0.13995076 0.40101756 0.62731701]
[0.32415089 0.24475928 0.69475518 0.5939024 0.63179202]
[0.44025718 0.08372648 0.71233018 0.42786349 0.2977805 ]
[0.49208478 0.74029639 0.35772892 0.41720995 0.65472131]
[0.37380143 0.23451288 0.98799529 0.76599595 0.77700444]]

1.1.5利用 arange、linspace 函数生成数组

一些情况下,我们还希望获得一组具有特定规律的数据,这时可以使用NumPy提供的arange、linspace函数实现。
arange 是 numpy 模块中的函数,其格式为:
arange([start,] stop[,step,], dtype=None)
其中start 与 stop 用于指定范围,step 设定步长,生成一个 ndarray,start 默认为 0,步长 step 可为小数。Python中的内置函数range的功能与此类似。

linspace 也是 numpy 模块中常用的函数,其格式为:
np.linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None)
它可以根据输入的指定数据范围以及等份数量,自动生成一个线性等分向量,其中endpoint (包含终点)默认为 True,等分数量num默认为 50。如果将retstep设置为 True,则会返回一个带步长的 ndarray。

值得一提的是,这里并没有像我们预期的那样,生成 0.1, 0.2, ... 1.0 这样步长为0.1的 ndarray,这是因为 linspace 必定会包含数据起点和终点,那么其步长则为(1-0) / 9 = 0.11111111。如果需要产生 0.1, 0.2, ... 1.0 这样的数据,只需要将数据起点 0 修改为 0.1 即可。
除了上面介绍到的 arange 和 linspace函数,NumPy还提供了 logspace 函数,该函数的使用方法与linspace的使用方法一样,读者不妨自己动手试一下。

1.2读取元素

上节介绍了生成ndarray的几种方法,当生成数据后,如何读取我们需要的数据呢?这节将介绍几种常用的读取数据的方法。

如果对上面这些获取方式还不是很清楚,没关系,下面以图形的方式加以说明,如图1-4所示,左边为表达式,右边为表达式获取的元素。注意,不同的边界表示不同的表达式。
0 1 2 3 4
5 6 7 8 9
10 11 12 13 14
15 16 17 18 19
20 21 22 23 24

图1-4 获取多维数组中的元素
除了可以通过指定索引标签获取数组中的部分元素外,还可以使用一些函数来实现,如可以通过random.choice函数从指定的样本中随机抽取数据。

运行结果如下:
随机可重复抽取
[[ 7. 22. 19. 21.]
[ 7. 5. 5. 5.]
[ 7. 9. 22. 12.]]
随机但不重复抽取
[[ 21. 9. 15. 4.]
[ 23. 2. 3. 7.]
[ 13. 5. 6. 1.]]
随机但按制度概率抽取
[[ 15. 19. 24. 8.]
[ 5. 22. 5. 14.]
[ 3. 22. 13. 17.]]

1.3 NumPy的算术运算

机器学习和深度学习中涉及大量的数组或矩阵运算,这节将重点介绍两种常用的运算。一种是对应元素相乘,又称为逐元乘法(Element-Wise Product),或哈达玛积(Hadamard Product),运算符为np.multiply(), 或 *。另一种是点积或内积元素,运算符为np.dot()。

1.3.1遂元素操作

遂元素操作(又称为对应元素相乘)是两个矩阵中对应元素乘积。np.multiply 函数用于数组或矩阵对应元素相乘,输出的大小与相乘数组或矩阵的大小一致,其格式如下:

其中x1、x2之间的对应元素相乘遵守广播规则,NumPy的广播规则将在1.8节介绍。下面我们通过一些示例来进一步说明。

矩阵A和B的对应元素相乘,如图1-5所示。

图1-5 对应元素相乘示意图
NumPy数组不仅可以和数组进行对应元素相乘,也可以和单一数值(或称为标量)进行运算。运算时,NumPy数组的每个元素和标量进行运算,其间会用到广播机制,例如:

运行结果如下:
[[ 2. 4.]
[-2. 8.]]
[[ 0.5 1. ]
[-0.5 2. ]]
由此,推而广之,数组通过一些激活函数后,输出与输入形状一致。

运行结果如下:
输入参数X的形状: (2, 3)
激活函数sigmoid输出形状: (2, 3)
激活函数relu输出形状: (2, 3)
激活函数softmax输出形状: (2, 3)

1.3.2 点积运算

点积(Dot Product)运算又称为内积运算,在NumPy中用np.dot表示,其一般格式为:
numpy.dot(a, b, out=None)
下面通过一个示例来说明dot的具体使用及注意事项。

运行结果如下:
[[21 24 27]
[47 54 61]]
以上运算可表示为如图1-6所示形式。

图1-6 矩阵的点积示意图,对应维度的元素个数需要保持一致
如图1-6所示,矩阵X1和矩阵X2进行点积运算,其中X1和X2对应维度(即X1的第2个维度与X2的第1个维度)的元素个数必须保持一致,此外,矩阵X3是由矩阵X1的行数与矩阵X2的列数构成的。
点积运算在神经网络中使用非常频繁,如图1-7所示的神经网络,输入I与权重矩阵W之间的运算就是点积运算。

图1-7 内积运算可视化示意图

1.4数组变形

在机器学习以及深度学习的任务中,我们通常需要将处理好的数据以模型能接收的格式发送给模型,然后由模型通过一系列运算,最终返回一个处理结果。然而,由于不同模型所接收的输入格式不一样,往往需要先对其进行一系列变形和运算,将数据处理成符合模型要求的格式。最常见的是矩阵或者数组的运算,我们经常会遇到需要把多个向量或矩阵按某轴方向合并,或展平(如在卷积或循环神经网络中,在全连接层之前,需要把矩阵展平)的情况。下面介绍几种常用数据变形方法。

1.4.1 修改数组的形状

修改指定数组的形状是NumPy中最常见的操作之一,常见的方法有很多,表1-3 列出了一些常用函数。
表1-3 NumPy中改变向量形状的一些函数
下面我们来看一些示例。
1)reshape函数。

运行结果如下:
[0 1 2 3 4 5 6 7 8 9]
[[0 1 2 3 4]
[5 6 7 8 9]]
[[0 1]
[2 3]
[4 5]
[6 7]
[8 9]]
[[0 1 2 3 4]
[5 6 7 8 9]]
值得注意的是,reshape 函数支持只指定行数或列数,其余设置为-1即可。注意,所指定的行数或列数一定要能被整除,例如将上面代码修改为arr.reshape(3,-1)将报错,因为10不能被3整除。
2)resize函数。

运行结果如下:
[0 1 2 3 4 5 6 7 8 9]
[[0 1 2 3 4]
[5 6 7 8 9]]
3)T函数。

运行结果如下:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[ 0 4 8]
[ 1 5 9]
[ 2 6 10]
[ 3 7 11]]
4)ravel函数。
ravel 函数接收一个根据C语言格式(即按行优先排序)或者Fortran语言格式(即按列优先排序)来进行展平的参数,默认情况下是按行优先排序。

运行结果如下:
[[0 1 2]
[3 4 5]]
按照列优先,展平
[0 3 1 4 2 5]
按照行优先,展平
[0 1 2 3 4 5]
5)flatten(order='C')函数。
把矩阵转换为向量,展平方式默认是行优先(即参数order='C'),这种需求经常出现在卷积网络与全连接层之间。

运行结果如下:
[[4. 0. 8. 5.]
[1. 0. 4. 8.]
[8. 2. 3. 7.]]
[4. 0. 8. 5. 1. 0. 4. 8. 8. 2. 3. 7.]
Flatten(展平)运算,在神经网络中经常使用,一般在网络的后面需要把2维、3维等多维数组转换为一维数组,此时就需要用到展平这个操作,如图1-8所示。

图1-8 含flatten运算的神经网络示意图
6)squeeze函数。
squeeze函数是一个主要用于降维的函数,可以把矩阵中含1的维度去掉。

7)transpose函数
transpose函数主要用于对高维矩阵进行轴对换,在深度学习中经常用到,比如把图像表示颜色的RGB顺序改为GBR的顺序。

1.4.2 合并数组

合并数组也是最常见的操作之一,表1-4列举了常用的NumPy数组或向量合并的方法。
表1-4 常用的NumPy 数组合并的方法

[说明]
1)append、concatnate以及stack函数都有一个 axis 参数,用于控制数组合并是按行还是按列排序。
2)append和concatnate函数中待合并的数组必须有相同的行数或列数(满足一个即可)。
3)stack、hstack、dstack函数中待合并的数组必须具有相同的形状( shape)。
下面选择一些常用函数进行说明。
1.append
合并一维数组:

合并多维数组:

运行结果如下:
按行合并后的结果
[[0 1]
[2 3]
[0 1]
[2 3]]
合并后数据维度 (4, 2)
按列合并后的结果
[[0 1 0 1]
[2 3 2 3]]
合并后数据维度 (2, 4)
2.concatenate
沿指定轴连接数组或矩阵:

运行结果如下:
[[1 2]
[3 4]
[5 6]]
[[1 2 5]
[3 4 6]]
3.stack
沿指定轴堆叠数组或矩阵:

运行结果如下:
[[[1 2]
[3 4]]

[[5 6]
[7 8]]]
4.zip
zip是Python的一个内置函数,多用于张量运算中。

运行结果如下:
[1 2],[5 6]
[3 4],[7 8]
zip函数组合两个向量。

运行结果如下:
1,4
2,5
3,6

1.5 批量处理

在深度学习中,由于源数据都比较大,所以通常需要采用批处理。如利用批量来计算梯度的随机梯度法(SGD)就是一个典型应用。深度学习的计算一般比较复杂,加上数据量一般比较大,如果一次处理整个数据,往往出现资源瓶颈。为了更有效地计算,一般将整个数据集分成多个小批量。与处理整个数据集的另一个极端是每次处理一条记录,这种方法也不科学,因为一次处理一条记录无法充分发挥GPU、NumPy平行处理优势。因此,在实际使用中我们往往采用批量处理(mini-batch)。
如何把大数据拆分成多个批次呢?可采用如下步骤:
• 得到数据集
• 随机打乱数据
• 定义批大小
• 批处理数据集
下面通过一个示例来具体说明:

最后5行结果如下:
第9500批次,该批次的数据之和:17.63702580438092
第9600批次,该批次的数据之和:-1.360924607368387
第9700批次,该批次的数据之和:-25.912226239266445
第9800批次,该批次的数据之和:32.018136957835814
第9900批次,该批次的数据之和:2.9002576614446935
【说明】
批次从0开始,所以最后一个批次是9900。

1.6 节省内存

在NumPy操作数据过程中,有大量涉及变量、数组的操作,尤其在机器学习、深度学习中,参数越来越多,数据量也越来越大,如何有效保存、更新这些参数,将直接影响内存的使用。这里我们介绍几种节省内存的简单方法。
1. 使用X=X+Y与X += Y的区别
假设X、Y为向量或矩阵,这种操作在机器学习中非常普遍。两个表达式从数学角度来说是完全一样的,但对使用内存的开销来说,却完全不同。X += Y操作可减少内存开销。
下面我们用Python的id()函数来说明。id()函数提供了内存中引用对象的确切地址。 运行X = X+Y后,我们会发现id(X)指向另一个位置。 这是因为Python首先计算X+Y,为结果分配新的内存,然后使X指向内存中的这个新位置。

运行结果如下:
1852224075136
1852224037312
X在运行X=X+Y前后id不同,说明指向不同内存区域。

运行结果如下:
1852224018672
1852224018672
X在运行X+=Y前后id相同,说明指向一个内存区域。
2. X=X+Y与X[:]=X+Y的区别
实现代码如下:

运行结果如下:
1852224017152
1852224018672
X在运行X=X+Y前后id不同,说明指向不同内存区域。

X在运行X[:]=X+Y前后id相同,说明指向一个内存区域。

1.7通用函数

NumPy提供了两种基本的对象,即ndarray和ufunc对象。前面我们介绍了ndarray,本节将介绍NumPy的另一个对象通用函数——ufunc。ufunc是universal function的缩写,是一种能对数组的每个元素进行操作的函数。许多ufunc函数都是用C语言实现的,因此它们的计算速度非常快。此外,它们比math模块中函数更灵活。math模块的输入一般是标量,但NumPy中函数的输入可以是向量或矩阵,而利用向量或矩阵可以避免使用循环语句,这点在机器学习、深度学习中非常重要。表1-5列举了几个NumPy的常用通用函数。
表1-5 NumPy的常用通用函数


【说明】np.max,np.sum,np.min等函数中,都涉及一个有关轴的参数(即axis),该参数的具体含义,可参考图1-9。
-图1-9 可视化参数axis的具体含义

1.math与numpy函数的性能比较
实现代码如下:

运行结果如下:
math.sin: 0.5169950000000005
numpy.sin: 0.05381199999999886
由此可见,numpy.sin比math.sin快近10倍。
2.循环与向量运算比较
充分使用Python的NumPy库中的内建函数(built-in function),实现计算的向量化,可大大提高运行速度。NumPy库中的内建函数使用了SIMD指令。如下使用的向量化要比使用循环计算速度快得多。如果使用GPU,其性能将更强大,不过NumPy不支持GPU。

运行结果如下:
dot = 250215.601995
for loop----- Computation time = 798.3389819999998ms
dot = 250215.601995
verctor version---- Computation time = 1.885051999999554ms
从运行结果上来看,使用for循环的运行时间大约是向量运算的400倍。因此,深度学习算法中,一般都使用向量化矩阵运算。
3.np.where的使用
np.where()有两种使用方法,其功能类似于列表中的推导式。
(1)np.where(condition, x, y)
满足条件(condition)(可理解为非0),输出x,不满足输出y。其中condition、y和z都是数组,它的返回值是一个形状与condition相同的数组。
先看condition为一维的情况。

a的值: [-2 -1 0 1 2 3 4 5 6 7 8 9]
x的值 [ 1 1 -1 1 1 1 1 1 1 1 1 1]
如果condtion为二维的情况,实例如下:

运行结果:
z的值 [[1 8] [3 4]]
(2)np.where(condition)
只有条件 (condition),没有x和y,则输出满足条件 (即非0) 元素的索引 (等价于numpy.nonzero),以元组方式输出。

运行结果
c的值 (array([2, 3, 4], dtype=int64),)

运行结果
d的值 [[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
d的值 (array([1, 1, 2, 2, 2, 2], dtype=int64), array([2, 3, 0, 1, 2, 3], dtype=int64))

1.8 广播机制

NumPy的通用函数中要求输入的数组形状(shape)是一致的,当数组的形状不相等时,则会使用广播机制。不过,调整数组使得shape一样时,需满足一定规则,否则将出错。这些规则可归结为以下4条:
1)让所有输入数组都向其中shape最长的数组看齐,shape中不足的部分都通过在前面加1补齐;如:a为2x3x2,b为3x2,则b向a看齐,在b的前面加1,变为1x3x2;
2)输出数组的shape是输入数组shape的各个轴上的最大值;
3)如果输入数组的某个轴和输出数组的对应轴的长度相同或者其长度为1时,这个数组能够用来计算,否则将出错;
4)当输入数组的某个轴的长度为1时,沿着此轴运算时都用(或复制)此轴上的第一组值。
广播在整个NumPy中用于决定如何处理形状迥异的数组;涉及算术运算包括(+,-,*,/…)。这些规则说的很严谨,但不直观,下面我们结合图形与代码进一步说明。
目的:A+B,其中A为4x1矩阵,B为一维向量 (3,)。
要实现A、B相加,需要做如下处理。
1)根据规则1,B需要向看齐,把B变为(1,3)。
2)根据规则2,输出的结果为各个轴上的最大值,即输出结果应该为(4,3)矩阵,
那么A如何由(4,1)变为(4,3)矩阵?B如何由(1,3)变为(4,3)矩阵?
3)根据规则4,用此轴上的第一组值(要主要区分是哪个轴),进行复制(但在实际处理中不是真正复制,否则太耗内存,而是采用其他对象如ogrid对象,进行网格处理)即可,
详细处理如图1-10所示。

图1-10 NumPy广播规则示意图
代码实现如下:

运行结果如下:
A矩阵的形状:(4, 1),B矩阵的形状:(3,)
C矩阵的形状:(4, 3)
[[ 0 1 2]
[10 11 12]
[20 21 22]
[30 31 32]]

1.9小结

本章主要介绍了NumPy的使用。机器学习、深度学习涉及很多向量与向量、向量与矩阵、矩阵与矩阵的运算,这些运算都离不开NumPy,NumPy为各种运算提供了各种高效方法,同时NumPy也是PyTorch张量运算的重要基础。