1. 程式人生 > 實用技巧 >第六章 深入python的set和dict

第六章 深入python的set和dict

6.1 dict的abc繼承關係

首先講解map型別,dict實際上就屬於Mapping型別。

from collections.abc import Mapping, MutableMapping
#dict屬於mapping型別

a = {}
print (isinstance(a, MutableMapping))

點進去看

__all__ = ["Awaitable", "Coroutine",
           "AsyncIterable", "AsyncIterator", "AsyncGenerator",
           "Hashable", "Iterable", "Iterator", "Generator", "Reversible",
           "Sized", "Container", "Callable", "Collection",
           "Set", "MutableSet",
           "Mapping", "MutableMapping",
           "MappingView", "KeysView", "ItemsView", "ValuesView",
           "Sequence", "MutableSequence",
           "ByteString",
           ]

有Mapping和MutableMapping,MutableMapping屬於一個可修改的Mapping。

dict就是屬於MutableMapping。

點選看MutableMapping的structure。

定義有__setitem__,__delitem__這兩個抽象方法,跟前面的序列型別是一樣的。還定義了很多方法如pop、update、setdefault......

我們先看繼承關係

繼承的是Mapping,點進去看Mapping

Mapping裡面多了一個__getitem__這個抽象方法,還有__contaions__實際上是過載了Collection

這個Collection和序列當中是一樣的,繼承於Sized、Iterable和Container。所以我們也可以知道,dict和list有很多方法是一樣的。

之前說過dict屬於mapping型別,那麼可以判斷dict是否屬於MutableMapping

from collections.abc import Mapping, MutableMapping
#dict屬於mapping型別
a = {}
print (isinstance(a, MutableMapping))
D:\python3\python.exe E:/pyproject/AdvancePython-master/chapter06/dict_abc.py
True

Process finished with exit code 0

來看看isinstance是怎麼做的,首先a是一個dict型別。這個a,不是繼承了MutableMapping,而是實現了MutableMapping裡邊的一些方法(魔法函式)。

6.2 dict的常用操作

點進去看dict的原始碼,Python的dict是用C語言寫的。

1.首先看clear方法

“remove all all items from D"就是清空資料

用簡單的方式宣告一個dict,然後直接clear

a = {"bobby1":{"company":"imooc"},
     "bobby2": {"company": "imooc2"}
     }
print(a)
a.clear()
print(a)
{'bobby1': {'company': 'imooc'}, 'bobby2': {'company': 'imooc2'}}
{}

2.然後看copy方法

”a shallow copy of D“,返回的是一個淺拷貝(共享記憶體,物件指向同一個指標)。

如果做深拷貝,要用一個包copy

import copy

new_dict = copy.deepcopy(a)
new_dict["bobby1"]["company"] = "imooc3"

修改new_dict的資料不會同時修改a的資料

3.再看fromkeys

fromkeys是一個靜態方法,從一個可迭代物件(key,value)建立一個新的字典。

new_list = ["bobby1", "bobby2"]
new_dict = dict.fromkeys(new_list, {"company":"imooc"})
new_dict1 = dict.fromkeys(new_list, [1,2])
print(new_dict)
print(new_dict1)
{'bobby1': {'company': 'imooc'}, 'bobby2': {'company': 'imooc'}}
{'bobby1': [1, 2], 'bobby2': [1, 2]}

4.get方法

get方法用來增強從dict通過key獲取資料的功能,如果key不存在,則返回一個制定的預設值。

new_list = ["bobby1", "bobby2"]
new_dict = dict.fromkeys(new_list, {"company":"imooc"})
value = new_dict.get("bobby",{})
print(value)
{}

5.items方法

經常在for迴圈中使用

new_list = ["bobby1", "bobby2"]
new_dict = dict.fromkeys(new_list, {"company":"imooc"})
for key,value in new_dict.items():
    print(key,value)
bobby1 {'company': 'imooc'}
bobby2 {'company': 'imooc'}

6......還有很多方法

7.setdefault方法

setdefault會做兩件事

1.如果dict裡面沒有對應的key,則新增該key和預設值

2.從dict裡取出對應key的值

new_list = ["bobby1", "bobby2"]
new_dict = dict.fromkeys(new_list, {"company":"imooc"})
default_value = new_dict.setdefault("bobby","imooc")
print(default_value)
print(new_dict)
imooc
{'bobby1': {'company': 'imooc'}, 'bobby2': {'company': 'imooc'}, 'bobby': 'imooc'}

8.update方法

update可以合併兩個dict物件

new_list = ["bobby1", "bobby2"]
new_dict = dict.fromkeys(new_list, {"company":"imooc"})
print(new_dict)
new_dict.update({"bobby":"imooc"})
print(new_dict)
{'bobby1': {'company': 'imooc'}, 'bobby2': {'company': 'imooc'}}
{'bobby1': {'company': 'imooc'}, 'bobby2': {'company': 'imooc'}, 'bobby': 'imooc'}

update也可以處理iterable物件

new_list = ["bobby1", "bobby2"]
new_dict = dict.fromkeys(new_list, {"company":"imooc"})
print(new_dict)
new_dict.update(bobby='imooc')
print(new_dict)
{'bobby1': {'company': 'imooc'}, 'bobby2': {'company': 'imooc'}}
{'bobby1': {'company': 'imooc'}, 'bobby2': {'company': 'imooc'}, 'bobby': 'imooc'}

這個iterable物件可以是list裡面是tuple的形式

new_list = ["bobby1", "bobby2"]
new_dict = dict.fromkeys(new_list, {"company":"imooc"})
print(new_dict)
new_dict.update([("bobby","imooc")])
print(new_dict)
{'bobby1': {'company': 'imooc'}, 'bobby2': {'company': 'imooc'}}
{'bobby1': {'company': 'imooc'}, 'bobby2': {'company': 'imooc'}, 'bobby': 'imooc'}

這個iterable物件可以是tuple裡面是tuple的形式

new_list = ["bobby1", "bobby2"]
new_dict = dict.fromkeys(new_list, {"company":"imooc"})
print(new_dict)
new_dict.update((("bobby","imooc")))
print(new_dict)
{'bobby1': {'company': 'imooc'}, 'bobby2': {'company': 'imooc'}}
{'bobby1': {'company': 'imooc'}, 'bobby2': {'company': 'imooc'}, 'bobby': 'imooc'}

6.3 dict的子類

python中一切都可以繼承,所以dict也是可以繼承的,但是不建議去繼承python中用C語言寫的資料結構(list,dict...)。舉個例子說明一下:

#不建議繼承list和dict
class Mydict(dict):
    def __setitem__(self, key, value):
        super().__setitem__(key, value*2)

my_dict = Mydict(one=1)
print (my_dict)
my_dict['one'] = 1
print(my_dict)

子類重寫__setitem__方法,然後呼叫呼叫父類。可以看到類例項化,並沒有呼叫覆蓋的方法,而使用[]的方法,則生效。這事因為在某些情況下,用C語言寫的dict不會去呼叫覆蓋的方法。

如果要繼承dict,建議用collections模組中的UserDict,我們可以點進去看下UserDict。

它裡面的一些方法,都是用python重新寫過,用的不是C語言。

from collections import UserDict

class Mydict(UserDict):
    def __setitem__(self, key, value):
        super().__setitem__(key, value*2)

my_dict = Mydict(one=1)
print (my_dict)
{'one': 2}

說到繼承的問題,講解一個python內建的dict的一個子類,叫做defaultdict。先看UserDict,裡面有一個__missing__方法,在找不到key的時候,會呼叫這個方法。再看defaultdict的原始碼

實際上defaultdict重寫了__missing__方法,當找不到某個key時,會把這個key存進來,賦值為self.default_facory()。

from collections import defaultdict

my_dict = defaultdict(dict)
my_value = my_dict["bobby"]
print(my_value)
{}

本來如果dict裡沒有key的話,是會報keyerror的,但這裡就會是一個空的dict。沒有bobby這個key的時候,進入了defaultdict的__missing__方法。

6.4 set和frozenset

set是集合,frozenset是不可變集合(凍結集合)。set是無序,不重複的。

看set的原始碼

接受的引數是一個iterable(可迭代物件),所以我們用字串,list,tuple都是可以的。

s = set('abcdee')
print(s)
s = set(['a','b','c','d','e'])
print(s)
{'d', 'b', 'c', 'e', 'a'}
{'d', 'b', 'c', 'e', 'a'}

set的初始化方式很類似dict,要注意

s={'a','b'}
# s={'a':"11",'b':"22"}
print(type(s))
<class 'set'>

對set可以用add新增值,但是對frozenset是無法使用add新增值的,frozenset一旦設定就無法修改。

s={'a','b'}
s.add('c')
print(s)

s = frozenset("abcde") #frozenset 可以作為dict的key
print(s)

frozenset是不可變的,相對於可變型別來說,有一個好處是可以作為dict的key,這個非常有用,對dict來說,key是需要一個恆定的值。

向set新增 資料,最簡單的方式就是add方法。我們可以看一下set的原始碼

set有很多魔法函式,還有很多方法。

update可以把兩個set合併成一個set

s = {'a','b', 'c'}
another_set = set("cef")
s.update(another_set)
print(s)
{'e', 'a', 'f', 'c', 'b'}

difference可以計算集合的差集

s = {'a','b', 'c'}
another_set = set("cef")
re_set = s.difference(another_set)
print(re_set)
{'a', 'b'}

關於差集、交集、並集的運算是由魔法函式實現的

s = {'a','b', 'c'}
another_set = set("cef")
print(s - another_set)
print(s & another_set)
print(s | another_set)
{'b', 'a'}
{'c'}
{'b', 'c', 'e', 'f', 'a'}

__ior__實現了並集,__isub__實現了差集,__ixor__實現了交集。set的效能是非常高的,實現原理和list的原理相同(雜湊),所以查詢元素的時間複雜度是很低的(o(1))。

判斷一個元素是否在set中,直接使用if,in

re_set = {'a','b', 'c'}
if "c" in re_set:
    print ("i am in set")
i am in set

能使用in是因為裡面有一個魔法函式

frozenset和set的操作是一樣的。

6.5 dict和set的實現原理

為什麼要了解實現原理,當我們使用一個數據結構的時候,當我們知道它的原理之後就會知道在什麼情況下用dict以及為什麼要用dict。

來看一下測試dict和list效能的程式碼。去10000(100000,1000000)個元素裡查詢1000個元素要花費的時間(dict和list都嘗試)。

def load_list_data(total_nums, target_nums):
    """
    從檔案中讀取資料,以list的方式返回
    :param total_nums: 讀取的數量
    :param target_nums: 需要查詢的資料的數量
    """
    all_data = [] # 我們要查詢的1000個元素
    target_data = [] # 所有元素
    file_name = "G:/慕課網課程/AdvancePython/fbobject_idnew.txt"
    with open(file_name, encoding="utf8", mode="r") as f_open:
        for count, line in enumerate(f_open):
            if count < total_nums: # 小於total_nums時新增到all_data
                all_data.append(line)
            else:
                break

    for x in range(target_nums):
        random_index = randint(0, total_nums) # 生成隨機值(int 索引)
        if all_data[random_index] not in target_data: # 如果取出來的值不在target_data
            										# 裡,則新增到target_data裡
            target_data.append(all_data[random_index])
            if len(target_data) == target_nums: # target_data裡面的資料量=target_nums
                break           				# 停止

    return all_data, target_data

load_list_data函式去檔案讀取資料。返回資料後使用find_test測試。

def find_test(all_data, target_data):
    #測試執行時間
    test_times = 100 # 測試次數
    total_times = 0
    import time
    for i in range(test_times): # 查詢邏輯,for迴圈
        find = 0
        start_time = time.time()
        for data in target_data:
            if data in all_data: # 用if、in的方式查詢
                find += 1
        last_time = time.time() - start_time
        total_times += last_time
    return total_times/test_times # 返回一個平均時間

還有一個load_dict_data函式,功能和load_list_data是一樣的,但是變成了測試dict。

def load_dict_data(total_nums, target_nums):
    """
    從檔案中讀取資料,以dict的方式返回
    :param total_nums: 讀取的數量
    :param target_nums: 需要查詢的資料的數量
    """
    all_data = {}
    target_data = []
    file_name = "G:/慕課網課程/AdvancePython/fbobject_idnew.txt"
    with open(file_name, encoding="utf8", mode="r") as f_open:
        for count, line in enumerate(f_open):
            if count < total_nums:
                all_data[line] = 0
            else:
                break
    all_data_list = list(all_data)
    for x in range(target_nums):
        random_index = randint(0, total_nums-1)
        if all_data_list[random_index] not in target_data:
            target_data.append(all_data_list[random_index])
            if len(target_data) == target_nums:
                break

    return all_data, target_data

執行測試

all_data, target_data = load_dict_data(2000000, 1000)
last_time = find_test(all_data, target_data)
print(last_time)

結論是

  1. dict查詢的效能遠遠大於list
  2. dict查詢元素開銷不會隨著dict裡資料量的增大而增大

這個就涉及到dict後面實現的原理,是雜湊表

dict中必須要保證key是可雜湊的,如上圖計算a的雜湊值(雜湊值),和7做 &(與)位運算,如果計算出來是0這個偏移量,就把key:'a' value:1 放在0位置處。對b、c做同樣的雜湊運算,計算偏移量並存儲資料。如果雜湊z時有衝突,則重新計算z的雜湊值(重新計算的方法有很多種)。

可以看到雜湊表裡有很多空白,因為當雜湊表可利用空間少於1/3時,就會去申請更多的空間,然後把整張表拷貝到另外的空間中去,保證資料之間的衝突概率較小。

在雜湊表中查詢資料,是對key進行雜湊處理後,直接獲得了記憶體地址,查詢步驟只有一步(因為陣列是連續的空間,不需要像連結串列那樣需要從頭遍歷),所以陣列(array)取數的時間複雜度是O(1)。

比如查詢a的值,首先計算a的雜湊值,然後利用雜湊值定位表元

如果表元為空,意味著a沒有對應的資料

如果表元不為空,還要去判斷裡面資料的key和a是否相同,因為可能有衝突存在,如果存a資料時有衝突,那麼a的雜湊值是修改過的,意味著a的對應資料在另一個表元上。

dict的幾個特性

  1. dict的key和set的值,都是用的雜湊儲存,所以必須要可雜湊的。不可變物件都是可雜湊的,比如str、fronzenset、tuple,這些都可以放到set中或作為dict的key。如果自己實現一個類,就可以過載__hash__這個魔法函式,保證返回一個固定的值,那麼這個類物件就是可雜湊的。
  2. dict的記憶體花銷大,使用的雜湊表會有很大的空白python內部的物件,和自己定義的物件,都是用dict包裝的。
  3. dict的儲存順序和元素新增順序有關,如果有衝突,修改的是後新增元素的雜湊值,可能會儲存在靠前的位置,也可能儲存在靠後的位置。所以dict一般就是無順序的儲存。當然orderdict是有順序的儲存。
  4. 新增資料有可能改變已有資料的順序,當表元小於1/3時,會重新申請空間,然後把原來的資料重新插入新的空間,由於新空間比原空間大,很有可能原來資料儲存位置會改變(因為重新分配記憶體位置了)。