【数据结构Python描述】仿照Python解释器使用哈希表手动实现一个字典类

osc_cgh49c55 2020-11-08 09:40:04
人工智能 Python


在文章【数据结构Python描述】使用列表手动实现一个字典类中,UnsortedListMap的5个主要方法进行效率分析后可知,使用列表这种数据结构来实现的映射效率较低,本文将使用哈希表这种数据结构实现的映射,通过这种方式实现的映射,其效率要远高于前者。

一、哈希表简介

通过列表保存键值对实现的映射之所以效率较低,原因在于最坏情况下,__getitem____setitem____delitem____iter__方法均需要遍历整个列表。

然而,由于列表天然具有通过索引以 O ( 1 ) O(1) O(1)时间复杂度访问其中元素的特点,因此可以考虑是否可以将键值对中的键通过某种转换关系转换成列表的索引值,然后将键值对保存在使用索引标识的列表单元处。

实际上,上面描述的这种特殊列表就是哈希表,而转换关系就可以认为是哈希函数

1. 哈希函数简介

因此,哈希函数 h h h的目标是为了将每一个键值对中的键 k k k转换为 [ 0 , N − 1 ] [0, N-1] [0,N1]范围内的一个整数 h ( k ) h(k) h(k)哈希值,其中 N N N哈希表 A A A这种特殊列表的容量,而键值对(k, v)就保存在 A [ h ( k ) ] A[h(k)] A[h(k)]单元处。

在希望获取某一个键值对时,只需先对 k k k执行相同的哈希函数获得哈希值,然后通过哈希值获取哈希表对应单元处的键值对即可。

2. 哈希碰撞简介

需要注意的是,针对不同的键,如果通过哈希函数得到了相同的哈希值,这时就发生了所谓的哈希碰撞,此时需要对哈希表做额外的处理,这类额外处理一般都会降低哈希表的效率,具体的处理方案包括分离链接法开放寻址法以及线性查找法等,后面将对其做详细介绍。

3. 哈希函数深究

实际上,通常将哈希函数分为两个组成部分,即哈希码压缩函数

  • 哈希码负责将一个键 k k k转换为一个整数,该整数甚至可以为负;
  • 压缩函数负责将上述整数进一步转换为 [ 0 , N − 1 ] [0, N-1] [0,N1]范围内的一个整数。

将哈希函数分为哈希码和压缩函数两个部分的好处在于哈希码的计算可以独立于哈希表的容量(和普通列表一样,哈希表的容量需要根据其中元素的数量动态调整)。

这样一来,我们可以对作为键的任意不可变对象先通过一种统一的方式计算出其哈希码,然后由压缩函数使用哈希码并根据哈希表的容量来计算该键对应的哈希值。

哈希码

哈希函数的第一部分会先使用作为键的任意不可变对象 k k k计算出一个整数,该整数被称为哈希码

Python有一个专门用于计算哈希码的内建函数hash(x),该函数对任意对象x可返回一个用作哈希码的整数,而如本文所一直强调的,在Python中只有不可变类型才是可哈希的。

这一限制可确保一个对象的哈希码在其生命周期内保持不变,则该对象的哈希值在其生命周期内保持不变。

上述重要性质可避免这样的可能异常情况:将键值对插入哈希表时,键的哈希值对应表中某一个单元;在获取一个键值对时,键的哈希值可能对应表中另外一个单元。

在Python所有的内建数据类型中,因intfloatstrtuple、以及frozenset的对象均为不可变的,因此这些类型的对象都是可哈希的;而list的对象可变,对于list的实例对象x,执行hash(x)将抛出TypeError异常。

压缩函数

一般而言,键 k k k的哈希码一般不能立即作为哈希表的索引用,因为哈希码可能为负,甚至可能超过哈希表的容量。

因此,我们需要一种方式能够将哈希码映射成 [ 0 , N − 1 ] [0, N-1] [0,N1]范围内的一个证书,该方式就是压缩函数。评价一个压缩函数好坏的标准是:对于给定的一组各不相同的哈希码,压缩函数可以将哈希碰撞的可能性降到最低。

下面是一些典型的压缩函数:

取模运算法

一种简单的压缩函数为取模运算法,即取:

i   m o d   N i \ mod \ N i mod N

其中 i i i为哈希码, N N N为哈希表的容量,而且在 N N N为质数时发生哈希碰撞的可能性将比较小。

N N N不是质数时,发生哈希碰撞的可能性将大大增加,例如:当一组键的哈希码为 { 200 , 205 , 210 , 215 , 220 , ⋅ ⋅ ⋅ , 600 } \{200, 205, 210, 215, 220, \cdot\cdot\cdot, 600\} { 200,205,210,215,220,,600},且哈希表的容量为100时,则每一个哈希码都将和另外3个哈希码发生碰撞。

如果压缩函数选择得当,那么两个不同的哈希码发生碰撞的概率为 1 / N {\left. 1\middle/ N\right.} 1/N

MAD方法

上述取模算法有一个缺点是,当哈希码为 p N + q pN+q pN+q的形式,则发生哈希碰撞的概率依然很大,而所谓的MAD(Multiply-Add-and-Divide)方法可以很好的改善这个问题,即:

[ ( a i + b )   m o d   p ]   m o d   N [(ai+b) \ mod \ p] \ mod \ N [(ai+b) mod p] mod N

其中 N N N依然为哈希表的容量, p p p是比 N N N更大的质数,而 a a a b b b都是 [ 0 , p − 1 ] [0, p-1] [0,p1]范围内的整数且 a > 0 a\gt0 a>0

4. 处理哈希碰撞

由上述讨论可知,使用哈希表实现用于保存键值对的映射的主要思想在于:给定一个哈希表表 A A A,以及一个哈希函数 h h h,实现将键值对(k, v)保存在哈希表的 A [ h ( k ) ] A[h(k)] A[h(k)]单元。

然而,问题在于:当对于两个不同的键 k 1 k_1 k1 k 2 k_2 k2 h ( k 1 ) = h ( k 2 ) h(k_1)=h(k_2) h(k1)=h(k2),即发生了哈希碰撞,此时不能简单的将键值对(k, v)插入哈希表。同样,在进行查找、删除等操作时也需要对这种情况进行考虑。

因此,下面就将介绍几种处理哈希碰撞的方案:

分离链接法

处理哈希碰撞的一种简单而高效的方式是分离链接法(Separate Chaining),即将所有满足 h ( k ) = j h(k)=j h(k)=j的键值对(k, v)保存一个二级容器(如:使用列表保存键值对实现的映射对象)中,而哈希表的单元 A [ j ] A[j] A[j]引用该二级容器。

如下图所示,一个容量为13的哈希表保存了10个键值对(图中省略了所有键值对中的值),且哈希函数为 h ( k ) = k   m o d   13 h(k)=k \ mod \ 13 h(k)=k mod 13

在这里插入图片描述

对于上述处理哈希碰撞的方案,对某一个单元 A [ j ] A[j] A[j]所引用的二级容器进行查找时,在最坏的情况下,时间复杂度和二级容器的大小呈正比。

假设现在有一个能尽量降低哈希碰撞发生概率的哈希函数,待存储的键值对数目为 n n n,哈希表容量为 N N N,则期望每一个哈希表单元处引用的二级容器的容量为 n / N {\left. n\middle/ N\right.} n/N

基于以上设定,则映射__getitem____setitem__以及__delitem__这三个核心方法的最坏时间复杂度为 O ( ⌈ n / N ⌉ ) O(\lceil {\left. n\middle/ N\right.} \rceil) O(n/N)。一般将 λ = n / N \lambda={\left. n\middle/ N\right.} λ=n/N记为哈希表的负载系数。显然 λ \lambda λ最好应当小于1,否则必然会发生哈希碰撞,此时映射几个核心方法的期望时间复杂度为 O ( 1 ) O(1) O(1)

线性查找法

上述解决哈希碰撞的分离链接法虽然实现起来较为方便,但是其也有一个缺陷:需要一个辅助的二级容器来保存发生哈希碰撞的键值对。这对内存敏感的可穿戴设备不利。下面将要介绍的线性查找法就可以确保不使用额外的二级容器来避免哈希冲突:

在使用线性查找法时,当尝试将键值对(k, v)向哈希表 A [ j ] A[j] A[j](其中 j = h ( k ) j=h(k) j=h(k))单元插入时发现后者已被占用,则尝试 A [ ( j + 1 )   m o d   N ] A[(j+1) \ mod \ N] A[(j+1) mod N]单元。如果 A [ ( j + 1 )   m o d   N ] A[(j+1) \ mod \ N] A[(j+1) mod N]依然已被占用则尝试 A [ ( j + 2 )   m o d   N ] A[(j+2) \ mod \ N] A[(j+2) mod N],以此类推,直到在哈希表中找到空的单元。

为解释上述过程,假定哈希表容量为11,键均为整数,哈希函数为 h ( k ) = k   m o d   11 h(k) = k \ mod \ 11 h(k)=k mod 11,则下图(省略键值对中的值)表示了在如图状态下插入键为15的键值对所要尝试的操作:

在这里插入图片描述

上述策略使得映射的三个核心方法__getitem____setitem__以及__delitem__在实现的第一步就需要进行特别的考虑。

具体地,当实现__delitem__时,我们不能简单地将找到的键值对进行删除,例如:如插入键为15的键值对后,如果删除了键为37的键值对,则后续对于键为15的键值对的搜索就将失败,因为线性查找策略将先从索引为4的单元处开始,然后到索引为5处,然后再发现索引为6的单元为空。因此:

  • 对于__delitem__:在删除某单元处的键值对后,将其引用一个用于标记的对象(一般为object的实例);
  • 对于__getitem__:在查找键值对时,当遇到object的实例时就跳过,直到找到期望的键值对或空的单元,抑或回到查找开始的单元;
  • 对于__setitem__

5. 负载系数和再哈希

负载系数

负载系数 λ = n / N \lambda={\left. n\middle/ N\right.} λ=n/N会极大地影响哈希表的操作效率,因为:

对于使用分离链接法处理哈希碰撞的哈希表,当 λ \lambda λ非常接近1时,发生哈希碰撞的几率就会大大增加,这会降低后续操作的效率,因此实验表明这种哈希表的的负载系数应满足 λ < 0.9 \lambda\lt0.9 λ<0.9

对于使用线性查找法处理哈希碰撞的哈希表,实验证明当 λ \lambda λ从0.5开始接近1时,键值对将在哈希表一系列连续单元处发生簇集,此时也会降低后续操作的效率,因此实验表明这种哈希表的负载系数应满足 λ < 0.5 \lambda\lt0.5 λ<0.5

再哈希

为了保持哈希表的负载系数在合理范围内,当插入键值对后导致负载系数超过某范围,则通常的做法都是先扩充哈希表的容量以降低负载系数,然后重新将所有键值对插入扩充后的哈希表中,这个过程就叫做再哈希

实际上,由于哈希函数被分为两个部分,而第一部分计算哈希码的过程和哈希表的容量无关,因此再哈希时只需要通过压缩函数计算处最终的哈希值(代表键值对应该保存的哈希表单元的索引)即可。

二、哈希表实现映射

根据使用的哈希函数在产生哈希碰撞时是采用分离链接法还是线性查找法处理,下面给出两种基于哈希表实现的两个映射类。

1. 基于哈希表实现映射的基类HashMapBase

虽然处理哈希碰撞的两种策略各异,但是通过二者实现的映射依然具有很多共通之处,为此这里先将映射基类MapBase进行扩充得到HashMapBase,具体地:

  • 映射中的键值对的哈希表用列表来表示,即映射中有self._table实例属性,且所有单元初始化为None
  • 映射中的键值对数量使用实例属性self._n来表示;
  • 如果哈希表的负载系数超过0.5,则倍增哈希表容量且通过再哈希将所有键值对转到新的哈希表中;
  • 定义了一个新的实用方法_hash_function(),该方法基于Python内建的函数hash()先计算出一个哈希码,然后实用本文介绍的MAD方法作为压缩函数。

扩充后的基类HashMapBase中留待具体子类实现的是如何表示哈希表的每一个单元。对于分离链接法,一个单元是一个二级容器(如:列表、链表等);对于线性查找法,单元的含义不一定是哈希表一个索引代表的位置,有可能是几个索引代表的位置。

因此,这里的HashMapBase类假定下列方法为抽象方法,留待具体子类实现:

  • _bucket_getitem(j, k):根据键 k k k的哈希值 j j j搜索第 j j j个单元处键为 k k k的键值对,返回找到的键值对,否则抛出KeyError异常;
  • _bucket_setitem(j, k, v):根据键 k k k的哈希值 j j j修改第 j j j个单元处键为 k k k的键值对,如果键 k k k已存在则修改已有的值,否则插入新的键值对然后为self._n加一;
  • _bucket_delitem(j, k):根据键 k k k的哈希值 j j j删除第 j j j个单元处键为 k k k的键值对并将self._n减一,如不存在该键值对则抛出KeyError异常。

__init__

def __init__(self, cap=11, p=109345121):
"""创建一个空的映射"""
self._table = cap * [None]
self._n = 0
self._prime = p # MAD压缩函数中大于哈希表容量的大质数
self._scale = 1 + randrange(p-1) # MAD压缩函数中的缩放系数a
self._shift = randrange(p) # MAD压缩函数中的负载系数b

_hash_function

根据下列公式计算哈希值:

[ ( a i + b )   m o d   p ]   m o d   N [(ai+b) \ mod \ p] \ mod \ N [(ai+b) mod p] mod N

def _hash_function(self, k):
"""哈希函数"""
return (self._scale * hash(k) + self._shift) % self._prime % len(self._table)

__len__

def __len__(self):
return self._n

__getitem__

def __getitem__(self, k):
j = self._hash_function(k)
return self._bucket_getitem(j, k) # 可能抛出KeyError异常

__setitem__

def __setitem__(self, k, v):
j = self._hash_function(k)
self._bucket_setitem(j, k, v)
if self._n > len(self._table) // 2: # 确保负载系数小于0.5
self._resize(2 * len(self._table) - 1) # 通常2 * n - 1为质数
def _resize(self, cap):
"""将哈希表容量调整为cap"""
old = list(self.items()) # 通过迭代获得已有的所有键值对
self._table = cap * [None]
self._n = 0
for k, v in old:
self[k] = v # 将键值对重新插入新的哈希表中

需要注意的是,items()方法来源于继承链上的Mapping类,在Mapping类中该方法的返回值是ItemsView(self)self是一个映射对象)。

ItemsView的初始化方法直接继承自MappingView,因此映射对象用以初始化ItemsView的实例,因为其初始化方法具体为:

def __init__(self, mapping):
self._mapping = mapping

因此,ItemsView有一个实例属性_mapping为映射实例对象,而ItemsView的实例又是一个迭代器对象,因为其中的__iter__方法使用yield (key, self._mapping[key])

__delitem__

def __delitem__(self, k):
j = self._hash_function(k)
self._bucket_delitem(j, k) # 可能抛出KeyError异常
self._n -= 1

2. 基于分离链接法实现的映射ChainHashMap

下面先给出基于分离链接法处理哈希碰撞实现的映射类ChainHashMap,在其下面实现的前三个方法均使用索引j来访问哈希表的某单元处,且检查该单元处是否为空(即引用None),只有在调用_bucket_setitem且当该单元处为空时才需要使之引用一个二级容器(这里使用的是基于普通列表实现的映射UnsortedListMap的的实例)。

_bucket_getitem(j, k)

def _bucket_getitem(self, j, k):
bucket = self._table[j]
if bucket is None:
raise KeyError('Key Error: ' + repr(k))
return bucket[k]

_bucket_setitem(j, k, v)

需要注意的是,当键k对应的键值对第一次插入映射中时,需要将映射中键值对的数量加一。

def _bucket_setitem(self, j, k, v):
if self._table[j] is None:
self._table[j] = UnsortedListMap() # 使得哈希表该单元处引用一个二级容器
oldsize = len(self._table[j])
self._table[j][k] = v
if len(self._table[j]) > oldsize: # k为新的键
self._n += 1

_bucket_delitem(j, k)

def _bucket_delitem(self, j, k):
bucket = self._table[j]
if bucket is None:
raise KeyError('Key Error: ' + repr(k))
del bucket[k]

__iter__()

需要注意的是,映射对象的每一个非空单元处都引用了一个二级容器,这里使用的二级容器是UnsortedListMap,其是一个迭代器。

def __iter__(self):
for bucket in self._table:
if bucket is not None:
for key in bucket:
yield key

3. 基于线性查找法实现的映射ProbeHashMap

_is_available(j)

def _is_available(self, j):
"""当哈希表索引为j的单元处为空或键值对被删除,则返回True"""
return self._table[j] is None or self._table[j] is ProbeHashMap._AVAIL

_find_slot(j, k)

def _find_slot(self, j, k):
"""查找索引为j的哈希表单元处是否有键k
该方法的返回值为一个元组,且返回的情况如下:
- 当在索引为j的哈希表单元处找到键k,则返回(True, fisrt_avail);
- 当未在哈希表任何单元处找到键k,则返回(False, j)。
"""
first_avail = None
while True:
if self._is_available(j):
if first_avail is None:
first_avail = j
if self._table[j] is None:
return False, first_avail
elif k == self._table[j].key:
return True, j
j = (j + 1) % len(self._table)

_bucket_getitem(j, k)

def _bucket_getitem(self, j, k):
found, s = self._find_slot(j, k)
if not found:
raise KeyError('Key Error: ' + repr(k))
return self._table[s].value

_bucket_setitem(j, k, v)

def _bucket_setitem(self, j, k, v):
found, s = self._find_slot(j, k)
if not found:
self._table[s] = self._Item(k, v)
self._n += 1
else:
self._table[s].value = v

_bucket_delitem(j, k)

def _bucket_delitem(self, j, k):
found, s = self._find_slot(j, k)
if not found:
raise KeyError('Key Error: ' + repr(k))
self._table[s] = ProbeHashMap._AVAIL

__iter__()

def __iter__(self):
for j in range(len(self._table)):
if not self._is_available(j):
yield self._table[j].key

三、哈希表实现映射的效率

操作 基于普通列表的效率 基于哈希表的期望效率 基于哈希表的最坏效率
__getitem__ O ( n ) O(n) O(n) O ( 1 ) O(1) O(1) O ( n ) O(n) O(n)
__setitem__ O ( n ) O(n) O(n) O ( 1 ) O(1) O(1) O ( n ) O(n) O(n)
__delitem__ O ( n ) O(n) O(n) O ( 1 ) O(1) O(1) O ( n ) O(n) O(n)
__len__ O ( 1 ) O(1) O(1) O ( 1 ) O(1) O(1) O ( 1 ) O(1) O(1)
__iter__ O ( n ) O(n) O(n) O ( n ) O(n) O(n) O ( n ) O(n) O(n)

四、哈希表实现映射的测试代码

from abc import ABC, abstractmethod
from collections.abc import MutableMapping
from random import randrange
class MapBase(MutableMapping, ABC):
"""提供用于保存键值对记录类的自定义映射基类"""
class _Item:
__slots__ = 'key', 'value'
def __init__(self, key, value):
self.key = key
self.value = value
def __eq__(self, other):
return self.key == other.key # 使用'=='语法基于键比较两个键值对是否相等
def __ne__(self, other):
return not (self == other) # 使用'!='语法基于键比较两个键值对是否不等
def __lt__(self, other):
return self.key < other.key # 使用'<'语法基于键比较两个键值对
class UnsortedListMap(MapBase):
def __init__(self):
"""创建一个空的映射对象"""
self._table = [] # 映射中的键值对记录保存在列表中
def __getitem__(self, key):
"""返回与键key关联的值value,当键key不存在则抛出KeyError异常"""
for item in self._table:
if key == item.key:
return item.value
raise KeyError('key error: ', repr(key))
def __setitem__(self, key, value):
"""将key-value添加至映射对象中,当存在键值key时将其值替换为value"""
for item in self._table: # 遍历查询映射中是否存在键key
if key == item.key:
item.value = value
return
self._table.append(self._Item(key, value)) # 映射中不存在键key
def __delitem__(self, key):
"""删除键key代表的键值对,当键key不存在则抛出KeyError异常"""
for j in range(len(self._table)): # 遍历查询映射中是否存在键key
if key == self._table[j].key:
self._table.pop(j)
return
raise KeyError('key error: ', repr(key)) # 映射中不存在键key
def __len__(self):
"""返回映射中键值对数量"""
return len(self._table)
def __iter__(self):
"""生成一个映射中所有键的迭代"""
for item in self._table:
yield item.key
def __str__(self):
"""返回映射对象的字符串表示形式"""
return str(dict(self.items()))
class HashMapBase(MapBase):
"""使用哈希表实现映射的基类"""
def __init__(self, cap=11, p=109345121):
"""创建一个空的映射"""
self._table = cap * [None]
self._n = 0
self._prime = p # MAD压缩函数中大于哈希表容量的大质数
self._scale = 1 + randrange(p-1) # MAD压缩函数中的缩放系数a
self._shift = randrange(p) # MAD压缩函数中的便宜系数b
def _hash_function(self, k):
"""哈希函数"""
return (self._scale * hash(k) + self._shift) % self._prime % len(self._table)
 @abstractmethod
def _bucket_getitem(self, j, k):
pass
 @abstractmethod
def _bucket_setitem(self, j, k, v):
pass
 @abstractmethod
def _bucket_delitem(self, j, k):
pass
def __len__(self):
return self._n
def __getitem__(self, k):
j = self._hash_function(k)
return self._bucket_getitem(j, k) # 可能抛出KeyError异常
def __setitem__(self, k, v):
j = self._hash_function(k)
self._bucket_setitem(j, k, v)
if self._n > len(self._table) // 2: # 确保负载系数小于0.5
self._resize(2 * len(self._table) - 1) # 通常2 * n - 1为质数
def __delitem__(self, k):
j = self._hash_function(k)
self._bucket_delitem(j, k) # 可能抛出KeyError异常
self._n -= 1
def _resize(self, cap):
"""将哈希表容量调整为cap"""
old = list(self.items()) # 通过迭代获得已有的所有键值对
self._table = cap * [None]
self._n = 0
for k, v in old:
self[k] = v # 将键值对重新插入新的哈希表中
class ChainHashMap(HashMapBase):
"""使用分离链接法处理哈希碰撞实现的哈希映射"""
def _bucket_getitem(self, j, k):
bucket = self._table[j]
if bucket is None:
raise KeyError('Key Error: ' + repr(k))
return bucket[k]
def _bucket_setitem(self, j, k, v):
if self._table[j] is None:
self._table[j] = UnsortedListMap() # 使得哈希表该单元处引用一个二级容器
oldsize = len(self._table[j])
self._table[j][k] = v
if len(self._table[j]) > oldsize: # k为新的键
self._n += 1
def _bucket_delitem(self, j, k):
bucket = self._table[j]
if bucket is None:
raise KeyError('Key Error: ' + repr(k))
del bucket[k]
def __iter__(self):
for bucket in self._table:
if bucket is not None:
for key in bucket:
yield key
class ProbeHashMap(HashMapBase):
"""使用线性查找法处理哈希碰撞实现的哈希映射"""
_AVAIL = object() # 哨兵标识,用于标识被键值对被删除的哈希表单元
def _is_available(self, j):
"""当哈希表索引为j的单元处为空或键值对被删除,则返回True"""
return self._table[j] is None or self._table[j] is ProbeHashMap._AVAIL
def _find_slot(self, j, k):
"""查找索引为j的哈希表单元处是否有键k
该方法的返回值为一个元组,且返回的情况如下:
- 当在索引为j的哈希表单元处找到键k,则返回(True, fisrt_avail);
- 当未在哈希表任何单元处找到键k,则返回(False, j)。
"""
first_avail = None
while True:
if self._is_available(j):
if first_avail is None:
first_avail = j
if self._table[j] is None:
return False, first_avail
elif k == self._table[j].key:
return True, j
j = (j + 1) % len(self._table)
def _bucket_getitem(self, j, k):
found, s = self._find_slot(j, k)
if not found:
raise KeyError('Key Error: ' + repr(k))
return self._table[s].value
def _bucket_setitem(self, j, k, v):
found, s = self._find_slot(j, k)
if not found:
self._table[s] = self._Item(k, v)
self._n += 1
else:
self._table[s].value = v
def _bucket_delitem(self, j, k):
found, s = self._find_slot(j, k)
if not found:
raise KeyError('Key Error: ' + repr(k))
self._table[s] = ProbeHashMap._AVAIL
def __iter__(self):
for j in range(len(self._table)):
if not self._is_available(j):
yield self._table[j].key
版权声明
本文为[osc_cgh49c55]所创,转载请带上原文链接,感谢
https://my.oschina.net/u/4380905/blog/4707950

  1. 利用Python爬虫获取招聘网站职位信息
  2. Using Python crawler to obtain job information of recruitment website
  3. Several highly rated Python libraries arrow, jsonpath, psutil and tenacity are recommended
  4. Python装饰器
  5. Python实现LDAP认证
  6. Python decorator
  7. Implementing LDAP authentication with Python
  8. Vscode configures Python development environment!
  9. In Python, how dare you say you can't log module? ️
  10. 我收藏的有关Python的电子书和资料
  11. python 中 lambda的一些tips
  12. python中字典的一些tips
  13. python 用生成器生成斐波那契数列
  14. python脚本转pyc踩了个坑。。。
  15. My collection of e-books and materials about Python
  16. Some tips of lambda in Python
  17. Some tips of dictionary in Python
  18. Using Python generator to generate Fibonacci sequence
  19. The conversion of Python script to PyC stepped on a pit...
  20. Python游戏开发,pygame模块,Python实现扫雷小游戏
  21. Python game development, pyGame module, python implementation of minesweeping games
  22. Python实用工具,email模块,Python实现邮件远程控制自己电脑
  23. Python utility, email module, python realizes mail remote control of its own computer
  24. 毫无头绪的自学Python,你可能连门槛都摸不到!【最佳学习路线】
  25. Python读取二进制文件代码方法解析
  26. Python字典的实现原理
  27. Without a clue, you may not even touch the threshold【 Best learning route]
  28. Parsing method of Python reading binary file code
  29. Implementation principle of Python dictionary
  30. You must know the function of pandas to parse JSON data - JSON_ normalize()
  31. Python实用案例,私人定制,Python自动化生成爱豆专属2021日历
  32. Python practical case, private customization, python automatic generation of Adu exclusive 2021 calendar
  33. 《Python实例》震惊了,用Python这么简单实现了聊天系统的脏话,广告检测
  34. "Python instance" was shocked and realized the dirty words and advertisement detection of the chat system in Python
  35. Convolutional neural network processing sequence for Python deep learning
  36. Python data structure and algorithm (1) -- enum type enum
  37. 超全大厂算法岗百问百答(推荐系统/机器学习/深度学习/C++/Spark/python)
  38. 【Python进阶】你真的明白NumPy中的ndarray吗?
  39. All questions and answers for algorithm posts of super large factories (recommended system / machine learning / deep learning / C + + / spark / Python)
  40. [advanced Python] do you really understand ndarray in numpy?
  41. 【Python进阶】Python进阶专栏栏主自述:不忘初心,砥砺前行
  42. [advanced Python] Python advanced column main readme: never forget the original intention and forge ahead
  43. python垃圾回收和缓存管理
  44. java调用Python程序
  45. java调用Python程序
  46. Python常用函数有哪些?Python基础入门课程
  47. Python garbage collection and cache management
  48. Java calling Python program
  49. Java calling Python program
  50. What functions are commonly used in Python? Introduction to Python Basics
  51. Python basic knowledge
  52. Anaconda5.2 安装 Python 库(MySQLdb)的方法
  53. Python实现对脑电数据情绪分析
  54. Anaconda 5.2 method of installing Python Library (mysqldb)
  55. Python implements emotion analysis of EEG data
  56. Master some advanced usage of Python in 30 seconds, which makes others envy it
  57. python爬取百度图片并对图片做一系列处理
  58. Python crawls Baidu pictures and does a series of processing on them
  59. python链接mysql数据库
  60. Python link MySQL database