第六章 深入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。

image-20200818114003008

定义有__setitem__,__delitem__这两个抽象方法,跟前面的序列类型是一样的。还定义了很多方法如pop、update、setdefault......

我们先看继承关系

image-20200819141419564

继承的是Mapping,点进去看Mapping

image-20200819141629649

Mapping里面多了一个__getitem__这个抽象方法,还有__contaions__实际上是重载了Collection

image-20200819141926898

这个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方法

image-20200824094527000

“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方法

image-20200824100826612

”a shallow copy of D“,返回的是一个浅拷贝(共享内存,对象指向同一个指针)。

如果做深拷贝,要用一个包copy

import copy

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

修改new_dict的数据不会同时修改a的数据

3.再看fromkeys

image-20200824102517269

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方法

image-20200824103410533

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方法

image-20200824103815949

经常在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方法

image-20200824104029367

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方法

image-20200824110030215

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。

image-20200824113032187

它里面的一些方法,都是用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的源码

image-20200824141608232

实际上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的源码

image-20200824142540036

接受的参数是一个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的源码

image-20200824150345825

set有很多魔法函数,还有很多方法。

image-20200824150442313

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'}

image-20200824152522553

__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是因为里面有一个魔法函数

image-20200824172309231

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后面实现的原理,是哈希表

image-20200825141640771

dict中必须要保证key是可哈希的,如上图计算a的哈希值(散列值),和7做 &(与)位运算,如果计算出来是0这个偏移量,就把key:'a' value:1 放在0位置处。对b、c做同样的哈希运算,计算偏移量并存储数据。如果哈希z时有冲突,则重新计算z的哈希值(重新计算的方法有很多种)。

可以看到哈希表里有很多空白,因为当哈希表可利用空间少于1/3时,就会去申请更多的空间,然后把整张表拷贝到另外的空间中去,保证数据之间的冲突概率较小。

在哈希表中查找数据,是对key进行哈希处理后,直接获得了内存地址,查找步骤只有一步(因为数组是连续的空间,不需要像链表那样需要从头遍历),所以数组(array)取数的时间复杂度是O(1)。

image-20200825142939444

比如查找a的值,首先计算a的散列值,然后利用散列值定位表元

image-20200825143124749

如果表元为空,意味着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时,会重新申请空间,然后把原来的数据重新插入新的空间,由于新空间比原空间大,很有可能原来数据存储位置会改变(因为重新分配内存位置了)。
posted @ 2020-09-10 20:34  yueqiudian  阅读(346)  评论(0编辑  收藏  举报