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 "<stdin>", line 1, in <module>
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 "<pyshell#18>", line 1, in <module>
    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] 规则**

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容