2024 Python3.10 系统入门+进阶(十):Python字典及其常用操作详解

一、初始化

在 Python 中,字典与列表类似,也是可变数据结构,不过与列表不同,它是无序的,保存的内容是以 键-值对 的形式存放的。这类似于我们的新华字典,它可以把拼音和汉字关联起来,通过音节表可以快速找到想要的汉字。其中新华字典里的音节表相当于键(key),而对应的汉字,相当于值(value)。键是唯一的,而值可以有多个。字典在定义一个包含多个命名字段的对象时,很有用。字典的主要特征如下:

  1. 无序的数据结构。字典内的数据随机排列。
  2. 可变的数据类型。可以随意添加、删除和更新字典内的数据,字典对象会自动伸缩,确保内部数据无缝隙排列在一起。
  3. 通过键映射访问内部数据,不能够通过下标索引访问内部数据。字典有时也称为关联数组或者散列表(hash)。它是通过键将一系列的值联系起来的,这样就可以通过键从字典中获取指定项,但不能通过索引来获取。
  4. 内部数据统称为元素(Item,也称为 Entry),每个元素都由键和值组成。
  5. 键名必须是可哈希的,即确保键名的唯一性、不可变性。值可以为任意类型的数据。ps:不允许同一个键出现两次,如果出现两次,则后一个值会被记住。
  6. 字典的字面值使用大括号包含所有元素,元素之间使用逗号隔开,键和值之间使用冒号分隔。

1.1 {}–直接创建字典

定义字典时,每个元素都包含两个部分 。以水果名称和价格的字典为例,键为水果名称,值为水果价格,如下图所示:
在这里插入图片描述
在创建字典时,使用一对大括号 "{}",在大括号中间放置使用冒号分隔的 作为元素,相邻两个元素使用逗号分隔。其语法格式如下:

dictionary = {'key1':'value1', 'key2':'value2',, 'keyn':'valuen',}
参数说明:
1.dictionary: 表示字典名称;只要是符合python标识符命名规则即可
2.key1、key2…keyn: 表示元素的键,必须是唯一的,并且不可变,例如可以是字符串、数字或者元组
3.value1、value2…valuen: 表示元素的值,可以是任何数据类型,不是必须唯一。

示例代码:

# 键为字符串的字典
link = {'name': 'amo', 'age': 18, 'height': 1.68, 'address': '重庆市', 'has_girlfriend': False}
record = {'english': 97, 'chinese': 99, 'python': 100, 'c': 96}  # 定义值为数字的字典
language = {'python': ['优雅', '明确', '简单'], 'java': ['继承', '封装', '多态']}  # 定义值为列表的字典
student = {1: '明日', 2: '零语', 3: '惜梦'}  # 定义键为数值的字典
temp = {('a', 'b'): ('1000', '1001')}  # 定义键为元组的字典
dic1 = {'name': 'amo', 2: 'a', 13.14: '520', True: 1}  # 定义混合类型的字典
dic2 = {'name': 'amo', 2: 'a', 13.14: '520', 1: '数值', True: '布尔值'}  # 定义混合类型的字典
dic3 = {}  # 定义空字典
# 映射解构 ** 可在 dict 字面量中使用,同样可以多次使用
print({'a': 0, **{'x': 1}, 'y': 2, **{'z': 3, 'x': 4}})
print('link字典: ', link)
print('record字典: ', record)
print('language字典: ', language)
print('student字典: ', student)
print('temp字典: ', temp)
print('混合类型字典: ', dic1)
print('混合类型字典: ', dic2)
print('空字典: ', dic3)

1.2 dict()函数–创建字典

dict() 函数用于创建一个字典对象,其语法格式如下:

In [1]: dict?
Init signature: dict(self, /, *args, **kwargs)
Docstring:
dict() -> new empty dictionary
dict(mapping) -> new dictionary initialized from a mapping object's
    (key, value) pairs
dict(iterable) -> new dictionary initialized as if via:
    d = {}
    for k, v in iterable:
        d[k] = v
dict(**kwargs) -> new dictionary initialized with the name=value pairs
    in the keyword argument list.  For example:  dict(one=1, two=2)
Type:           type
参数说明:
1.**kwargs: 一到多个关键字参数,如dict(one=1, two=2, three=3)
2.mapping: 元素容器,如zip函数
3.iterable: 可迭代对象
4.返回值: 一个字典。如果不传入任何参数时,则返回空字典;

示例代码:

# 1.创建字典
dict1 = dict()
print('创建空字典: ', dict1)
# 通过给定"键-值对"的方式来创建字典
dict1 = dict(刘能='刘', 赵四='赵', 谢广坤='谢', 王长贵='王')
print(dict1)
# 2.通过给定的关键字参数创建字典
# 通过给定的关键字参数创建字典
d1 = dict(name='Amo')  # 字符串的key与值
print(d1)  # 打印d1字典内容
d2 = dict(Python=98, English=78, Math=81)  # 字符串的key,int类型的值
print(d2)  # 打印d2字典内容
d3 = dict(name='Tom', age=21, height=1.5)  # 字符串的key,多种类型的值
print(d3)  # 打印d3字典内容
# ps: 使用dic()函数通过给定的关键字参数创建字典时,name键名必须都是Python中的标识符,否则会提示SyntaxError。
"""
In [2]: dict(1='a', 2='b')
  Cell In[2], line 1
    dict(1='a', 2='b')
         ^
SyntaxError: expression cannot contain assignment, perhaps you meant "=="?
In [3]: dict(a=1,b=2)
Out[3]: {'a': 1, 'b': 2}
"""
# 3.通过传入映射函数创建字典
# 通过映射函数创建字典
a1 = dict(zip((1, 2, 3, 4), ('杰夫•贝佐斯', '比尔•盖茨', '沃伦•巴菲特', '伯纳德•阿诺特')))
print(a1)  # 打印a1字典内容,key为int类型,值为字符串类型
a2 = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
print(a2)  # 打印a2字典内容,key为字符串类型,值为int类型
a3 = dict(zip(['北京', '上海', '长春'], ['晴', '大雨', '小雪']))
print(a3)  # 打印a3字典内容,key与值都是字符串类型

# 通过传入可迭代对象创建字典
# 通过可迭代对象创建字典
b1 = dict([('Monday', 1), ('Tuesday', 2), ('Wednesday', 3)])  # 元组对象
print(b1)  # 打印b1字典内容
b2 = dict([['apple', 6.5], ['orange', 3.98], ['pear', 8], ['banana', 2.89]])  # 列表对象
print(b2)  # 打印b2字典内容
b2 = dict([['apple', 6.5], ['orange', 3.98], ['pear', 8], ['banana', 2.89]], a=3, b=4)  # 也是可以这样写的
print(b2)

1.3 fromkeys()方法–创建一个新字典

字典对象中的 fromkeys() 方法用于创建一个新的字典。以序列 seq 中的元素作为字典的键,用 value 作为字典所有键对应的值。其语法格式如下:

In [4]: dict.fromkeys?
Signature: dict.fromkeys(iterable, value=None, /)
Docstring: Create a new dictionary with keys from iterable and values set to value.
Type:      builtin_function_or_method
参数说明: 
1.dict: 字典对象
2.seq: 序列(如字符串、列表、元组等),作为新字典的键
3.value: 可选参数,如果提供value值则该值将被设置为字典的值,字典所有键对应同一个值,如果不提供value值,则默认返回None4.返回值: 返回一个新字典

示例代码:

# 创建字典中所有值为None的字典
dict1 = dict.fromkeys(['hour', 'minute', 'second'])  # 创建新的字典
print(dict1)  # 打印所有值都为None的字典
# 创建一个所有值为相同值的字典
dict2 = dict.fromkeys(['小明', '李四'], 1)  # 两名同学考试并列第一
print(dict2)  # 打印相同值的字典
# 循环创建多个同值字典
a = [1, 2, 3]  # 键列表
b = [6, 7, 8]  # 值列表
for i in b:  # 循环更换值
    print(dict.fromkeys(a, i))

二、元素访问

2.1 使用中括号[]语法

使用 print() 函数可以查看字典的数据结构,而使用 中括号[] 语法可以直接访问字典的元素。语法格式如下:

dict[key]  # dict表示字典对象,key表示键

示例代码:

dict1 = {'a': 1, 'b': 2, 'c': 3}  # 定义字典
print(dict1['a'])  # 访问键为a的元素,输出为1
# 在使用该方法获取指定键的值时,如果指定的键不存在,就会抛出异常。
print(dict1['e'])  # KeyError: 'e'
# 所以在使用[]方法的时候,我们可以先使用 in 关键字判断key是否存在

2.2 get()方法–获取字典中指定键的值

在使用字典时,很少直接输出它的内容。一般需要根据指定的键得到相应的结果。Python 中推荐的方法是使用字典对象的 get() 方法获取指定键的值。其语法格式如下:

In [6]: dict.get?
Signature: dict.get(self, key, default=None, /)
Docstring: Return the value for key if key is in the dictionary, else default.
Type:      method_descriptor
参数说明: 
1.dict: 字典对象,即要从中获取值的字典
2.key: 字典中要查找的键
3.default: 可选参数,当指定的键不存在时,返回默认值,如果省略default参数,则返回None4.返回值: 如果字典中键存在,则返回键所对应的值;如果键不存在,则返回default默认值。

示例代码:

import random  # 导入随机模块

# 获取字典中键对应的值
dic1 = {'北京': '晴', '上海': '阵雨转晴', '广州': '阴'}
print(dic1)  # 输出字典
print('北京的天气为:', dic1.get('北京'))  # 获取"北京"的天气报告,返回键对应的值
# 与setdefault()方法不同 提示:get()方法只是用于获取键的值,并不会添加键到字典。
# 获取字典中不存在的键所对应的值
dic2 = {'name': 'Tom', 'age': 21}  # 创建字典
# 通过get()方法访问一个字典里的没有的键,返回None,也可自定义默认值
print(dic2.get('height'))  # 返回None
print(dic2.get('height', '165cm'))  # 返回165cm
# 如果原字典中存在某个key对应的值为None,需要特别小心,所以最好在使用get()方法时来个缺省值
# 获取字典中最小键所对应的值
# 公司内部运动成绩排名
dict_info = {1: '小王', 2: '老黄', 3: '小李', 4: '小张', 5: '老刘'}
minimal_key = min(dict_info.keys(), key=lambda i: i)  # 获取字典数据中最小的键
print('公司内容运动会第一名是:', dict_info.get(minimal_key))  # 根据最小的键获取对应的值
print('-----------------------------------------------------')
name = ['绮梦', '冷伊一', '香凝', '黛兰']  # 作为键的列表
sign_person = ['水瓶座', '射手座', '双鱼座', '双子座']  # 作为值的列表
person_dict = dict(zip(name, sign_person))  # 转换为个人字典
sign_all = ['白羊座', '金牛座', '双子座', '巨蟹座', '狮子座', '处女座', '天秤座', '天蝎座', '射手座', '摩羯座',
            '水瓶座', '双鱼座']
nature = ['有一种让人看见就觉得开心的感觉,阳光、乐观、坚强,性格直来直往,就是有点小脾气。',
          '很保守,喜欢稳定,一旦有什么变动就会觉得心里不踏实,性格比较慢热,是个理财高手。',
          '喜欢追求新鲜感,有点小聪明,耐心不够,因你的可爱性格会让很多人喜欢和你做朋友。',
          '情绪容易敏感,缺乏安全感,做事情有坚持到底的毅力,为人重情重义,对朋友和家人特别忠实。',
          '有着宏伟的理想,总想靠自己的努力成为人上人,向往高高在上的优越感,也期待被仰慕被崇拜的感觉。',
          '坚持追求自己的完美主义者。',
          '追求平等、和谐,擅于察言观色,交际能力很强,因此真心的朋友不少。最大的缺点就是面对选择总是犹豫不决。',
          '精力旺盛,占有欲强,对于生活很有目标,不达目的誓不罢休,复仇心重。',
          '崇尚自由,勇敢、果断、独立,身上有一股勇往直前的劲儿,只要想做,就能做。',
          '是最有耐心的,为事最小心,也是最善良的星座。做事脚踏实地,也比较固执,不达目的不放手,而且非常勤奋。',
          '人很聪明,最大的特点是创新,追求独一无二的生活,个人主义色彩很浓重的星座。',
          '集所有星座的优缺点于一身。最大的优点是有一颗善良的心,愿意帮助别人。']

sign_dict = dict(zip(sign_all, nature))  # 转换为星座字典
name_str = random.choice(name)  # 随机获取名字
print('【', name_str, '】的星座是', person_dict.get(name_str))  # 输出星座
print('\n 她的性格特点是:\n\n', sign_dict.get(person_dict.get(name_str)))  # 输出性格特点

2.3 setdefault()方法–获取字典中指定键的值

字典 setdefault() 方法和 get() 方法类似,用于获取字典中指定键的值。如果键不在字典中,则会添加键到字典且将 default 值设为该键的值,并返回该值。其语法格式如下:

In [5]: dict.setdefault?
Signature: dict.setdefault(self, key, default=None, /)
Docstring:
Insert key with a value of default if key is not in the dictionary.

Return the value for key if key is in the dictionary, else default.
Type:      method_descriptor
参数说明: 
1.dict: 字典对象,即要从中获取值的字典。
2.key: 字典中要查找的键。
3.default: 可选参数,如果指定键的值不存在时,返回该值,默认为None4.返回值: 如果键在字典中,则返回键所对应的值。如果键不在字典中,则向字典中添加这个键,且设置default为这个键的值,
并返回default值。

注意: 在使用字典 setdefault() 方法时需要注意,如果指定的键存在,仍设置了 default 值,default 值不会覆盖原来已经存在的键所对应的值。示例代码:

# 获取空字典的值
dict1 = {}  # 定义空字典
print(dict1.setdefault('name'))  # # 获取空字典中的值 None
print(dict1)  # 输出dict1 {'name': None}
dict2 = {}  # 定义空字典
dict2.setdefault('name', 'Anna')  # 在没有获取到name键与对应的值时,为字典添加默认数据
print(dict2)  # 打印结果
dict3 = {'name': 'amo'}
print(dict3.setdefault('name', 'gg'))  # key值name存在,故返回其对应的值amo
print(dict3)

三、新增和修改

3.1 直接赋值

由于字典是可变数据结构,所以可以随时在字典中添加 键-值对 向字典中添加元素的语法格式如下:

dictionary[key] = value
参数说明: 
1.dictionary: 表示字典名称
2.key: 表示要添加元素的键,必须是唯一的,并且不可变,例如可以是字符串、数字或者元组
3.value: 表示元素的值,可以是任何数据类型,不是必须唯一的

示例代码:

dictionary = dict((('绮梦', '水瓶座'), ('冷伊一', '射手座'), ('香凝', '双鱼座'), ('黛兰', '双子座')))
dictionary["碧琦"] = "巨蟹座"  # 添加一个元素
# {'绮梦': '水瓶座', '冷伊一': '射手座', '香凝': '双鱼座', '黛兰': '双子座', '碧琦': '巨蟹座'}
print(dictionary)

从上面的结果中可以看到,字典中又添加了一个键为 "碧琦" 的元素。由于在字典中,"键" 必须是唯一的,如果新添加元素的 "键" 与已经存在的 "键" 重复,那么将使用新的 "值" 替换原来该 "键" 的值,这也相当于 修改字典的元素。 例如,再添加一个 "键""香凝" 的元素,设置她的星座为 "天蝎座"。可以使用下面的代码:

dictionary = dict((('绮梦', '水瓶座'), ('冷伊一', '射手座'), ('香凝', '双鱼座'), ('黛兰', '双子座')))
dictionary["香凝"] = "天蝎座"  # 添加一个元素,当元素存在时,则相当于修改功能
# {'绮梦': '水瓶座', '冷伊一': '射手座', '香凝': '天蝎座', '黛兰': '双子座'}
print(dictionary)

从上面的结果可以看到,字典中并没有添加一个新的 "键""香凝",而是直接对 "香凝" 进行了修改,"香凝" 对应的值变为了 "天蝎座"

3.2 update()方法–更新字典

字典中的 update() 方法用于更新字典,其参数可以是字典或者某种可迭代的数据类型。其语法格式如下:

In [10]: dict.update?
Docstring:
D.update([E, ]**F) -> None.  Update D from dict/iterable E and F.
If E is present and has a .keys() method, then does:  for k in E: D[k] = E[k]
If E is present and lacks a .keys() method, then does:  for k, v in E: D[k] = v
In either case, this is followed by: for k in F:  D[k] = F[k]
Type:      method_descriptor

示例代码:

# 将一个字典的键值对更新到(添加到)另一个字典中
dic1 = {'a': 1, 'b': 2}
print('更新前:', dic1)  # 输出更新前的字典内容
dict2 = {'c': 3}
dic1.update(dict2)  # 将字典dict2中的"键值对"添加到字典dict中
print('更新后:', dic1)  # 输出更新后的字典内容
# 以元组为参数更新字典
dict1 = {'apple': 5.98, 'banana': 3.68}
print('更新前:', dict1)  # 输出更新前的字典内容
list1 = [('pear', 3.00), ('watermelon', 2.89)]  # 列表中的每个元组是一个键值对
dict1.update(list1)  # 更新字典
print('更新后:', dict1)  # 输出更新后的字典内容
'''
提示:如果字典update()方法的参数是可迭代对象,则可迭代对象中的每一项自身必须是可迭代的,
并且每一项只能有两个对象。第一个对象将作为新字典的键,第二个对象将作为其键对应的值。
'''
# 更新字典中相同的键
dict1 = {'apple': 5.98, 'banana': 3.68}
print('更新前:', dict1)  # 输出更新前的字典内容
# 如果两个字典中有相同的键,则字典dict2中的键值将覆盖源字典dict1中的键值
dict2 = {'apple': 8.89, 'pear': 3.00}
dict1.update(dict2)  # 将字典dict2中的"键值对"添加到字典dict1中
print('更新后:', dict1)  # 输出更新后的字典内容
# 更新原字典中不存在的键值对
# 小明第一次学习的单词
dict1 = {'Tiger': '老虎', 'Giraffe': '长颈鹿', 'Lion': '狮子'}
# 第二次复习了第一次的两个单词,又新学习了两个单词
dict2 = {'Tiger': '老虎', 'Lion': '狮子', 'Horse': '马', 'Bear': '熊'}
for i in dict2.items():  # 遍历dict2中的键值对
    if i not in list(dict1.items()):  # 如果dict2中的键值对不存在dict1当中
        dict1.update([i])  # 就将不存在的键值对更新至dict1中
print('小明共学习了以下这些单词:\n', dict1)

3.3 使用“|”合并映射

Python 3.9 支持使用 ||= 合并映射。这不难理解,因为二者也是并集运算符。| 运算符创建一个新映射。示例代码:

d1 = {'a': 1, 'b': 3, 'd': 7}
d2 = {'a': 2, 'b': 4, 'c': 6}
# 通常,新映射的类型与左操作数(本例中的 d1)的类型相同
print(d1, d2)
print(d1 | d2)
print(d1, d2)
d1 |= d2
print(d1, d2)

四、删除

4.1 del–删除字典或字典中指定的键

在实现字典内容的删除时,可以使用 del 删除整个字典或字典中指定的键。使用 del 删除字典的语法格式如下:

del dict[key]  # 删除字典中指定的键
del dict  # 删除字典
参数说明: 
1.dict: 字典对象
2.key: 要删除的键

示例代码:

# 先删除字典中指定的键再删除整个字典
dic1 = {'一等奖': '500万元', '二等奖': '100万元', '参与奖': '零食大礼包一份'}
print('删除前:', dic1)
del dic1['参与奖']  # 删除指定键
print('删除后:', dic1)
del dic1  # 删除字典
# 当使用del命令删除一个字典中不存在的键时,将抛出KeyError异常
dic1 = {'一等奖': '500万元', '二等奖': '100万元', '参与奖': '零食大礼包一份'}
# 当删除一个不存在的键时,将抛出KeyError异常
# del dic1['三等奖']  # 提示KeyError: '三等奖'
# 为防止删除不存在的键时抛出异常,可以使用操作符in先判断指定键是否存在与字典中,然后再使用del命令删除指定的键
dic2 = {'一等奖': '500万元', '二等奖': '100万元', '参与奖': '零食大礼包一份'}
if '参与奖' in dic2:  # 返回True
    del dic2['参与奖']  # 删除指定键
print(dic2)

4.2 pop()方法–删除字典中指定键对应的键值对并返回被删除的值

字典中的 pop() 方法用于删除字典内指定键所对应的键值对,并返回被删除的值。指定的键如果不在字典中,则必须设置一个 default 值,否则会报错,此时返回的就是 default 值。其语法格式如下:

In [9]: dict.pop?
Docstring:
D.pop(k[,d]) -> v, remove specified key and return the corresponding value.

If the key is not found, return the default if given; otherwise,
raise a KeyError.
Type:      method_descriptor
参数说明: 
1.D: 字典对象
2.key: 指定字典中要删除的键
3.default: 可选参数,指定的键不在字典中,必须设置default默认值,否则会报错
4.返回值: 如果指定的键在字典中,则返回指定键所对应的值,否则返回设置的default值。

示例代码:

# 删除字典中指定键所对应的键值对,并返回被删除的值
d = {'壹': 1, '贰': 2, '叁': 3}  # 创建字典
print('删除前: ', d)
print(d.pop('贰'))  # 返回键所对应的值
print('删除后: ', d)
# 删除字典中指定键所对应的键值对,并设置default值
dic1 = {'name': 'amo', 'age': 18, 'height': 1.68}  # 创建字典
print('删除前: ', dic1)
# 键不在字典中,必须设置default值,返回default值
print(dic1.pop('qq', '123456789'))
# 注意:键在字典中,设置default值,此时default值无效,返回的是源字典中键对应的值
print(dic1.pop('name', 'paul'))  # 返回amo
print('删除后: ', dic1)

4.3 popitem()方法–返回并删除字典中的键值对

字典中的 popitem() 方法用于返回并删除字典中的一个键值对(一般删除字典末尾的键值对)。其语法格式如下:

In [8]: dict.popitem?
Signature: dict.popitem(self, /)
Docstring:
Remove and return a (key, value) pair as a 2-tuple.

Pairs are returned in LIFO (last-in, first-out) order.
Raises KeyError if the dict is empty.
Type:      method_descriptor
参数说明: 
1.dict: 字典对象
2.返回值: 返回一个(key,value)形式的键值对

示例代码:

# 返回并删除字典中的一个键值对
dict1 = {'001': '小王', '002': '小张', '003': '小梁'}  # 创建字典
print(dict1.popitem())  # 删除键值
print(dict1)
# 删除空字典中的键值对
dict1 = {'001': '小王', '002': '小张', '003': '小梁'}  # 创建字典
dict1.clear()  # 删除字典中的全部元素
# 如果字典为空,却调用了popitem()方法,会提示KeyError异常
print(dict1.popitem())  # 提示KeyError: 'popitem(): dictionary is empty'
# 分别获取返回并删除字典中的键与值
student = {'name': 'Amo', 'age': '18'}  # 创建字典
key, value = student.popitem()  # 删除键值对
print('随机删除的键为:', key)
print('随机删除的值为:', value)

4.4 clear()方法–删除字典中的全部元素

字典中的 clear() 方法用于删除字典内的全部元素。执行 clear() 方法后,原字典将变为空字典。其语法格式如下:

In [7]: dict.clear?
Docstring: D.clear() -> None.  Remove all items from D.
Type:      method_descriptor
参数说明: 
1.D: 字典对象
2.返回值: None

示例代码:

# 删除字典中的全部元素
d = {'壹': 1, '贰': 2, '叁': 3}  # 创建字典
print('删除前:', d)
d.clear()  # 删除字典中的全部元素
print('删除后:', d)
# 清空通过推导式所创建的字典
# 生成由数字和数字转换成的字符串组成的字典
dict1 = {i: '000' + str(i) for i in range(1, 6)}  # 0~5(包括5)
print('清空前:', dict1)
dict1.clear()  # 清空字典
print('清空后:', dict1)

五、遍历

5.1 items()方法–获取字典的所有"键值对"

使用字典对象的 items() 方法可以获取字典的所有 键值对 返回的是一个可迭代对象,可以使用 list() 函数转换为列表。其语法格式如下:

In [11]: dict.items?
Docstring: D.items() -> a set-like object providing a view on D's items
Type:      method_descriptor

参数说明: 
1.dict: 字典对象
2.返回值: 返回一个可迭代对象

示例代码:

# 获取字典中所有"键值对"
dictionary = {'hour': 3, 'minute': 45, 'second': 21}  # 创建字典
print(dictionary.items())  # 获取字典的键值对,返回一个可迭代对象
print(list(dictionary.items()))  # 使用list()函数转换为列表
# 循环遍历字典中的全部"键值对"
dictionary = {'语文': 98, '数学': 95, '英语': 88}  # 创建字典
for item in dictionary.items():  # 通过for循环获取字典中的全部"键值对"
    print(item)
# 通过for循环分别遍历字典中的键和值
dictionary = {'2017年': 1682, '2018年': 2135}  # 创建字典
for key, value in dictionary.items():  # 通过for循环获取字典中具体的每个键和值
    print(key, '天猫双十一当天成交额:', value, '亿元')

# 根据给出的值获取对应的键
dict_name = {'刘能': '刘', '赵四': '赵', '谢广坤': '坤'}  # 名字数据


def get_key(dic, val):  # 自定义根据值获取键的函数
    return [k for (k, v) in dic.items() if v == val]  # 列表推导式获取指定值对应的键


key = get_key(dict_name, '赵')  # 调用函数获取“赵”对应的键
print('"赵"对应的键为:', key[0])

5.2 keys()方法–获取字典的所有键

字典 keys() 方法用于获取一个字典所有的键。返回的是一个可迭代对象,可以使用 list() 函数转换为列表。其语法格式如下:

In [12]: dict.keys?
Docstring: D.keys() -> a set-like object providing a view on D's keys
Type:      method_descriptor
参数说明: 
1.D: 字典对象
2.返回值: 返回一个可迭代对象

示例代码:

# 获取字典中的所有键
dictionary = {'hour': 3, 'minute': 45, 'second': 21}  # 创建字典
print(dictionary.keys())  # 获取字典中所有的键,返回一个可迭代对象
print(list(dictionary.keys()))  # 使用list()函数转换为列表
# 循环遍历字典中的键
dictionary = {'杰夫•贝佐斯': 1, '比尔•盖茨': 2, '沃伦•巴菲特': 3, '伯纳德•阿诺特': 4}  # 创建字典
for key in dictionary.keys():  # 通过for循环获取字典中的具体的key(键)
    print(key)
for key in dictionary:
    print(key)  # 默认就遍历字典的key
# 通过keys()方法获取字典中所有的值
# 字典数据
dict_demo = {1: '杰夫•贝佐斯', 2: '比尔•盖茨', 3: '沃伦•巴菲特', 4: '伯纳德•阿诺特'}
for i in dict_demo.keys():  # 遍历字典中所有键
    # value = dict_demo.get(i)  # 获取键对应的值
    value = dict_demo[i]  # 获取键对应的值
    print('字典中的值有:', value)

5.3 values()方法–获取字典的所有值

字典 values() 方法用于获取一个字典所有的值。返回的是一个可迭代对象,可以使用 list() 函数转换为列表。其语法格式如下:

In [13]: dict.values?
Docstring: D.values() -> an object providing a view on D's values
Type:      method_descriptor
参数说明: 
1.D: 字典对象
2.返回值: 返回一个可迭代对象

示例代码:

# 获取字典中所有值
dictionary = {'hour': 3, 'minute': 45, 'second': 21}  # 创建字典
print(dictionary.values())  # 获取字典的所有值,返回一个可迭代对象
print(list(dictionary.values()))  # 使用list()函数转换为列表
# 循环遍历获取字典中的值
dictionary = {'小英子': '3月5日', '阳光': '12月8日', '茜茜': '7月6日', }  # 创建字典
for value in dictionary.values():  # 通过for循环获取字典中具体的value(值)
    print(value)
# 通过lambda表达式获取字典中指定条件的值
dict_demo = {'apple': 6.5, 'orange': 3.98, 'pear': 8, 'banana': 2.89}
max_value = max(dict_demo.values(), key=lambda i: i)  # 获取字典中所有值最大的那个
print('字典中值最大的为:', max_value)
# 获取字典中所有float类型的值
type_value = list(filter(lambda i: isinstance(i, float), dict_demo.values()))
print('字典中所有float类型的值为:', type_value)
# 获取字典中长度为3的那个值
len_value = list(filter(lambda i: len(str(i)) == 3, dict_demo.values()))
print('字典中长度为3的值是:', len_value[0])

5.4 遍历与删除

错误写法:

# 错误的做法
d = dict(a=1, b=2, c=3)
for k, v in d.items():
    print(d.pop(k))
# 抛出RuntimeError: dictionary changed size during iteration

在使用 keys()、values()、items() 等方法遍历的时候,不可以改变字典的 size,正确写法:

d = dict(a=1, b=2, c=3)
while len(d):
    print(d.popitem())
while d:
    print(d.popitem())

上面的 while 循环虽然可以移除字典元素,但是很少使用,不如直接 clear。for 循环正确删除:

d = dict(a=1, b=2, c=3)
keys = []
for k, v in d.items():
    keys.append(k)
for k in keys:
    d.pop(k)
print(d)

集合 set 在遍历中,也不能改变其长度。

5.5 字典视图

dict 的实例方法 .keys()、.values() 和 .items() 分别返回 dict_keys、dict_values 和 dict_items 类的实例。这些字典视图是 dict 内部实现使用的数据结构的只读投影。Python 2 中对应的方法返回列表,重复 dict 中已有的数据,有一定的内存开销。另外,视图还取代了返回迭代器的旧方法。下面的示例展示所有字典视图均支持的一些基本操作。示例 .values() 方法返回 dict 对象的值视图:

In [9]: d = dict(a=10,b=20,c=30)

In [10]: values = d.values()

In [11]: values
Out[11]: dict_values([10, 20, 30])

In [12]: len(values)
Out[12]: 3

In [13]: list(values)
Out[13]: [10, 20, 30]

In [14]: reversed(values)
Out[14]: <dict_reversevalueiterator at 0x2559fd935b0>

In [15]: values[0]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[15], line 1
----> 1 values[0]

TypeError: 'dict_values' object is not subscriptable

通过视图对象的字符串表示形式查看视图的内容。可以查询视图的长度。视图是可迭代对象,方便构建列表。视图实现了 __reversed__ 方法,返回一个自定义迭代器。不能使用 [] 获取视图中的项。视图对象是动态代理。更新原 dict 对象后,现有视图立即就能看到变化。续上面的示例:

In [16]: d
Out[16]: {'a': 10, 'b': 20, 'c': 30}

In [17]: d['z'] = 99

In [18]: values
Out[18]: dict_values([10, 20, 30, 99])

In [19]: values
Out[19]: dict_values([10, 20, 30, 99])

In [20]: d['x'] = 88

In [21]: values
Out[21]: dict_values([10, 20, 30, 99, 88])

dict_keys、dict_values 和 dict_items 是内部类,不能通过 __builtins__ 或标准库中的任何模块获取,尽管可以得到实例,但是在 Python 代码中不能自己动手创建。

In [22]: values_class = type({}.values())

In [23]: v = values_class()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[23], line 1
----> 1 v = values_class()

TypeError: cannot create 'dict_values' instances

dict_values 类是最简单的字典视图,只实现了 __len____iter____reversed__ 这 3 个特殊方法。除此之外,dict_keys 和 dict_items 还实现了多个集合方法,基本与 frozenset 类相当。需要特别注意的是,dict_keys 和 dict_items 实现了一些特殊方法,支持强大的集合运算符,包括 &(交集)、|(并集)、-(差集)和 ^(对称差集)。例如,使用 & 运算符可以轻易获取两个字典都有的键。

In [24]: d1 = dict(a=1, b=2, c=3, d=4)

In [25]: d2 = dict(b=20, d=40, e=50)

In [26]: d1.keys() & d2.keys()
Out[26]: {'b', 'd'}

注意,& 运算符返回一个 set。更方便的是,字典视图的集合运算符均兼容 set 实例,如下所示:

In [29]: d1
Out[29]: {'a': 1, 'b': 2, 'c': 3, 'd': 4}

In [30]: s = {'a', 'e', 'i'}

In [31]: d1.keys() | s
Out[31]: {'a', 'b', 'c', 'd', 'e', 'i'}

仅当 dict 中的所有值均可哈希时,dict_items 视图才可当作集合使用。倘若 dict 中有不可哈希的值,对 dict_items 视图做集合运算将抛出 TypeError: unhashable type 'T',其中 T 是不可哈希的类型。相反,dict_keys 视图始终可当作集合使用,因为按照其设计,所有键均可哈希。使用集合运算符处理视图可以省去大量循环和条件判断。繁重工作都可交给 C 语言实现的 Python 高效完成。

六、字典的key

6.1 “可哈希”指什么

字典的 key 和 set 的元素要求一致:

  1. set 的元素可以就是看做 key,set 可以看做 dict 的简化版
  2. hashable 可哈希才可以作为 key,可以使用 hash() 函数进行测试
  3. 使用 key 访问,就如同列表使用 index 访问一样,时间复杂度都是 O(1),这也是最好的访问元素的方式

示例代码:

d = {
    1: 0,
    2.0: 3,
    "abc": None,
    ('hello', 'world', 'python'): "string",
    b'abc': '135'
}
print(d)

可哈希 在 Python 术语表中有定义,摘录(有部分改动)如下:如果一个对象的哈希码在整个生命周期内永不改变(依托 __hash__() 方法),而且可与其他对象比较(依托 __eq__() 方法),那么这个对象就是可哈希的。两个可哈希对象仅当哈希码相同时相等。数值类型以及不可变的扁平类型 str 和 bytes 均是可哈希的。如果容器类型是不可变的,而且所含的对象全是可哈希的,那么容器类型自身也是可哈希的。frozenset 对象全部是可哈希的,因为按照定义,每一个元素都必须是可哈希的。仅当所有项均可哈希,tuple 对象才是可哈希的。请考察以下示例中的 tt、tl 和 tf。

C:\Users\amoxiang>ipython
Python 3.10.11 (tags/v3.10.11:7d4cc5a, Apr  5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.26.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: tt = (1, 2, (30, 40))

In [2]: hash(tt)
Out[2]: -3907003130834322577

In [3]: tl = (1, 2, [30, 40])
In [5]: hash(tl)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[5], line 1
----> 1 hash(tl)

TypeError: unhashable type: 'list'

In [6]: tf = (1, 2, frozenset([30, 40]))

In [7]: hash(tf)
Out[7]: 5149391500123939311

一个对象的哈希码根据所用的 Python 版本和设备架构有所不同。如果出于安全考量而在哈希计算过程中 加盐,那么哈希码也会发生变化。 正确实现的对象,其哈希码在一个 Python 进程内保持不变。默认情况下,用户定义的类型是可哈希的,因为自定义类型的哈希码取自 id(),而且继承自 object 类的 __eq__() 方法只不过是比较对象 ID。如果自己实现了 __eq__() 方法,根据对象的内部状态进行比较,那么仅当 __hash__() 方法始终返回同一个哈希码时,对象才是可哈希的。实践中,这要求 __eq__()__hash__() 只考虑在对象的生命周期内始终不变的实例属性。

6.2 哈希表(散列表)是什么?

6.2.1 哈希表的构建

在初中的数学课本中学习过函数的相关知识,给定一个 x,通过一个数学公式,只需要将 x 的值带入公式就可以求出一个新的值 y。哈希表的建立同函数类似,把函数中的 x 用查找记录时使用的关键字来代替,然后将关键字的值带入一个精心设计的公式中,就可以求出一个值,用这个值来表示记录存储的 哈希地址。即:

数据的哈希地址=f(关键字的值)

哈希地址只是表示在查找表中的存储位置,而不是实际的物理存储位置。f() 是一个函数,通过这个函数可以快速求出该关键字对应的的数据的哈希地址,称之为 "哈希函数" 例如,这里有一个电话簿(查找表),电话簿中有 4 个人的联系方式:

张三 13912345678
李四 15823457890
王五 13409872338
赵六 13805834722

假如想查找李四的电话号码,对于一般的查找方式最先想到的是从头遍历,一一比较。而如果将电话簿构建成一张哈希表,可以直接通过名字 "李四" 直接找到电话号码在表中的位置。在构建哈希表时,最重要的是哈希函数的设计。例如设计电话簿案例中的哈希函数为:每个名字的姓的首字母的 ASCII 值即为对应的电话号码的存储位置。这时会发现,张三和赵六两个关键字的姓的首字母都是 Z ,最终求出的电话号码的存储位置相同,这种现象称为 冲突。 在设计哈希函数时,要尽量地避免冲突现象的发生。对于哈希表而言,冲突只能尽可能地少,无法完全避免。

6.2.2 哈希函数的构造

常用的哈希函数的构造方法有 6 种:直接定址法、数字分析法、平方取中法、折叠法、除留余数法和随机数法。

直接定址法: 其哈希函数为一次函数,即以下两种形式:

H(key)= key 或者 H(key)=a * key + b
其中 H(key)表示关键字为 key 对应的哈希地址,a 和 b 都为常数。

例如有一个从 1 岁到 100 岁的人口数字统计表,如下表所示:
在这里插入图片描述
假设其哈希函数为第一种形式,其关键字的值表示最终的存储位置。若需要查找年龄为 25 岁的人口数量,将年龄 25 带入哈希函数中,直接求得其对应的哈希地址为 25(求得的哈希地址表示该记录的位置在查找表的第 25 位)。

数字分析法: 如果关键字由多位字符或者数字组成,就可以考虑抽取其中的 2 位或者多位作为该关键字对应的哈希地址,在取法上尽量选择变化较多的位,避免冲突发生。例如下表中列举的是一部分关键字,每个关键字都是有 8 位十进制数组成:
在这里插入图片描述
通过分析关键字的构成,很明显可以看到关键字的第 1 位和第 2 位都是固定不变的,而第 3 位不是数字 3 就是 4,最后一位只可能取 2、7 和 5,只有中间的 4 位其取值近似随机,所以为了避免冲突,可以从 4 位中任意选取 2 位作为其哈希地址。

平方取中法 是对关键字做平方操作,取中间得几位作为哈希地址。此方法也是比较常用的构造哈希函数的方法。例如关键字序列为{421,423,436},对各个关键字进行平方后的结果为{177241,178929,190096},则可以取中间的两位 {72,89,00} 作为其哈希地址。

折叠法 是将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。此方法适合关键字位数较多的情况。例如,在图书馆中图书都是以一个 10 位的十进制数字为关键字进行编号的,若对其查找表建立哈希表时,就可以使用折叠法。若某书的编号为:0-442-20586-4,分割方式如下图中所示,在对其进行折叠时有两种方式:一种是移位折叠,另一种是间界折叠:移位折叠 是将分割后的每一小部分,按照其最低位进行对齐,然后相加;间界折叠 是从一端向另一端沿分割线来回折叠。
在这里插入图片描述
除留余数法: 若已知整个哈希表的最大长度 m,可以取一个不大于 m 的数 p,然后对该关键字 key 做取余运算,即:H(key)= key % p。在此方法中,对于 p 的取值非常重要,由经验得知 p 可以为不大于 m 的质数或者不包含小于 20 的质因数的合数。

随机数法: 是取关键字的一个随机函数值作为它的哈希地址,即:H(key)= random(key),此方法适用于关键字长度不等的情况。 注意:这里的随机函数其实是伪随机函数,随机函数是即使每次给定的 key 相同,但是 H(key)都是不同;而伪随机函数正好相反,每个 key 都对应的是固定的 H(key)。

如此多的构建哈希函数的方法,在选择的时候,需要根据实际的查找表的情况采取适当的方法。通常考虑的因素有以下几方面:

  1. 关键字的长度。如果长度不等,就选用随机数法。如果关键字位数较多,就选用折叠法或者数字分析法;反之如果位数较短,可以考虑平方取中法;
  2. 哈希表的大小。如果大小已知,可以选用除留余数法;
  3. 关键字的分布情况;
  4. 查找表的查找频率;
  5. 计算哈希函数所需的时间(包括硬件指令的因素)

6.2.3 处理冲突的方法

对于哈希表的建立,需要选取合适的哈希函数,但是对于无法避免的冲突,需要采取适当的措施去处理。通常用的处理冲突的方法有以下几种:

  1. 开放定址法 H(key)=(H(key)+ d)MOD m(其中 m 为哈希表的表长,d 为一个增量) 当得出的哈希地址产生冲突时,选取以下 3 种方法中的一种获取 d 的值,然后继续计算,直到计算出的哈希地址不在冲突为止,这 3 种方法为:
    • 线性探测法:d=1,2,3,…,m-1
    • 二次探测法:d=12,-12,22,-22,32,…
    • 伪随机数探测法:d=伪随机数
    • 例如,在长度为 11 的哈希表中已填写好 17、60 和 29 这 3 个数据(如图 (a) 所示),其中采用的哈希函数为:H(key)=key MOD 11,现有第 4 个数据 38 ,当通过哈希函数求得的哈希地址为 5,与 60 冲突,则分别采用以上 3 种方式求得插入位置如图(b)所示:
      在这里插入图片描述
      注释:在线性探测法中,当遇到冲突时,从发生冲突位置起,每次 +1,向右探测,直到有空闲的位置为止;二次探测法中,从发生冲突的位置起,按照 +12,-12,+22,…如此探测,直到有空闲的位置;伪随机探测,每次加上一个随机数,直到探测到空闲位置结束。
  2. 再哈希法。当通过哈希函数求得的哈希地址同其他关键字产生冲突时,使用另一个哈希函数计算,直到冲突不再发生。
  3. 链地址法。将所有产生冲突的关键字所对应的数据全部存储在同一个线性链表中。例如有一组关键字为{19,14,23,01,68,20,84,27,55,11,10,79},其哈希函数为:H(key)=key MOD 13,使用链地址法所构建的哈希表如下图所示:
    在这里插入图片描述
  4. 建立一个公共溢出区。建立两张表,一张为基本表,另一张为溢出表。基本表存储没有发生冲突的数据,当关键字由哈希函数生成的哈希地址产生冲突时,就将数据填入溢出表。

七、有序性

字典元素是按照 key 的 hash 值无序存储的。但是,有时候我们却需要一个有序的元素顺序,Python 3.6 之前,使用 OrderedDict 类可以做到,3.6开始 dict 自身支持。到底 Python 对一个无序数据结构记录了什么顺序?

(py35) C:\Users\amoxiang>python
Python 3.5.6 |Anaconda, Inc.| (default, Aug 26 2018, 16:05:27) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> d = {'a':300, 'b':200, 'c':100, 'd':50}
>>> d
{'a': 300, 'b': 200, 'c': 100, 'd': 50}
>>> d
{'a': 300, 'b': 200, 'c': 100, 'd': 50}
>>> d
{'a': 300, 'b': 200, 'c': 100, 'd': 50}
>>> list(d.keys())
['a', 'b', 'c', 'd']
>>>
>(py35) C:\Users\amoxiang>python
Python 3.5.6 |Anaconda, Inc.| (default, Aug 26 2018, 16:05:27) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> d = {'a':300, 'b':200, 'c':100, 'd':50}
>>> d
{'b': 200, 'c': 100, 'a': 300, 'd': 50}
>>> d
{'b': 200, 'c': 100, 'a': 300, 'd': 50}
>>>

Python 3.6 之前,在不同的机器上,甚至同一个程序分别运行2次,都不能确定不同的 key 的先后顺序。Python 3.6+ 表现如下:

C:\Users\amoxiang>python
Python 3.10.11 (tags/v3.10.11:7d4cc5a, Apr  5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> d = {'c': 100, 'a': 300, 'b': 200, 'd': 50}
>>> d
{'c': 100, 'a': 300, 'b': 200, 'd': 50}
>>> d.keys()
dict_keys(['c', 'a', 'b', 'd'])
C:\Users\amoxiang>python
Python 3.10.11 (tags/v3.10.11:7d4cc5a, Apr  5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> d = {'c': 100, 'a': 300, 'b': 200, 'd': 50}
>>> d
{'c': 100, 'a': 300, 'b': 200, 'd': 50}

Python 3.6+,记录了字典 key 的录入顺序,遍历的时候,就是按照这个顺序。如果使用 d = {'a':300, 'b':200, 'c':100, 'd':50} ,就会造成以为字典按照 key 排序的错觉。目前,建议不要 Python 3.6+ 提供的这种字典特性,还是认为字典返回的是无序的,可以在 Python 不同版本中考虑使用 OrderedDict 类来保证这种录入顺序。

collections.OrderedDict。自 Python 3.6 起,内置的 dict 也保留键的顺序。使用 OrderedDict 最主要的原因是编写与早期 Python 版本兼容的代码。不过,dict 和 OrderedDict 之间还有一些差异,Python 文档中有说明,摘录如下(根据日常使用频率,顺序有调整)。

  1. OrderedDict 的等值检查考虑顺序。
  2. OrderedDict 的 popitem() 方法签名不同,可通过一个可选参数指定移除哪一项。
  3. OrderedDict 多了一个 move_to_end() 方法,便于把元素的位置移到某一端。
  4. 常规的 dict 主要用于执行映射操作,插入顺序是次要的。
  5. OrderedDict 的目的是方便执行重新排序操作,空间利用率、迭代速度和更新操作的性能是次要的。
  6. 从算法上看,OrderedDict 处理频繁重新排序操作的效果比 dict 好,因此适合用于跟踪近期存取情况(例如在 LRU 缓存中)。

八、小结

Python 使用哈希表实现 dict,因此字典的效率非常高,不过这种设计对实践也有一些影响,不容忽视。

  1. 键必须是可哈希的对象。必须正确实现 __hash____eq__ 方法。
  2. 通过键访问项速度非常快。对于一个包含数百万个键的 dict 对象,Python 通过计算键的哈希码就可以直接定位键,然后找出索引在哈希表中的偏移量,稍微尝试几次就能找到匹配的条目,因此开销不大。
  3. 在 CPython 3.6 中,dict 的内存布局更为紧凑,顺带的一个副作用是键的顺序得以保留。Python 3.7 正式支持保留顺序。
  4. 尽管采用了新的紧凑布局,但是字典仍然占用大量内存,这是不可避免的。对容器来说,最紧凑的内部数据结构是指向项的指针的数组 (元组就是这样存储的)。 与之相比,哈希表中的条目存储的数据更多,而且为了保证效率,Python 至少需要把哈希表中三分之一的行留空。
  5. 为了节省内存,不要在 __init__ 方法之外创建实例属性。最后一点背后的原因是,Python 默认在特殊的 __dict__ 属性中存储实例属性,而这个属性的值是一个字典,依附在各个实例上。 自从 Python 3.3 实现 "PEP 412—Key-Sharing Dictionary" 之后,类的实例可以共用一个哈希表,随类一起存储。如果新实例与 __init__ 返回的第一个实例拥有相同的属性名称,那么新实例的 __dict__ 属性就共享这个哈希表,仅以指针数组的形式存储新实例的属性值。__init__ 方法执行完毕后再添加实例属性,Python 就不得不为这个实例的 __dict__ 属性创建一个新哈希表(在 Python 3.3 之前,这是所有实例的默认行为)。根据 PEP 412,这种优化可将面向对象程序的内存使用量减少 10%~20%。
  • 16
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Amo Xiang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值