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