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

3.2 元组:轻量级列表

3.2.1 元组创建与元素访问

列表的功能虽然很强大,但负担也很重,在很大程度上影响了运行效率。有时候我们并不需要那么多功能,很希望能有个轻量级的列表,元组(tuple)正是这样一种类型。在形式上,元组的所有元素放在一对圆括号中,元素之间使用逗号分隔,如果元组中只有一个元素则必须在最后增加一个逗号。

        >>>x=(1,2,3)                     #直接把元组赋值给一个变量
        >>>type(x)                       #使用type()函数查看变量类型
        <class'tuple'>
        >>>x[0]                          #元组支持使用下标访问特定位置的元素
        1
        >>>x[-1]                         #最后一个元素,元组支持双向索引
        3
        >>>x[1]=4                        #元组是不可变的
        TypeError: 'tuple'object does not support item assignment
        >>>x=(3)                         #这和x=3是一样的
        >>>x
        3
        >>>x=(3, )                       #如果元组中只有一个元素,必须在后面多写一个逗号
        >>>x
        (3, )
        >>>x=()                          #空元组
        >>>x=tuple()                     #空元组
        >>>tuple(range(5))               #将其他迭代对象转换为元组
        (0,1,2,3,4)

除了上面的方法可以直接创建元组之外,很多内置函数的返回值也是包含了若干元组的可迭代对象,例如enumerate()、zip()等。

        >>>list(enumerate(range(5)))
        [(0,0), (1,1), (2,2), (3,3), (4,4)]
        >>>list(zip(range(3), 'abcdefg'))
        [(0, 'a'), (1, 'b'), (2, 'c')]

3.2.2 元组与列表的异同点

列表和元组都属于有序序列,都支持使用双向索引访问其中的元素,以及使用count()方法统计元素的出现次数和index()方法获取元素的索引,len()、map()、filter()等大量内置函数和+、*、+=、in等运算符也都可以作用于列表和元组。虽然有着一定的相似之处,但列表和元组在本质上和内部实现上都有着很大的不同。

元组属于不可变(immutable)序列,不可以直接修改元组中元素的值,也无法为元组增加或删除元素。因此,元组没有提供append()、extend()和insert()等方法,无法向元组中添加元素;同样,元组也没有remove()和pop()方法,也不支持对元组元素进行del操作,不能从元组中删除元素,而只能使用del命令删除整个元组。元组也支持切片操作,但是只能通过切片来访问元组中的元素,而不允许使用切片来修改元组中元素的值,也不支持使用切片操作来为元组增加或删除元素。从一定程度上讲,可以认为元组是轻量级的列表,或者“常量列表”。

Python的内部实现对元组做了大量优化,访问速度比列表更快。如果定义了一系列常量值,主要用途仅是对它们进行遍历或其他类似用途,而不需要对其元素进行任何修改,那么一般建议使用元组而不用列表。元组在内部实现上不允许修改其元素值,从而使得代码更加安全,例如,调用函数时使用元组传递参数可以防止在函数中修改元组,而使用列表则很难保证这一点。

然而,虽然元组属于不可变序列,其元素的值是不可改变的,但是如果元组中包含可变序列,情况就复杂了。

        >>>x=([1,2],3)                   #包含列表的元组
        >>>x[0][0]=5                     #修改元组中的列表元素
        >>>x[0].append(8)                #为元组中的列表增加元素
        >>>x
        ([5,2,8],3)
        >>>x[0]=x[0]+[10]                #试图修改元组的值,失败
        TypeError: 'tuple'object does not support item assignment
        >>>x
        ([5,2,8],3)
        >>>x[0]+=[10]                    #抛出异常,但元组中的元素已被修改
        TypeError: 'tuple'object does not support item assignment
        >>>x
        ([5,2,8,10],3)
        >>>y=x[0]                        #y和x[0]指向同一个列表
        >>>y+=[11]                       #通过y可以影响元组x中的第一个列表
        >>>x
        ([5,2,8,10,11],3)
        >>>y=y+[12]                      #注意,这和y+=[12]是有本质区别的
        >>>y
        [5,2,8,10,11,12]
        >>>x
        ([5,2,8,10,11],3)

最后,作为不可变序列,与整数、字符串一样,元组可用作字典的键,也可以作为集合的元素。而列表则永远都不能当作字典键使用,也不能作为集合中的元素,因为列表不是不可变的。内置函数hash()可以用来测试一个对象是否可哈希。一般来说,并不需要关心该函数的返回值具体是什么,重点是对象是否可哈希,如果对象不可哈希会抛出异常。

        >>>hash((1, ))                        #元组、数字、字符串都是可哈希的
        3430019387558
        >>>hash(3)
        3
        >>>hash('hello world.')
        -4012655148192931880
        >>>hash([1,2])                        #列表不可哈希
        TypeError: unhashable type: 'list'

3.2.3 生成器推导式

生成器推导式也称为生成器表达式(generator expression),用法与列表推导式非常相似,在形式上生成器推导式使用圆括号(parentheses)作为定界符,而不是列表推导式所使用的方括号(square brackets)。与列表推导式最大的不同是,生成器推导式的结果是一个生成器对象。生成器对象类似于迭代器对象,具有惰性求值的特点,只在需要时生成新元素,比列表推导式具有更高的效率,空间占用非常少,尤其适合大数据处理的场合。

使用生成器对象的元素时,可以将其转化为列表或元组,也可以使用生成器对象的__next__()方法或者内置函数next()进行遍历,或者直接使用for循环来遍历其中的元素。但是不管用哪种形式,只能从前往后正向访问其中的元素,没有任何方法可以再次访问已访问过的元素,也不支持使用下标访问其中的元素。当所有元素访问结束以后,如果需要重新访问其中的元素,必须重新创建该生成器对象。enumerate、filter、map、zip等对象也具有同样的特点。最后,包含yield语句的函数也可以用来创建生成器对象,详见第5章。

      >>>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))    #重新创建生成器对象
      >>>g.__next__()                       #使用生成器对象的__next__()方法获取元素
      4
      >>>g.__next__()                       #获取下一个元素
      9
      >>>next(g)                            #使用函数next()获取生成器对象中的元素
      16
      >>>g=((i+2)**2 for i in range(10))
      >>>for item in g:                     #使用循环直接遍历生成器对象中的元素
        print(item, end='')
      4 9 16 25 36 49 64 81100 121
      >>>x=filter(None, range(20))          #filter对象也具有类似的特点
      >>>1 in x
      True
      >>>5 in x
      True
      >>>2 in x                             #不可再次访问已访问过的元素
      False
      >>>x=map(str, range(20))              #map对象也具有类似的特点
      >>>'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))
      >>>next(g)                           #第二个for语句有问题,抛出异常
      NameError: name'y'is not defined

如果生成器推导式作为单参数函数时,可以省略两侧的圆括号。

      >>>sum(x for x in range(3))
      3