自学Python:编程基础、科学计算及数据分析
上QQ阅读APP看书,第一时间看更新

2.2 数据类型

数据类型(Data Type)是一个编程语言的基础,它决定了数据在计算机内存中的存储方式。每一个变量都有一种对应的数据类型,基于不同的数据类型,我们可以实现很多复杂的功能。

在Python中,常用的数据类型如表2-1所示:

表2-1 Python中的常用数据类型

其中,数字(包括布尔型)和字符串是Python中最基本的类型;列表、元组、字典、集合是Python中的内置容器类型;NumPy数组是一类自定义的容器类型。

容器(Container)是用来存放基本对象或者其他容器对象的一种类型。

2.2.1 数字

Python中的数字类型主要包括五种,分别是整形、长整型、浮点型、复数型和布尔型:

1. 整型

整型(Integer)是最基本的数字类型,支持加减乘除运算:

注意:在Python 2中,两个整型数字的运算结果只能返回整型。对于除不尽的情况,Python会将结果向下取整,返回小于这个数的最大整数,如2.4变成2,-1.2变成-2。

例如:

除了加减乘除,还可以用“%”进行取余操作,用“**”进行指数计算:

与数学世界的整数不同,Python中的整型只能表示一定范围的整数。在Python中,整型的最大值为:

最小值为-2147483648。

2. 长整型

当整数超出最大最小值的范围时,Python会自动将超出范围的整数转化为长整型(Long Integer):

In [16]: sys.maxint + 1

Out[16]: 2147483648L

长整型以字母L结尾为标志。可以在普通整数后面加上一个字母L,使之变成长整型:

In [17]: type(1234L)

Out[17]: long

长整型没有最大最小值的限制,不过计算速度会比整型慢一些。

3. 浮点型

为了让除法12/5返回的结果是2.4而不是2,我们可以使用浮点型(Floating Point Number)进行计算:

In [18]: 12.0 / 5.0

Out[18]: 2.4

浮点型与整型的运算结果还是浮点型,因为整数是一种特殊的浮点数,所以Python会将计算结果转化为更一般的类型:

In [19]: 12 / 5.0

Out[19]: 2.4

对于用浮点型表示的整数,我们可以省略小数点后的0,直接写成整数加“.”的形式:

In [20]: 12. / 5.

Out[20]: 2.4

如果浮点型的整数部分为0,前面的0也可以省略。比如,可以用.23表示浮点型0.23。

浮点型可以与整数一样进行四则运算,包括取余:

表达式3.4-3.2得到的结果是Python中最接近0.2的浮点数:

0.199999999999999733546474089962430298328399658203125

浮点数是一种对实数的近似表示,本身存在一定的误差。Python显示的结果是该浮点数在内存中的表示,并不是出现了错误。

使用print打印时,Python会对结果进行自动校正:

In [25]: print 3.4 - 3.2

0.2

浮点型的相关信息可以通过sys.float_info查看。

例如,浮点数能表示的最大值:

In [26]: sys.float_info.max

Out[26]: 1.7976931348623157e+308

浮点数能表示的最接近0的值:

In [27]: sys.float_info.min

Out[27]: 2.2250738585072014e-308

浮点数还支持整数除法。在Python中,整数除法是一种特殊的除法,用“//”表示,返回的是比实际结果小的最大整数值:

4. 复数型

复数型(Complex)是表示复数的类型,定义时,Python使用字母j来表示复数的虚部:

In [30]: a = 1 + 2j

a的实部和虚部分别为:

虽然定义a时,实部和虚部使用的是整型数字,但复数型存储的实部和虚部都是浮点型。

a的复共轭为:

In [33]: a.conjugate()

Out[33]: (1-2j)

5. 布尔型

布尔型(Boolean)可以看成是一种取值为True和False的二值变量,分别对应逻辑上的真和假。

布尔型变量可以使用比较表达式得到:

In [34]: 1 > 2

Out[34]: False

常用的比较符号包括小于“<”、大于“>”、小于或等于“<=”、大于或等于“>=”、等于“==”、不等于“!=”等。

Python支持链式比较表达式:

In [35]: 1 < 2 <= 3

Out[35]: True

6. 混合运算

可以将多个操作放在一起进行混合计算:

In [36]: 1 + 2 - (3 * 4 / 6) ** 5 + 7 % 5

Out[36]: -27

通常,返回结果的类型由表达式中最复杂的数值类型决定。上面的表达式的数值都是整型,所以最后的结果也是整型。

与四则运算规则类型,Python中各种运算也有一定的优先级顺序,优先级从高到低排列如下:

● (),括号

● **,乘幂运算

● *、/、//、%,乘、除、整数除法、取余

● +、-,加、减

不同优先级的组合按照优先级次序计算,同一优先级按照先后次序计算。

7. 原地运算

Python支持原地运算的操作,其形式如下:

其中,b+=1的作用相当于b=b+1,类似的运算符还有-=、*=、/=等。

8. 数学函数

Python提供了一些简单的数学函数对数字进行处理。

例如,求绝对值:

In [40]: abs(-12.4)

Out[40]: 12.4

四舍五入取整:

In [41]: round(21.6)

Out[41]: 22.0

求一组数的最大、最小值:

9. 类型转换

不同类型的数字间可以进行类型转换。

int()函数可以将浮点型转化为整型,但只保留整数部分:

整型转浮点型的函数为float():

In [46]: float(1)

Out[46]: 1.0

整型、浮点型转长整型的函数为long():

In [47]: long(1)

Out[47]: 1L

整数型、浮点型转复数型的函数为complex():

In [48]: complex(1)

Out[48]: (1+0j)

10. 整型的其他表示方法

通常整型的表示是以十进制为基础的。十进制(Decimal)是以10为基数的计数方法,使用数字0到9,十进制下有:9+1=10。

在计算机科学中,还存在其他进制的表示方法,如二进制、八进制和十六进制。

二进制(Binary)是以2为基数的计数方法,使用数字0和1,二进制下有:1+1=10。

Python中的二进制数字以0b开头:

In [49]: 0b101010

Out[49]: 42

八进制(Octal)是以8为基数的计数方法,使用数字0到7,八进制下有:7+1=10。

Python中的八进制数字以0或者0o开头:

In [50]: 067

Out[50]: 55

十六进制(Hexadecimal)是以16为基数的计数方法,使用数字0到9以及字母A到F (或者a到f),其中A到F分别对应十进制中的10到15。

Python中的十六进制数字以0x开头:

In [51]: 0xFF

Out[51]: 255

除了不同的进制,数字也可以用科学计数法表示。

科学记数法(Scientific Notation)中,一个数写成一个绝对值在1与10之间的实数a与一个10的n次幂的积,并用e表示10的幂次:

In [52]: 1e-6

Out[52]: 1e-06

2.2.2 字符串

字符串(String)是由零个或多个字符组成的有限序列,在编程语言中表示文本。

1. 字符串的生成

Python用一对引号来生成字符串,单引号或者双引号都可以:

一对双引号可以生成一个包含单引号的文本:

In [3]: print "I'm good."

I'm good

2. 字符串的基本操作

字符串支持两种运算:加法和数乘。

加法操作是将两个字符串按照顺序连接,如:

In [4]: "hello" + "world"

Out[4]: 'helloworld'

数乘是将字符串与一个整数相乘,得到重复的字符串:

In [5]: "abc" * 3

Out[5]: 'abcabcabc'

字符串的长度可以通过len()函数得到:

In [6]: len('hello world')

Out[6]: 11

3. 字符串的方法

Python是一种面向对象的语言,字符串是一种常见的对象。

在计算机科学中,对象(Object)指在内存中装载的一个实例,有唯一的标识符。对象通常具有属性(Attribute)和方法(Method)。其中,方法是面向对象编程的一个重要特性,Python使用以下形式来调用方法:

object.method()

字符串有一些常用的方法。

(1).split()方法

默认情况下,.split()方法将字符串按照空白字符分割,并返回一个字符串列表:

也可以使用自定义的符号对字符串进行分割:

还可以指定分割的最大次数:

In [11]: line.split(',', 2) # 最多分割两次

Out[11]: ['1', '2', '3,4,5']

字符串分割了两次,得到了3个字符串。

字符串还支持.rsplit()方法,从字符串的结尾开始进行分割。

.rsplit()方法的参数与.split()方法相同,在不指定分割的最大次数时,它的表现与.split()方法相同:

In [12]: line.rsplit(',')

Out[12]: ['1', '2', '3', '4', '5']

指定了最大次数时,从字符串右端开始分割:

In [13]: line.rsplit(',', 2) # 最多分割2次

Out[13]: ['1,2,3', '4', '5']

(2).join()方法

连接是一个与分割相反的操作,.join()方法的作用是以当前字符串为连接符将字符串序列中的元素连成一个完整的字符串。

例如,以空格为分隔符将numbers连接起来:

换一种连接符:

In [17]: ':'.join(numbers)

Out[17]: '1:2:3:4:5'

(3).replace()方法

.replace()方法将字符中的指定部分替换成新的内容,并得到新的字符串。

例如,将s中的“world”替换为“python”:

调用.replace()方法不会改变原始字符串的值:

In [20]: s

Out[20]: 'hello world'

.replace()方法会将字符串中所有可替换的部分都替换掉:

可以在.replace()方法中使用一个额外参数指定替换的次数。

例如,只替换一个空格:

In [23]: s.replace(' ', ':', 1)

Out[23]: '1:2 3 4 5'

(4).upper()和.lower()方法

.upper()方法返回一个字母全部大写的新字符串:

In [24]: "hello world".upper()

Out[24]: 'HELLO WORLD'

.lower()方法返回一个字母全部小写的新字符串:

调用这两个方法不会改变原来字符串s的值:

In [27]: s

Out[27]: 'HELLO WORLD'

(5).strip()方法

.strip()方法返回一个将两端的多余空格除去的新字符串:

.lstrip()方法返回一个将开头的多余空格除去的新字符串:

In [30]: s.lstrip()

Out[30]: 'hello world '

.rstrip()方法返回一个将结尾的多余空格除去的新字符串:

In [31]: s.rstrip()

Out[31]: ' hello world'

这些方法都不会改变原始字符串的值:

In [32]: s

Out[32]: ' hello world '

.strip()方法也可以传入参数,去除字符串首尾两端的指定字符。

例如,去掉两端所有的“=”或“-”:

In [33]: "---=hello world===".strip("-=")

Out[33]: 'hello world'

4. 多行字符串的生成

Python用一对三引号"""或者'''来生成多行字符串:

在存储形式上,Python用转义字符“\n”来表示换行:

In [36]: a

Out[36]: 'hello world.\n it is a nice day.'

转义字符(Escape Sequence)是一种表示字符的机制,它以一个反斜杠“\”开头,后面跟另一个字符、一个3位八进制数或者一个16进制数,用来表示一些不可打印的符号,如换行符“\n”或制表符“\t”。由于反斜杠被用于转义字符的开头,所以在字符串中表示反斜杠需要使用转义字符“\\”。

5. 代码的换行

当某一行代码太长时,为了美观起见,我们通常将一行太长的代码转化为多行代码。在Python中,我们有两种方式可以将单行代码变为多行代码:使用括号“()”或使用反斜杠“\”。

使用括号换行的例子:

使用反斜杠换行的例子:

6. 数字与字符串的相互转化

(1)数字转字符串

将数字转化为字符串通常有两种方式:str()函数和repr()函数,它们都是将一个对象转化为字符串表示。两个函数的区别在于,repr()函数返回的是适合Python解释器(机器)阅读的表示;而str()函数返回的是适合常规(人)阅读的形式:

这两个函数不仅能作用于数字,也能作用在其他的类型上。

整数除了十进制之外,还有其他的表示形式。Python提供了整数到其他进制字符串的转化函数:str()(十进制),hex()(十六进制),oct()(八进制)和bin()(二进制):

(2)字符串转数字

同样,我们也可以将字符串转化为数字。

int()函数可以将字符串转化为整数,默认是按照十进制进行转化:

In [47]: int('255')

Out[47]: 255

也可以指定进制:

float()函数可以将字符串转化为浮点数:

In [51]: float('2.5')

Out[51]: 2.5

(3)ASCII码与字符串的相互转化

ASCII(American Standard Code for Information Interchange)即美国信息交换标准代码,是基于拉丁字母的一套计算机编码系统,主要用于显示现代英语和其他西欧语言。它用0~255的值来表示单个字符,我们可以用ord()函数查看单个字符的ASCII码值:

In [52]: ord('a')

Out[52]: 97

反过来,我们可以使用chr()函数将0~255的ASCII值转化为字符串:

In [53]: chr(97)

Out[53]: 'a'

7. 字符串的格式化

有时候,我们需要使用字符串向屏幕打印一些文字和数值,最容易想到的方法是将这些值转化为字符串后与文字拼接,如:

在需要输出多变量值的时候,这种形式显得很不方便。Python提供了一种使用百分号进行格式化输出的方式:

In [56]: '%s is %d years old.' % (name, age)

Out[56]: 'John is 10 years old.'

其中,字符串中以百分号开头的部分表示格式控制符,常用的控制符有:

● %s,表示字符串;

● %d,表示整数;

● %f,表示浮点数。

字符串后的百分号将要格式化的变量按照顺序排列。按照变量的排列顺序,name的值替代了字符串中的%s,age的值则替代了%d。

此外,Python还提供了功能更强大的.format()方法来实现字符串的格式化:

In [57]: '{} is {} years old.'.format(name, age)

Out[57]: 'John is 10 years old.'

使用.format()方法时,字符串中的花括号{}会被传入的参数依次替代。

.format()方法支持在花括号中指定传入参数的位置,位置的计数从0开始。例如,我们将上面.format()方法中的name参数与age参数互换位置,并指定第一个花括号用位置为1的参数name,第二个花括号用位置为0的参数age:

In [58]: '{1} is {0} years old.'.format(age, name)

Out[58]: 'John is 10 years old.'

再如,只传入两个参数,指定四处替换:

还可以在花括号中使用名称对参数进行格式化,并在.format()方法中指定这些参数的值:

In [61]: '{nm} is {ag} years old.'.format(ag=age, nm=name)

Out[61]: 'John is 10 years old.'

花括号还可以控制输出的类型和格式,其形式为:

{<field name>:<format>}

其中,<field name>是参数的位置或名称,如“1”或者“ag”,可以省略;<format>可以是类似s、d、f这样的输出类型。

例如,“{:10s}”指定字符串,长度为10,长度不够时在后面补空格:

“{:10d}”指定整数,长度为10,长度不够时在前面补空格:

“{:010d}”指定整数,长度为10,长度不够时在前面补0:

In [64]: '{:010d} is good'.format(100)

Out[64]: '0000000100 is good'

“{:.2f}”指定浮点数,显示小数点后两位:

In [65]: 'pi is around {:.2f}'.format(3.1415926)

Out[65]: 'pi is 3.14'

在指定输出格式时,中间的冒号不能省略。

2.2.3 Unicode字符串

Python的字符串类型在表示其他语言的文字时可能会遇到一些问题,以中文为例:

In [1]: a = "中国"

a的长度:

In [2]: len(a)

Out[2]: 6

两个中文字符构成的字符串长度是6,这不符合我们的认知。直接查看a的值会发现,a其实存储了六个ASCII编码:

In [3]: a

Out[3]: '\xe4\xb8\xad\xe5\x9b\xbd'

其中,“\xe4”是一个ASCII编码。在数字中,“0x”表示十六进制,在字符串中,“\x”是16进制转义字符的开头。因此,“\xe4”表示的是ASCII码中的228:

In [4]: 0xe4

Out[4]: 228

国际标准的ASCII码是0~127,后面的128~255经过组合可以生产一些比较不规则的文字的编码,如汉字等。

使用print打印a:

In [5]: print a

中国

为了更好地表示其他语言的文字,人们创建了另一种新的编码方式:Unicode。Unicode,又称万国码、国际码、统一码、单一码等,它将世界上大部分的文字系统进行了整理、编码,并进行一对一的编码,使得计算机可以用更为简单的方式来呈现和处理文字。

Python提供了Unicode字符串来使用Unicode编码,定义时,只需要在字符串前面加上字母u:

In [6]: b = u"中国"

b的类型不是字符串str,而是unicode:

In [7]: type(b)

Out[7]: unicode

b的长度:

In [8]: len(b)

Out[8]: 2

b的表示:

In [9]: b

Out[9]: u'\u4e2d\u56fd'

其中,“\u”是Unicode字符的转义表示,它对应于数字上的十六进制表示。

对于普通的英文字符,可以直接使用str()函数和unicode()函数实现字符串和Unicode字符串的相互转换。但是,对于非英文字符串a,直接使用unicode()函数是不可以的:

对于非英文Unicode字符串b,直接使用str()函数也是不可以的:

对于这种情况,Python提供了.decode()方法将字符串解码为Unicode字符串,以及.encode()方法将Unicode字符串编码为字符串。

对于字符串a,可以使用.decode()方法将其解码为Unicode字符串:

In [12]: a.decode("utf-8")

Out[12]: u'\u4e2d\u56fd'

对于Unicode字符串b,可以使用.encode()方法将其编码为字符串:

In [13]: b.encode("utf-8")

Out[13]: '\xe4\xb8\xad\xe5\x9b\xbd'

事实上,unicode()函数不能直接使用的原因是我们没有指定字符的编码方式:

In [14]: unicode(a, "utf-8")

Out[14]: u'\u4e2d\u56fd'

str()函数则没有这样的用法。

2.2.4 索引与分片

1. 索引

序列(Sequence)是多个元素按照一定规则组成的对象。对于一个有序序列,我们可以通过索引位置的方法来访问对应位置的值。

索引(Indexing)的作用好比一本书的目录,利用目录中的页码,可以快速找到所需的内容。Python使用中括号[]来对有序序列进行索引。字符串可以看成一个由字符元素组成的有序序列:

Python的索引位置是从0开始的,所以0对应与序列的第1个元素。为了得到序列的第i个元素,需要使用索引值i-1:

In [3]: s[4]

Out[3]: 'o'

Python还引入了负数索引值,负数表示从后向前的索引,如-1索引序列的倒数第1个元素,-2索引倒数第2个元素:

In [4]: s[-2]

Out[4]: 'l'

使用超过序列范围的索引值会抛出异常:

2. 分片

索引只能从序列中提取单个元素,想要从序列中提取出多个元素,可以使用分片。

在有序序列中,分片(Slicing)可以看成是一种特殊的索引,只不过它得到的内容是一个子序列。其用法为:

var[lower:upper:step]

分片的范围包括lower,但不包括upper。step表示子序列取值间隔大小,如果没有取值,则默认为1。例如:

In [6]: s[1:3]

Out[6]: 'el'

在这个例子中,分片的起始为位置1,包含的元素个数为3-1=2。

分片也支持负数索引值:

In [7]: s[1:-2]

Out[7]: 'ello wor'

使用分片时,lower和upper也可以省略。

省略lower,相当于使用默认值0:

In [8]: s[:3]

Out[8]: 'hel'

省略upper,相当于使用默认值len(s):

In [9]: s[-3:]

Out[9]: 'rld'

如果都省略,得到一个复制的字符串:

In [10]: s[:]

Out[10]: 'hello world'

令step为2,得到一个每两个值取一个的子串:

In [11]: s[::2]

Out[11]: 'hlowrd'

当step取负值时,开头和结尾会反过来,省略lower对应结尾,省略upper对应开头。因此,s[::-1]表示字符串s的一个反序:

In [12]: s[::-1]

Out[12]: 'dlrow olleh'

当给定的upper超出字符串的长度时,Python并不会抛出异常,只会计算到结尾:

In [13]: s[:100]

Out[13]: 'hello world'

3. 索引和分片规则的探究

在这里简单解释一下Python中索引与分片规则的由来。

(1)使用[low, up)的原因

基于0开头索引规则的假设,讨论使用[low, up)分片规则的原因。

如表2-2所示,考虑不同规则下,字符串“hello”中的子串“el”的表示方式。可以发现,前两种规则在计算序列长度上十分方便,不需要进行额外的加一减一操作。

表2-2 子串“el”的不同表示方式

现在只考虑前两种规则。如表2-3所示,考虑在前两种规则下,字符串“hello”的前3个字符“hel”的表示方式。可以看到,第二种规则的索引位置从-1开始,与我们平时的认知有冲突。所以,Python使用[low, up)的规则对序列进行分片。

表2-3 子串“hel”的不同表示方式

(2)使用0开头的原因

基于[low, up)分片规则的假设,讨论使用0开头索引规则的原因。

考虑以下两种情况。表示开始的前n个元素时:

● 使用0-base:[0, n)

● 使用1-base:[1, n+1)

表示第i+1到第i+n这n个元素:

● 使用0-base:[i, n+i)

● 使用1-base:[i+1, n+i+1)

如果使用1-base,总会有一个+1成分在后面,这样不是很方便。所以,Python使用0开头的规则进行索引。

2.2.5 列表

列表(List)是一个有序的Python对象序列。

1. 列表的生成

列表可以用一对中括号“[]”生成,中间的元素用逗号“,”隔开:

空列表可以用“[]”或者list()函数生成:

2. 列表的基本操作

与字符串类似,列表也支持使用len()函数得到长度:

In [6]: len(l)

Out[6]: 3

列表相加,相当于将这两个列表按顺序连接:

In [7]: [1, 2, 3] + [3.2, 'hello']

Out[7]: [1, 2, 3, 3.2, 'hello']

列表数乘,相当于将这个列表重复多次:

In [8]: l * 2

Out[8]: [1, 2.0, 'hello', 1, 2.0, 'hello']

3. 索引和分片

列表是个有序的序列,因此也支持索引和分片的操作。

列表的正负索引:

分片:

In [12]: a[2:-1]

Out[12]: [12, 13]

与字符串不同,我们可以通过索引和分片来修改列表。

Python的字符串是一种不可变的类型,一旦创建就不能修改。例如,在尝试使用索引将字符串的首字母大写时,Python会抛出一个异常:

列表是一种可变的数据类型,支持通过索引和分片修改自身。例如,将列表的第一个元素改为100:

这种赋值也适用于分片。例如,将列表a中的[12, 13]换成[1, 2]:

对于间隔为1的连续分片,Python采用的是整段替换的方法,直接用一个新列表替换原来的分片,两者的元素个数并不需要相同。

例如,可以将列表a中的[1, 2]换成[4, 4, 4, 4]:

这种机制还可以用来删除列表中的连续分片:

对于间隔不为1的不连续分片,赋值时,两者的元素数目必须一致:

数目不一致时,会抛出异常:

4. 元素的删除

Python提供了一种更通用的删除列表元素的方法:关键字del。

用del关键字删除列表位置0处的元素:

删除第二到最后一个元素:

删除间隔为2的元素:

5. 从属关系的判断

可以用关键字in和关键字not in判断某个元素是否在某个序列中。例如:对于列表

关键字in也可以用来测试字符串中的从属关系。对于字符串来说,它的从属关系的范畴比列表更大,我们可以测试一个字符串是不是在另一个字符串中:

6. 不改变列表的方法

字符串调用方法的时候,不会改变字符串本身的值。列表也提供了一些方法,这些方法不会改变列表本身的值。

(1).count()方法

.count()方法返回列表中某个特定元素出现的次数:

(2).index()方法

.index()方法返回列表中某个元素第一次出现的索引位置:

In [46]: a.index(12)

Out[46]: 1

元素不存在时抛出异常:

7. 改变列表的方法

列表还有一些改变自己值的方法。

(1).append()方法

.append()方法向列表最后添加单个元素:

该方法每次只向列表中添加一个元素,如果这个元素是序列,那么列表的最后一个元素就是这个序列,并不会将其展开:

(2).extend()方法

.extend()方法将另一个序列中的元素依次添加到列表的最后:

(3).insert()方法

.insert()方法在指定索引位置处插入一个元素,令列表的该位置等于这个元素,插入位置后的元素依次后移:

(4).remove()方法

.remove()方法将列表中第一个出现的指定元素删除,若该元素不在列表中会抛出异常:

(5).pop()方法

.pop()方法将列表中指定索引位置处的元素删除,并返回这个元素值。

例如,删除列表位置2的元素12,并返回了它的值:

.pop()方法支持负数索引的弹出。

(6).sort()方法

.sort()方法将列表中的元素按照一定的规则从小到大排序:

对于不同类型元素之间的排序,Python有一套自己的规则:

可以在调用时加入一个reverse参数,如果它等于True,列表会反过来按照从大到小进行排序:

如果不想改变原来列表的值,可以使用sorted()函数得到一个排序后的新列表:

(7).reverse()方法

.reverse()方法将列表中的元素逆序排列:

同样的,如果不想改变原来列表中的值,可以使用reversed()函数进行反序。

2.2.6 元组

元组(Tuple)是一种与列表类似的序列类型。元组的基本用法与列表十分类似,只不过元组一旦创建,就不能改变,因此,元组可以看成是一种不可变的列表。

1. 元组的生成和基本操作

Python用一对括号“()”生成元组,中间的元素用逗号“,”隔开:

In [1]: (10, 11, 12, 13, 14)

Out[1]: (10, 11, 12, 13, 14)

对于含有两个或以上元素的元组,在构造时可以省略括号:

元组可以通过索引和切片来查看元素,规则与列表一样。

元组的索引得到单个元素:

In [4]: t[0]

Out[4]: 10

切片得到一个子元组:

In [5]: t[1:3]

Out[5]: (11, 12)

不过,元组不支持用索引和切片进行修改:

2. 只含单个元素的元组

由于小括号“()”在表达式中有特殊的含义,因此,对于只含有单个元素的元组,如果我们只加括号,Python会认为这是普通的表达式:

a的类型并不是元组,而是整型。

为了定义只含一个元素的元组,我们需要在元素后面加上一个额外的逗号“,”,告诉Python这是一个元组:

单元素元组的括号也可以省略,但额外的逗号不可以:

3. 元组的方法

由于元组是不可变的,所以它只支持.count()方法和.index()方法,其用法与列表一致:

4. 列表与元组的互相转换

列表和元组可以使用tuple()函数和list()函数互相转换:

5. 列表与元组的生成速度比较

在IPython中,可以使用魔术命令%timeit来对代码的运行进行计时。%timeit可以计算后面一行代码的运行速度。

我们比较一下Python中列表与元组的生成速度:

可以看到,元组的生成速度比列表的生成速度快得多。

6. 元组的不可变性

元组具有不可变性:创建元组后,不能修改元组的形状,也不能给元组的某个元素重新赋值。不过,这种不可变性不是绝对的。如果元组中的元素本身可变,那么我们可以通过调用该元素的方法来修改元组。

考虑这样的一个元组,其第一个元素是列表:

In [21]: a = ([1, 2], 3, 4)

In [22]: a

Out[22]: ([1, 2], 3, 4)

直接用a[0]赋值仍然是不允许的操作,但可以调用a[0]的方法来改变a[0]。

例如,使用.append()方法向a[0]添加新值后,整个元组也发生了改变:

7. 元组与多变量赋值

Python支持多变量赋值的模式:

多变量赋值本质是将两个元组中的元素一一对应,这种情况下,Python要求等号两边的元素数目必须相等。例如,将有两个元素的元组t的值分别赋给a和b:

使用多变量赋值,可以轻易地交换a,b的值:

多变量赋值支持超过两个值的操作,只要等号两边的元素数相同:

In [33]: a, b, c = 1, 2, 3

多变量赋值还支持嵌套的形式:

除了元组,列表也支持多变量赋值:

2.2.7 可变与不可变类型

按照创建后是否可以改变,Python中的对象可以分成两类:可变类型和不可变类型。

可变类型(Mutable Type)可以通过一些操作来改变自身的值。

例如,列表是一种可变类型,我们可以通过索引改变它的值:

通过调用方法改变它的值:

通过del关键字改变它的值:

不可变类型(Immutable Type)不能通过这些操作来改变它的值。

例如,字符串是一种不可变类型,不能通过索引来改变它的值:

调用字符串的方法会返回一个新的字符串,并不改变原来的值。

可以用重新赋值的方法改变s的值:

对变量s重新赋值,Python会创建一个新的字符串,原来的字符串并没有被修改,因此,这不违反字符串不可变的性质。

我们可以将Python中的数据类型大致分为两类:

● 不可变类型:整数、浮点数、长整数、复数、字符串、元组、不可变集合;

● 可变类型:列表、字典、集合、NumPy数组、自定义类型;

Python中的数字和字符串都是不可变类型;常用的容器类型列表、字典、集合等都是可变的;元组和不可变集合相当于对列表和集合的一种不可变实现。

2.2.8 字典

字典(Dictionary)是Python中一种由“键-值”组成的常用数据结构。我们可以把“键”类比成单词,“值”类比成单词对应的意思,这样,“键-值”相当于一种“单词-意思”的对应。我们可以通过查询“单词”,来得到它对应的“意思”。

1. 字典的生成和基本操作

Python中使用一对花括号“{}”或者dict()函数来创建字典。

空的字典可以用以下两种方法产生:

我们可以使用索引的方式,向字典中插入键值对:

字符串“one”和“two”是索引的键,1和2是对应的值。键可以是数字,也可以是字符串。

可以通过索引查看字典中对应键的值:

In [7]: a["one"]

Out[7]: 1

可以直接赋值修改它:

字典中的元素是没有顺序的,因此,字典只支持用键去进行索引,不支持用位置索引。如果索引的数字不是字典的键,Python会抛出一个异常:

Python用类似于“{'one': 1, 'two': 2}”的结构来表示一个字典,可以直接使用这样的结构来创建字典:

2. 键的不可变性

字典是一种高效的存储结构,其内部使用基于哈希值的算法,用来保证从字典中读取键值对的效率。不过,哈希值算法要求字典的键必须是一种不可变类型。

使用可变类型作为键时,Python会抛出异常,如列表:

字典中值的类型则没有任何限制。

3. 键的常用类型

在不可变类型中,整数和字符串是键最常用的两种类型。由于精度问题,我们一般不使用浮点数作为键的类型。

例如,考虑这样的情况:

In [14]: data = {}

In [15]: data[1.1 + 2.2] = 6.6

由于浮点数存在精度问题,虽然看起来字典中的键为3.3,但是当我们查看键3.3的值时,Python会抛出异常:

查看data的值就会发现,这的确是因为浮点数的精度问题所引起:

In [17]: data

Out[17]: {3.3000000000000003: 6.6}

元组也是一种常用的键值。

例如,假设某航空公司从纽约到北京有15架航班,从北京到纽约有20架航班,从上海到纽约有25架航班,我们用一个字典存储这些信息:

元组是有序的,因此,在这个字典中,('New York', 'Beijing')和('Beijing', 'New York')是两个不同的键。

4. 从属关系的判断

与列表类似,可以用关键字in来判断某个键是否在字典中:

5. 字典的方法

(1).get()方法

用索引查询字典中不存在的键时,Python会抛出异常:

为了避免这种异常,我们可以使用字典的.get()方法代替索引。.get()方法接受两个参数key和default,其中default可以省略。该方法返回字典中键key对应的值,键不存在时,返回default指定的值。

改用.get()方法:

In [28]: a.get("three")

虽然看起来并没有返回任何结果,但事实上,这是因为.get()方法默认的default参数为None,而IPython解释器在输出时自动忽略了None:

In [29]: print a.get("three")

None

我们可以通过传入第二个参数来改变default参数的默认值:

In [30]: a.get("three", "undefined")

Out[30]: 'undefined'

(2).pop()方法

列表的.pop()方法可以删除并返回指定索引位置的元素,与之类似,字典的.pop()方法删除并返回指定键的值。不一样的地方在于,列表会对非法的索引值抛出异常,字典则不会。

.pop()方法也接受两个参数key和default,其中default参数的默认值是None。如果给定的键不存在,方法返回default参数指定的值:

也可以用del关键字删除字典中的元素:

(3).update()方法

之前已经知道,可以通过索引来插入、修改单个键值对,但是如果想一次性更新多个键值对,这种方法就显得比较麻烦。字典提供了.update()方法来一次更新多个键值对。

例如,将字典b中的内容一次更新到字典a中:

可以看到,.update()方法会更新原来已有的键值对,同时添加原来没有的键值对。

(4).keys()方法

.keys()方法返回一个由所有键组成的列表:

In [41]: a.keys()

Out[41]: ['three', 'two', 'one']

(5).values()方法

.values()方法返回一个由所有值组成的列表:

In [42]: a.values()

Out[42]: [3, 2, 1]

注意,虽然在这个例子中,.keys()方法返回的值跟.values()方法返回的值是一一对应的,但这个关系在一些情况下并不成立。

(6).items()方法

.items()方法返回一个由所有键值对元组组成的列表:

In [43]: a.items()

Out[43]: [('three', 3), ('two', 2), ('one', 1)]

6. 使用dict()初始化

除了通常的定义方式,还可以通过dict()函数来初始化字典,dict()函数的参数可以是另一个字典,也可以是一个由键值对元组构成的列表:

In [44]: dict([('three', 3), ('two', 2), ('one', 1)])

Out[44]: {'one': 1, 'three': 3, 'two': 2}

此外,.update()方法也可以接受一个由键值对元组构成的列表代替字典。

2.2.9 集合与不可变集合

字符串和列表都是一种有序序列,而集合(Set)是一种无序的序列。集合中的元素具有唯一性,即集合中不存在两个同样的元素。

Python为了确保集合中不包含同样的元素,规定在集合中放入的元素只能是不可变类型的,如数字,字符串,元组等。浮点数由于存在精度的问题,一般不适合做集合中的元素。

1. 集合的生成

空集合可以用set()函数来生成:

In [1]: type(set())

Out[1]: set

可以在set()函数中传入一个列表,初始化这个集合:

In [2]: set([1, 2, 3, 1])

Out[2]: {1, 2, 3}

因为集合中的元素有唯一性,所以重复的元素1只保留了一个。

我们还可以用一对大括号“{}”的来创建集合:

In [3]: {1, 2, 3, 1}

Out[3]: {1, 2, 3}

但是空集合只能用set()函数来创建,因为空的大括号“{}”创建的是一个空的字典:

In [4]: type({})

Out[4]: dict

2. 集合的基本运算

集合的运算可以用文氏图来描述。

如图2-1所示,两个圆形分别表示集合A和B,区域1表示A和B的交集;区域2表示A与B的差集;区域3表示B与A的差集;区域2和区域3一起是A和B的对称差集;区域1、区域2和区域3一起是A和B的并集。

图2-1 集合的文氏图表示

考虑这两个集合:

In [5]: a, b = {1, 2, 3, 4}, {3, 4, 5, 6}

(1)并集

两个集合的并集可以用操作符“|”实现,返回包含两个集合所有元素的集合:

并集运算是可交换的,即a|b与b|a返回的是相同的结果。也可以用.union()方法得到两个集合的并集:

In [8]: a.union(b)

Out[8]: {1, 2, 3, 4, 5, 6}

(2)交集

两个集合的交集可以用操作符“&”实现,返回包含两个集合公有元素的集合:

交集运算也是可交换的,即a & b与b & a返回的是相同的结果。也可以用.intersection()方法得到两个集合的交集:

In [11]: a.intersection(b)

Out[11]: {3, 4}

(3)差集

两个集合的差集可以用操作符“-”实现,返回在第一个集合而不在第二个集合中的元素组成的集合。

差集运算是不可交换的,因为a-b返回的是在a不在b中的元素组成的集合,而b-a返回的是返回在b不在a的元素组成的集合:

也可以用.difference()方法得到两个集合的差集:

(4)对称差集

两个集合的对称差集可以用操作符“^”实现,等于两个集合的并集与交集的差:

对称差是可交换的操作,它对应的方法是.symmetric_difference():

In [18]: a.symmetric_difference(b)

Out[18]: {1, 2, 5, 6}

3. 集合的包含关系

如果集合A的元素都是另一个集合B的元素,那么A就是B的一个子集。如果A与B不相同,那么A叫做B的一个真子集。因此,B是B自己的子集,但不是自己的真子集。

在Python中,判断子集关系可以用运算符“<=”实现:

也可以用.issubset()方法来判断是不是某个集合的子集:

In [22]: b.issubset(a)

Out[22]: True

反过来,我们也可以使用“>=”操作符或者集合的.issuperset()方法来验证一个集合是否包含另一个集合:

真子集可以用符号“<”或者“>”去判断:

4. 集合的方法

(1).add()方法

跟列表的.append()方法类似,集合用.add()方法添加单个元素。

添加的元素必须是不可变类型的。

如果添加的是集合中已有的元素,集合不改变:

(2).update()方法

跟列表的.extend()方法类似,.update()方法用来向集合添加多个元素,接受一个序列作为参数:

(3).remove()方法移除元素

.remove()方法可以从集合中移除单个元素:

元素不存在时会抛出异常:

(4).pop()方法

集合是一种没有顺序的结构,不能像列表一样使用位置进行索引,所以它的.pop()方法不接受参数,随机地从集合中删除并返回一个元素:

如果集合为空,调用.pop()方法会抛出异常。

(5).discard()方法

.discard()方法作用与.remove()方法一样,区别在于删除不存在的元素时不会抛出异常:

5. 判断从属关系

关键字in也可以用来判断集合中的从属关系:

6. 不可变集合

对于集合,Python提供了一种不可变集合(Frozen Set)的数据结构与之对应。不可变集合是一种不可变类型,一旦创建就不能改变,其构建方法如下:

不可变集合的一个主要用途是用来作为字典的键。如果用元组作为键,(a, b)和(b, a)是两个不同的键。可以使用不可变集合让它们代表同一个键。

例如,之前我们使用元组来表示从城市A到城市B之间的航班数,现在我们希望表示两个城市之间的飞行距离:

顺序不同不会影响查找的结果:

2.2.10 赋值机制

为了更好地理解Python中的数据类型,我们需要简单了解Python的赋值机制。先看一个例子。

假设有一个列表x,为了不改变列表x的值,我们将x“复制”一份给y,然后对列表y进行修改:

但是,在这样的操作下,x的值发生了变化:

In [4]: x

Out[4]: [100, 2, 3, 4]

这说明赋值并不能实真正的复制效果。为了理解上面的现象,需要从Python的内部赋值机制开始理解。

1. 基本类型的赋值机制

我们先考虑基本类型的赋值机制。

(1)赋值机制分析

来看下面一段代码在Python中的处理流程:

执行第一句:x = 500。

将基本类型的值赋给变量时,Python会向系统申请相应的内存空间存储相关的值,然后在命名空间中将变量指向这个内存空间的地址。

Python先向系统申请了一个PyInt大小的内存用来储存整数对象500,记该内存的地址为pos1。接着,Python在命名空间中让变量x指向内存地址pos1。

不可变类型一旦创建就不能修改,所以现在内存pos1中存储的内容是不可变的。内存和命名空间的情况如表2-4所示。

表2-4 内存和命名空间情况:第1句

执行第二句:y = x。

将变量赋给变量时,Python不会创建新的内存来存储变量的值,而是在命名空间中,这两个变量指向同一个内存地址。这里,Python让y与x同时指向了pos1。内存和命名空间的情况如表2-5所示。

表2-5 内存和命名空间情况:第2句

执行第三句:y = 'foo'。

字符串是基本类型,Python首先分配了一个地址为pos2,大小为PyStr的内存来储存字符串'foo',接着将变量y指向的位置变为pos2。内存和命名空间的情况如表2-6所示。

表2-6 内存和命名空间情况:第3句

(2)验证分析结果

现在我们来验证这一过程。

在Python中,id()函数可以用来查看一个变量的内存地址,例如之前提到的pos1和pos2:

返回的结果表示变量x现在指向的内存地址的值。

y和x的id值一样,说明y和x现在指向了同一块内存地址。

也可以使用关键字is来判断两个变量是不是同一个对象:

In [9]: x is y

Out[9]: True

最后让y指向新的内存地址,查看y的地址:

(3)处理值相同的情况

一般来说,Python会为每个出现的对象单独赋值,即使值是一样的:

虽然我们已经有了一个等于500的值x,但是z并不会指向这个现成的值,而是创建了一个新的对象。

不过,为了提高内存利用效率,对于一些简单的对象,比如一些数值较小的整数对象,Python采用了共用内存的办法:

2. 容器类型的赋值机制

接着,我们来讨论容器类型的赋值机制。

(1)赋值机制分析

以列表为例,分析下面一段代码的执行过程:

执行第一句:x = [500, 501, 502]。

基本类型在内存中存储的是相应的值,而容器类型在内存中存储的是值对应的内存地址。

Python首先会分配三个PyInt大小的内存去分别存储整数500、501、502,其内存地址分别为pos1、pos2、pos3;接着,为列表分配一段PyList大小的内存地址pos4,用来存储这些整数对应的内存地址;最后,在命名空间中,让x指向pos4。内存和命名空间的情况如表2-7所示。

表2-7 内存和命名空间情况:第1句

与基本类型不同的是,容器类型中存储的内存地址是可变的。

执行第二句:y = x。

与基本类型中一样,Python让x和y同时指向了pos4。内存和命名空间的情况如表2-8所示。

表2-8 内存和命名空间情况:第2句

执行第三句:y[1] = 600。

y[1]对应的内存地址是pos2,不过pos2对应的值501是不可变的。因此,Python首先为600分配了一个新内存pos5,再把y[1]对应的内存地址修改为pos5。由于变量x也是指向pos4的,所以,修改y导致x发生变化。

另一方面,没有变量在使用pos2位置的值,Python会自动调用垃圾处理机制将它回收。内存和命名空间的情况如表2-9所示。

表2-9 内存和命名空间情况:第3句

执行第四句:y = [700, 800]

Python会重新创建一个列表,然后让y指向它,x仍然指向原来的内存位置,没有发生变化。内存和命名空间的情况如表2-10所示。

表2-10 内存和命名空间情况:第4句

(2)验证分析结果

同样,我们对这一过程进行验证:

进行赋值,验证id(y)与id(x)相同:

修改y[1],id(y)并不改变:

而id(x[1])和id(y[1])的值都变为一个相同的新值:

In [32]: id(x[1]) == id(y[1])

Out[32]: True

最后更改y的值,id(y)的值改变,id(x)还是原来的值:

(3)其他类型的情况

除了列表,字典、集合等的赋值也采用了类似的机制:

对于比较大的列表、字典或集合,复制所有的元素通常比较耗时,也占用了额外内存,所以直接让它们共享一块内存的赋值机制显得更高效。

为了真正地得到一个复制,比较常用的做法是使用生成函数构造一个新的对象。

例如,复制一个列表:

修改y不会影响x的值:

类似地,字典、集合可以使用dict()、set()函数实现复制。