Python教学课程分享3-列表与元组详解
第3章 列表与元组
3.1 序列操作
3.1.1 创建序列
Python序列一般指列表、元组和字符串,这里以列表为例,当编程人员想创建一个列表序列时,可以遵照以下指令规则(names为随意设定的变量,用来存储列表中的内容):
l names = ['awei', 'xiaohao', 'ludashi','happy','perfect']
在这个序列中,第一个元素为'awei',索引为0,之后的元素以此类推。
在序列中,所有的序列类型都是可以进行某些特定操作的,这些特定操作包括元素访问,切片以及其他常用操作。
序列之间是可以相加的,使用加号可以进行序列的连接操作。在sequence_test.py中输入如下代码:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
names = ['awei', 'xiaohao', 'ludashi','happy','perfect']
names2 = ['dabo','xiaofeixiang']
print(names+names2)
print(type(names+names2))
用一个整数x乘以一个序列会生成新的序列。在新的序列中,原来序列将被重复x次,这就是序列中的乘法,在sequence_test.py中输入如下代码:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
a = 'hello' * 5
print(a)
b = [7] * 6
print(b)
为了检查一个值是否在序列中,Python提供了in运算符。示例代码如下:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
a = 'hello,world'
print('w' in a)
print('x' in a)
Python提供了计算序列长度、最大值和最小值的内建函数,分别为len、max和min。示例代码如下:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
numbers = [99,126,163,188,256]
print(len(numbers))
print(max(numbers))
print(min(numbers))
3.1.2 元素访问
元素访问的方式包括两种,分别是索引和切片,对单个的元素进行访问的方式称为索引,对一定范围内的元素进行访问的方式称为切片。本节首先来看一下索引操作。
序列中的所有元素都是有编号的,并且它们的编号都是由零开始,依次递增。这就方便了使用者。当使用者想要找到某个元素时,可以直接通过编号对元素进行访问。并且在Python中,序列支持负数索引,当使用者使用负数索引时,Python会从序列的最右端,也就是最后一个元素(位置编号为-1)开始反向计数。
下面通过一段代码来直观理解下通过编号取元素的例子,在交互式界面中写入如下代码:
>>>names = ['awei', 'xiaohao', 'ludashi','happy','perfect']
>>>print(names[0])
awei
>>>print(names[2])
ludashi
>>>print(names[-1])
perfect
>>>print(names[-2])
happy
同时更为方便的是,当需要对一串字符串进行索引操作时,编程人员可以直接使用索引,而不需要使用变量来引用他们。这种操作还可以延伸到需要对函数返回结果进行索引的情况中,当用一个函数调用返回一个序列时,也可以直接对返回结果进行索引操作来取得想要的元素。示例代码如下:
>>> 'Hello'[1]
'e'
>>> fourth = input('Year: ')[3]
Year: 2005
>>> fourth
'5'
3.1.3 切片
如果想要截取一定范围内的元素,索引操作就显得比较麻烦了。不必担心,Python提供了一种更为快捷的方法:切片。切片操作,又称为分片操作,可以对一定范围内的元素进行访问。切片通过冒号相隔的两个索引实现,根据冒号的个数来区分,它有两种不同的访问方式。一种是根据位置编号,只能按照顺序进行访问(单冒号);另一种是通过控制步长来达到跳步访问的目的(双冒号)的方式,它们的指令格式如下:
l 序列名name[起始元素编号 :结束元素编号 ]
l 序列名name[起始元素编号 :结束元素编号 :步长]
在刚才的交互式界面中输入以下代码中:
>>> names = ['awei', ‘xiaohao’, 'ludashi','happy','perfect']
>>> print(names[1:4])
>>> print(names[1:-1])
>>> print(names[0:3])
>>> print(names[:3])
>>> print(names[3:])
>>> print(names[3:-1])
>>> print(names[0::2])
>>> print(names[::2])
执行效果如下:
['xiaohao', 'ludashi','happy']
['xiaohao', 'ludashi','happy']
['awei', 'xiaohao', 'ludashi']
['awei', 'xiaohao', 'ludashi']
['happy','perfect']
['happy']
['awei', 'ludashi','perfect']
['awei', 'ludashi','perfect']
除了可以访问元素,切片还可以被用来快速实现很多操作,例如,可以借助切片来进行原地修改列表内容,实现列表元素的增、删、改等操作。并且由于切片返回的是列表元素的浅复制,与列表对象的直接赋值并不相同,所以在进行这些操作时,并不影响列表对象的内存地址。示例代码如下:
>>> aList = [3,5,7]
>>> aList[len(aList)]:]
[]
>>> aList[len(aList):] = [9] #在列表末尾添加元素9
>>> aList
[3,5,7,9]
>>> aList[:3] = [1,2,3] #替换列表元素
>>> aList
[1,2,3,9]
>>> aList[:3] = [] #删除列表元素
[9]
3.2 列表常用方法
3.2.1 append()、extend()、insert()
append()方法,功能是在列表的末尾添加新对象。
新建一个Python程序文件,在程序文件中输入如下代码:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
names = ['awei', 'xiaohao', 'ludashi', 'happy', 'perfect']
print(names)
names.append('我是新来的')
print(names)
extend()方法用于在列表末尾一次性追加另一个序列中的多个值(用新列表扩张原来的列表)。输入如下代码:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
names = ['awei', 'xiaohao', 'ludashi', 'happy', 'perfect']
names2 = ['neimeng','dongyang']
names.extend(names2)
print(names)
print(names2)
insert()方法用于将对象插入列表。输入如下代码:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
names = ['awei', 'xiaohao', 'ludashi', 'happy', 'perfect']
names2 = ('xiaoxin')
print(names)
names.insert(2,names2)
print(names)
成功将xiaoxin插入到2之后3之前。
3.2.2 count()
count()方法用于统计某个元素在列表中出现的次数。输入如下代码:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
names = ['awei', 'xiaohao', 'ludashi', 'happy', 'perfect','ludashi',1,2,3]
print(names.count('ludashi'))
print(names.count('Mars'))
3.2.3 index()
index()方法用于从列表中找出某个值第一个匹配项的索引位置。输入如下代码:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
names = ['awei', 'xiaohao', 'ludashi', 'happy', 'perfect']
print(names.index('xiaohao'))
print(names.index('perfect'))
3.2.4 pop()、remove()
pop()方法用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。输入如下代码来删除names列表中得第一个元素和最后一个元素:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
names = ['awei', 'xiaohao', 'ludashi', 'happy', 'perfect']
print(names)
names.pop()
print(names)
names.pop(0)
print(names)
remove()方法用于移除列表中某个值的第一个匹配项。输入如下代码:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
names = ['awei', 'xiaohao', 'ludashi', 'happy', 'perfect']
print(names)
names.remove('happy')
print(names)
删除指定元素成功。
3.2.5 sort()、reverse()
sort()方法用于对原列表进行排序,如果指定参数,就使用参数指定的比较方法进行排序。输入如下代码:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
names = ['awei', 'xiaohao', 'ludashi', 'happy', '#perfect','Hello','1']
print(names)
names.sort()
print(names)
reverse()方法用于翻转列表中的元素。输入如下代码:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
names = ['awei', 'xiaohao', 'ludashi', 'happy', 'perfect']
print(names)
names.reverse()
print(names)
至此,列表中所有元素都反过来了。
3.3 元组
3.3.1 列表和元组的区别
Python的元组与列表类似,不同之处在于元组的元素不能修改。
tuple函数的功能和list函数基本上一样,以一个序列作为参数,并把它转换为元组。新建一个Python文件,并输入如下代码:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-.
print(tuple('hello'))
print(tuple(('hello','world')))
列表和元组的区别在于元组的元素不能修改,元组一旦初始化就不能修改。不可变的元组有什么意义?因为元组不可变,所以代码更安全。如果可能,能用元组代替列表就尽量用元组来进行编译。输入如下代码:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-.
test1 = ('a','b',['A','B'])
print(test1)
test1[2][0] = 'X'
test1[2][1] = 'Y'
print(test1)
在这里,一个元组中包含另一个列表,类似于二维数组。取二维数组中元素的方式为:先取得二维数组里嵌套的数组,如上例中的test1[2],取得的是['A','B'],test1[2]是一个一维数组,从一维数组中获取元素是以a[0]的方式获取的,因而从test1[2]中取得编号为0的元素的方式是test1[2][0]。
上面的元组定义时有3个元素,分别是'a'、'b'和一个list列表。那么问题来了,不是说元组一旦定义就不可变了吗?表面上看,元组确实变了,其实变的不是元组的元素,而是list列表的元素。元组一开始指向的list列表并没有改成别的list列表,所以元组的“不变”是指每个元素的指向永远不变,但指向的list列表本身是可变的。
3.3.2 元组的优点
和列表相比,元组不可变,这也就使得遍历元组所花费的时间要比列表少。并且由于字典的key必须是不可变的,所以只能用元组来做字典的key,列表就不行。
当编程人员在规定一些不需要或者不可以被更改的数据时,也会优先选择元组来进行操作,这样就可以确保规定的数据被写成“写保护”了。
还有就是使用者在编程时,一般会将元组用于不同的数据类型,而将列表用于相同或者相似的数据类型。并且元组可以被用在字符串格式化中。
以上这些都是元组的比较明显的优势,也正是这些优势保证了元组在Python语言体系中占有一席之地。
3.4 列表解析式与生成器表达式
(1)列表解析式
列表解析式在逻辑上相当于一个循环,但是相比于循环,它的书写形式更加简洁。列表解析式可以使用非常简洁的方式来快速生成满足特定需求的列表,它的代码具有非常强的可读性。同时Python编译器在编译时,编译器会优化,不会因为简写而影响效率,反而因优化提高了效率。并且Python的内部实现了对列表解析式的大量优化,这保证了列表解析式可以很快的被运行。
列表解析式有两种语法形式,分别为:
l [表达式 for 变量 in 序列或迭代对象]
l [表达式 for 变量 in序列或迭代对象 if 条件表达式]
在第一种语法找中,首先迭代序列或者迭代对象中的所有内容,每进行一次迭代,都把序列或者迭代对象中的相应的内容放入到变量中,然后再在表达式中应用该变量的内容,最后再用表达式的计算值生成一个列表进行输出。
而在第二种语法中,加入了判断语句,当需要进行迭代时,需要先进行条件满足判断,只有满足条件的内容才会把序列或者迭代对象中的相应内容放入到变量中,再在该表达式中应用该变量的内容,最后用表达式的计算值生成一个列表。
示例代码如下:
>>> aList = [x * x for x in range(10)]
这段指令用普通的for循环体系来写的话,代码表达如下:
>>> aList = []
>>> for x in range(10):
aList.append(x * x)
列表解析式的功能十分的强大,除上述功能外它还具有实现嵌套列表的平铺,过滤不符合元素等功能。其他常用功能如下:
① 使用列表解析式实现嵌套列表的平铺
以一段代码为例:
>>> vector = [[1,2,3],[4,5,6],[7,8,9]]
>>> [num for elem in vector for num in elem]
[1,2,3,4,5,6,7,8,9]
在这个列表解析式中有2个循环,其中第一个循环可以看做是外循环,执行的比较慢;而第二个循环可以看做是内循环,执行的比较快。上面的代码的执行过程用普通for循环写出来是下面的格式形式:
>>> vector = [[1,2,3],[4,5,6],[7,8,9]]
>>> result = []
>>> for elem in vector:
for num in elem:
result.append(num)
>>> result
[1,2,3,4,5,6,7,8,9]
② 过滤不符合条件的元素
在列表解析式中可以使用if子句来进行筛选,只在结果列表中保留符合条件的元素。例如,下面的代码可以列出当前文件夹下所有Python文件。代码示例如下:
>>> import os
>>> [filename for filename in os.listdir('.') if filename.endswith('.py')]
可以利用列表解析式从列表中选择符合条件的元素组成新的列表,示例代码如下:
>>> aList = [-1,-4,6,7.5,-2.3,9,-11]
>>> [i for i in aList if i > 0] #找出所有大于零的数字并组成一个新列表
[6,7.5,9]
③ 在列表解析式中使用多个循环,实现多序列元素的任意组合,并且可以结合条件语句过滤特定元素
示例代码如下:
>>> [(x,y)for x in [1,2,3] for y in [3,1,4] if x != y]
[(1,3),(1,4),(2,3),(2,1),(2,4),(3,1),(3,4)]
对于包含了多个循环的列表解析式,一定要清楚多个循环的执行顺序或“嵌套关系”,上面的这段代码就符合这种情况。例如,上面的代码等价于:
>>> result = []
>>> for x in [1,2,3]:
for y in [3,1,4]:
if x != y:
result.append((x,y))
>>> result
[(1,3),(1,4),(2,3),(2,1),(2,4),(3,1),(3,4)]
④ 使用列表解析式实现矩阵转置
下面的代码使用列表解析式实现矩阵转置:
>>> matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
>>> [[row[i] for row in matrix]for i in range(4)]
[[1,5,9],[2,6,10],[3,7,11],[4,8,12]]
⑤ 列表解析式中可以使用函数或者复杂表达式
用一段示例代码来解释一下:
>>> def f(v)
if v%2 == 0:
v = v**2
else:
v = v+1
return v
>>> print([f(v) for v in [2,3,4,-1]if v > 0])
[4,4,16]
>>> print([v**2 if v%2 == 0 else v+1 for v in [2,3,4,-1] if v>0])
[4,4,16]
⑥ 列表解析式支持文件对象迭代
>>> fp = open('C:\install.log','r')
>>> print([line for line in fp])
>>> fp.close()
(2)生成器表达式
当序列过长并且每次只需要获取一个元素时,应当考虑使用生成器表达式而不是列表解析。生成器表达式的语法和列表解析一样,只不过生成器表达式是被()括起来的,而不是[]。生成器表达式并不真正创建列表,而是返回一个生成器,这个生成器在每次计算出一个条目后,把这个条目“产生”(yield)出来。使用生成器对象的元素时,可以根据需要将其转换为列表或者元组,也可以使用生成器对象的_ _next_ _()方法或者内置函数next()进行遍历,或者直接将其作为迭代器对象来进行使用。但是不管哪种方法访问其元素,当所有元素访问结束以后,如果需要重新访问其中的元素,必须重新创建该生成器对象。生成器表达式使用了“惰性计算”(lazy evaluation,也有翻译为“延迟求值”),只有在检索时才被计算(evaluated),所以在列表比较长的情况下使用内存上更有效。
生成器表达式的语法如下:
l (expression for iter_val in iterable)
l (expression for iter_val in iterable if cond_expr)
示例代码如下:
#创建生成器对象
>>> g = ((i+2)**2 for i in range(10))
>>> g
<generator object <genexpr> at 0x0000000003095200>
#将生成器对象转换为元组
>>> tuple(g)
(4, 9, 16, 25, 36, 49, 64, 81, 100, 121)
#生成器对象已遍历结束,没有元素了
>>> list(g)
[]
#重新创建生成器对象
>>> g = ((i+2)**2 for i in range(10))
#使用生成器对象的__next__()方法获取元素
>>> g.__next__()
4
#获取下一个元素
>>> g.__next__()
9
#使用函数next()获取生成器对象中的元素
>>> next(g)
16
>>> g = ((i+2)**2 for i in range(10))
#使用循环直接遍历生成器对象中的元素
>>> for item in g:
print(item, end=' ')
4 9 16 25 36 49 64 81 100 121
#filter对象也具有类似的特点
>>> x = filter(None, range(20))
>>> 1 in x
True
>>> 5 in x
True
#不可再次访问已访问过的元素
>>> 2 in x
False
#map对象也具有类似的特点
>>> x = map(str, range(20))
>>> '0' in x
True
#不可再次访问已访问过的元素
>>> '0' in x
False
与列表推导式不同,当生成器推导式中包含多个for语句时,在创建生成器对象时只对第一个for语句进行检查和计算,在调用内置函数next()或生成器对象的__next__()方法获取值的时候才会检查和计算其他for语句。
>>> [x*y for x in range(3) for z in range(5)]
NameError: name 'y' is not defined
>>> g = (x*y for x in range(3) for z in range(5))
#第二个for语句有问题,抛出异常
>>> next(g)
NameError: name 'y' is not defined
最后,如果生成器推导式作为单参数函数时,可以省略两侧的圆括号。例如:
>>> sum(x for x in range(3))
3
(3)列表解析式与生成器表达式的区别
总结:
列表解析式语法:
① [返回值 for 元素 in 可迭代对象 if 条件判断]
② 使用中括号[],内部是for循环,if条件语句可选
③ 立即返回一个新的列表
④ if条件判断可以使用逻辑元算符:and、or、not
定义和初始化:
① 列表解析式是一种语法糖
② 编译器会优化,不会因为简写而影响效率,反而因优化提高了效率
③ 减少程序员工作量,减少出错
④ 简化了代码,但可读性增强
生成器表达式语法:
① (返回值 for 元素 in 可迭代对象 if 条件)
② 生成器表达式使用小括号
③ 生成器表达式返回的是一个生成器(generator)对象
④ 生成器对象是可迭代对象
从计算方式来看,生成器表达式延迟计算,惰性求值;列表解析式则立即计算并返回。
从内存占用来看,从返回值本身来说,生成器表达式省内存,返回的是一个生成器对象;而列表解析式直接计算完并返回一个新的列表。
从计算速度来看,单从计算时间,生成器表达式耗时短,列表解析式耗时长;但是生成器本身并没有返回任何值也就是计算的结果,只返回一个生成器对象,而列表解析式返回了一个计算后新的列表。
对比结束后,只能说两者打成平手,各有优缺点,至于应该在何时何地使用何种方式,还是要看具体情况和应用场景。