主要內容源自解讀《Fluent Python》峦萎,理解如有錯誤敬請指正:-)
Python標準庫提供了大量使用C來實現(xiàn)的序列類型,從序列中的元素類型是否一致作為標準倦沧,包括容器序列(Container sequences,包括list征炼、tuple摩泪、collections.deque等)和固定序列(Flat sequences,包括str韩脑、bytes氢妈、bytearray、memoryview段多、array.array)等首量。
容器序列中實際存放的元素是對其他任意對象的引用,而固定序列中存放是真正的是元素值进苍,因此所有的元素必須是相同類型加缘,并且只能是Python基本類型(字符、字節(jié)觉啊、數(shù)字等)的數(shù)據(jù)拣宏。
如果從序列中的元素是否能夠被修改的標準來看,Python的序列類型又分為可變序列(Mutable sequences杠人,包括list勋乾、bytearray、array.array嗡善、collections.deque辑莫、memoryview等)和不可變序列(Immutable sequences,包括tuple罩引、str各吨、bytes等)有兩種最常使用的快速生成一個序列的方法:列表推導(List Comprehensions,簡稱listcomps)和生產(chǎn)器表達式(Generator Expressions袁铐,簡稱genexps)揭蜒。listcomps用于生成一個list横浑,而genexps則可以生成任何list之外的序列類型
listcomps表達式使用方括號[ ],其工作對象是另外一個支持迭代的對象屉更,然后通過對該對象中的元素進行過濾和轉換伪嫁,從而得到一個新的list。謹記一點偶垮,使用listcops唯一的目的就是用于生成一個list张咳,如果是試圖利用其副作用的場景,就不要使用listcomps表達式似舵,而應該使用for循環(huán)等方式將任務分解開來完成
需要注意的是脚猾,在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類型的場景大多數(shù)也都可以通過listcomps來完成提鸟,但是使用genexps最大的優(yōu)點是節(jié)省內存空間,因為genexps生成的是一個generator仅淑。
list或者其他的iterator對象一旦生成称勋,其中包含的所有元素都會一直隨著這個對象保留在內存空間中,而generator對象生成之后并不會占據(jù)多少內容涯竟,僅當對這個對象每一次進行迭代讀取時才會一次吐一個式的生產(chǎn)出各個元素赡鲜,并且迭代讀取完成之后,再次進行迭代就無法獲取任何內容了庐船。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對象,既可以通過傳統(tǒng)的index方式訪問tuple中的元素篓冲,又可以像dickt那樣通過名字來訪問tuple中的元素李破。另外,named tuple還有一些傳統(tǒng)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)偷俭,其中對于帶步長的截取方式,如果步長為負數(shù)缰盏,表示倒序來分段截扔坑:
>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'
除了部分讀取sequences的內容之外,slicing還可以用于動態(tài)修改可變序列的內容口猜,如下所示:
>>> 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份而已密幔,每個復制的引用與原引用指向的仍然是同一個對象,一旦序列中存儲的原引用指向的對象本身發(fā)生了變化撩轰,從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 # 現(xiàn)在L1中存放的兩個引用地址已經(jīng)發(fā)生變化了
>>> for i,item in enumerate(L2):
print(id(item), end="\t")
if i % 3 == 2: print("")
45524680 53894584 53879032
45524680 53894584 53879032 # 而L2中存放的引用地址沒有發(fā)生變化
# 在這里如果不想L1的變化引起L2中內容的變化溉苛,則需要使用 listcomps方式镜廉,并使用copy.deepcopy() 函數(shù)創(chuàng)建新的對象
>>> 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中愚战,基本數(shù)據(jù)類型(int娇唯、str)元素對應的引用值是不變的,其他對象元素保存的引用地址是各不相同的
- Python中的大多數(shù)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對象流纹,這是因為可能出現(xiàn)下面的非預期結果:
>>> 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] 這一步的執(zhí)行步驟是現(xiàn)將 t1[2] 對象置棧頂,并對其執(zhí)行增量加操作——這是允許的漱凝,因為t1[2]引用的對象是一個List疮蹦;然后嘗試使用這個更新后的對象,重新對t1中的第三個元素進行賦值——這是不允許的茸炒,因為t1是一個tuple
- 多數(shù)序列對象都可以進行內部的元素排序操作愕乎,排序的方式可以有 seqObj.sort( cmp=None, key=None, reverse=False ) 和 sorted(seqObj, cmp=None, key=None, reverse=False) 兩種。 前者是在seqObj內部直接進行元素重排序壁公,將會改變seqObj內部的元素結構妆毕,返回的是None;后者是調用Python的內建函數(shù)sorted( )贮尖,不會改變seqObj對象本身笛粘,而是生成一個新的序列對象并返回給調用者。
兩種排序的參數(shù)含義是一致的湿硝,cmp是一個兩參數(shù)輸入的函數(shù)薪前,key是一個1參數(shù)輸入的函數(shù),通常使用key參數(shù)排序的效率更高关斜,甚至可以通過 functools.cmp_to_key() 函數(shù)將已有的cmpFunc函數(shù)轉換為單參數(shù)的key函數(shù)示括;sort排序后默認的元素順序是從小到大升序的,reverse參數(shù)則是表示是否采用降序的方式來排序痢畜。
一個序列對象內部元素也可以通過random模塊的的shuffle函數(shù)來實現(xiàn)隨機亂序垛膝,該函數(shù)的返回值也是None,同樣表示沒有產(chǎn)生新的序列對象丁稀,而是對元對象內部元素進行了變動
>>> 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]
- 將一個序列對象進行排序將會是后續(xù)很多操作的基礎吼拥,例如很常見的插入、刪除元素操作线衫。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( ) 方法針對待查找的元素已經(jīng)在序列中存在的情況,返回的位置是已存在元素的左側授账,而默認返回的是已存在元素的右側
>>> 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中如果純粹存放數(shù)字內容枯跑,因為每一個數(shù)字都對應使用一個object,實際存放所占用的內存空間遠比純粹的數(shù)字多——對于這種基本類型(對應于C語言中的基本類型)序列白热,尤其是包含大量基本類型的序列敛助,使用array更合適。
>>> from array import array
>>> import random
>>> floats = array('d', (random.random() for i in range(10**7))) # 在array定義時即初始化保存海量的浮點數(shù)
>>> 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支持且僅支持基本類型數(shù)據(jù)屋确,因為其底層就是直接對應的C的數(shù)組結構纳击,類型表示字符包括:字符型'c'续扔,Unicode字符'u',帶符號整型'i'评疗,無符號整形'I',無符號長整型'L'茵烈、單精度浮點數(shù)'f'百匆,雙精度浮點數(shù)'d' 等
array對象不支持使用內部排序方法,排序必須使用外部排序函數(shù)sorted
array對象支持文件級的序列化和反序列化呜投,序列化的二進制文件就是純粹的array元素對應的字節(jié)內容:
>>> 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對象來優(yōu)化這樣的操作。
首先出場的是collections.deque 對象粘招,這是專門用來進行兩端操作double-ended queu啥寇,并且一旦隊列中的元素超過了初始化時設定的最大數(shù),在一端添加新的元素會自動將另一端的
>>> 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中元素的順序不會改變
!:5讼摺!;突帧骇陈!特別注意:deque對象的方法是線程安全的 !9宓帧K跬帷!5尽7蓑!习贫!
queue模塊下提供的 Queue逛球、LifoQueue,PriorityQueue 等也是queue特性的對象苫昌,它們主要用于多線程通信颤绕,與collections.deque不同的是,當這些queue對象中的元素個數(shù)達到maxLen之后再添加元素,并不會像deque那樣從另一端刪除一個元素奥务,而是將會阻塞這一次的添加動作物独,直至其他線程從這個queue中刪除一個元素。
multiprocessing 模塊下提供的Queue類適用于進程間通信氯葬,功能和queue.Queue完全類似heapq模塊定義的函數(shù)可以對list對象執(zhí)行一系列類似queue的序列結構操作挡篓,它本質上是維護一個基于二叉樹結構的array,始終保證 heap[k] <= heap[2k+1] and heap[k] <= heap[2k+2] 規(guī)則帚称,從而可以保證在這個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] 規(guī)則**