Python基础指南之字典dict创建与底层原理详解
一、开篇:Python中第二重要的数据结构
前面八篇文章我们学完了列表和元组两个序列类型。今天我们要进入Python中效率最高、使用最频繁的映射类型——字典(dict)。
如果列表是Python中使用频率第一名的数据结构,那字典就是当之无愧的第二名,而且在某些领域(配置管理、缓存、数据转换)甚至超越列表。字典的核心能力是键值映射——用一个"键"快速找到对应的"值",时间复杂度接近O(1)。这意味着无论字典里有100个元素还是100万个元素,查找一个键的时间基本不变。
这篇文章会从字典的创建方式开始,深入到哈希表的底层原理,让你真正理解为什么字典这么快,以及如何正确选择字典的键。
二、字典是什么——从概念到代码
2.1 现实世界的类比
# 字典就像一本真实的词典(或电话簿)
# - 你要查"Python"这个词(键)
# - 词典告诉你它的释义(值)
# - 你不需要从头翻到尾(O(n)),而是可以根据索引快速定位(O(1))
# 在代码中
word_meaning = {
'Python': '一种高级编程语言',
'dict': '字典,键值对映射结构',
'API': '应用程序编程接口',
}
print(word_meaning['Python']) # 一种高级编程语言
print(word_meaning['API']) # 应用程序编程接口
2.2 字典的核心特征
# 特征一:键值对存储
person = {
'name': '小明',
'age': 25,
'city': '北京',
'job': '工程师',
}
# 特征二:通过键访问值(O(1)平均时间复杂度)
print(person['name']) # 小明
print(person['age']) # 25
# 特征三:键必须是不可变类型(可哈希)
# 字符串、数字、元组(不含可变元素)可以作为键
valid_keys = {
'string': 1,
42: 2,
3.14: 3,
True: 4,
(1, 2): 5,
}
# 特征四:键是唯一的(后添加的会覆盖前面的)
d = {'a': 1, 'b': 2, 'a': 999}
print(d) # {'a': 999, 'b': 2}('a'被覆盖了)
# 特征五:Python 3.7+ 字典保持插入顺序
d = {}
d['c'] = 3
d['a'] = 1
d['b'] = 2
print(list(d.keys())) # ['c', 'a', 'b'](按插入顺序)
# 这是Python 3.7+的官方保证
三、字典的八种创建方式
3.1 花括号字面量(最常用)
# 空字典
empty = {}
print(empty) # {}
# 带初始数据的字典
person = {'name': '小明', 'age': 25, 'city': '北京'}
print(person) # {'name': '小明', 'age': 25, 'city': '北京'}
# 嵌套字典
config = {
'database': {
'host': 'localhost',
'port': 5432,
},
'cache': {
'host': 'redis://localhost',
'port': 6379,
},
}
print(config['database']['host']) # localhost
3.2 dict() 构造函数
# 方式一:关键字参数(键必须是合法标识符)
d1 = dict(name='小明', age=25, city='北京')
print(d1) # {'name': '小明', 'age': 25, 'city': '北京'}
# 这种方式的限制:键必须是合法的Python标识符
# d = dict(1='one') # SyntaxError
# 方式二:可迭代对象(每个元素是(key, value)对)
d2 = dict([('name', '小明'), ('age', 25), ('city', '北京')])
print(d2) # {'name': '小明', 'age': 25, 'city': '北京'}
# 方式三:zip组合两个列表
keys = ['name', 'age', 'city']
values = ['小明', 25, '北京']
d3 = dict(zip(keys, values))
print(d3) # {'name': '小明', 'age': 25, 'city': '北京'}
# 方式四:混合使用
d4 = dict(name='小明', age=25) # 关键字参数
d4.update({'city': '北京'})
print(d4) # {'name': '小明', 'age': 25, 'city': '北京'}
3.3 字典推导式
# 基本推导式
squares = {x: x ** 2 for x in range(6)}
print(squares) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# 带条件的推导式
even_squares = {x: x ** 2 for x in range(10) if x % 2 == 0}
print(even_squares) # {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
# 从两个列表推导
keys = ['name', 'age', 'city']
values = ['小明', 25, '北京']
d = {k: v for k, v in zip(keys, values)}
print(d) # {'name': '小明', 'age': 25, 'city': '北京'}
# 转换现有字典
original = {'a': 1, 'b': 2, 'c': 3}
reversed_dict = {v: k for k, v in original.items()}
print(reversed_dict) # {1: 'a', 2: 'b', 3: 'c'}
# 过滤字典
scores = {'小明': 85, '小红': 92, '小刚': 78, '小李': 95}
passing = {name: score for name, score in scores.items() if score >= 90}
print(passing) # {'小红': 92, '小李': 95}
3.4 其他创建方式
# fromkeys():用相同的值初始化多个键
keys = ['a', 'b', 'c', 'd']
d = dict.fromkeys(keys, 0)
print(d) # {'a': 0, 'b': 0, 'c': 0, 'd': 0}
# 不指定值时默认None
d2 = dict.fromkeys(['x', 'y', 'z'])
print(d2) # {'x': None, 'y': None, 'z': None}
# ⚠️ fromkeys的陷阱:可变默认值
# d3 = dict.fromkeys(['a', 'b'], [])
# d3['a'].append(1)
# print(d3) # {'a': [1], 'b': [1]} ← 两个键共享同一个列表!
# 如果每个值需要独立的对象,用推导式
d3 = {k: [] for k in ['a', 'b']}
d3['a'].append(1)
print(d3) # {'a': [1], 'b': []} ← 各自独立
# 从Counter创建
from collections import Counter
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
counter = Counter(words)
print(dict(counter)) # {'apple': 3, 'banana': 2, 'cherry': 1}
# 从JSON字符串创建
import json
json_str = '{"name": "小明", "age": 25, "scores": [85, 92, 78]}'
data = json.loads(json_str)
print(data) # {'name': '小明', 'age': 25, 'scores': [85, 92, 78]}
四、字典的底层原理——哈希表
4.1 为什么字典这么快
# 字典的底层实现是哈希表(Hash Table)
# 核心思想:通过哈希函数将任意键映射到一个固定范围的整数
# 这个整数就是数据存储的位置索引
# 简化模型:
# 1. 计算键的哈希值 hash(key)
# 2. 将哈希值映射到内部数组的索引
# 3. 在索引位置存储/查找值
# 这就是为什么字典查找是O(1)——不需要遍历,直接定位
# 展示哈希值
print(hash('hello')) # 某个很大的整数(每次运行可能不同)
print(hash(42)) # 42(整数的哈希值通常等于自身)
print(hash((1, 2, 3))) # 某个整数
# print(hash([1, 2, 3])) # TypeError: unhashable type: 'list'
4.2 哈希冲突
# 哈希冲突:不同的键可能产生相同的哈希值(或映射到相同的索引) # Python的字典使用"开放寻址法"解决冲突 # 你不需要手动处理哈希冲突——Python已经搞定了 # 但你需要知道:哈希冲突会影响性能 # 如果所有键的哈希值都相同(极端情况),字典退化为O(n) # 正常情况:O(1)查找 # 最坏情况:O(n)查找(极少发生,需要有严重bug的哈希函数)
4.3 键的哈希性要求
# 字典的键必须满足两个条件:
# 1. 可哈希(hashable)——实现了 __hash__ 方法
# 2. 可比较相等——实现了 __eq__ 方法
# 可哈希的类型:
print(hash('hello')) # 字符串 ✓
print(hash(42)) # 整数 ✓
print(hash(3.14)) # 浮点数 ✓
print(hash(True)) # 布尔值 ✓
print(hash((1, 2, 3))) # 元组(全不可变元素) ✓
print(hash(None)) # None ✓
# 不可哈希的类型:
# hash([1, 2, 3]) # 列表 ✗
# hash({'a': 1}) # 字典 ✗
# hash({1, 2, 3}) # 集合 ✗
# hash((1, [2, 3])) # 包含可变元素的元组 ✗
# 自定义对象默认是可哈希的(除非定义了__eq__)
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(hash(p)) # 有哈希值(基于对象id)
# 但如果定义了__eq__,就需要同时定义__hash__
# 关键原则:相等的对象必须有相等的哈希值
# a == b ⇒ hash(a) == hash(b)
4.4 Python 3.6+的字典优化
# Python 3.6+字典使用更紧凑的内存布局
# 官方在3.7中保证了插入顺序
# 旧实现(Python 3.5及以前)
# - 使用稀疏表
# - 内存效率较低
# - 不保证顺序
# 新实现(Python 3.6+)
# - 使用稠密表+索引表
# - 内存减少20-25%
# - 保持插入顺序(3.7正式保证)
import sys
# 对比空字典在不同版本的大小(不可直接对比,仅供参考)
d = {}
print(f'空字典内存: {sys.getsizeof(d)} 字节')
# 添加一些数据
for i in range(100):
d[f'key_{i}'] = i
print(f'100元素的字典: {sys.getsizeof(d)} 字节')
五、字典的键类型深入
5.1 哪些类型可以作为键
# 可以作为字典键的类型(不可变 + 可哈希)
# 字符串(最常用)
d = {'name': '小明'}
# 数字(整数、浮点数)
d = {1: 'one', 2: 'two', 3.14: 'pi'}
# 布尔值(True=1, False=0——注意可能与数字键冲突)
d = {True: 'yes', False: 'no'}
print(d) # {True: 'yes', False: 'no'}
# 元组(所有元素都不可变)
d = {(1, 2): 'point A', (3, 4): 'point B'}
# frozenset(不可变集合)
d = {frozenset([1, 2]): 'set12', frozenset([3, 4]): 'set34'}
# None
d = {None: 'nothing'}
# 自定义对象(默认可哈希)
class User:
def __init__(self, name):
self.name = name
u1 = User('小明')
d = {u1: '用户数据'}
print(d[u1]) # 用户数据
5.2 字符串键的特殊地位
# 字符串是字典键的绝对主力——90%以上的字典都用字符串作键
# 原因:人类可读、方便序列化(JSON)、适合配置和API
# 关键字参数语法要求键是合法标识符
config = dict(host='localhost', port=8080, debug=True)
# 字符串键支持的关键字参数创建
person = {'name': '小明', 'age': 25}
# 以下三种方式等价:
print(person['name'])
print(person.get('name'))
# 但person.name不行(对象属性语法)
# Python 3.8+ 如果键是字符串且不含特殊字符,可用更简洁的方式
# 普通情况还是用 [] 或 get()
5.3 处理缺失键的模式
# 字典最常遇到的错误之一:KeyError
d = {'name': '小明', 'age': 25}
# print(d['city']) # KeyError: 'city'
# 方法一:get()——最常用
city = d.get('city', '未知')
print(city) # 未知
# 方法二:in 检查
if 'city' in d:
city = d['city']
else:
city = '未知'
# 方法三:setdefault()——获取值,如果不存在则设置默认值
city = d.setdefault('city', '北京')
print(city) # 北京
print(d) # {'name': '小明', 'age': 25, 'city': '北京'}
# setdefault的妙用——分组
words = ['apple', 'banana', 'apricot', 'cherry', 'blueberry']
by_first_letter = {}
for word in words:
first = word[0]
by_first_letter.setdefault(first, []).append(word)
print(by_first_letter)
# {'a': ['apple', 'apricot'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}
# 方法四:collections.defaultdict(更优雅的分组方式)
from collections import defaultdict
by_first_letter = defaultdict(list)
for word in words:
by_first_letter[word[0]].append(word)
print(dict(by_first_letter))
六、字典的性能特征
6.1 时间复杂度
# 字典操作的平均时间复杂度
# O(1) 操作:
# - d[key]:通过键获取值
# - d[key] = value:设置键值对
# - del d[key]:删除键值对
# - key in d:检查键是否存在
# O(n) 操作:
# - for key in d:遍历(O(n))
# - len(d):获取长度(O(1)——字典维护了计数)
# - d.clear():清空(O(n)——需要释放所有元素)
# - d.copy():浅拷贝(O(n))
# 实际性能测试
import time
n = 1000000
# 创建大字典
d = {i: i ** 2 for i in range(n)}
# 查找——O(1)
start = time.perf_counter()
for _ in range(1000000):
_ = d[500000]
t_lookup = time.perf_counter() - start
print(f'100万次查找: {t_lookup:.3f}秒')
print(f'每次查找: {t_lookup / 1000000 * 1e9:.1f}纳秒')
# 检查存在——O(1)
start = time.perf_counter()
for _ in range(1000000):
_ = 500000 in d
t_check = time.perf_counter() - start
print(f'100万次in检查: {t_check:.3f}秒')
6.2 字典 vs 列表查找
import time
n = 10000
# 创建数据
lst = list(range(n))
d = {i: True for i in range(n)}
# 列表查找(O(n))
start = time.perf_counter()
for _ in range(10000):
_ = 9999 in lst
t_list = time.perf_counter() - start
# 字典查找(O(1))
start = time.perf_counter()
for _ in range(10000):
_ = 9999 in d
t_dict = time.perf_counter() - start
print(f'列表in查找: {t_list:.4f}秒')
print(f'字典in查找: {t_dict:.4f}秒')
print(f'字典比列表快 {t_list / t_dict:.0f} 倍')
# 随着n增大,字典的优势更加明显
七、字典视图——keys()、values()、items()
7.1 三种视图对象
d = {'name': '小明', 'age': 25, 'city': '北京'}
# keys()——键视图
keys = d.keys()
print(keys) # dict_keys(['name', 'age', 'city'])
print(type(keys)) # <class 'dict_keys'>
# values()——值视图
values = d.values()
print(values) # dict_values(['小明', 25, '北京'])
# items()——键值对视图
items = d.items()
print(items) # dict_items([('name', '小明'), ('age', 25), ('city', '北京')])
# 视图是动态的——能反映字典的变化!
d['job'] = '工程师'
print(keys) # dict_keys(['name', 'age', 'city', 'job'])(自动更新!)
7.2 视图的操作
d = {'a': 1, 'b': 2, 'c': 3}
# 视图支持迭代
for key in d.keys():
print(key)
for value in d.values():
print(value)
for key, value in d.items():
print(f'{key} = {value}')
# 视图支持成员检查
print('a' in d.keys()) # True
print(1 in d.values()) # True
print(('a', 1) in d.items()) # True
# 视图支持集合操作(keys()和items())
keys = d.keys()
print(keys & {'a', 'c', 'e'}) # {'a', 'c'}(交集)
print(keys | {'d', 'e'}) # {'a', 'b', 'c', 'd', 'e'}(并集)
print(keys - {'a'}) # {'b', 'c'}(差集)
print(keys ^ {'a', 'd'}) # {'b', 'c', 'd'}(对称差)
# values()通常不支持集合操作(因为值可能重复且可能是不可哈希的)
八、实战:字典的构建模式
8.1 构建查找表
# 用字典构建查找表替代多个if-elif
def get_status_text(status_code):
"""这种写法比if-elif链更清晰高效"""
status_map = {
200: '成功',
301: '永久重定向',
404: '未找到',
500: '服务器错误',
502: '网关错误',
503: '服务不可用',
}
return status_map.get(status_code, '未知状态')
print(get_status_text(200)) # 成功
print(get_status_text(418)) # 未知状态
# 构建函数调度表
def add(a, b): return a + b
def sub(a, b): return a - b
def mul(a, b): return a * b
def div(a, b): return a / b if b != 0 else None
operations = {
'+': add,
'-': sub,
'*': mul,
'/': div,
}
def calculate(operator, a, b):
func = operations.get(operator)
if func:
return func(a, b)
raise ValueError(f'不支持的运算符: {operator}')
print(calculate('+', 10, 5)) # 15
print(calculate('*', 10, 5)) # 50
8.2 构建缓存
# 字典天然适合做缓存
def memoize(func):
"""装饰器:为函数添加缓存"""
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
"""斐波那契数列——不带缓存是O(2^n),带缓存是O(n)"""
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# 没有缓存,fibonacci(40)可能需要几十秒
# 有了字典缓存,秒出结果
print(fibonacci(40)) # 102334155
九、本篇小结
字典是Python的键值映射容器,掌握它的创建和原理至关重要:
- 创建方式:花括号
{}、dict()、推导式、fromkeys()、zip()、JSON加载 - 底层原理:哈希表实现,O(1)平均查找时间
- 键的要求:必须可哈希(不可变),字符串是最常用的键类型
- 缺失键处理:
get()(推荐)、setdefault()、in检查 - 保持顺序:Python 3.7+字典保持插入顺序
- 视图:
keys()、values()、items()是动态视图
字典的使用频率和列表不相上下。理解它的底层原理(哈希表),能帮助你更好地选择键类型、优化查找性能。下一篇我们将进入字典的增删改查操作——掌握字典的所有修改方法。
到此这篇关于Python基础指南之字典dict创建与底层原理详解的文章就介绍到这了,更多相关Python字典dict创建内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
Python将Office文档(Word、Excel、PDF、PPT)转为OFD格式的实现方法
OFD(Open Fixed-layout Document )是我国自主制定的一种开放版式文件格式标准,如果想要通过Python将Office文档(如Word、Excel或PowerPoint)及PDF文档转换为OFD格式,可以参考本文中提供的实现方法,需要的朋友可以参考下2024-06-06
Python中的split()、rsplit()、splitlines()的区别解析
Python提供了三种字符串分割的方法:split()、rsplit()和splitlines(),本文主要通过案例介绍这三种字符串分割函数的区别,感兴趣的朋友一起看看吧2023-12-12


最新评论