Python學習之路21-序列構成的陣列

VPointer發表於2018-05-29

《流暢的Python》筆記。

接下來的三篇都是關於Python的資料結構,本篇主要是Python中的各序列型別

1. 內建序列型別概覽

Python標準庫用C實現了豐富的序列型別,可分為兩大類:

  • 容器序列:listtuplecollections.deque等這些序列能存放不同型別的資料。
  • 扁平序列:strbytesbytearraymemoryviewarray.array等,這些序列只能容納一種型別。

容器序列存放的是它們所包含的任意型別的物件的引用,而扁平序列存放的是值而不是引用。即,扁平序列其實是一段連續的記憶體空間,更加緊湊。

序列型別還可以按能否被修改來分來:

  • 可變序列(MutableSequence):listbytearrayarray.arraycollections.dequememoryview
  • 不可變序列(Sequence):tuplestrbyte

以下是這兩大類的繼承關係:

Python學習之路21-序列構成的陣列

雖然Python中內建的序列型別並不是直接從SequenceMutableSequence這兩個抽象基類繼承而來,但瞭解這些基類可以總結出那些完整的序列型別包含了哪些功能,以及將上述兩種分類方式融會貫通。

下面我們從最常用的列表(list)開始。

2. 列表推導和生成器表示式

列表推導(list comprehension,簡稱listcomps)是構建列表的快捷方式,而生成器表示式(generator expression, 簡稱genexps)則可以用來建立其它任何型別的序列。

有時候,比起用for迴圈,列表推導可能會更簡單可讀。通常的原則是,只用列表推導來建立新的列表,並且儘量保持簡短。如果列表推導的程式碼超過了兩行,應該考慮是不是得用for迴圈重寫,不過這個度得自己把握。(句法提示:Python會忽略[],{},()中的換行,所以可以省略不太好看的換行符\)

**注意:**在Python3中,列表推導、生成器表示式,以及和它們很相似的集合(set)推導和字典(dict)推導都有了自己的區域性作用域,不會影響外部的同名變數(Python2中則可能會影響),如下:

>>> x = "a"
>>> test = [x for x in "ABC"]
>>> x
"a"   # 在Python2中,該結果則可能是 "C"
複製程式碼

2.1 列表推導同filtermap比較

列表推導可以過濾或加工一個序列或其他可迭代型別中的元素,然後生成一個新列表。而Python內建的filtermap函式組合起來也能達到這一效果(一般需要藉助lambda表示式),但可讀性卻比不上列表推導,比如下面的程式碼:

>>> symbols = "ABCDEFG"
>>> ascii = [ord(s) for s in symbols if ord(s) > 66]
>>> ascii
[67, 68, 69, 70, 71]
>>> ascii = list(filter(lambda c: c > 66, map(ord, symbols)))
>>> ascii
[67, 68, 69, 70, 71]
複製程式碼

原本以為map/filter組合起來會比列表推導快一些,但有測試證明該結論不一定成立。對於map, filter的詳細介紹將放在後面的文章中。

2.2 笛卡爾積

簡單說就是簡化巢狀for迴圈,例子如下:

colors = ["black", "white"]
sizes = ["S", "M", "L"]
tshirts = [(color, size) for color in colors for size in sizes]

tshirts_for = [] # 最後它的內容等價於上面的tshirts
for color in colors:
    for size in sizes:
        tshirts_for.append((color, size))
複製程式碼

列表推導的作用只有一個:生成列表。如果想生成其他型別的序列,則需要使用生成器表示式。

2.3 生成器表示式

雖然也可以用列表推導式來初始化元組,陣列或其他序列型別,但生成器表示式是更好的選擇,因為生成器表示式背後遵循了迭代器協議,可以逐個生成元素(可節省記憶體),而不是一次性生成所有元素。

生成器表示式語法跟列表推導差不多,只是把方括號換成了圓括號而已,如下:

>>> symbols = "ABCDEFG"
>>> tuple(ord(symbol) for symbol in symbols)  # ①
(65, 66, 67, 68, 69, 70, 71)
>>> import array
>>> array.array("I", (ord(symbol) for symbol in symbols))  # ②
array('I', [65, 66, 67, 68, 69, 70, 71])
複製程式碼

①如果生成器表示式是一個函式呼叫過程中的唯一引數,則可不加括號將其圍起來;

②array的構造方法需要兩個引數,因此括號是必需的。

下面用生成器表示式改寫上面的笛卡爾積程式碼:

colors = ["black", "white"]
sizes = ["S", "M", "L"]
for tshirt in ("%s %s" % (c, s) for c in colors for s in sizes):
    print(tshirts)

# 結果:
black S
black M
black L
white S
white M
white L
複製程式碼

生成器表示式逐個生成元素,不會一次性生成一個含有6個元素的列表。關於生成器表示式的工作原理將在後面的文章中介紹。

3. 元組

元組除了用作不可變的列表,它還可以用於沒有欄位名的記錄,比如座標,身份資訊等,這裡不再舉例。

3.1 元祖拆包

此概念之前涉及過,這裡將其總結一下:

# 平行賦值
a, b = ("test1", "test2")
# 不用中間變數交換兩個變數的值
b, a = a, b
# *號運算將可迭代物件拆開作為函式引數
t = (20, 8)
divmod(*t)  # 該函式的意思是: 20 ÷ 8 = 2 …… 4, 函式返回商和餘數的元組
# 用*來處理剩下的元素,Python3支援
a, b, *rest = range(5)  # rest的值為[2, 3, 4]
a, b, *rest = range(3)  # rest的值為[2]
a, b, *rest = range(2)  # rest的值為[]
# 在平行賦值中,*字首只能用在一個變數前,但該變數可在任意位置
>>> a, *body, c, d = range(5) # 值依次為 0, [1, 2], 3, 4
>>> *head, b, c, d = range(5) # 值依次為 [0, 1], 2, 3, 4
複製程式碼

3.2 巢狀元組拆包

接受表示式的元組可以是巢狀式的,例如(a, b, (c, d)),只要這個接受元組的巢狀結構符合表示式本身的巢狀結構,以下用巢狀元組來獲取經緯度:

metro_areas = [
    ("Tokyo", "JP", 36.933, (35.689722, 139.691667)),
    ("Delhi NCR", "IN", 21.935, (28.613889, 77.208889)),
    ("Mexico City", "MX", 20.142, (19.433333, -99.133333)),
    ("New York-Newark", "US", 20.104, (40.808611, -74.020386)),
    ("Sao Paulo", "BR", 19.649, (-23.547778, -46.635833)),
]

print("{:15} | {:^9} | {:^9}".format(" ", "lat.", "long."))
fmt = "{:15} | {:9.4f} | {:9.4f}"
# 把輸入元組的最後一個元素拆包到由變數構成的元組中
for name, cc, pop, (latitude, longitude) in metro_areas:
    if longitude <= 0:
        print(fmt.format(name, latitude, longitude))
        
# 結果:
                |   lat.    |   long.  
Mexico City     |   19.4333 |  -99.1333
New York-Newark |   40.8086 |  -74.0204
Sao Paulo       |  -23.5478 |  -46.6358
複製程式碼

3.3 具名元組(命名元組)

上篇中有所涉及。collections.namedtuple是一個工廠函式,它可以建立一個帶欄位名的元組和一個有名字的類——這個帶名字的類對除錯程式有很大幫助。

namedtuple構造的類的例項所消耗的記憶體跟元組是一樣的,因為欄位名都存在對於的類中。這個例項跟普通物件例項比起來要小一些,因為Python不會用__dict__來存放這些例項的屬性。

from collections import namedtuple

City = namedtuple("City", "name country population coordinates")
tokyo = City("Tokyo", "JP", 36.933, (35.689722, 139.691667))
print(tokyo)
print(tokyo.population)
print(tokyo[1])

print(City._fields)
LatLong = namedtuple("LatLong", "lat long")
delhi_data = ("Delhi NCR", "IN", 21.935, LatLong(28.613889, 77.208889))
delhi = City._make(delhi_data)
print(delhi._asdict())
for key, value in delhi._asdict().items():
    print(key + ":", value)

# 結果:
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722, 139.691667))
36.933
JP
('name', 'country', 'population', 'coordinates')
OrderedDict([('name', 'Delhi NCR'), ('country', 'IN'), ('population', 21.935),
             ('coordinates', LatLong(lat=28.613889, long=77.208889))])
name: Delhi NCR
country: IN
population: 21.935
coordinates: LatLong(lat=28.613889, long=77.208889)
複製程式碼
  • 第3行:建立一個具名元組需要兩個引數,一個是類名,一個是類的各欄位名。後者可以是由數個字串組成的可迭代物件,或者是由空格分隔的字串;
  • 第6,7行:可通過欄位名或位置來獲取一個欄位的資訊;
  • 第9行:_fields屬性是一個包含這個類所有欄位名的元組;
  • 第12行:_make()通過接受一個可迭代物件來生成這個類的一個例項,它的作用跟City(*delhi_data)是一樣的。
  • 第13行:_asdict()把具名元組以collections.OrderedDict的形式返回。
  • 注意第10,27行!

3.4 作為不可變列表的元組

除了跟增減元素相關的方法外,元組支援列表的其他所有方法。還有一個例外就是元組沒有__reversed__方法,但這方法只是個優化,reversed(my_tuple)這個方法在沒有__reversed__的情況下也是合法的。

4. 切片

切片在Python基礎中介紹了一些遍歷的基本操作,這裡補充一些高階的用法。

4.1 切片賦值

>>> test = list(range(6))
>>> test
[0, 1, 2, 3, 4, 5]
# 指定步長賦值
>>> test[3::2] = [11, 22]
>>> test
[0, 1, 2, 11, 4, 22]
# 將列表變長(也可以變短)
>>> test[1:3] = [7, 8, 9]
>>> test
[0, 7, 8, 9, 11, 4, 22]
>>> test[1:3] = 100
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: can only assign an iterable
>>> test[1:3] = [100]
[0, 100, 9, 11, 4, 22]
複製程式碼

4.2 有名字的切片

Python中有一個切片類(slice),可以用它建立切片物件:

temp = "adfadfadfadfafasdf"
TEST = slice(2, 8)  # 一般大寫
print(temp[TEST])

# 結果:
fadfad
複製程式碼

4.3 多維切片和省略

[ ]運算子中還可以使用以逗號分開的多個索引或者切片,比如第三方庫Numpy中就用到了這個特性,二維的numpy.ndarray就可以用a[i, j]來獲取值(這裡的語法和C#一樣,相當於C/C++中的a[i][j]),或者a[m:n, k:l]來獲得二維切片。要正確處理這種語法,物件的特殊方法__getitem____setitem__需要以元組的形式來接收a[i, j]中的索引,即,如果要得到a[i, j],Python會呼叫a.__getitem__((i, j))。關於多維切片的例子在本文後面演示。

省略(ellipsis)的寫法是三個英語句點(...),而不是Unicode碼位U+2026表示的半個省略號(和前面三個句點幾乎一模一樣)。省略在Python直譯器眼裡是一個符號,而實際上它是Elllipsis物件的別名,而Ellipsis物件又是ellipsis類的單一例項(ellipsis是類名,全小寫,而它的內建例項寫作Ellipsis。這跟bool是小寫,而它的兩個例項TrueFalse是大寫一個道理)。它可以當做切片規範的一部分,也可用在函式的引數列表中,如f(a,...,z),或a[i: ...]。在Numpy中,...用作多維陣列切片的快捷方式,即x[i, ...]就是x[i, :, :, :]的縮寫。

筆者暫時還沒發現Python標準庫中有任何Ellipsis或者多維索引的用法。這些句法上的特性主要是為了支援使用者自定義類或者擴充套件,Numpy就是一個例子。

5. 對序列使用+和*

通常+號兩側的序列由相同型別的資料所構成(當然不同型別的也可以相加),返回一個新序列。如果想把一個序列複製幾份再拼接,更快捷的做法是乘一個整數:

>>> [1, 2] + [3]
[1, 2, 3]
>>> [1, 2] * 2
[1, 2, 1, 2]
>>> 5 * "abc"
'abcabcabcabcabc'
複製程式碼

注意:這裡有深淺複製的問題,如果在A * n這個語句中,序列A中的元素b是對其他可變物件的引用的話,則新序列中A2中的n個元素b1……bn都指向同一個位置,即對b1bn中任意一個賦值,都會影響其他元素。下面以一個建立多維陣列的例子來說明這個情況(字串是不可變物件,而列表是可變物件!):

正確的寫法:

board = [["_"] * 3 for i in range(3)]
print(board)
board[1][2] = "X"
print(board)
# 等價於:
board = []
for i in range(3):
    row = ["_"] * 3
    board.append(row)

# 結果:
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
複製程式碼

錯誤的寫法:

weird_board = [["_"] * 3] * 3
print(weird_board)
weird_board[1][2] = "X"
print(weird_board)
# 等價於:
weird_board = []
row = ["_"] * 3
for i in range(3):
    weird_board.append(row)

# 結果:
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
[['_', '_', 'X'], ['_', '_', 'X'], ['_', '_', 'X']]
複製程式碼

6. 序列的增量賦值

增量賦值運算子+=*=的表現取決於它們的第一個操作物件,以+=為例。+=背後的特殊方法是__iadd__(用於“就地加法”),如果一個類沒有實現該方法,則會呼叫__add__。例如 a += b,如果a實現了__iadd__,則直接呼叫該方法,修改的是a,不會產生新物件,而如果沒有實現該方法,則會呼叫__add__,執行的運算實際是 a = a + b,該運算會生成一個新變數,儲存a + b的結果,然後再把該新變數賦值給a

總體來說,可變序列一般都實現了__iadd__,而不可變序列根本就不支援這個操作。對不可變序列執行重複拼接操作的話,效率很低,因為每次都會生成新物件,而直譯器需要把原來物件中的元素先複製到新物件中,然後再追加新元素。但str是個例外,因為對字串做+=操作是在太普遍了,於是CPython對它做了優化:str初始化時,程式會為它預留額外的可擴充套件空間,因此做增量操作時不會涉及複製原有字串到新位置的操作。

一個關於+=的謎題

對於以下操作,大家猜想會得到什麼樣的結果:

>>> t = (1, 2, [3, 4])
>>> t[2] += [5, 6]
複製程式碼

它的結果是報錯,但t依然被改變了:

# 緊接上述程式碼
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [3, 4, 5, 6])
# 如果是t[2].extend([5, 6])則不會報錯
複製程式碼

如果我們看Python表示式 s[a] += b的位元組碼,便不難理解上述結果:

>>> import dis
>>> dis.dis("s[a] += b")
  1           0 LOAD_NAME                0 (s)
              2 LOAD_NAME                1 (a)
              4 DUP_TOP_TWO
              6 BINARY_SUBSCR
              8 LOAD_NAME                2 (b)
             10 INPLACE_ADD
             12 ROT_THREE
             14 STORE_SUBSCR
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE
複製程式碼

從上述結果可以看出:

  • 第6行:將s[a]的值存入TOS(Top Of Stack,棧頂);
  • 第8行:計算TOS += b, 這一步能夠完成,因為TOS指向一個可變物件;
  • 第14行:s[a] = TOS,報錯,因為s是個元組,不可變。

從上述操作可以得到3個教訓:

  • 不要把可變物件放在元組中;
  • 增量賦值不是一個原子操作。從上面的結果可以看出,它雖丟擲了異常,但仍完成了操作;
  • 檢視Python位元組碼並不難,而且它對我們瞭解程式碼背後的執行機制很有幫助。

7. 用bisect來管理已排序的序列

bisect模組包含兩個主要函式,bisectinsort,這兩個函式都利用二分查詢演算法在有序列表中查詢或插入元素。

bisect用於查詢元素的位置:biisect(haystack, needle)。它返回needlehaystack中的位置index,如果要插入元素,可以在找到位置後,再呼叫haystack.insert(index, new_ele),但也可以用bisect模組中的insert直接插入,並且該方法速度更快。

Python的高產貢獻者Raymond Hettinger寫了一個排序集合模組sortedcollection,該模組整合了bisect功能,且比獨立的bisect更易用。

bisect需要注意兩點:

  • 兩個可選引數lohilo預設值是0,hi預設值是序列的長度,即len()作用域該序列的返回值。
  • bisect函式其實是bisect_right函式的別名,它返回的位置是與needle相等的元素的後一個位置,而它的兄弟函式bisect_left則返回的是與needle相等的元素的位置。
>>> import bisect
>>> test = [1, 2, 3, 4, 5, 6, 7]
>>> bisect.bisect(test,1)
1
>>> bisect.bisect_left(test,1)
0
複製程式碼

相應的,模組中insort也有兩個版本,insortinsort_right的別名,它也有兩個可選引數lohiinsort_left的背後呼叫的就是bisect_left

>>> bisect.insort(test, 1.0)
>>> test
[1, 1.0, 2, 3, 4, 5, 6, 7]
>>> bisect.insort_left(test, 1.0)
>>> test
[1.0, 1, 1.0, 2, 3, 4, 5, 6, 7]
複製程式碼

8. 當列表不是首選時

當我們有特定的資料集時,list並不一定是首選,比如存放1000萬個浮點數,陣列(array)的效率就要高很多,因為陣列的背後並不是float物件,而是數字的機器翻譯,也就是位元組表述。這點和C語言中的陣列一樣。再比如,如果要頻繁對序列做先進先出的操作,deque(雙端佇列)的速度應該會更快。

8.1 陣列

如果需要一個只含數字的列表,array.array會比list更高效,它支援所有跟可變列表有關的操作,包括.pop.insert.extend等。另外陣列還支援從檔案讀取和存入檔案的更快的方法,比如.frombytes.tofile

陣列跟C語言陣列一樣精簡,建立一個陣列需要指定一個型別碼,這個型別碼用來表示在底層的C語言應該存放怎樣的資料型別,以下是array.array的操作例子:

from array import array
from random import random

print("\n\n")
floats = array("d", (random() for i in range(10 ** 7)))
print(floats[-1])
with open("floats.bin", "wb") as fp:
    floats.tofile(fp)

floats2 = array("d")
with open("floats.bin", "rb") as fp:
    floats2.fromfile(fp, 10 ** 7)
print(floats2[-1])
print(floats2 == floats)

# 結果:
0.8220703930498271
0.8220703930498271
True
複製程式碼

有人做過實驗,用array.fromfile從一個二進位制檔案讀出1000萬個雙精度浮點數只需要0.1秒(筆者電腦有點年代了,達不到這個速度),速度是從文字檔案裡讀取的60倍,因為後者會使用內建的float方法把每一行文字轉換成浮點數。另外,array.tofile寫入二進位制檔案也比寫入文字檔案快7倍。另外,這1000萬個數的bin檔案只佔8千萬位元組,如果是文字檔案的話,需要181515739位元組。

另一個快速序列化數字型別的方法是使用pickle模組,pickle.dump處理浮點陣列的速度幾乎和array.tofile一樣快,而且pickle可以處理幾乎所有的內建數字型別

8.2 記憶體檢視memoryview

memoryview是個內建類,它讓使用者在不復制記憶體的情況下操作同一個陣列的不同切片。memoryview的概念受到了Numpy的啟發。

記憶體檢視其實是泛化和去數學化的Numpy陣列。它讓你在不需要複製內容的前提下,在資料結構之間共享記憶體。其中資料結構可以是任何形式,比如PIL圖片、SQLite資料庫和Numpy陣列等待。這個功能在處理大型資料集合的時候非常重要。

memoryview.cast的概念跟陣列模型類似,能用不同的方式讀取同一塊記憶體資料,而且記憶體位元組不會隨意移動。這有點類似於C語言的型別轉換。memoryview.cast會把同一塊記憶體裡的內容打包成一個全新的memoryview物件返回。

下面這個例子精確地修改一個陣列的某個位元組:

import array
# 16位二進位制整數
numbers = array.array("h", [-2, -1, 0, 1, 2])
memv = memoryview(numbers)
print(len(memv))
print(memv[0])
# 轉換成8位的無符號整數
memv_oct = memv.cast("B")
print(memv_oct.tolist())
# 這個座標剛好是第3個16位二進位制數的高位位元組
memv_oct[5] = 4
print(numbers)

# 結果:
5
-2
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
array('h', [-2, -1, 1024, 1, 2])
複製程式碼

8.3 NumPy和SciPy

拼接這NumPy和SciPy提供的高階陣列和矩陣操作,Python稱為科學計算應用的主流語言。NumPy實現了多維同質陣列(homogeneous array)和矩陣,這些資料結構不但能處理數字,還能存放其他由使用者定義的記錄。SciPy是基於NumPy的另一個庫,他提供了很多跟科學計算有關的演算法,專為線性代數、數值積分和統計學而設計。SciPy的高校和可靠性歸功於背後的C和Fortran程式碼,而這些跟計算有關的部分都源自於Netlib。SciPy把基於C和Fortran的工業級數學計算功能用互動式且高度抽象的Python包裝起來。

以下是一些NumPy二維陣列的基本操作:

>>> import numpy
>>> a = numpy.arange(12)
>>> a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

>>> type(a)
<class 'numpy.ndarray'>
# 陣列a的維度
>>> a.shape
(12,)
# 手動設定陣列維度,3行4列
>>> a.shape = 3, 4
>>> a
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
# 第2行
>>> a[2]
array([ 8,  9, 10, 11])
# 第2行第1列元素
>>> a[2, 1]
9
# 第1列元素
>>> a[:, 1]
array([1, 5, 9])
# 轉置
>>> a.transpose()
array([[ 0,  4,  8],
       [ 1,  5,  9],
       [ 2,  6, 10],
       [ 3,  7, 11]])
# 全部資料乘2
>>> a *= 2
>>> a
array([[ 0,  2,  4,  6],
       [ 8, 10, 12, 14],
       [16, 18, 20, 22]])
複製程式碼

NumPy也可讀取、寫入檔案:

# 從文字檔案中讀取資料
floats = numpy.loadtxt("filename.txt")
# 把陣列存入字尾為.npy的二進位制檔案,會自動加字尾名
numpy.save("filesave", floats)
# 從.npy檔案中讀取資料,這次load方法利用了一種叫做記憶體對映的機制,它讓
# 我們在記憶體不足的時候仍可以對陣列切片
floats2 = numpy.load("filesave.npy", "r+")
複製程式碼

這兩個庫都異常強大,它們也是一些其他庫的基礎,比如Pandas和Blaze資料分析庫。

8.4 雙向佇列和其他形式的佇列

利用.append.pop方法,可以將列表(list)變成棧和佇列。但刪除列表的第一個元素或在第一個元素前插入元素之類的操作會很耗時,因為會移動資料。如果經常要在列表兩端運算元據,推薦使用collections.deque類(雙向佇列)。它是一個執行緒安全、可快速從兩端新增刪除元素的資料型別。下面是它的操作示範:

# maxlen是個可選引數,表示佇列最大長度,該屬性一旦設定變不能修改
>>> dq = deque(range(10), maxlen=10)
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
# 佇列旋轉操作,接收引數n,當n>0時,佇列最右邊n個元素移動到最左邊
# 當n<0時,佇列最左邊n個元素移動到最右邊
>>> dq.rotate(3)
>>> dq
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)

>>> dq.rotate(-4)
>>> dq
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
# 佇列左邊新增一個元素-1,由於佇列長10,所以元素0被刪除
>>> dq.appendleft(-1)
>>> dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
# 佇列右邊新增三個元素,擠掉了最前面的三個元素
>>> dq.extend([11, 22, 33])
>>> dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
# 注意新增的順序
>>> dq.extendleft([10, 20, 30, 40])
>>> dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)
複製程式碼

該資料結構還有許多其他操作,appendpopleft是原子操作,可在多執行緒中安全地使用,不用擔心資源鎖的問題。

8.5 Python標準庫中的佇列

  • queue:提供了同步(執行緒安全)類QueueLifoQueuePriorityQueue,不同的執行緒可以利用這些資料型別來交換資訊。這三個類在佇列滿的時候不會丟掉舊元素,而是被鎖住,直到某執行緒移除了某個元素。這一特性讓這些類很適合用來控制活躍執行緒的數量。
  • multiprocessing:實現了自己的Queue,和queue.Queue類似,設計給程式間通訊用的。同時還有一個專門的multiprocessing.JoinableQueue類,該類讓任務管理變得方便。
  • asyncio:從Python3.4新增的包,包含QueueLifoQueuePriorityQueueJoinableQueue,這些類受queuemultiprocessing模組的影響,但是為非同步程式設計裡的任務管理提供了專門的便利。
  • heapq:和上述三個模組不同,它沒有佇列類,而是提供了heappushheappop方法,讓使用者可以把可變序列當作堆佇列或者優先佇列來使用。

9. 補充

  • Python入門教材往往會強調列表可以容納不同型別的元素,但實際上這樣做並沒有什麼特別的好處。之所以用列表來存放東西,是期待在稍後使用它的時候,其中的元素能有一些共有的特性。Python3中,如果列表裡的元素不能比較大小,則是不能對列表進行排序的。元組則恰恰相反,它經常用來存放不同型別的元素,這也符合它的本質,元組就是用作存放彼此之間沒有關係的資料的記錄。
  • list.sortsortedmaxmin函式的key引數是個很棒的設計,相比於其他語言中雙引數比較函式,這裡的引數key只需提供一個單引數函式來提取或計算一個值作為比較大小的標準。說它更高效,是因為在每個元素上,key函式只被呼叫一次。誠然,在排序的時候,Python總會比較兩個鍵(key),但那一階段的計算髮生在C語言那一層,這樣會比呼叫使用者自定義的Python比較函式更快。key引數也能讓你對一個混有數字字元和數值的列表進行排序,只需決定到底是將字元看做數值(數值排序),還是將數值看成字元(ASCII排序),即key到底是等於int還是等於str
  • sortedlist.sort背後的排序演算法是Timsort,它是一種自適應演算法,會根據原始資料的順序特點交替使用插入排序(數列基本有序時)和歸併排序(沒什麼規律時),以達到最佳效率。這樣的演算法被證明是有效的,因為來自真實世界的資料通常是有一定的順序特點的。Timsort在2002年的時候首次用在CPython中,自2009年起,Java和Android也開始使用這個演算法。後來該演算法被廣為人知,是因為在Google對Sun的侵權案中,Oracle把Timsort中的一些相關程式碼作為了呈堂證供。Timsort的創始人是Tim Peters,一位高產的Python核心開發者,他也是“Python之禪”的作者之一。

迎大家關注我的微信公眾號"程式碼港" & 個人網站 www.vpointer.net ~

Python學習之路21-序列構成的陣列

相關文章