1. 程式人生 > >[python]list, tuple, dictionary, set的底層細節

[python]list, tuple, dictionary, set的底層細節

list, tuple, dictionary, set是python中4中常見的集合型別。在筆者之前的學習中,只是簡單了學習它們4者的使用,現記錄一下更深底層的知識。

列表和元組

列表和元組的區別是顯然的:列表是動態的,其大小可以該標;而元組是不可變的,一旦建立就不能修改。

實現細節

python中的列表的英文名是list,因此很容易和其它語言(C++, Java等)標準庫中常見的連結串列混淆。事實上CPython的列表根本不是列表(可能換成英文理解起來容易些:python中的list不是list)。在CPython中,列表被實現為長度可變的陣列。

從細節上看,Python中的列表是由對其它物件的引用組成的連續陣列

。指向這個陣列的指標及其長度被儲存在一個列表頭結構中。這意味著,每次新增或刪除一個元素時,由引用組成的陣列需要該標大小(重新分配)。幸運的是,Python在建立這些陣列時採用了指數過分配,所以並不是每次操作都需要改變陣列的大小。但是,也因為這個原因新增或取出元素的平攤複雜度較低。

不幸的是,在普通連結串列上“代價很小”的其它一些操作在Python中計算複雜度相對過高。

  • 利用 list.insert方法在任意位置插入一個元素——複雜度O(N)
  • 利用 list.delete或del刪除一個元素——複雜度O(N)
操作 複雜度
複製 O(N)
新增元素(在尾部新增) O(1)
插入元素(在指定位置插入) O(N)
獲取元素 O(1)
修改元素 O(1)
刪除元素 O(N)
遍歷 O(N)
獲取長度為k的切片 O(k)
刪除切片 O(N)
列表擴充套件 O(k)
測試是否在列表中 O(N)
min()/max() O(n)
獲取列表長度 O(1)

列表推導

要習慣用列表推導,因為這更加高效和簡短,涉及的語法元素少。在大型的程式中,這意味著更少的錯誤,程式碼也更容易閱讀。

>>>[i for
i in range(10) if i % 2 == 0] [0, 2, 4, 6, 8]

其它習語

1.使用enumerate.在迴圈使用序列時,這個內建函式可以方便的獲取其索引:

for i, element in enumerate(['one', 'two', 'three']):
    print(i, element)

result:

0 one
1 two
2 three

2.如果需要一個一個合併多個列表中的元素,可以使用zip()。對兩個大小相等的可迭代物件進行均勻遍歷時,這是一個非常常用的模式:

for item in zip([1, 2, 3], [4, 5, 6]):
    print(item)
(1, 4)
(2, 5)
(3, 6)

3.序列解包

#帶星號的表示式可以獲取序列的剩餘部分
>>>first, second, *reset = 0, 1, 2, 3
>>>first
0
>>>second
1
>>>reset
[2, 3]

字典

字典是python中最通用的資料結構之一。dict可以將一組唯一的鍵對映到相應的值。

我們也可以用前面列表推導的方式來建立一個字典。

squares = {number: number**2 for number in range(10)}
print(squares)

result:

{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

在遍歷字典元素時,有一點需要特別注意。字典裡的keys(), values()和items()3個方法的返回值不再是列表,而是檢視物件(view objects)。

  • keys(): 返回dict_keys物件,可以檢視字典所有鍵
  • values():返回dict_values物件,可以檢視字典的所有值
  • items():返回dict_items物件,可以檢視字典所有的{key, value}二元元組。

檢視物件可以動態檢視字典的內容,因此每次字典發生變化的時候,檢視都會相應的改變,見下面這個例子:

words = {'foo': 'bar', 'fizz': 'bazz'}
items= words.items()
words['spam'] = 'eggs'
print(items)

result:

dict_items([('foo', 'bar'), ('fizz', 'bazz'), ('spam', 'eggs')])

檢視無需冗餘的將所有值都儲存在記憶體中,像列表那樣。但你仍然可以獲取其長度(使用len),也可以測試元素是否包含在其中(使用in子句)。當然,檢視是迭代的。

實現細節

CPython使用偽隨機探測(pseudo-random probing)的散列表(hash table)作為字典的底層資料結構。由於這個實現細節,只有可雜湊的物件才能作為字典的鍵。

Python中所有不可變的內建型別都是可雜湊的。可變型別(如列表,字典和集合)就是不可雜湊的,因此不能作為字典的鍵。

字典的三個基本操作(新增元素,獲取元素和刪除元素)的平均事件複雜度為O(1),但是他們的平攤最壞情況複雜度要高得多,為O(N).

操作 平均複雜度 平攤最壞情況複雜度
獲取元素 O(1) O(n)
修改元素 O(1) O(n)
刪除元素 O(1) O(n)
複製 O(n) O(n)
遍歷 O(n) O(n)

還有一點很重要,在複製和遍歷字典的操作中,最壞的複雜度中的n是字典曾經達到的最大元素數目,而不是當前的元素數目。換句話說,如果一個字典曾經元素個數很多,後來又大大減小了,那麼遍歷這個字典可能會花費相當長的事件。因此在某些情況下,如果需要頻繁的遍歷某個詞典,那麼最好建立一個新的字典物件,而不是僅在舊字典中刪除元素。

字典的缺點和替代方案

使用字典的常見陷阱就是,它並不會按照鍵的新增順序來儲存元素的順序。在某些情況下,字典的鍵是連續的,對應的雜湊值也是連續值(例如整數),那麼由於字典的內部實現,元素的實現可能和新增的順序相同:

keys = {num: None for num in range(5)}.keys()
print(keys)

result:

dict_keys([0, 1, 2, 3, 4])

但是,如果雜湊方法不同的其它資料型別,那麼字典就不會儲存元素順序。

age = {str(i): i for i in range(100)}
keys = age.keys()
print(keys)

result:

dict_keys(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '90', '91', '92', '93', '94', '95', '96', '97', '98', '99'])

理論上,鍵的順序不應該是這樣的,應該是亂序。。。具體為什麼這樣,等以後明白了再補充

如果我們需要儲存新增順序怎麼辦?python 標準庫的collections模組提供了名為OrderedDicr的有序字典。

集合

集合是一種魯棒性很好的資料結構,當元素順序的重要性不如元素的唯一性和測試元素是否包含在集合中的效率時,大部分情況下這種資料結構極其有用。

python的內建集合型別有兩種:

  • set(): 一種可變的、無序的、有限的集合,其元素是唯一的、不可變的(可雜湊的)物件。
  • frozenset(): 一種不可變的、可雜湊的、無序的集合,其元素是唯一的,不可變的雜湊物件。
set([set([1, 2, 3]), set([2, 3, 4])])

result:

Traceback (most recent call last):
  File "/pycharm_project/LearnPython/Part1/demo.py", line 1, in <module>
    set([set([1, 2, 3]), set([2, 3, 4])])
TypeError: unhashable type: 'set'
set([frozenset([1, 2, 3]), frozenset([2, 3, 4])])

result:不會報錯

set裡的元素必須是唯一的,不可變的。但是set是可變的,所以set作為set的元素會報錯。

實現細節

CPython中集合和字典非常相似。事實上,集合被實現為帶有空值的字典,只有鍵才是實際的集合元素。此外,集合還利用這種沒有值的對映做了其它的優化。

由於這一點,可以快速的向集合中新增元素、刪除元素、檢查元素是否存在。平均時間複雜度為O(1),最壞的事件複雜度是O(n)。