Python字典hash表的模拟实现

本文学习自:《Python 源码深度剖析》

  • 感谢作者的传授
  • 若还不清楚字典的知识, 详细内容和知识点可点击了解

功能

主类(字典):PyDictObject

  • (1) add_entry(self, key, value) : 添加元素
  • (2)del_entry(self, key) : 删除元素
  • (3) get_entry(self, key): 查找键对应的值(与删除采用相同的函数,通过flag 标志区分)
  • (4) hash_list : (属性),可以遍历底层hash数组
  • (5) entry_list: (属性),遍历底层键值对数组
  • (6) 可通过简单的 for循环迭代,很方便
pd = PyDictObject()
''' 查看原始hash表存储情况 '''
# for 循环遍历
for i in pd.ma_keys:
    print(i)

# 或者 生成器表达式, 再转化为字典
a = (i for i in pd.hash_list)
print(list(a))

# 查看底层键值对数组情况
b = (i for i in pd.entry_list)
print(list(b))
内部功能:
  • 扩容, 伪扩容,缩容的实现。 hash冲突的预防,探测(线性探测,平方探测)
  • 其中探测分为 两种方式(线性探测和平方探测依次循环探测)
  • 修改或者是增加使用相同的函数(底层自动监测是否有相同的key,决定是否 更新数据 或者新增数据)

PyDictKeysObject

  • 完成主要的所有核心的功能

De_Entries

  • 键值对数组

Dk_Indices

  • 散列表

Entry

  • 散列表数组元素
  • 包含状态 和 键值对存储下标 两个属性

PyDictKeyEntry

  • 键值对对象
  • 定义了几个方法,操作键值对

主要的功能示意图:

扩容缩容(可能与源码的实现有些不同)

在这里插入图片描述

删除逻辑(字体较为潦草~)

  • 因为总是先进行比较,若不相同才会有 探测这一步,所以总觉得是 do while循环,先执行一次,再做做判断循环,但是 python中没有do while循环, 所有就是用了 while True: pass (具体参考删除逻辑的代码)
    在这里插入图片描述

测试

  • 循环10000次,除性能问题外,基本可以排除所有bug,不会因代码本身逻辑问题,导致代码异常出 bug,健壮性还可以。
  • 每次哈希探测的次数,均少于10次(异常退出设置的是十次,可能探测次数更低)

问题

  • (1) 首先并没有复刻一个对象的 头部,也就是没有考虑到删除或者修改一个对象时,内存管理,垃圾回收的问题。 因为没有设置 refcnt字段去 考虑垃圾回收。
  • (2)字典当中,有些字段并没有使用上,也就荒废了。
  • (3) 很多功能感觉很简单~,但是测试时会有很多逻辑上的漏洞和bug, 代码不断更新,完善。 即使是一个很小的东西,要想万无一失,都需要钻牛角尖,仔细考虑完善,很佩服 语言的开发者,实现整个语言的功能,还要避免漏洞。
  • (4) 本来想复刻一下,字典的遍历, for i in range dict.items等 按照键,按照值 等方式遍历, 但是 底层都是去 遍历 键值对数组, 但键值对数组的迭代器,是共用一个 i变量遍历的, 这就涉及到,当同时去 遍历键, 值等的时候, i值的问题所以就没实现

感悟:

起初设计的时候,感觉要写个很多代码,能实现一个大的功能,结果实现的时候,仅仅用了300行代码~ 另外很多东西只有实践的时候,才能去真正了解他的细节, 包括一个字段, 一段处理逻辑, 一丁点地方都需要去反复求证,才能下手。 另外对 程序的设计 还有思路方面有很大的收获, 诚然现在的代码,质量还不高,很多重要的地方,因为思维局限,写代码时还未考虑,但进步还是有的。

代码展示

class Me_Key():
    """ 定义数据描述器,限定 Me_key 值为可hash的 """

    def __init__(self, name):
        self.attrname = name

    def __get__(self, instance, owner):
        return getattr(instance, self.attrname)

    def __set__(self, instance, value):
        # 当数据是 字典或者列表,我们就认为是不可哈希的,返回错误
        if isinstance(value, dict) or isinstance(value, list):
            raise("键错误,不可哈希")
        setattr(instance, self.attrname, value)


class PyDictKeyEntry():
    """ 定义存储的键值对 对象,PyDictKeyEntry """

    me_key = Me_Key('_key')
    def __init__(self):
        """
        :param    me_hash: 键对象的 哈希值 ,避免重复调用 __hash__ 计算哈希值;
        :param    me_key: 键对象指针;
        :param    me_value: 值对象指针;
        """
        self.me_key = None 
        self.me_value = None
        self.me_hash = None
    
    def set_data(self, key, value):
        
        self.me_key = key
        self.me_value = value
        self.me_hash = hash(key)

    def del_data(self):   # 默认全部设置为None, 就是删除了
        self.me_key = None
        self.me_hash = None
        self.me_value = None
    

class Entry():
    """ 定义散列表每一个格子 """

    # 定义三种状态
    EMPTY = 1
    DUMMY = 2
    USED = 3

    def __init__(self):
        self.status = self.EMPTY
        self.index = None

class Dk_Indices():
    """ 定义dk_indices 散列表 """
    entry = Entry

    def __init__(self, n = 8):
        """
            散列表定义初始长度为8
        """
        self.hash_table = [self.entry() for i in range(n)]


class De_Entries():
    """ 定义存储键值对数组 """
    pke = PyDictKeyEntry

    def __init__(self, n = 5):
        self.i = -1
        self.length = n
        self.entries = [self.pke() for i in range(n)]
    
    def __iter__(self):
        self.i = -1
        return self
        
    def __next__(self):

        while self.i < self.length -1:
            self.i += 1
            return self.entries[self.i].me_key, self.entries[self.i].me_value
        raise StopIteration


class PyDictKeysObject():
    """ 定义哈希表对象结构 """

    indices = Dk_Indices
    entries = De_Entries
    def __init__(self):
        """
            :param    dk_size: 哈希表大小
            :param    dk_usable: 键值对数组可用个数
            :param    dk_nentries: 键值对已用个数 (因为删除数据了, 可用加上已用,并不等于键值对数组大小,还有的删除状态不再使用了),当数据删除时,已用个数减一,但是可用个数不变
            :param    dk_indices: 存储dk_entries数组对应元素下标(也是hash表)
            :param    dk_entries: 保存键值对的数组
            :param    de_deldatas: 删除元素的个数
        """
        # 初始化数据,散列表总长度8,键值对数组长度5(三分之二长),已用个数为0
        self.dk_size = 8
        self.dk_usable = 5
        self.dk_nentries = 0
        self.de_deldatas = 0
        self.dk_indices = self.indices()
        self.dk_entries = self.entries()

        # 用于迭代器计数
        self.i = -1
    
    def find_location(self, me_key, me_value):
        """ 寻找数据的 hash表存储位置,并插入 """
        ''' 官网的根据 对象hash值选择,生成不同的探测序列,我们搞简单的,利用线性和平方探测,交替进行,减少冲突 '''

        # 这里用一个技巧,取模运算,被代替为 按位与& 运算, 因为hash表的长度为dk_size =  2的n次方, 则2的n次方减一,二进制恰好是 低位全1,高位为0,相与即是等于 与 dk_size 取模运算,但更速度!https://blog.csdn.net/u014266077/article/details/80672995
        a = self.add_entries_data(me_key, me_value)
        if not a: 
            return
        self.insert_hash_index(a)
        # 数据插入成功,更新 self.dk_usable     self.dk_nentries 字段的值
        self.dk_usable -= 1
        self.dk_nentries += 1
        print("+++++", self.dk_usable)
    
    def insert_hash_index(self, a):
        location = a.me_hash & (self.dk_size - 1)
        entry = self.dk_indices.hash_table[location]
        s = self.status_1_2(entry)

        if not s:
            sequence = entry.status
            n = 1
            while sequence == 3:
                # 防止冲突算法不好,程序陷入死循环,最多查找十次
                n += 1
                if n > 10:
                    print("这~~ 执行十次都找不到插入的位置。。")
                    raise TimeoutError
                location = self.linear_detect(location)  # 线性探测
                temp =  self.dk_indices.hash_table[location]  
                sequence = temp.status
                if sequence == 3:
                    location = self.square_detect(location)  # 平方探测
                    temp =  self.dk_indices.hash_table[location]
                    sequence = temp.status
            self.status_1_2(temp)
    
    def del_element(self, key, flag = 1):
        """ 定义删除元素的函数 """

        try:
            hash_key = hash(key)
        except TypeError as e:
            print("提示:传入的键是不合法的,请重新尝试!")
            raise(e)
        location = hash_key & (self.dk_size - 1)
        entry = self.dk_indices.hash_table[location]
        n = 0
        while True:
            if n != 0:
                if n & 1 == 1:
                    location = self.linear_detect(location)  # 线性探测
                else:
                    location = self.square_detect(location)  # 平方探测
                entry = self.dk_indices.hash_table[location]

            if entry.status == 1:
                raise KeyError
            elif entry.status == 2:
                n += 1
                continue
            else:
                array_key_value = self.dk_entries.entries[entry.index]
                if hash_key == array_key_value.me_hash:
                    if flag == 2:
                        print("执行get()")
                        return array_key_value.me_value
                    print("执行删除")
                    self.de_deldatas += 1
                    entry.status = 2
                    entry.index = None
                    array_key_value.del_data()
                    break
                else:
                    n += 1
    

    def add_entries_data(self, key, value):
        """ 向键值对数组插入数据 """

        temp = self.dk_entries.pke()
        temp.set_data(key, value)
        for i in self.dk_entries.entries:
            if i.me_hash == temp.me_hash:
                print("执行修改操作")
                i.me_value = temp.me_value
                break
        else:
            # 判断是否需要扩容
            if not self.cheack():
                # 执行扩容
                self.expansion()
            a = self.dk_entries.entries[self.dk_nentries]
            a.set_data(key, value)
            return a
    
    def cheack(self):
        """ 插入数据时对 键值对数组进行检查,是否会出现元素溢出,进行相应的扩容,或者缩容操作 """
        return False if self.dk_usable == 0 else True

    def expansion(self):
        """ 扩容函数 """

        # used, 除去删除的,真正已用的, entries就是键值对数组的长度
        used = self.dk_nentries - self.de_deldatas 
        entries = (self.dk_size*2 // 3)
        if used > entries*3 // 4 :
            print("执行双扩容")
            # 重新初始化数据
            self.dk_size *= 2
            self.dk_indices = self.indices(self.dk_size)  # 和下面的初始化操作一样,可以 重写一个方法,但是程序执行速度太低了
            self.dk_usable = self.dk_size*2 // 3
            self.dk_nentries = 0
            # 分配键值对数组,与重新插入 hash表
            self.redistribute()
        elif used >= entries // 2 :
            print("执行伪扩容")
            self.dk_indices = self.indices(self.dk_size)
            self.dk_usable = self.dk_size*2 // 3
            self.dk_nentries = 0
            self.redistribute()
        else:
            print("执行缩容")
            # 默认最小长度是8,不允许缩容
            if self.dk_size == 8:
                return 
            n = 1
            i = 0
            while n < used * 3:  # 计算最小的满足 键值对为hash大小1/3, 的n (n表示2的n次方)
                n *= 2
                i += 1
            # 初始化数据
            self.dk_size = 2 ** i
            self.dk_indices = self.indices(self.dk_size)
            self.dk_usable = self.dk_size*2 // 3
            self.dk_nentries = 0
            self.redistribute()

    def redistribute(self):
        """ 重新分配 hash,键值对数组 """

        i = 0
        temp = self.entries(self.dk_size*2 // 3)
        for data in self.dk_entries.entries:
            if data.me_hash == None:
                continue
            temp.entries[i] = data
            self.insert_hash_index(data)
            self.dk_usable -= 1
            self.dk_nentries += 1
            i += 1
        self.dk_entries = temp

    def status_1_2(self, b):
        """ 转态转换: EMPTY 与 DUMMY """
        ''' 用来判断状态 1,2 说明不存在hash冲突,返回True,否则返回False '''
        if b.status != 3:
            b.index = self.dk_nentries
            b.status = 3    
            return True
        else:
            return False    

    def linear_detect(self, index):
        """解决hash冲突,开放地址法,线性探测方法"""
        # 经过试验,当后面 +3时, 循环一万次添加数据时,都不会出现死循环, 为0或 1都不行, 看来解决冲突的算法真的很重要
        return (index * 2 + 3) & (self.dk_size - 1)

    def square_detect(self, index):
        """ 平方探测 """
        return (index ** 2) & (self.dk_size - 1)

    # 定义成迭代器,方便查看 散列表内的元素存储情况
    def __iter__(self):
        self.i = -1  # 当多次for 循环时, i值必须重新初始化,从头开始
        return self
    
    def __next__(self):
        while self.i < self.dk_size: 
            self.i += 1
            if self.i == self.dk_size:
                break
            return self.dk_indices.hash_table[self.i].index, self.dk_indices.hash_table[self.i].status
        raise StopIteration


class PyDictObject():

    pdk = PyDictKeysObject
    
    def __init__(self):
        """初始化大整数
        :param    ma_used: 对象当前所保存的键值对个数
        :param    ma_version_tag:对象当前版本号,每次修改时更新
        :param    ma_keys:指向映射的hash表结构
        :param    ma_values: 分离模式下指向由所有 值对象 组成的数组
        """
        
        # 定长对象 公共头字段:PyObject_HEAD, 里面ob_refcnt, 
        self.ma_used = 0
        self.ma_version_tag = 1
        self.ma_keys = self.pdk()
        self.ma_values = None
    
    @property  # hash数组
    def hash_list(self):
        return self.ma_keys
    
    @property  # 键值对数组
    def entry_list(self):
        return self.ma_keys.dk_entries

    # 添加 键值对数据
    def add_entry(self, key, value):
        self.hash_list.find_location(key, value)

    # 查询一个 键对应的值
    def get_entry(self, key):
        return self.hash_list.del_element(key, flag = 2)

    # 删除键值对数组
    def del_entry(self, key):
        self.hash_list.del_element(key)

    def __iter__(self):
        return self

    # Entry数组的"不可变性", Python3.7中,支持按插入顺序迭代
    def __next__(self):
        pass



pd = PyDictObject()
# pd.add_entry(123, 789)

for i in range(0, 42):
    pd.add_entry(i, i)


for i in range(37):
    pd.del_entry(i)
pd.add_entry(48, 34)

pd.add_entry(39, "hash")

print(pd.get_entry(37))
''' 查看原始hash表存储情况 '''
# for 循环遍历
# for i in pd.ma_keys:
#     print(i)

# 或者 生成器表达式, 再转化为字典
a = (i for i in pd.hash_list)
print(list(a))

b = (i for i in pd.entry_list)
print(list(b))


 
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值