一串鱼蛋——列表

小叮当学Python2020-07-29 11:02:01

到目前为止,我们已经学了三四种数据类型:整数、浮点数、布尔、None。可是单独使用它们似乎没有什么意义。如何才能将它们有效地组织起来呢?Python 给我们提供了几种组织数据的方法,上一次的分享列举了列表、元组、字典等等,这一次分享我们来详细介绍一下列表。

为了让列表能装下更多东西,我们先再介绍一种数据类型。(小叮当你耍我呢!)


初识字符串

对,这种数据类型就是 字符串(string)。字符串这个数据类型是一个很大的坑,在 Python 2.x 和 3.x 的实现完全不同。不过,我们先把这种区别留到以后讨论。在本次分享中提到的字符串都以 Python 3.x 为基础。

字符串的构成是怎样的呢?简单说来,就是引号包裹字符内容。可是引号也有很多种啊,有单引号、双引号还有全角引号,在 Python 中还有三个引号的。为了讨论方便,我们先把这些区别放在一边。现在只需要记住:在一个字符串中,允许只使用半角单引号或只使用半角双引号。

>>> a = "apple"
>>> a
'apple'
>>> b = 'boy'
>>> b
'boy'
>>> c = '真实'
>>> c
'真实'
>>> d = '哎呀呀引号出错了"
SyntaxError: EOL while scanning string literal
>>> e = “引号又错了”
SyntaxError: invalid character in identifier

可以看到,只使用半角单引号或只使用半角双引号来表示字符串都是可以的。但是,一旦出现了全角引号,或者单双引号混用,就会出错啦!字符串这个词的字面意思,就是把“字符”串起来。咦,这个名词莫名触动了小叮当饥饿的神经:

是的,你可以将“字符串”理解成一串鱼蛋,每一颗鱼蛋,就是一个 字符(character)。关于字符串这个数据类型,我们以后还会深入讨论的。这次分享就说到这里。

是时候让我们今天的主角—— 列表(list) 出场了。


列表


初识列表

看完上次的分享,大家应该对列表的结构有一些印象,就是用半角中括号 [] 括起,半角逗号 , 分隔的数据。其实在代码里面,一切标点符号都是半角的,原因很简单,因为计算机是外国人发明的,外国人不使用全角符号……所以,如果你的代码出现了 SyntaxError: invalid character in identifier 错误,有可能是你粗心将符号打成了全角的哦。下面展示几个列表的例子。

>>> list((1, 2, 3))
[1, 2, 3]
>>> list() []
>>> [1, 2, 3] [1, 2, 3]

还记得工厂函数这个概念吗?第一个箭嘴行中的 (1, 2, 3) 原来是一个元组,硬是通过工厂函数 list() 转化成了一个列表。空列表的表示方式,可以是 list() 也可以是 []。由于底层代码实现不同,使用 []list() 快。

>>> [1, 2, 3]
[1, 2, 3]
>>> [9.2, 2.1, 1.4] [9.2, 2.1, 1.4]
>>> [True, False, 1] [True, False, 1]
>>> [[90, 80,  89], [25, 188, 260]] [[90, 80,  89], [25, 188, 260]]
>>> ['1', 2, '3'] ['1', 2, '3']

细心的同学应该能发现,列表几乎什么都能装。第一个箭嘴行的列表都装整数,第二个箭嘴行的列表都装浮点数。第三个箭嘴行的列表装了布尔型和整数。那第四个箭嘴行的列表呢?竟然装了两个列表!这种 666 的操作,叫做 二维列表 (2D list)。二维列表有啥用?稍微格式化一下,就能看出来。

[
  [90, 80,  89],
  [25, 188, 260]
]

假设两位选手参加一场三局两胜制的吃鱼蛋比赛,第一位选手在三场比赛中分别吃了 90、80、和 89 颗,第二位分别吃了 25、 188、260 颗,那我们就可以用这样的二维列表把这几个数据储存起来。是不是觉得有点像表格呢,是不是顿时觉得可以抛弃 Excel 了呢?说句题外话,微软最近在考虑把 Python 嵌入 Excel 中,作为 Excel 的官方编程语言哦,Python 是不是一座很大的碉堡呢!

以此类推,还有三维列表、四维列表……不过这些结构比较少用,知道一下就好啦。

那第五个箭嘴行中的列表呢?看出它装了两种不同的类型了吗?'1' 是字符串, 2 是整数,'3' 是字符串(虽然只有一颗鱼蛋,但习惯上还是称“串”)。理论上,列表是可以存放不同类型的数据的,但是鉴于列表是可以被修改的,修改前和修改后其元素会发生变化,假如获得数据的方式不变的话,得出的结果就会有所不同了哦!请看以下例子:

>>> egg_info = ["五湖鱼蛋", 9, "Salty"]
>>> egg_info[1]
9
>>> egg_info.insert(1, "curry")
>>> egg_info[1]
'curry'
>>> egg_info ['五湖鱼蛋', 'curry', 9, 'Salty']

假设用一个列表表示一串鱼蛋的信息,它的名字叫 "五湖鱼蛋",有 9 颗,味道是 "Salty"。用 [1] 这个索引可以获得数量这个信息。忽然之间,很想把鱼蛋和咖喱一起吃最好吃的这个事实放进列表里,于是在第 [1] 个(注意用的是 “[1]” 而不是“一”)元素之前增加了 "curry" 这个字符串,可是再去访问 egg_info[1]的时候,却发现得出的是 "curry" 是鱼蛋的最佳搭配,而不是数量。因此,一般不在同一列表中放入不同类型的数据。


列表的长度 len()、索引 [N] 与切片 [a:b:c]

列表是有长度的,用内置函数 len()可以求出。列表的元素是有顺序的,用 [N] 可以获得序号为 N +1 的元素。

>>> len([1, 2, 3])
3
>>> [1, 2, 3][0]
1
>>> a = [1, 2, 3]
>>> a[1]
2

高级的程序员/媛有时会被称为“怪客”(geek),“怪”得连数数的方式都不一样。人们一般从 1 开始数,但是大部分程序员/媛是从 0 开始数的。

看上面的例子,有个列表叫 [1, 2, 3][1, 2, 3][0] 能获取到第一个元素 1,这个 [0] 我们叫 “索引”(index)

从 0  开始数列表索引的这个习惯是从古老的 C 语言流传下来的,其原因说起来很简单,因为计算机的内存可以理解为一段由多个二进制盒子组成的、连续的容器,假如让这段容器装 1、0 这两个二进制数,且容器的开始地址是 a,那么 a + 0 这个地址装的就是第一个元素 1 , a + 1 这个地址装的就是第二个元素 0 。所谓的开始地址,其实就是第一个元素所在的位置,a + 0 可以看作从 a 开始,偏移 0 个格子得到的数据。a + 1 可以看作从 a 开始,偏移 1 个格子得到的数据。这个格子的大小与数据的二进制大小相关。为了将寻址和索引联系起来,人们就用 [0] 来代表一个序列的第一个元素啦。虽然 Python 与 C 是两种语言,但是如果要寻找列表 a 的第 N 个元素,还是要记得用 a[N - 1] 来表示哦。

除了正数索引之外,还可以使用负数索引。

>>> a = [1, 2, 3]
>>> a[-1]
3
>>> a[-3]
1

上面的代码比较好理解:索引 [-1] 表示倒数第一个元素。有些同学可能会问,为什么不用 [-0] 啊?因为 -0 == 0 啊!

索引超出列表数量时会发生什么情况?

>>> [3][2]
Traceback (most recent call last):  File "<pyshell#45>", line 1, in <module>    [3][2] IndexError: list index out of range

>>> [2][-2]
Traceback (most recent call last):  File "<pyshell#46>", line 1, in <module>    [2][-2] IndexError: list index out of range

以上两个例子不要看花眼了!第一个中括号是一个只有一个元素的列表,第二个中括号是它的索引!在第一个例子中,列表 [3]只有一个元素,最大的正索引值是 [0],而 [3][2] 试图获取第三个元素,于是就报错了!第二个例子也是一样:列表 [2]只有一个元素,绝对值最小的负索引值只能是-1,输入[2][-2] 就会抛出错误 IndexError

更复杂的来了!真正的 Python “索引”,是三个参数,叫做 切片(slice)。格式如下。

[开始:结束:步长]

其英文分别是 startstopstep。两个冒号前面的数值是 开始,后面是 结束,两个冒号之间的数值是 步长

假设我们用一个包含字符串的列表,来表示一串 10 颗的鱼蛋。

>>> fishball = ['鱼蛋0号', '鱼蛋1号', '鱼蛋2号', '鱼蛋3号','鱼蛋4号', '鱼蛋5号', '鱼蛋6号', '鱼蛋7号', '鱼蛋8号', '鱼蛋9号']

>>> fishball[0:2] ['鱼蛋0号', '鱼蛋1号']
>>> fishball[5:8] ['鱼蛋5号', '鱼蛋6号', '鱼蛋7号']
>>> fishball[0:10:2] ['鱼蛋0号', '鱼蛋2号', '鱼蛋4号', '鱼蛋6号', '鱼蛋8号']
>>> fishball[10:0:-2] ['鱼蛋9号', '鱼蛋7号', '鱼蛋5号', '鱼蛋3号', '鱼蛋1号']

先看看这串鱼蛋~从 "鱼蛋0号""鱼蛋9号",这串鱼蛋一共有 10 颗。fishball[0:2] 表示开始是 0,结束是2,从 "鱼蛋0号" 一直到 "鱼蛋1号",不包括 fishball[2]

同理,fishball[5:8]"鱼蛋5号"fishball[5]) 到 "鱼蛋7号"fishball[7]),不包括 "鱼蛋8号"。这种特性有点像数学中的左闭右开区间,[5, 8)。

第三个参数步长是什么意思呢?请看 fishball[0:10:2] 的输出,首先切片取值的范围是 "鱼蛋0号""鱼蛋10号" 但是 "鱼蛋10号" 不存在,所以退到 "鱼蛋9号",取值范围是 "鱼蛋0号""鱼蛋9号"。从"鱼蛋0号" 开始,每次往前跳两颗鱼蛋,得出来就是 0、2、4、6、8。要倒着数( [::-3] )的时候,就必须以 "鱼蛋9号" 作为开始,以 "鱼蛋0号" 作为结束,不断往后跳三颗鱼蛋。

>>> fishball[5:]
['鱼蛋5号', '鱼蛋6号', '鱼蛋7号', '鱼蛋8号', '鱼蛋9号']
>>> fishball[:9] ['鱼蛋0号', '鱼蛋1号', '鱼蛋2号', '鱼蛋3号', '鱼蛋4号', '鱼蛋5号', '鱼蛋6号', '鱼蛋7号', '鱼蛋8号']
>>> fishball[::3] ['鱼蛋0号', '鱼蛋3号', '鱼蛋6号', '鱼蛋9号']
>>> fishball[::-3] ['鱼蛋9号', '鱼蛋6号', '鱼蛋3号', '鱼蛋0号']

fishball[5:] 把结束和步长省略了,而结束的默认值是列表的最后一个元素 + 1,步长默认值是 1,所以等于从 "鱼蛋5号" 开始一直数到列表最后。

fishball[:9] 把开始和步长省略了,开始的默认值就是列表的开始,所以等于从 "鱼蛋0号" 一直数到 "鱼蛋8号"

fishball[::3] 把开始和结束省略了,取值范围是整个列表,然后从 "鱼蛋0号" 开始,一直往前跳三颗鱼蛋。

fishball[::-3] 把开始和结束省略了,取值范围是整个列表,然后从 "鱼蛋9号" 开始,一直往后跳三颗鱼蛋。

也就是说,无论开始和结束是正值还是负值,都可以先转成相对的正值计算,而步长决定了是正着数还是倒着数。


itemgetter 与 slice

Python 中的 operator.itemgetter  和 slice 分别对应着索引和列表:

>>> from operator import itemgetter
>>> food = ["tart", "fishball", "apple"]
>>> name = ["Jerry", "Tom", "Lucy"]
>>> get = itemgetter(1)
>>> get(food)
'fishball'
>>> get(name)
'Tom'

假设有一堆像  food  和 name 这样的三元素列表,小叮当若想逐个获得它们的第二个元素([1]),就可以先把 get 赋值为 itemgetter(1),然后分别把 foodname作为参数传进去,前提是导入了 itemgetter。(听起来比较少用……)看到了这种将函数赋值给变量的做法了吗,我们以后会深入研究它。

同样, slice :

>>> fishball = ['鱼蛋0号', '鱼蛋1号', '鱼蛋2号', '鱼蛋3号','鱼蛋4号', '鱼蛋5号', '鱼蛋6号', '鱼蛋7号', '鱼蛋8号', '鱼蛋9号']
>>> fishball[0:2] ['鱼蛋0号', '鱼蛋1号']
>>> fslice = slice(2)
>>> fishball[fslice] ['鱼蛋0号', '鱼蛋1号']
>>> fishball[0:10:2]
['鱼蛋0号', '鱼蛋2号', '鱼蛋4号', '鱼蛋6号', '鱼蛋8号']
>>> fslice2 = slice(0, 10, 2)
>>> fishball[fslice2] ['鱼蛋0号', '鱼蛋2号', '鱼蛋4号', '鱼蛋6号', '鱼蛋8号']

这里需要记住一件事:当 slice() 工厂函数只有一个参数时,这个参数代表结束;如果是两个参数,就分别代表开始与结束;如果是三个参数就分别代表开始、结束与步长。不过sliceitemgetter一样,都属于较少使用的函数或对象……


python [文件] 命令运行代码

在这里再分享一种运行 Python 代码的方式。假设把代码写成文件 test.py ,放在了 D 盘的 PythonWork 目录下。


我们可以打开命令行(Macbook 是终端):

开始 - 搜索框中输入 cmd - 回车

会弹出一个黑框框,它叫命令行。以下是黑框框中的代码

C:\Users\ron>cd D:/PythonWork

C:\Users\ron>d
:

D:\PythonWork>python test.py
牛肉丸 虾滑 鱼蛋

> 之后的红色代码才是你要输入的内容,每一行写好后敲回车。第一步的 cd 命令是改变当前目录,相当于手动点开了 D 盘的 PythonWork 文件夹;第二步非 Windows 用户不需要做。第三步,就是输入 python [文件名],运行这个 py 文件。当然,如果你知道文件的详细路径,也可以在任何地方使用以下命令运行代码:python [文件的详细路径]

C:\Users\ron>python  D:/PythonWork/test.py
牛肉丸 虾滑 鱼蛋

in 关键字运算符、 for 循环与 pass

有些时候,我们需要将列表的所有元素都列出来。假如列表的长度是 100,总不能把列表的所有索引都写出来吧……这会让人吐血的。面对这种重复的工作时应该怎么办呢?最好的解决方法就是循环。Python 的循环有两种,一种是 for 循环,一种是 while 循环,这次我们先来看看 for 循环。

因为 for 循环需要缩进,使用了游标卡尺大法,所以小叮当决定新创建一种代码表示方法。>>>>>>>>>> 符号之前的是写进 py 文件的代码,>>>>>>>>>> 符号之后的是运行之后的结果。如果忘记了如何运行代码的话,可以使用刚刚提到的方法,也可以翻到前面的分享(第1-3期)去看哦,关注公众号并点击往期内容吧。

回到正题,请看以下的 in 关键字和 for 循环:

hotpot = ["牛肉丸", "虾滑", "鱼蛋"]

print("牛肉丸" in hotpot)
print("\n")
print("牛筋丸" in hotpot)
print("\n")

for
i in hotpot:    
   print
(i) >>>>>>>>>>
True

False

牛肉丸 虾滑 鱼蛋

本来,in 关键字的作用是判断元素是否在序列(列表是序列的一种)当中。因为字符串 "牛肉丸" 在列表 hotpot 中,所以返回的是 True。如果要看看 "牛肉丸" 是否不在 hotpot 中,可以用 not in

之后的 "\n" 是一个换行符,其中 \ 是转义字符,把跟在后面的一个字符转变成其他意思,也就是说 \n 必须放在一起理解,意思是按下了回车,换了一行。其实,不同系统的换行符内置实现有所不同。Windows 的换行是 "\r\n" Unix 系统是"\n",Mac系统是 "\r",不过 Python 是跨平台的,会根据平台来转换换行符,无须担心,只需要记住 "\n"

使用 for 关键字之后,相当于把每一个 in hotpot 的对象都拿出来赋值给 i,然后执行缩进之下的代码 print(i),其中print(i) 自动换行。每循环一次,缩进之下的代码就会逐步运行。使用 Pycharm 可以追踪 i 的变化:


如果忘了怎么使用 Pycharm,可以关注公众号并查看往期内容哦~

有些同学可能会问:如果不缩进 print(i) 这句,会怎么样呢?

for i in hotpot:    
print(i)

首先,这样写会报错。因为 for 的语法是需要缩进的。那如果真的很想知道这么写会发生什么呢?要想在缩进里面什么都不干,使用关键词 pass 就好。

for i in hotpot:
   pass
print(i) >>>>>>>>>> 鱼蛋

你会发现,系统并没有打印三次,而是只打印了一次。而且打印的是最后一个元素 "鱼蛋"。没有缩进 print(i) 意味着 print(i)for i in hotpot: 是同步的,会按照顺序运行。以下是代码的执行过程,记住  pass 是什么都不干的意思。

  1. i == “牛肉丸” pass

  2. i == “虾滑” pass

  3. i == “鱼蛋” pass 

  4. print(i) => 打印出 鱼蛋


现在明白了为什么要使用游标卡尺大法了吗?就是告诉系统,哪些代码是被哪些代码管着的,比如上面的 for i in hotpot: 语句就管住了  pass ,管不住 print(i) 。


range

有些时候,我们会需要 [1,2,3,4,...10] 这样的数字列表,有没有更简单的表示方式呢?有。range 是最佳选择。和 slice 一样,range 通过工厂函数 range() 创建,也有三个参数:开始、结束和步长,但是,range 转化后可以直接作为列表使用,而 slice 只能用于列表的索引。

>>> range(9)
range(0, 9)
>>> list(range(9)) [0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> range(1, 9)
range(1, 9)
>>> list(range(1, 9)) [1, 2, 3, 4, 5, 6, 7, 8]
>>> range(1, 9, 3)
range(1, 9, 3)
>>> list(range(1, 9, 3)) [1, 4, 7]

只输入一个参数 9 ,代表从 0 为开始 9 - 1 为结束的“列表”。

输入两个参数 19,代表 1 为开始 9 - 1 为结束的“列表”。

输入三个参数  193,代表 1 为开始 9 - 1 为结束,每次往前移动 3 个元素的“列表”。

range 常与 for 循环一起用。

for i in range(10):    
   print(i, end=" ") >>>>>>>>>>  
0 1 2 3 4 5 6 7 8 9

注意这里的 print 是 Python 3.x 的 print() 函数,可以指定参数 end,也就是每次执行一句打印之后在行末应该添加什么字符,end 参数默认是 "\n",所以这个例子之前的 print() 都会自动换行。end=" " 这个语法 Python 2.x 无法使用。

在 Python 2.x 中,range 是直接返回列表的。而 xrange 才是 Python 3.x 的 rangexrange 的行为是把产出的元素一个一个地扔出去,而 Python 2.x range 的行为是把产出的元素收集成一坨再一起扔出去。理论上, xrange 会比较节省内存,你想想,如果碗里存了一百万颗鱼蛋,一颗一颗地扔和存够了一百万颗一次扔出去,哪个比较难办到?当然是存够了一百万颗比较难扔啊(话说为什么要扔鱼蛋……)。但是 Python 3.x 的 range 已经完全废弃了原来一次扔一百万个鱼蛋的做法,转而采用 xrange 的做法抛出结果,所以也就没必要担心啦。

所以,对于如何打印列表中的每个元素,是不是又有另一种写法了?

hotpot = ["牛肉丸", "虾滑", "鱼蛋"]
for i in range(len(hotpot)):    
   print(hotpot[i]) >>>>>>>>>> 牛肉丸 虾滑 鱼蛋

enumerate()

有时我们既想知道元素的索引,又想知道元素的实质内容,那么可以用 enumerate()函数。

hotpot = ["牛肉丸", "虾滑", "鱼蛋"]
for i, o in enumerate(hotpot):    
   print(i, ': ', o) >>>>>>>>>>
0 :  牛肉丸
1 :  虾滑
2 :  鱼蛋

enumerate()函数会将每个索引号和每个元素组成一个二元组,然后一个一个抛出来。

Python 3.x 的 print() 可以传入多个参数,连续打印。

在以上到三种方法中,哪一种才是最佳的呢?小叮当觉得,除了第二种比较挫之外,其他都很棒棒哦,第二种的可读性实在有点差。只需要打印元素时,可以使用第一种,而第三种是很 Pythonic (有 Python style)的做法。

在 Python 中,上文提到的一些内置函数,比如 len()slice()enumerate(),可以无差别应用在不同的数据类型上,这也是它们成为内置函数的原因哦。在谈到面向对象的时候,我们会谈到“多态”,就是同一个东西作用在不同物体身上,会有类似但不太一样的反应。不过现在就深入展开实在太难懂了,还是放一放吧!

好啦,这次分享涉及的内容有:


  • 字符串 string

  • 字符 character

  • python [文件] 命令运行代码

  • 列表 list
    ++  多维列表 multi-dimensional list
    ++  列表的长度 len()、索引 [N] 与切片 [a:b:c]

  • itemgetter()

  • slice()

  • in

  • for 循环

  • pass

  • range()

  • enumerate()

  • "\n" 换行符