Python自悟:Python中的Sequence Type

主要内容源自解读《Fluent Python》,理解如有错误敬请指正:-)

  • Python标准库提供了大量使用C来实现的序列类型,从序列中的元素类型是否一致作为标准,包括容器序列(Container sequences,包括list、tuple、collections.deque等)和固定序列(Flat sequences,包括str、bytes、bytearray、memoryview、array.array)等。
    容器序列中实际存放的元素是对其他任意对象的引用,而固定序列中存放是真正的是元素值,因此所有的元素必须是相同类型,并且只能是Python基本类型(字符、字节、数字等)的数据。
    如果从序列中的元素是否能够被修改的标准来看,Python的序列类型又分为可变序列(Mutable sequences,包括list、bytearray、array.array、collections.deque、memoryview等)和不可变序列(Immutable sequences,包括tuple、str、bytes等)

  • 有两种最常使用的快速生成一个序列的方法:列表推导(List Comprehensions,简称listcomps)和生产器表达式(Generator Expressions,简称genexps)。listcomps用于生成一个list,而genexps则可以生成任何list之外的序列类型

  • listcomps表达式使用方括号[ ],其工作对象是另外一个支持迭代的对象,然后通过对该对象中的元素进行过滤和转换,从而得到一个新的list。谨记一点,使用listcops唯一的目的就是用于生成一个list,如果是试图利用其副作用的场景,就不要使用listcomps表达式,而应该使用for循环等方式将任务分解开来完成
    需要注意的是,在Pythin 2.7中,listcomps表达式中使用的临时变量没有自己单独的作用域,因此不要与代码段中其他变量同名。
    典型的一个listcomps代码段如下所示:

>>> colors = ("black", "white")
>>> sizes = ['S', 'M', 'L']
>>> tshirts = [("%s, %s" % (c,s) for c in colors for s in sizes];  tshirts
["black, S",    "black, M",    "black,  L",  "white, S",  "white, M",  "white, L"]

需要注意的是,这里的 ... for c in colors for s in sizes 语法等同于:

for c in colors:
    for s in sizes:
         ......
  • 使用genexps方式来构造tuple、arrays或其他sequences类型的场景大多数也都可以通过listcomps来完成,但是使用genexps最大的优点是节省内存空间,因为genexps生成的是一个generator。
    list或者其他的iterator对象一旦生成,其中包含的所有元素都会一直随着这个对象保留在内存空间中,而generator对象生成之后并不会占据多少内容,仅当对这个对象每一次进行迭代读取时才会一次吐一个式的生产出各个元素,并且迭代读取完成之后,再次进行迭代就无法获取任何内容了。generator常用于生成不需要保存在内存中的序列对象
    genexps的语法使用的是圆括号( ),除此之外的语法和listcomps是一样的
>>> for tshirt in ("%s  %s" % (c, s) for c in colors for s in sizes):
       print tshirt
black  S
black  M
black  L
black  XL
white  S
white  M
white  L
white  XL
  • python的 collections.namedTuple( ) 方法可以用例构造一个简单的class,这个class没有任何方法而只有成员变量。
>>> from collections import namedtuple
>>> City = namedtuple('City', 'name country population coordinates')  
>>> tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))  
>>> tokyo
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722,
139.691667))
>>> tokyo.population  
36.933
>>> tokyo.coordinates
(35.689722, 139.691667)
>>> tokyo[1]
'JP'

如上所示,named tuple相当于是一个每个位置都有命名的tuple对象,既可以通过传统的index方式访问tuple中的元素,又可以像dickt那样通过名字来访问tuple中的元素。另外,named tuple还有一些传统tuple没有的变量和方法,包括 _fields、_make()、_asdict()

>>> City._fields  
('name', 'country', 'population', 'coordinates')
>>> LatLong = namedtuple('LatLong', 'lat long')
>>> delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.613889, 77.208889))
>>> delhi = City._make(delhi_data)   # 等同于 City(*delhi_data)
>>> delhi._asdict()  
OrderedDict([('name', 'Delhi NCR'), ('country', 'IN'), ('population',
21.935), ('coordinates', LatLong(lat=28.613889, long=77.208889))])
>>> for key, value in delhi._asdict().items():
        print(key + ':', value)
name: Delhi NCR
country: IN
population: 21.935
coordinates: LatLong(lat=28.613889, long=77.208889)
>>>
  • 几乎所有的序列对象都支持 [x:y]、[x:]、[:y]、[x:y:z]、[x::z]、[:y:z]、[::z] 等方式的分段截取(slicing),其中对于带步长的截取方式,如果步长为负数,表示倒序来分段截取:
>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'

除了部分读取sequences的内容之外,slicing还可以用于动态修改可变序列的内容,如下所示:

>>> l = list(range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:5] = [20, 30]   # 对于slice赋值的sequence不一定要和原来的slice等长
>>> l
[0, 1, 20, 30, 5, 6, 7, 8, 9]
>>> del l[5:7]
>>> l
[0, 1, 20, 30, 5, 8, 9]
>>> l[3::2] = [11, 22]
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5] = 100  1   # 给slice赋值的必须是iterable对象
Traceback (most recent call last):
  File "", line 1, in 
TypeError: can only assign an iterable
>>> l[2:5] = [100]
>>> l
[0, 1, 100, 22, 9]
  • 在对sequences对象使用纯粹的 + 和 * 运算的时候,实际调用的是序列对象的__add__( ) 和 __mul__( ) 方法,并不会修改原来参与运算的序列对象,而是生成一个新的sequences对象。
    对序列对象seq进行 seq * n 运算本质就是将序列对象中的所有元素的值全部复制n份,构成一个新的长度为原来seq对象长度n倍的序列对象seqNew。对于容器序列(list、tuple等),因为他的元素其实只是一个对象引用(CPython背景下其实就是一个C指针),所以进行了 *n 操作只是把原有的所有引用复制n份而已,每个复制的引用与原引用指向的仍然是同一个对象,一旦序列中存储的原引用指向的对象本身发生了变化,从seqNew中获取这些指向同一个对象的引用的值时,都能够看到值的同步变化,这点需要特别注意,如下例所示:
>>> L1 = [1, "One", ["raLph","LiLy"]]; L2 = L1 * 2
>>> L1; L2
[1, 'One', ['raLph', 'LiLy']]
[1, 'One', ['raLph', 'LiLy'], 1, 'One', ['raLph', 'LiLy']]
>>> for item in L1: print(id(item), end="\t")
45524680 53894584 53879032 
>>> for i,item in enumerate(L2):
 print(id(item), end="\t")
 if i % 3 == 2: print("")
45524680 53894584 53879032 
45524680 53894584 53879032    # 可以看出L2中的引用的地址和L1是完全一样的
>>> L1[2][0] = "Lucy"  # 修改 53879032 这个地址存放的List对象中的元素值
>>> L1;L2
[1, 'One', ['Lucy', 'LiLy']]
[1, 'One', ['Lucy', 'LiLy'], 1, 'One', ['Lucy', 'LiLy']]  # 可以看出L2中所有 53879032 这个地址对应的内容都相应改变了
>>> for item in L1: print(id(item), end="\t")
45524680 53894584 53879032 
>>> for i,item in enumerate(L2):
 print(id(item), end="\t")
 if i % 3 == 2: print("")
45524680 53894584 53879032 
45524680 53894584 53879032   # 但是L2中的引用的地址和L1仍然是完全一样的
>>> L1[1:] = ('ONE',["Gates", "CLiton"])  # 这次修改的是L1中后两个元素的值,改变的是引用的地址值,也就意味着指向新的对象了
>>> L1; L2
[1, 'ONE', ['Gates', 'CLiton']]
[1, 'One', ['Lucy', 'LiLy'], 1, 'One', ['Lucy', 'LiLy']]  #  这次L2中的内容没有跟随L1而变化
>>> for item in L1: print(id(item), end="\t")
45524680 53894416 53880232    # 现在L1中存放的两个引用地址已经发生变化了
>>> for i,item in enumerate(L2):
 print(id(item), end="\t")
 if i % 3 == 2: print("")
45524680 53894584 53879032 
45524680 53894584 53879032   # 而L2中存放的引用地址没有发生变化
# 在这里如果不想L1的变化引起L2中内容的变化,则需要使用 listcomps方式,并使用copy.deepcopy() 函数创建新的对象
>>> L2 = [copy.deepcopy(item) for i in range(2) for item in L1 ]; L2
[1, 'ONE', ['Gates', 'Cliton'], 1, 'ONE', ['Gates', 'Cliton']]
>>> for item in L1: print(id(item), end="\t")
45524680 53894416 53880232 
>>> for i,item in enumerate(L2):
 print(id(item), end="\t")
 if i % 3 == 2: print("")
45524680 53894416 53882712 
45524680 53894416 53938416  
# 可以看到,deepcopy生成的L2中,基本数据类型(int、str)元素对应的引用值是不变的,其他对象元素保存的引用地址是各不相同的
  • Python中的大多数sequence对象都支持增量加 += 和 增量乘 *= 两种运算,它们本质上是调用的该对象的 __iadd__( ) 和 __imul__( ) 方法,当没有定义这两个方法的时候,Python解释器会转而调用 __add__( ) 和 __mul__( ) 方法
    对于mutable序列对象,增量加乘将会直接改变该对象的元素内容,而对于immutable序列对象增量加乘则是新生成一个immutable sequence对象,如下:
>>> L1 = [1,2,3]; id(L1)
4382032024
>>> L1 += [4,5];L1;id(L1)   #  L1引用的对象没有改变
[1, 2, 3, 4, 5]
4382032024
>>> T1 = (1,2,3); id(T1)
4382524352
>>> T1 += (4,5); T1; id(T1)  #  T1引用的则是一个新对象
(1, 2, 3, 4, 5)
4380428144

需要特别注意的是,应该尽量避免在immutable sequence中包含mutable sequence对象,这是因为可能出现下面的非预期结果:

>>> t1 = (1, 2, [3, 4])
>>> t1[2] += [4, 5]
Traceback (most recent call last):
  File "", line 1, in 
    t1[2] += [4, 5]
TypeError: 'tuple' object does not support item assignment
>>> t1
(1, 2, [3, 4, 4, 5])

这是因为上述操作中,Python解释器对于 t1[2] += [4, 5] 这一步的执行步骤是现将 t1[2] 对象置栈顶,并对其执行增量加操作——这是允许的,因为t1[2]引用的对象是一个List;然后尝试使用这个更新后的对象,重新对t1中的第三个元素进行赋值——这是不允许的,因为t1是一个tuple

  • 多数序列对象都可以进行内部的元素排序操作,排序的方式可以有 seqObj.sort( cmp=None, key=None, reverse=False )sorted(seqObj, cmp=None, key=None, reverse=False) 两种。 前者是在seqObj内部直接进行元素重排序,将会改变seqObj内部的元素结构,返回的是None;后者是调用Python的内建函数sorted( ),不会改变seqObj对象本身,而是生成一个新的序列对象并返回给调用者。
    两种排序的参数含义是一致的,cmp是一个两参数输入的函数,key是一个1参数输入的函数,通常使用key参数排序的效率更高,甚至可以通过 functools.cmp_to_key() 函数将已有的cmpFunc函数转换为单参数的key函数;sort排序后默认的元素顺序是从小到大升序的,reverse参数则是表示是否采用降序的方式来排序。
    一个序列对象内部元素也可以通过random模块的的shuffle函数来实现随机乱序,该函数的返回值也是None,同样表示没有产生新的序列对象,而是对元对象内部元素进行了变动
>>> import random
>>> L1 = range(11); L1
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> result = random.shuffle(L1)
>>> result is None
True
>>> L1
[7, 8, 0, 2, 5, 4, 9, 10, 6, 1, 3]
  • 将一个序列对象进行排序将会是后续很多操作的基础,例如很常见的插入、删除元素操作。Python中提供了bisect模块来基于二分查找算法对已排序序列对象进行查找和插入操作,常用方法如下:
>>> L1 = [i for i in range(30) if i%2==0]
>>> L1
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]
>>> pos = bisect.bisect(L1, 23); pos
12
>>> pos = bisect.bisect(L1, 16); pos
9
>>> pos = bisect.bisect_left(L1, 16); pos  
8
# bisect_left( ) 方法针对待查找的元素已经在序列中存在的情况,返回的位置是已存在元素的左侧,而默认返回的是已存在元素的右侧
>>> bisect.insort(L1, 17); L1
[0, 2, 4, 6, 8, 10, 12, 14, 16, 17, 18, 20, 22, 24, 26, 28]
>>> bisect.insort_left(L1, 16); L1
[0, 2, 4, 6, 8, 10, 12, 14, 16, 16, 17, 18, 20, 22, 24, 26, 28]
  • 在list中如果纯粹存放数字内容,因为每一个数字都对应使用一个object,实际存放所占用的内存空间远比纯粹的数字多——对于这种基本类型(对应于C语言中的基本类型)序列,尤其是包含大量基本类型的序列,使用array更合适。
>>> from array import array
>>> import random
>>> floats = array('d', (random.random() for i in range(10**7)))  # 在array定义时即初始化保存海量的浮点数
>>> ints = array('i');ints  # 也可以只定义而不进行初始化
array('i')
>>> ints.extend([random.randrange(100) for i in range(10)]); ints
array('i', [43, 64, 53, 17, 9, 51, 10, 17, 68, 42])

array支持且仅支持基本类型数据,因为其底层就是直接对应的C的数组结构,类型表示字符包括:字符型'c',Unicode字符'u',带符号整型'i',无符号整形'I',无符号长整型'L'、单精度浮点数'f',双精度浮点数'd'
array对象不支持使用内部排序方法,排序必须使用外部排序函数sorted
array对象支持文件级的序列化和反序列化,序列化的二进制文件就是纯粹的array元素对应的字节内容:

>>> with open("floats.bin", "wb") as f:
        floats.tofile(f)
>>> os.path.getsize('floats.bin')  # 序列化文件大小为预期的80MB,即 10**7*8 bytes
80000000
>>> with open("floats.bin", "rb") as f:
        floats2.fromfile(f, 10**7)
        print len(floats2)
        print floats[-1]
10000000
0.474310427794
>>> floats == floats2
True
  • list可以很方便地通过append( )和 pop(0) 动作模拟LIFO操作,但是要在序列开头进行元素的添加和删除代价是很高的,因为整个list中的元素都需要进行移动。Python中提供了各种queue对象来优化这样的操作。
    首先出场的是collections.deque 对象,这是专门用来进行两端操作double-ended queu,并且一旦队列中的元素超过了初始化时设定的最大数,在一端添加新的元素会自动将另一端的
>>> from collections import deque
>>> queue1 = deque(range(10), maxlen=10)
>>> queue1
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> queue1.append(10); queue1
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], maxlen=10)
>>> queue1.appendleft(0); queue1
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> queue1.append(10); queue1
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], maxlen=10)
>>> queue1.rotate(4); queue1
deque([7, 8, 9, 10, 1, 2, 3, 4, 5, 6], maxlen=10)
>>> queue1.rotate(-3); queue1
deque([10, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> queue1.extend([11,12]); queue1
deque([2, 3, 4, 5, 6, 7, 8, 9, 11, 12], maxlen=10)
>>> queue1.extendleft([13,14]); queue1
deque([14, 13, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)

上面的extendleft( )需要特别注意,在queue左侧添加iterable对象时,是遍历其中的元素然后一个一个地添加到queue左侧,所以最后deque对象的左侧是 14,13 而不是 13,14,而rotateleft()则是直接将选定的slice直接放到最左侧,slice中元素的顺序不会改变
!!!!!!特别注意:deque对象的方法是线程安全的 !!!!!!!!

  • queue模块下提供的 Queue、LifoQueue,PriorityQueue 等也是queue特性的对象,它们主要用于多线程通信,与collections.deque不同的是,当这些queue对象中的元素个数达到maxLen之后再添加元素,并不会像deque那样从另一端删除一个元素,而是将会阻塞这一次的添加动作,直至其他线程从这个queue中删除一个元素。
    multiprocessing 模块下提供的Queue类适用于进程间通信,功能和queue.Queue完全类似

  • heapq模块定义的函数可以对list对象执行一系列类似queue的序列结构操作,它本质上是维护一个基于二叉树结构的array,始终保证 heap[k] <= heap[2k+1] and heap[k] <= heap[2k+2] 规则,从而可以保证在这个list的最左侧始终是最小的那个元素

>>> import heapq
>>> for i in [43, 64, 53, 17, 9, 51, 10, 17, 68, 42]:
          heapq.heappush(heap, i)
          heap
[43]
[43, 64]
[43, 64, 53]
[17, 43, 53, 64]
[9, 17, 53, 64, 43]
[9, 17, 51, 64, 43, 53]
[9, 17, 10, 64, 43, 53, 51]
[9, 17, 10, 17, 43, 53, 51, 64]
[9, 17, 10, 17, 43, 53, 51, 64, 68]
[9, 17, 10, 17, 42, 53, 51, 64, 68, 43]
>>> [heapq.heappop(heap) for i in range(4)]  # 每一次heappop弹出的都是最小的那个元素
[9, 10, 17, 17]
>>> heap
[42, 51, 43, 64, 68, 53]
>>> heapq.nlargest(3, heap)  # 获取heap中最大的三个元素
[68, 64, 53]
>>> heapq.nsmallest(3, heap)  # 获取heap中最小的三个元素
[42, 43, 51]
>>> heapq.heappushpop(heap, 40); heap  # 插入一个元素后,再弹出更新后的heap中最小的那个元素
40
[42, 43, 51, 64, 68, 53]
>>> heapq.heapreplace(heap, 40); heap  # 先弹出heap中当前最小的元素之后,再插入新元素
42
[40, 43, 51, 64, 68, 53]

** 需要注意的是 heapq 操作后的list对象中的元素并不是按照从小到大排序的,它保证的只是 heap[k] <= heap[2k+1] and heap[k] <= heap[2k+2] 规则**

你可能感兴趣的:(Python自悟:Python中的Sequence Type)