Python程序设计开发宝典
上QQ阅读APP看书,第一时间看更新

3.1 列表:打了激素的数组

列表(list)是最重要的Python内置对象之一,是包含若干元素的有序连续内存空间。当列表增加或删除元素时,列表对象自动进行内存的扩展或收缩,从而保证相邻元素之间没有缝隙。Python列表的这个内存自动管理功能可以大幅度减少程序员的负担,但插入和删除非尾部元素时涉及列表中大量元素的移动,会严重影响效率。另外,在非尾部位置插入和删除元素时会改变该位置后面的元素在列表中的索引,这对于某些操作可能会导致意外的错误结果。因此,除非确实有必要,否则应尽量从列表尾部进行元素的追加与删除操作。

在形式上,列表的所有元素放在一对方括号[]中,相邻元素之间使用逗号分隔。在Python中,同一个列表中元素的数据类型可以各不相同,可以同时包含整数、实数、字符串等基本类型的元素,也可以包含列表、元组、字典、集合、函数以及其他任意对象。如果只有一对方括号而没有任何元素则表示空列表。下面几个都是合法的列表对象:

        [10,20,30,40]
        ['crunchy frog', 'ram bladder', 'lark vomit']
        ['spam',2.0,5, [10,20]]
        [['file1',200,7], ['file2',260,9]]
        [{3}, {5:6}, (1,2,3)]

Python采用基于值的自动内存管理模式,变量并不直接存储值,而是存储值的引用或内存地址,这也是python中变量可以随时改变类型的重要原因。同理,Python列表中的元素也是值的引用,所以列表中各元素可以是不同类型的数据。

需要注意的是,列表的功能虽然非常强大,但是负担也比较重,开销较大,在实际开发中,最好根据实际的问题选择一种合适的数据类型,要尽量避免过多使用列表。

3.1.1 列表创建与删除

使用“=”直接将一个列表赋值给变量即可创建列表对象。

        >>>a_list=['a', 'b', 'mpilgrim', 'z', 'example']
        >>>a_list=[]                              #创建空列表

也可以使用list()函数把元组、range对象、字符串、字典、集合或其他可迭代对象转换为列表。需要注意的是,把字典转换为列表时默认是将字典的“键”转换为列表,而不是把字典的元素转换为列表,如果想把字典的元素转换为列表,需要使用字典对象的items()方法明确说明,当然也可以使用values()来明确说明要把字典的“值”转换为列表。

        >>>list((3,5,7,9,11))                         #将元组转换为列表
        [3,5,7,9,11]
        >>>list(range(1,10,2))                        #将range对象转换为列表
        [1,3,5,7,9]
        >>>list('hello world')                        #将字符串转换为列表
        ['h', 'e', 'l', 'l', 'o', '', 'w', 'o', 'r', 'l', 'd']
        >>>list({3,7,5})                              #将集合转换为列表
        [3,5,7]
        >>>list({'a':3, 'b':9, 'c':78})               #将字典的“键”转换为列表
        ['a', 'c', 'b']
        >>>list({'a':3, 'b':9, 'c':78}.items())       #将字典的“键:值”对转换为列表
        [('b',9), ('c',78), ('a',3)]
        >>>x=list()                                   #创建空列表

当一个列表不再使用时,可以使用del命令将其删除,这一点适用于所有类型的Python对象。

        >>>x=[1,2,3]
        >>>del x                               #删除列表对象
        >>>x                                   #对象删除后无法再访问,抛出异常
        NameError: name'x'is not defined

严格来说,del命令并不删除变量对应的值,只是删除变量并解除变量和值的绑定。Python内部每个值都维护一个计数器,每当有新的变量引用该值时其引用计数器加1,当该变量被删除或不再引用该值时其引用计数器减1,当某个值的引用计数器变为0时则由垃圾回收器负责清理和删除。如果需要立刻进行垃圾回收,可以导入gc模块后调用其collect()方法。

        >>>import sys
        >>>sys.getrefcount(1)                  #查看值的引用次数
        1243
        >>>x=1
        >>>sys.getrefcount(1)                  #有新变量引用该值,其引用计数器加1
        1244
        >>>y=1
        >>>sys.getrefcount(1)
        1245
        >>>del x                               #删除变量并解除引用,该值的引用计数器减1
        >>>del y
        >>>sys.getrefcount(1)
        1243
        >>>import gc
        >>>gc.collect()                        #立刻进行垃圾回收,返回被清理的对象数量
        0

3.1.2 列表元素访问

创建列表之后,可以使用整数作为下标来访问其中的元素,其中下标为0的元素表示第1个元素,下标为1的元素表示第2个元素,下标为2的元素表示第3个元素,以此类推;列表还支持使用负整数作为下标,其中下标为-1的元素表示最后一个元素,下标为-2的元素表示倒数第2个元素,下标为-3的元素表示倒数第3个元素,以此类推,如图3-2所示(以列表['P', 'y', 't', 'h', 'o', 'n']为例)。

图3-2 双向索引示意图

        >>>x=list('Python')                    #创建类别对象
        >>>x
        ['P', 'y', 't', 'h', 'o', 'n']
        >>>x[0]                                #下标为0的元素,第一个元素
        'P'
        >>>x[-1]                               #下标为-1的元素,最后一个元素
        'n'

3.1.3 列表常用方法

列表、元组、字典、集合、字符串等Python序列有很多操作是通用的,而不同类型的序列又有一些特有的方法或者支持某些特有的运算符和内置函数。列表对象常用的方法如表3-1所示。

表3-1 列表对象常用的方法

1.append()、insert()、extend()

这3个方法都可以用于向列表对象中添加元素,其中append()用于向列表尾部追加一个元素,insert()用于向列表任意指定位置插入一个元素,extend()用于将另一个列表中的所有元素追加至当前列表的尾部。这3个方法都属于原地操作,不影响列表对象在内存中的起始地址。对于长列表而言,使用insert()方法在列表首部或中间位置插入元素时效率较低。如果确实需要在首部按序插入多个元素的话,可以先在尾部追加,然后再使用reverse()方法进行翻转,或者考虑使用标准库collections中的双端队列deque对象提供的appendleft()方法。

        >>>x=[1,2,3]
        >>>id(x)                               #查看对象的内存地址
        50159368
        >>>x.append(4)                         #在尾部追加元素
        >>>x.insert(0,0)                       #在指定位置插入元素
        >>>x.extend([5,6,7])                   #在尾部追加多个元素
        >>>x
        [0,1,2,3,4,5,6,7]
        >>>id(x)                               #列表在内存中的地址不变
        50159368

2.pop()、remove()、clear()

这3个方法用于删除列表中的元素,其中pop()用于删除并返回指定位置(默认是最后一个)上的元素,如果指定的位置不是合法的索引则抛出异常,对空列表调用pop()方法也会抛出异常;remove()用于删除列表中第一个值与指定值相等的元素,如果列表中不存在该元素则抛出异常;clear()用于清空列表中的所有元素。这3个方法也属于原地操作,不影响列表对象的内存地址。另外,还可以使用del命令删除列表中指定位置的元素,同样也属于原地操作。

        >>>x=[1,2,3,4,5,6,7]
        >>>x.pop()                             #弹出并返回尾部元素
        7
        >>>x.pop(0)                            #弹出并返回指定位置的元素
        1
        >>>x.clear()                           #删除所有元素
        >>>x
        []
        >>>x=[1,2,1,1,2]
        >>>x.remove(2)                         #删除首个值为2的元素
        >>>del x[3]                            #删除指定位置上的元素
        >>>x
        [1,1,1]

必须要再次强调的是,由于列表具有内存自动收缩和扩张功能,在列表中间位置插入或删除元素时,不仅效率较低,该位置后面所有元素在列表中的索引也会发生变化,必须牢牢记住这一点。

3.count()、index()

列表方法count()用于返回列表中指定元素出现的次数;index()用于返回指定元素在列表中首次出现的位置,如果该元素不在列表中则抛出异常。

        >>>x=[1,2,2,3,3,3,4,4,4,4]
        >>>x.count(3)                          #元素3在列表x中的出现次数
        3
        >>>x.count(5)                          #不存在,返回0
        0
        >>>x.index(2)                          #元素2在列表x中首次出现的索引
        1
        >>>x.index(5)                          #列表x中没有5,抛出异常
        ValueError: 5 is not in list

通过前面的介绍我们已经知道,列表对象的很多方法在特殊情况下会抛出异常,而一旦出现异常,整个程序就会崩溃,这是我们不希望的。为避免引发异常而导致程序崩溃,一般来说有两种方法:①使用选择结构确保列表中存在指定元素再调用有关的方法;②使用异常处理结构。下面的代码使用异常处理结构保证用户输入的是三位数,然后使用关键字in来测试用户输入的数字是否在列表中,如果存在则输出其索引,否则提示不存在。

        from random import sample

        lst=sample(range(100,1000),100)

        while True:
            x=input(’请输入一个三位数:')
            try:
                assert len(x)==3, ’长度必须为3'
                x=int(x)
                break
            except:
                pass


        if x in lst:
            print(’元素{0}在列表中的索引为:{1}'.format(x, lst.index(x)))
        else:
            print(’列表中不存在该元素.')

4.sort()、reverse()

列表对象的sort()方法用于按照指定的规则对所有元素进行排序,默认规则是所有元素从小到大升序排序;reverse()方法用于将列表所有元素逆序或翻转,也就是第一个元素和倒数第一个元素交换位置,第二个元素和倒数第二个元素交换位置,以此类推。

        >>>x=list(range(11))                   #包含11个整数的列表
        >>>import random
        >>>random.shuffle(x)                   #把列表x中的元素随机乱序
        >>>x
        [6,0,1,7,4,3,2,8,5,10,9]
        >>>x.sort(key=lambda item:len(str(item)), reverse=True)
                                               #按转换成字符串以后的长度降序排列
        >>>x
        [10,6,0,1,7,4,3,2,8,5,9]
        >>>x.sort(key=str)                     #按转换为字符串后的大小升序排序
        >>>x
        [0,1,10,2,3,4,5,6,7,8,9]
        >>>x.sort()                            #按默认规则排序
        >>>x
        [0,1,2,3,4,5,6,7,8,9,10]
        >>>x.reverse()                         #把所有元素翻转或逆序
        >>>x
        [10,9,8,7,6,5,4,3,2,1,0]

列表对象的sort()和reverse()分别对列表进行原地排序(in-place sorting)和逆序,没有返回值。所谓“原地”,意思是用处理后的数据替换原来的数据,列表首地址不变,列表中元素原来的顺序全部丢失。

如果不想丢失原来的顺序,可以使用2.4.4节介绍的内置函数sorted()和reversed()。其中,内置函数sorted()返回排序后的新列表,参数key和reverse的含义与列表方法sort()完全相同;内置函数reversed()返回一个逆序后的reversed对象。充分利用列表对象的sort()方法和内置函数sorted()的key参数,可以实现更加复杂的排序,以内置函数sorted()为例:

        >>>gameresult=[['Bob',95.0, 'A'],
                        ['Alan',86.0, 'C'],
                        ['Mandy',83.5, 'A'],
                        ['Rob',89.3, 'E']]
        >>>from operator import itemgetter 
        >>>sorted(gameresult, key=itemgetter(2))      #按子列表第3个元素进行升序排序
        [['Bob',95.0, 'A'], ['Mandy',83.5, 'A'], ['Alan',86.0, 'C'], ['Rob',89.3, 'E']]
        >>>sorted(gameresult, key=itemgetter(2,0))
                                          #先按第3个元素升序并排列,再按第一个元素升序排序
        [['Bob',95.0, 'A'], ['Mandy',83.5, 'A'], ['Alan',86.0, 'C'], ['Rob',89.3, 'E']]
        >>>sorted(gameresult, key=itemgetter(2,0), reverse=True)
        [['Rob',89.3, 'E'], ['Alan',86.0, 'C'], ['Mandy',83.5, 'A'], ['Bob',95.0, 'A']]
        >>>list1=["what", "I'm", "sorting", "by"]     #以一个列表内容为依据
        >>>list2=["something", "else", "to", "sort"]  #对另一个列表内容进行排序
        >>>pairs=zip(list1, list2)                    #把两个列表中的对应位置元素配对
        >>>[item[1] for item in sorted(pairs, key=lambda x:x[0], reverse=True)]
        ['something', 'to', 'sort', 'else']
        >>>x=[[1,2,3], [2,1,4], [2,2,1]]
        >>>sorted(x, key=lambda item:(item[1], -item[2]))
                                                      #以第2个元素升序
                                                      #第3个元素降序排序
                                                      #这里的负号只适用于数值型元素
        [[2,1,4], [1,2,3], [2,2,1]]
        >>>x=['aaaa', 'bc', 'd', 'b', 'ba']
        >>>sorted(x, key=lambda item: (len(item), item))
                                                      #先按长度排序,长度一样的正常排序
        ['b', 'd', 'ba', 'bc', 'aaaa']

5.copy()

列表对象的copy()方法返回列表的浅复制。所谓浅复制,是指生成一个新的列表,并且把原列表中所有元素的引用都复制到新列表中。如果原列表中只包含整数、实数、复数等基本类型或元组、字符串这样的不可变类型的数据,一般是没有问题的。但是,如果原列表中包含列表之类的可变数据类型,由于浅复制时只是把子列表的引用复制到新列表中,于是修改任何一个都会影响另外一个。

        >>>x=[1,2, [3,4]]                          #原列表中包含子列表
        >>>y=x.copy()                              #浅复制
        >>>y                                       #两个列表中的内容看起来完全一样
        [1,2, [3,4]]
        >>>y[2].append(5)                          #为新列表中的子列表追加元素
        >>>x[0]=6                                  #整数、实数等不可变类型不受此影响
        >>>y.append(6)                             #在新列表尾部追加元素
        >>>y
        [1,2, [3,4,5],6]
        >>>x                                       #原列表不受影响
        [6,2, [3,4,5]]

列表对象的copy()方法和切片操作以及标准库copy中的copy()函数一样都是返回浅复制,如果想避免上面代码演示的问题,可以使用标准库copy中的deepcopy()函数实现深复制。所谓深复制,是指对原列表中的元素进行递归,把所有的值都复制到新列表中,对嵌套的子列表不再是复制引用。这样一来,新列表和原列表是互相独立,修改任何一个都不会影响另外一个。

        >>>import copy
        >>>x=[1,2, [3,4]]
        >>>y=copy.deepcopy(x)                     #深复制
        >>>x[2].append(5)                         #为原列表中的子列表追加元素
        >>>y.append(6)                            #在新列表尾部追加元素
        >>>y
        [1,2, [3,4],6]
        >>>x
        [1,2, [3,4,5]]

不论是浅复制还是深复制,与列表对象的直接赋值都是不一样的情况。下面的代码把同一个列表赋值给两个不同的变量,这两个变量是互相独立的,修改任何一个都不会影响另外一个。

        >>>x=[1,2, [3,4]]
        >>>y=[1,2, [3,4]]                          #把同一个列表对象赋值给两个变量
        >>>x.append(5)
        >>>x[2].append(6)                          #修改其中一个列表的子列表
        >>>x
        [1,2, [3,4,6],5]
        >>>y                                       #不影响另外一个列表
        [1,2, [3,4]]

下面的代码演示的是另外一种情况,把一个列表变量赋值给另外一个变量,这样两个变量指向同一个列表对象,对其中一个做的任何修改都会立刻在另外一个变量得到体现。

        >>>x=[1,2, [3,4]]
        >>>y=x                                    #两个变量指向同一个列表
        >>>x[2].append(5)
        >>>x.append(6)
        >>>x[0]=7
        >>>x
        [7,2, [3,4,5],6]
        >>>y                                      #对x做的任何修改,y都会受到影响
        [7,2, [3,4,5],6]

3.1.4 列表对象支持的运算符

加法运算符(+)也可以实现列表增加元素的目的,但这个运算符不属于原地操作,而是返回新列表,并且涉及大量元素的复制,效率非常低。使用复合赋值运算符+=实现列表追加元素时属于原地操作,与append()方法一样高效。

        >>>x=[1,2,3]
        >>>id(x)
        53868168
        >>>x=x+[4]                                #连接两个列表
        >>>x
        [1,2,3,4]
        >>>id(x)                                  #内存地址发生改变
        53875720
        >>>x+=[5]                                 #为列表追加元素
        >>>x
        [1,2,3,4,5]
        >>>id(x)                                  #内存地址不变
        53875720

乘法运算符*可以用于列表和整数相乘,表示序列重复,返回新列表,从一定程度上来说也可以实现为列表增加元素的功能。与加法运算符(+)一样,该运算符也适用于元组和字符串。另外,运算符*=也可以用于列表元素重复,与运算符+=一样属于原地操作。

        >>>x=[1,2,3,4]
        >>>id(x)
        54497224
        >>>x=x*2                                   #元素重复,返回新列表
        >>>x
        [1,2,3,4,1,2,3,4]
        >>>id(x)                                   #地址发生改变
        54603912 
        >>>x*=2                                    #元素重复,原地进行
        >>>x
        [1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4]
        >>>id(x)                                   #地址不变
        54603912
        >>>[1,2,3]*0                               #重复0次,清空
        []

由于Python列表中元素存储的是地址而不是值,当包含子列表的列表进行元素重复的时候,情况会复杂一些。

      >>>x=[[1]]*3
      >>>x
      [[1], [1], [1]]
      >>>id(x[0])==id(x[1])==id(x[2])            #新列表x中的3个元素是同一个列表对象
      True
      >>>x[0].append(3)                          #为其中一个子列表追加新元素
      >>>x                                       #另外两个子列表会受到同样的影响
      [[1,3], [1,3], [1,3]]
      >>>x[0]=[1,2,3]                            #直接修改第一个元素的值
      >>>x                                       #不影响另外两个元素
      [[1,2,3], [1,3], [1,3]] 
      >>>id(x[1])==id(x[2])
      True
      >>>id(x[0])==id(x[1])                      #不再是同一个对象
      False

不过,上面的描述并不适用于下面的情况:

        >>>x=[[] for i in range(3)]                 #列表推导式
        >>>x
        [[], [], []]
        >>>x[0].append(1)                           #3个子列表互不影响
        >>>x[1].append(3)
        >>>x[2].append(5)
        >>>x
        [[1], [3], [5]]

成员测试运算符in可用于测试列表中是否包含某个元素,查询时间随着列表长度的增加而线性增加,而同样的操作对于集合而言则是常数级的。

        >>>3 in [1,2,3]
        True
        >>>3 in [1,2, '3']
        False

3.1.5 内置函数对列表的操作

除了列表对象自身方法之外,很多Python内置函数也可以对列表进行操作。例如,max()、min()函数用于返回列表中所有元素的最大值和最小值,sum()函数用于返回列表中所有元素之和,len()函数用于返回列表中元素个数,zip()函数用于将多个列表中元素重新组合为元组并返回包含这些元组的zip对象,enumerate()函数返回包含若干下标和值的迭代对象,map()函数把函数映射到列表上的每个元素,filter()函数根据指定函数的返回值对列表元素进行过滤,all()函数用来测试列表中是否所有元素都等价于True, any()用来测试列表中是否有等价于True的元素。另外,标准库functools中的reduce()函数以及标准库itertools中的compress()、groupby()、dropwhile()等大量函数也可以对列表进行操作。这里重点介绍内置函数对列表的操作,关于reduce()函数请参考第2章的介绍,标准库itertools的用法请参考第4章。

        >>>x=list(range(11))                      #生成列表
        >>>import random
        >>>random.shuffle(x)                      #打乱列表中元素的顺序
        >>>x
        [0,6,10,9,8,7,4,5,2,1,3]
        >>>all(x)                                 #测试是否所有元素都等价于True
        False
        >>>any(x)                                 #测试是否存在等价于True的元素
        True
        >>>max(x)                                 #返回最大值
        10
        >>>max(x, key=str)                        #按指定规则返回最大值
        9
        >>>min(x)
        0
        >>>sum(x)                                 #所有元素之和
        55 
        >>>len(x)                                 #列表元素个数
        11
        >>>list(zip(x, [1]*11))                   #多列表元素重新组合
        [(0,1), (6,1), (10,1), (9,1), (8,1), (7,1), (4,1), (5,1), (2,1), (1,1), (3,1)]
        >>>list(zip(range(1,4)))                  #zip()函数也可以用于一个序列或迭代对象
        [(1, ), (2, ), (3, )]
        >>>list(zip(['a', 'b', 'c'], [1,2]))      #如果两个列表不等长,以短的为准
        [('a',1), ('b',2)]
        >>>enumerate(x)                           #枚举列表元素,返回enumerate对象
        <enumerate object at 0x00000000030A9120>
        >>>list(enumerate(x))                     #enumerate对象可以转换为列表、元组、集合
        [(0,0), (1,6), (2,10), (3,9), (4,8), (5,7), (6,4), (7,5), (8,2), (9,1), (10,3)]

3.1.6 使用列表模拟向量运算

3.1.4节已经介绍过,Python列表支持与整数的乘法运算,表示列表元素进行重复并生成新列表。Python列表不支持与整数的加、减、除运算,也不支持列表之间的减、乘、除操作。列表之间的加法运算表示列表元素的合并,生成新列表。

        >>>[1,2,3]+[4,5,6]
        [1,2,3,4,5,6]

然而,向量运算经常涉及这样的操作,例如向量所有分量同时加、减、乘、除同一个数,或者向量之间的加、减、乘运算,Python列表对象本身不支持这样的操作,不过可以借助于内置函数、列表推导式和标准库operator中的方法来实现。如果需要更加丰富和强大的向量或矩阵运算,可以借助于Python扩展库numpy实现。

        >>>from random import randint
        >>>x=[randint(1,100) for i in range(10)]  #生成10个[1,100]区间内的随机数
        >>>x
        [46,76,47,28,5,15,57,29,9,40]
        >>>list(map(lambda i: i+5, x))            #所有元素同时加5
        [51,81,52,33,10,20,62,34,14,45]
        >>>[i+5 for i in x]                       #使用列表推导式实现同样的功能
        [51,81,52,33,10,20,62,34,14,45]
        >>>x=[randint(1,10) for i in range(10)]   #生成两个列表
        >>>y=[randint(1,10) for i in range(10)]
        >>>x
        [2,2,9,6,7,9,2,1,2,7]
        >>>y
        [8,1,9,7,1,5,8,4,1,9]
        >>>import operator
        >>>sum(map(operator.mul, x, y))           #向量内积
        278
        >>>sum((i*j for i, j in zip(x, y)))       #使用内置函数计算向量内积
        278
        >>>list(map(operator.add, x, y))          #两个等长的向量对应元素相加
        [10,3,18,13,8,14,10,5,3,16]
        >>>list(map(lambda i, j: i+j, x, y))      #使用lambda表达式实现同样的功能
        [10,3,18,13,8,14,10,5,3,16]
        >>>[i+j for i, j in zip(x, y)]            #使用列表推导式实现同样的功能
        [10,3,18,13,8,14,10,5,3,16]

3.1.7 列表推导式语法与应用案例

列表推导式(list comprehension),也称为列表解析式,可以使用非常简洁的方式对列表或其他可迭代对象的元素进行遍历、过滤或再次计算,快速生成满足特定需求的新列表,代码非常简洁,具有很强的可读性,是Python程序开发时应用最多的技术之一。Python的内部实现对列表推导式做了大量优化,可以保证很快的运行速度,也是推荐使用的一种技术。列表推导式的语法形式为

        [expression for expr1 in sequence1 if condition1
                    for expr2 in sequence2 if condition2
                    for expr3 in sequence3 if condition3
                    
                    for exprN in sequenceN if conditionN]

列表推导式在逻辑上等价于一个循环语句,只是形式上更加简洁。例如:

     >>>aList=[x*x for x in range(10)]

相当于

        >>>aList=[]
        >>>for x in range(10):
            aList.append(x*x)

当然,如果不使用列表推导式的话,也可以借助于Python函数式编程的特点使用下面的代码实现同样的功能。

      >>>aList=list(map(lambda x: x*x, range(10)))
      >>>aList=list(map(lambda x: pow(x,2), range(10)))

再例如:

        >>>freshfruit=['banana', 'loganberry', 'passion fruit']
        >>>aList=[w.strip() for w in freshfruit]

等价于下面的代码:

        >>>aList=[]
        >>>for item in freshfruit:
            aList.append(item.strip())

当然也等价于:

     >>>aList=list(map(lambda x: x.strip(), freshfruit))

     >>>aList=list(map(str.strip, freshfruit))

大家应该听过一个故事,说是阿凡提(也有的说是阿基米德,这不是重点)与国王比赛下棋,国王说要是自己输了的话阿凡提想要什么他都可以拿得出来。阿凡提说那就要点米吧,棋盘一共64个小格子,在第一个格子里放1粒米,第二个格子里放2粒米,第三个格子里放4粒米,第四个格子里放8粒米,以此类推,后面每个格子里的米都是前一个格子里的2倍,一直把64个格子都放满。那么到底需要多少粒米呢?使用列表推导式再结合内置函数sum()就很容易知道答案。

      >>>sum([2**i for i in range(64)])
      18446744073709551615

按一斤大米约26000粒计算,为放满棋盘,需要大概350亿吨大米。结果可想而知,最后国王没有办法拿出那么多米。

接下来再通过几个示例来进一步展示列表推导式的强大功能。

1.实现嵌套列表的平铺

        >>>vec=[[1,2,3], [4,5,6], [7,8,9]]
        >>>[num for elem in vec for num in elem]
        [1,2,3,4,5,6,7,8,9]

在这个列表推导式中有两个循环,其中第一个循环可以看作是外循环,执行得慢;第二个循环可以看作是内循环,执行得快。上面代码的执行过程等价于下面的写法:

        >>>vec=[[1,2,3], [4,5,6], [7,8,9]]
        >>>result=[]
        >>>for elem in vec:
            for num in elem:
              result.append(num)
        >>>result
        [1,2,3,4,5,6,7,8,9]

如果不使用列表推导式的话,也可以借助于标准库itertools中的chain()函数把子列表串起来成为一个列表。

        >>>vec=[[1,2,3], [4,5,6], [7,8,9]]
        >>>from itertools import chain
        >>>list(chain(*vec))
        [1,2,3,4,5,6,7,8,9]

当然,这里演示的只是一层嵌套列表的平铺,如果有多级嵌套或者不同子列表嵌套深度不同的话,就不能使用上面的思路了。这时,可以使用函数递归实现。

        def flatList(lst): 
            result=[]                           #存放最终结果的列表
            def nested(lst):                    #函数嵌套定义
                for item in lst:
                    if isinstance(item, list):
                      nested(item)              #递归子列表
                    else:
                      result.append(item)       #扁平化列表
            nested(lst)                         #调用嵌套定义的函数
            return result                       #返回结果

另外,作为一个扩充,下面的代码可以看作上面嵌套列表平铺的一个逆运算,用来把一维列表转换成包含r个子列表的嵌套列表,每个子列表中包含c个元素,并且新列表恰好容纳原列表中的所有元素。如果需要更加复杂的数组和矩阵运算功能,可以借助于Python扩展库numpy。

        def resize(lst, r, c=-1):
            ''’把一维列表转换成r行c列的嵌套列表’''
            #第一个参数必须是列表,并且只包含数字
            if not isinstance(lst, list):
                return'must be a list'
            for item in lst:
                if not isinstance(item, (int, float, complex)):
                    return'must be a list of numbers'

            #第二个和第三个参数必须是整数
            if not (isinstance(r, int) and isinstance(c, int)):
                return'Wrong size.'

            #原列表长度
            originLen=len(lst)

            #新的大小恰好能够容纳原列表中的所有元素
            if c==-1:
                if originLen% r ! =0:
                    return'Wrong size.'
                c=originLen//r
            else:
                if r*c ! =originLen:
                  return'Wrong size.'

          #使用切片生成新的嵌套列表
          result=[]
          for i in range(r):
              result.append(lst[i*c:i*c+c])

          #返回新列表
          return result

      #测试
      lst=list(range(20))
      result=resize(lst,4,5)
      if type(result)==list:
          for row in result:
              print(row)
      else:
          print(result)

2.过滤不符合条件的元素

在列表推导式中可以使用if子句对列表中的元素进行筛选,只在结果列表中保留符合条件的元素。下面的代码可以列出当前文件夹下所有Python源文件:

        >>>import os
        >>>[filename for filename in os.listdir('.') if filename.endswith(('.py', '.pyw'))]

下面的代码用于从列表中选择符合条件的元素组成新的列表:

        >>>aList=[-1, -4,6,7.5, -2.3,9, -11]
        >>>[i for i in aList if i>0]                     #所有大于0的数字
        [6,7.5,9]

再例如,已知有一个包含一些同学成绩的字典,现在需要计算所有成绩的最高分、最低分、平均分,并查找所有最高分同学,代码可以这样编写:

        >>>scores={"Zhang San": 45, "Li Si": 78, "Wang Wu": 40, "Zhou Liu": 96,
                    "Zhao Qi": 65, "Sun Ba": 90, "Zheng Jiu": 78, "Wu Shi": 99,
                    "Dong Shiyi": 60}
        >>>highest=max(scores.values())                #最高分
        >>>lowest=min(scores.values())                 #最低分
        >>>average=sum(scores.values())/len(scores)    #平均分
        >>>highest, lowest, average
        (99,40,72.33333333333333)
        >>>highestPerson=[name for name, score in scores.items() if score==highest]
        >>>highestPerson
        ['Wu Shi']

与上面的代码功能类似,下面的代码使用列表推导式查找列表中最大元素的所有位置:

        >>>from random import randint
        >>>x=[randint(1,10) for i in range(20)]                    #20个介于[1,10]的整数
        >>>x
        [10,2,3,4,5,10,10,9,2,4,10,8,2,2,9,7,6,2,5,6]
        >>>m=max(x)
        >>>[index for index, value in enumerate(x) if value==m]    #最大整数的所有出现位置
        [0,5,6,10]

3.同时遍历多个列表或可迭代对象

        >>>[(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)]
        >>>[(x, y) for x in [1,2,3] if x==1 for y in [3,1,4] if y! =x]
        [(1,3), (1,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)]

分析上面的代码和运行结果可以看出,这是两个序列元素笛卡儿积的一部分,作为一种技巧,也可以使用下面的代码实现同样的功能。

        >>>import itertools
        >>>list(itertools.product([1,2,3], [3,1,4]))
        [(1,3), (1,1), (1,4), (2,3), (2,1), (2,4), (3,3), (3,1), (3,4)]
        >>>list(filter(lambda x:x[0]! =x[1], itertools.product([1,2,3], [3,1,4])))
        [(1,3), (1,4), (2,3), (2,1), (2,4), (3,1), (3,4)]

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]]

对于嵌套了列表推导式的列表推导式,一定要清楚其执行顺序。例如,上面列表推导式的执行过程等价于下面的代码

    >>>matrix=[[1,2,3,4], [5,6,7,8], [9,10,11,12]]
    >>>result=[]
    >>>for i in range(len(matrix[0])):
        result.append([row[i] for row in matrix])
    >>>result
    [[1,5,9], [2,6,10], [3,7,11], [4,8,12]]

如果把内层的列表推导式也展开的话,完整的执行过程可以通过下面的代码来模拟:

    >>>matrix=[ [1,2,3,4], [5,6,7,8], [9,10,11,12]]
    >>>result=[]
    >>>for i in range(len(matrix[0])):
      temp=[]
      for row in matrix:
        temp.append(row[i])
      result.append(temp)
    >>>result
    [[1,5,9], [2,6,10], [3,7,11], [4,8,12]]

当然,也可以使用内置函数zip()和list()来实现矩阵转置:

    >>>list(map(list, zip(*matrix)))
    [[1,5,9], [2,6,10], [3,7,11], [4,8,12]]

5.列表推导式中可以使用函数或复杂表达式

    >>>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]

6.列表推导式支持文件对象迭代

    >>>with open('C:\\RHDSetup.log', 'r') as fp:              #为节约篇幅,略去输出结果
        print([line for line in fp])

7.使用列表推导式生成100以内的所有素数

    >>>[p for p in range(2,100) if 0 not in [p% d for d in range(2, int(sqrt(p))+1)]]
    [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97]

另外,从Python 3.6.x开始支持在协程函数中使用下面形式的异步列表推导式,同样的用法也适用于字典推导式和集合推导式以及生成器推导式。关于协程函数的介绍请参考本书5.8节和12.3节的内容。

        async def ticker(delay, to)                                 #协程函数,异步生成器
            for i in range(to):
                yield i
                await asyncio.sleep(delay)

        async def run():
            result=[i async for i in ticker(1,10) if i% 2]          #异步列表推导式
            print(result)

        import asyncio
        loop=asyncio.get_event_loop()
        try:
            loop.run_until_complete(run())
        finally:
            loop.close()

最后,Python 3.6.x及更新版本还支持下面的用法,在列表推导式和其他类型推导式中使用await表达式。

        import asyncio

        async def half(x):
            return x/2

        async def square(x):
            return x**2

        async def cube(x):
            return x**3

        async def run():
            result=[await f(3) for f in [half, square, cube]]
            print(result)

        loop=asyncio.get_event_loop()
        try:
            loop.run_until_complete(run())
        finally:
            loop.close()

3.1.8 切片操作的强大功能

切片是Python序列的重要操作之一,除了适用于列表之外,还适用于元组、字符串、range对象,但列表的切片操作具有最强大的功能。不仅可以使用切片来截取列表中的任何部分返回得到一个新列表,也可以通过切片来修改和删除列表中部分元素,甚至可以通过切片操作为列表对象增加元素。

在形式上,切片使用2个冒号分隔的3个数字来完成。

     [start:end:step]

其中,3个数字的含义与内置函数range(start, end, step)完全一致,第一个数字start表示切片开始的位置,默认为0;第二个数字end表示切片截止(但不包含)的位置(默认为列表长度);第三个数字step表示切片的步长(默认为1)。当start为0时可以省略,当end为列表长度时可以省略,当step为1时可以省略,省略步长时还可以同时省略最后一个冒号。另外,当step为负整数时,表示反向切片,这时start应该在end的右侧才行。

1.使用切片获取列表部分元素

使用切片可以返回列表中部分元素组成的新列表。与使用索引作为下标访问列表元素的方法不同,切片操作不会因为下标越界而抛出异常,而是简单地在列表尾部截断或者返回一个空列表,代码具有更强的健壮性。

        >>>aList=[3,4,5,6,7,9,11,13,15,17]
        >>>aList[::]                        #返回包含原列表中所有元素的新列表
        [3,4,5,6,7,9,11,13,15,17]
        >>>aList[::-1]                      #返回包含原列表中所有元素的逆序列表
        [17,15,13,11,9,7,6,5,4,3]
        >>>aList[::2]                       #隔一个取一个,获取偶数位置的元素
        [3,5,7,11,15]
        >>>aList[1::2]                      #隔一个取一个,获取奇数位置的元素
        [4,6,9,13,17]
        >>>aList[3:6]                       #指定切片的开始和结束位置
        [6,7,9]
        >>>aList[0:100]                     #切片结束位置大于列表长度时,从列表尾部截断
        [3,4,5,6,7,9,11,13,15,17]
        >>>aList[100]                       #抛出异常,不允许越界访问
        IndexError: list index out of range
        >>>aList[100:]                      #切片开始位置大于列表长度时,返回空列表
        []
        >>>aList[-15:3]                     #进行必要的截断处理
        [3,4,5]
        >>>len(aList)
        10
        >>>aList[3:-10:-1]                  #位置3在位置-10的右侧,-1表示反向切片
        [6,5,4]
        >>>aList[3:-5]                      #位置3在位置-5的左侧,正向切片
        [6,7]

2.使用切片为列表增加元素

可以使用切片操作在列表任意位置插入新元素,不影响列表对象的内存地址,属于原地操作。

        >>>aList=[3,5,7]
        >>>aList[len(aList):]
        []
        >>>aList[len(aList):]=[9]           #在列表尾部增加元素
        >>>aList[:0]=[1,2]                  #在列表头部插入多个元素
        >>>aList[3:3]=[4]                   #在列表中间位置插入元素
        >>>aList
        [1,2,3,4,5,7,9]

3.使用切片替换和修改列表中的元素

        >>>aList=[3,5,7,9]
        >>>aList[:3]=[1,2,3]                #替换列表元素,等号两边的列表长度相等
        >>>aList
        [1,2,3,9]
        >>>aList[3:]=[4,5,6]                #切片连续,等号两边的列表长度可以不相等
        >>>aList
        [1,2,3,4,5,6]
        >>>aList[::2]=[0]*3                 #隔一个修改一个
        >>>aList
        [0,2,0,4,0,6]
        >>>aList[::2]=['a', 'b', 'c']       #隔一个修改一个
        >>>aList
        ['a',2, 'b',4, 'c',6]
        >>>aList[1::2]=range(3)             #序列解包的用法
        >>>aList
        ['a',0, 'b',1, 'c',2]
        >>>aList[1::2]=map(lambda x: x! =5, range(3))
        >>>aList
        ['a', True, 'b', True, 'c', True]
        >>>aList[1::2]=zip('abc', range(3)) #map、filter、zip对象都支持这样的用法
        >>>aList
        ['a', ('a',0), 'b', ('b',1), 'c', ('c',2)]
        >>>aList[::2]=[1]                   #切片不连续时等号两边列表的长度必须相等
        ValueError: attempt to assign sequence of size 1 to extended slice of size 3

4.使用切片删除列表中的元素

        >>>aList=[3,5,7,9]
        >>>aList[:3]=[]                     #删除列表中前3个元素
        >>>aList
        [9]

另外,也可以结合使用del命令与切片结合来删除列表中的部分元素,并且切片元素可以不连续。

        >>>aList=[3,5,7,9,11]
        >>>del aList[:3]                    #切片元素连续
        >>>aList
        [9,11]
        >>>aList=[3,5,7,9,11]
        >>>del aList[::2]                   #切片元素不连续,隔一个删一个
        >>>aList
        [5,9]

5.切片得到的是列表的浅复制

在3.1.3节介绍列表对象的copy()方法时曾经提到,切片返回的是列表元素的浅复制,与列表对象的直接赋值并不一样,和3.1.3节介绍的深复制也有本质的不同。

        >>>aList=[3,5,7]
        >>>bList=aList[::]                   #切片,浅复制
        >>>aList==bList                      #两个列表的值相等
        True
        >>>aList is bList                    #浅复制,不是同一个对象
        False
        >>>id(aList)==id(bList)              #两个列表对象的地址不相等
        False
        >>>id(x[0])==id(y[0])                #相同的值在内存中只有一份
        True
        >>>bList[1]=8                        #修改bList列表元素的值不会影响aList
        >>>bList                             #bList的值发生改变
        [3,8,7]
        >>>aList                             #aList的值没有发生改变
        [3,5,7]
        >>>x=[[1], [2], [3]]                 #如果列表中包含列表或其他可变序列
        >>>y=x[:]                            #情况会复杂一些
        >>>y
        [[1], [2], [3]]
        >>>y[0]=[4]                          #直接修改y中下标为0的元素值,不影响x
        >>>y
        [[4], [2], [3]]
        >>>y[1].append(5)                    #通过列表对象的方法原地增加元素
        >>>y
        [[4], [2,5], [3]]
        >>>x                                 #列表x也受到同样的影响
        [[1], [2,5], [3]]