前面两篇我们分别介绍了python中的装饰器和多继承,了解了一些平时容易踩到的坑以及没能深入理解原理的常见语法。本节我们来介绍一个更基础也更加频繁使用的内容:python的序列。

所谓序列,就是指有序队列(sequence),是程序设计中常用到的数据存储方式。python常用的序列数据类型主要有三种:字符串(string)、元组(tuple)、列表(list),大家在python编码过程中肯定经常接触。但是最基础的内容往往容易被忽略,序列这几个最基础的数据类型中有一些特性需要我们注意,本节主要结合我自己在工作过程中遇到过的问题,来给大家提个醒,避免以后犯类似的错误。


1.索引

序列可以根据变量的下标来定位元素,这是最基础的知识。python所不同的是,支持从序列尾部来进行索引。


1. _str = "abc"
2. _tuple = (1, 2, 3)
3. _list = ['xyz', 4, 'bc']
4.  
5. print _str[1]          #b
6. print _tuple[-1]       #3
7. print _list[0]         #xyz



我们知道,元组是不可变序列,列表是可变序列,即元组定义完成后不可以增删改序列内容,但列表可以。那么字符串呢,它是可变还是不可变?我们做个简单的实验即可。


1. _str = "abc"
2. _tuple = (1, 2, 3)
3. _list = ['xyz', 4, 'bc']
4.  
5. _list[0] = 'zte'
6. # _tuple[-1] = 5
7. # TypeError: 'tuple' object does not support item assignment
8. _str[1] = 'd'
9. # TypeError: 'str' object does not support item assignment



可以看出字符串也是不可变序列,即定义后不可以改变。

另外关于元组的定义,这里有一个陷阱要提醒大家。我们来看下面这个代码片段。


1. _tuple_a = (1, 2, 3)
2. _tuple_b= (4)
3. print _tuple_a[0]
4. print _tuple_b[0]
5. # TypeError: 'int' object has no attribute '__getitem__'



运行报错,提醒_tuple_b是int类型,无法索引。稍微思考一下能够发现,这是因为python在运行时将"(4)"当做算术表达式来处理了,把结果4赋值给_tuple_b,因此是一个int型的变量。这里由于int型变量无法通过索引来定位元素,所以编译器会抛出异常,试想一下,如果赋值的元素是字符串的时候会是什么情况呢?请注意,这很危险!


    1. _tuple_a = ('xyz','abc')
    2. _tuple_b = ('xyz')
    3.  
    4. print _tuple_a[0]       #xyz
    5. print _tuple_b[0]       #x



    由于字符串类型也支持索引,导致代码能够运行,但是结果却是错误的,这给排查问题带来很大的干扰。我曾经在代码中踩过这类坑,某函数入参是元组,传入只有单个元素的“元组”后输出结果不正确,结果花费大量时间来排查问题。

    那如果实际需要用到单个元素的元组是如何定义呢?其实也很简单,如下代码段所示,在元素后添加逗号分隔即可。


    1. _tuple_a = ('xyz')
    2. _tuple_b = ('xyz',)
    3. _tuple_c  =  'a', 'b'
    4.  
    5. print _tuple_a, type(_tuple_a)        #xyz <type 'str'>
    6. print _tuple_b, type(_tuple_b)        #('xyz',) <type 'tuple'>
    7. print _tuple_c, type(_tuple_c)        #('a', 'b') <type 'tuple'>



    更为神奇的是从_tuple_c的定义我们可以看出,定义元组的关键是逗号分隔符,而不是括号。但为了代码的可读性,还是建议使用括号。


    2.分片

    索引支持定位序列的单个元素,而分片(也称 切片)则能够获取指定范围内的元素。python对分片的支持比较灵活,结合步长可以很方便的处理一个序列,简化操作。


      1. _str = "zte better future"
      2. _list = [1, 2, 3, 4]
      3. _tuple = (5, 6, 7, 8, 9, )
      4.  
      5. print _str[4:-7]                # better
      6. print _list[::2]                # [1, 3]
      7. print _tuple[-1::-1]            # (9, 8, 7, 6, 5)



      需要注意的是,分片返回的是一个新的序列对象。有些同学在初学python时肯定遇到过一类问题:遍历处理列表的过程中改变列表的长度,导致运行报错或者结果不正确。而这个问题正好可以利用分片来处理。


      1. _list = [1, 2, 3, 4]
      2. for i in _list:
      3.     if 2 != i:
      4.         _list.remove(i)
      5.  
      6. print _list
      7. #[2, 4]



      上面这个示例中,我们的需求是将列表中不等于2的元素删除,但实际结果却和预期的不同(可以简单断点调试,看一下为何最终输出这个结果)。最初我们能想到的解决方法是定义一个临时变量,将_list赋值给它,然后利用临时变量来遍历,修改原来_list列表的元素。


      1. _list = [1, 2, 3, 4]
      2. _temp = _list
      3. for i in _temp:
      4.     if 2 != i:
      5.         _list.remove(i)
      6.  
      7. print _list            #[2, 4]
      8. print id(_list)        #59014728
      9. print id(_temp)        #59014728



      实际结果仍然错误,打印两个变量的id发现,他们其实指向同一地址。这是因为python序列的赋值其实是对象引用(内存地址)传递,所有对_list和_temp的操作都落到同一个对象上去。此时我们可以利用分片返回新对象的特性,来实现这个功能。


      1. _list = [1, 2, 3, 4]
      2. _temp = _list[:]
      3. for i in _temp:
      4.     if 2 != i:
      5.         _list.remove(i)
      6.  
      7. print _list            #[2]
      8. print id(_list)        #58949192
      9. print id(_temp)        #59552456



      可以看出此时_list和_temp已经指向不同的对象了。


      3.拷贝

      接着上述示例,我们再深入了解一下分片的工作原理。我们来看一个稍微复杂一点的例子。


      1. _list = ["null", [1, 2, 3, 4]]
      2. _temp = _list[:]
      3.  
      4. _temp[0] = "zte"
      5. _temp[1][0] = 3000
      6.  
      7. print id(_list)     # 57778888
      8. print id(_temp)     # 57780296
      9.  
      10. print _list         # ['null', [3000, 2, 3, 4]]
      11. print _temp         # ['zte', [3000, 2, 3, 4]]


      同样是利用分片来拷贝一个新的对象,在修改元素时却发现对_temp[0]的修改没有影响原来的_list,对_temp[1][0]的修改却导致_list的元素也被修改了!这边我画一个简单的示意图,带大家理解分片的原理。

      Python列表的序号 python的序列数据都有哪些_Python列表的序号

      在分片赋值时,将_list指向的内容复制一份给_temp,由于_list存储的是复杂对象(包含字符串和列表),因此拷贝的实际只有目的对象的地址(引用),在改变_temp[0]字符串的内容时,由于字符串是不可变对象,因此_temp[0]存储了新字符串的地址,而_list[0]的内容并未被改变;在更新_temp[1][0]的内容时,实际是变更了57704008这个地址指向的列表的第一个元素,而_list[1]也指向同一个地址,所以最终_list[1]也被更新。

      实际上python提供了标准的对象拷贝机制,来处理不同的拷贝需求,分为深拷贝和浅拷贝。我们来比较一下他们的区别。


      1. import copy
      2.  
      3. _list = ["null", [1, 2, 3, 4]]
      4. _simple = _list
      5. _burst = _list[:]
      6. _shallow_copy = copy.copy(_list)
      7. _deep_copy = copy.deepcopy(_list)
      8.  
      9. _list[0] = "zte"
      10. _list[1][0] = 3000
      11.  
      12. print _list                    # ['zte', [3000, 2, 3, 4]]
      13. print _simple                  # ['zte', [3000, 2, 3, 4]]
      14. print _burst                   # ['null', [3000, 2, 3, 4]]
      15. print _shallow_copy            # ['null', [3000, 2, 3, 4]]
      16. print _deep_copy               # ['null', [1, 2, 3, 4]]



      从结果来看,只有深拷贝能做到和原来对象完全无关,而浅拷贝则和分片赋值一样(实际上分片就是浅拷贝的一种方式)。我们再通过简单的图示来了解一下他们的区别。

      Python列表的序号 python的序列数据都有哪些_字符串_02

      实际上浅拷贝是是指创建一个新的对象,其内容是原对象中元素的引用,但是不拷贝子对象,也就是说新的容器中的地址指向了旧的元素。如果原来的对象是个简单对象,那么浅拷贝就可以达到拷贝的要求,但如果对象又包含其他容器或对象,那么就需要深拷贝来遍历拷贝。所谓深拷贝是指创建一个新的对象,然后递归的拷贝原对象所包含的子对象。深拷贝出来的对象与原对象没有任何关联。

      在性能上深拷贝会牺牲时间和空间来保证操作的独立性,但通常来讲性能差别微乎其微。我们在使用复杂对象时一定要注意两类拷贝的区别,避免操作互相干扰。


      4.其他操作

      序列本身除了索引和分片外还包含一些通用的内置操作,包括操作符和内置函数。我以表格形式整理如下表所示(表格部分内容参考博客Python序列类型)。


      操作

      描述

      操作

      描述

      s+r

      连接操作

      any(s)

      s中所有项为False则返回False,否则返回True

      s*n, n*s

      制作序列s的n个副本,n为整数

      len(s)

      长度

      s[i]

      索引

      min(s)

      s中的最小项

      s[i:j:step]

      分片

      max(s)

      s中的最大项

      x in s, x not in s

      从属关系,返回值True/False

      sum(s, start)

      s所有项的和加上初值start

      for x in s

      迭代

      s.count(i)

      s中i出现的次数

      all(s)

      s中所有项为True则返回True,否则返回False

      s.index(i)

      s中i第一次出现位置的索引

      列表由于内容和长度可变,有一些特殊的操作,整理如下表所示。

      操作

      描述

      操作

      描述

      s[i] = x

      索引赋值

      s.insert(i,x)

      将x作为元素插入到s[i]之前

      s[i:j] = r

      分片赋值

      s.pop(i)

      移除s[i],默认i=-1

      del s[i]

      删除元素

      s.remove(x)

      移除s中第一个元素x

      del s[i:j]

      删除分片

      s.reverse()

      转制(逆序化)列表s

      s.append(x)

      将x作为元素追加至s的尾部

      s.sort()

      对s的元素进行升序排序

      s.extend(x)

      将x扩展至s的尾部

      s.clear()

      删除s中所有元素

      其中需要注意的是append和extend的区别,简单的示例了解一下即可。


      1. a = [1, 2, 3]
      2. b = [5, 6, 7]
      3.  
      4. a.append(b)
      5. print a         # [1, 2, 3, [5, 6, 7]]
      6.  
      7. a.extend(b)
      8. print a         # [1, 2, 3, [5, 6, 7], 5, 6, 7]



      可以看出,append是将参数作为一个整体追加到列表的尾部,而extend则是将参数展开后一一追加至列表的尾部(请注意,参数展开只有一层,如果参数展开后任然有列表等容器,则不会再次展开)。千万要小心这两个函数的区别,实际开发中经常会遇到错用导致的各类异常。


      5.小结

      本篇主要介绍了python序列中最常用到的三类数据类型,结合平时工作中踩到过的坑给大家提个醒。在编码过程中一定要小心序列相关操作,尤其是拷贝、扩展、变更列表的过程。另外在确认某些序列是否指向同一对象时,可以利用内置函数id()来打印对象的内存地址,判断是否指向正确的对象。