Python 中的哈希表:对字典的理解
有没有想过,Python中的字典为什么这么高效稳定。原因是他是建立在hash表上。了解Python中的hash表有助于更好的理解Python,因为Python中字典无处不在。
hash 函数
哈希函数是一个可以将任意长度的数据块映射到固定长度的值,这个步骤称为hash,也就是散列。
hash 函数有三个主要的特征:
- 计算迅速:计算一个数据块的hash值非常快
- 确定性:相同用字符串会产生相同的hash值
- 结果固定长度:不管输入的是,一个字节还是十个字节,或者上万个字节,结果总是预先确定的长度。
另一个特征在hash函数中非常普遍,即他们是单方向的:通过函数实现后,原始数据丢失了,我们可以通过字符串得到一个hash值,但不能通过一个hash也就是散列得到原始的字符串(因为有对数据降维的方法会造成数据的丢失)。这种特性并不是对所有hash函数的强制性规定,但是当需要加密安全时,这种性质还是挺好用的。
一些比较受欢迎的算法包括:MD5、SHA-1、SHA-2,NTLM.
关于hash的一些使用方法
很多东西都依赖hash,hash表就是其中一例,另一些用法是出于加密和的原因
一个具体的例子就是当你尝试从网上下载开源软件时。通常你都会发现一个关联文件,这个关联文件就是这个文件签名。这个签名仅仅是源文件的散列值它非常有用,你为你可以自己通过计算下载好的文件的散列值并与网站上提供的签名进行对比,这样就可以确认你自己下载的文件没有损坏。
另一种用法是存储用户的密码。你有没有问过你自己,当你忘掉了一个网站的登录密码的时候,你还想从这个网站上恢复登录,它只会让你重新确定一个新的登录密码而不是给你原来你选择的密码,这是因为网站并不会储存你完整的密码,而是你密码的hash值。
这么做是出于安全因素,因为如果黑客搞到了数据库的权限,他们不会知道你真实的密码,而是仅仅得到你密码的哈希值,又因为哈希函数是单向的,他们根本不可能从哈希值得到你的密码
Python 里的hash()函数
Python 中含有内置的函数去给对象生成哈希值,也就是hash()函数,这个函数将一个对象作为输入,返回一个整数的hash值。
内部的,这个函数涉及到.\__hash\__()这个输入对象的方法,所以呢,如果你想使你的自定义类可哈希化,你需要做的是实现.\__hash__()这个方法返回出一个整数,这个整数是基于你的对象的内部状态决定的。
先开始一些基本的小例子,先对数值进行散列
>>> hash(1)
1
>>> hash(10)
10
>>> hash(10.00)
10
>>> hash(10.01)
230584300921368586
>>> hash(-10.01)
-230584300921368586
如果你好奇为什么这些hash值看起来有不同的长度,请记住在Python中hash()函数返回的是整数对象,他们都会在标准的64位Python解释器中通过24字节呈现。
正如你可以看到的,整数的哈希值默认的就是它本身,不管你哈希的数据类型是什么,所以整数1和浮点数1.0哈希值都是1.
这个有什么特殊的么,这个展示了你之前学到的知识,也就是hash函数经常是单向的函数,如果两个不同的对象具有相同的hash值,根本不可能做反向工程,也就是从hash值返回到原始的对象,这也就使得被hash的原始数据类型的信息被丢失。
另几个有趣的可以关注的hash事情是,小数的散列值不同于它本身,负数具有负的哈希值。还有一点,就是如果两个对象具有相同的哈希值,称为哈希碰撞。
哈希一个字符串不同于对数值进行哈希。
>>> hash("Bad Behaviour")
7164800052134507161
DOS攻击(DOS代表拒绝服务)是指攻击者故意耗尽计算机系统的资源,使系统不再能够向客户端提供服务的攻击。在Scott Crosby演示的这个攻击的具体案例中,攻击可能会使目标系统充斥大量的数据,这些数据的哈希冲突导致目标系统使用更多的计算能力来解决冲突。
Python中可以hash的数据类型
在Python中,仅仅只有不可变数据类型可以被hash,然而每个自定义的对象在Python中都可以被hash,默认的他们的hash值是由他们的id派生的。也就意味着,同一个类的两个不同实例,默认的是得到不同的hash值
>>> class Car():
... velocity = 0
... direction = 0
... damage = 0
...
>>> first_car = Car()
>>> second_car = Car()
>>> hash(first_car)
274643597
>>> hash(second_car)
274643604
哈希表
现在你知道了什么是哈希函数,现在可以检测哈希表,哈希表是一个数据结构可以储存一堆键值对。
在哈希表中,键值对的所有建必须是可以哈希的,因为存储的对是通过使用其键的散列索引的。哈希表十分有用,Hash tables are very useful because the average number of instructions that are necessary to lookup an element of the table is independent of the number of elements stored in the table itself.哈希表非常有用,因为查找表中某个元素所需的平均指令数量与表中存储的元素数量无关,这就表明了不管你的表增长到成百上千次,查找特定元素的速度不会受到影响。
哈希表通常是通过创建可变数量的存储桶来实现的,这些存储桶将包含您的数据,并通过哈希它们的键对这些数据进行索引。键的散列值将确定用于特定数据段的正确存储桶。
import pprint
class Hashtable:
def __init__(self, elements):
self.bucket_size = len(elements)
self.buckets = [[] for i in range(self.bucket_size)]
self._assign_buckets(elements)
def _assign_buckets(self, elements):
for key, value in elements:
hashed_value = hash(key)
index = hashed_value % self.bucket_size
self.buckets[index].append((key, value))
def get_value(self, input_key):
hashed_value = hash(input_key)
index = hashed_value % self.bucket_size
bucket = self.buckets[index]
for key, value in bucket:
if key == input_key:
return(value)
return None
def __str__(self):
return pprint.pformat(self.buckets) # here pformat is used to return a printable representation of the object
if __name__ == "__main__":
capitals = [
('France', 'Paris'),
('United States', 'Washington D.C.'),
('Italy', 'Rome'),
('Canada', 'Ottawa')
]
hashtable = Hashtable(capitals)
print(hashtable)
print(f"The capital of Italy is {hashtable.get_value('Italy')}")
Moreover, the more you increase the number of buckets you will handle, the more space you will waste. To test this you can simply change the bucket size of your previous example using a number of buckets that is two times the length of the input list:
此外,处理的桶数增加越多,浪费的空间就越多。要测试这一点,只需使用输入列表长度的两倍的桶数来更改上一个示例的桶大小
两个散列值发生碰撞,将会存储到同一个桶中,因为冲突不可避免,实现一个哈希表就得有一个解决冲突的方法。
通常在哈希表解决冲突的常用策略是:
- open addressing 开放寻址法
- separate chaining 链地址法
连地址法是您在上面的示例中已经实现的,它由使用另一个数据结构在同一个bucket中创建一个值链组成。在那个示例中,您使用了一个嵌套列表,当在超额占用的bucket中查找特定值时,必须对该列表进行完全扫描。
在开放寻址策略中,如果您应该使用的bucket是忙碌的,那么您只需继续搜索要使用的新bucket。要实现这个解决方案,您需要对为新元素分配bucket的方式和检索键值的方式进行一些更改。从assign buckets()函数开始,您必须使用默认值初始化您的bucket,并且如果您应该使用的bucket已经被占用,则继续寻找空的bucket
Dictionaries in Python are built using hash tables and the **open addressing** collision resolution method.