Python字典核心操作与高级技巧全解析
1. 字典是什么以及为什么它如此重要如果你刚开始学Python或者已经写过一些脚本那么dict字典这个数据结构你肯定绕不过去。它几乎无处不在从读取配置文件、处理JSON数据到构建缓存、管理对象属性字典都扮演着核心角色。很多人觉得字典不就是“键值对”嘛有什么好讲的但恰恰是这种看似简单的结构里面藏着不少门道和“坑”。用好了你的代码简洁高效用不好可能就是性能瓶颈和Bug的温床。简单来说Python的字典是一个可变、无序的映射类型它存储的是键key和值value的对应关系。键必须是不可变类型如字符串、数字、元组而值可以是任意Python对象。它的核心优势在于通过键来查找、插入或删除对应的值其平均时间复杂度是O(1)也就是常数时间速度极快。这背后是哈希表Hash Table的功劳。你可以把它想象成一个超级高效的“电话本”你知道一个人的名字键就能瞬间找到他的电话号码值而不需要从头到尾翻一遍。为什么我要专门总结字典的常用操作因为在日常开发中我看到太多人只停留在dict[key] value和dict.get(key)的层面。实际上字典提供了丰富的方法和技巧来处理更复杂的场景比如合并多个字典、安全地访问嵌套数据、按照特定条件筛选内容甚至是利用字典来实现一些巧妙的逻辑。掌握这些能让你从“会用字典”升级到“善用字典”写出更Pythonic、更健壮的代码。2. 字典的创建与初始化不止一种方式创建字典是最基础的一步但你知道有多少种方法吗每种方法适用的场景又是什么这里我为你梳理了最实用的几种。2.1 最直接的方式花括号{}这是最常用、最直观的方法适合在代码中直接定义已知的键值对。# 创建一个空字典 empty_dict {} # 创建一个包含初始数据的字典 user_info { name: 张三, age: 30, city: 北京 }这种方式清晰明了键和值一目了然。在定义配置、常量映射或者小型数据集合时我强烈推荐使用它。2.2 使用dict()构造函数dict()函数更加灵活它可以从多种数据结构构造字典。# 1. 使用关键字参数键必须是合法的变量名即字符串且不含特殊字符 dict1 dict(name李四, age25, city上海) # 结果: {name: 李四, age: 25, city: 上海} # 2. 使用包含二元组或列表的可迭代对象 dict2 dict([(name, 王五), (age, 28), (city, 广州)]) # 结果: {name: 王五, age: 28, city: 广州} # 3. 使用zip函数合并两个列表 keys [a, b, c] values [1, 2, 3] dict3 dict(zip(keys, values)) # 结果: {a: 1, b: 2, c: 3}什么时候用dict()当你的数据源是动态生成的比如从两个列表合并而来或者键名包含连字符等不能作为变量名的字符时这时不能用关键字参数方式dict()就派上用场了。例如从API接口拿到两个列表一个存字段名一个存值用zip加dict()就能快速组装成字典。2.3 字典推导式优雅且强大如果你需要对数据进行一些处理再生成字典字典推导式是你的最佳选择。它的语法和列表推导式类似非常Pythonic。# 将一个列表的元素作为键其平方作为值 numbers [1, 2, 3, 4, 5] squared_dict {x: x**2 for x in numbers} # 结果: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25} # 带条件过滤的推导式 even_squared {x: x**2 for x in numbers if x % 2 0} # 结果: {2: 4, 4: 16} # 处理两个列表键值都做转换 keys [apple, banana, cherry] prices [10, 20, 30] price_dict {k.upper(): v*1.1 for k, v in zip(keys, prices)} # 键转大写值加10%税 # 结果: {APPLE: 11.0, BANANA: 22.0, CHERRY: 33.0}字典推导式不仅代码简洁而且执行效率通常也高于显式的循环。在处理数据转换和过滤时我几乎总是首选它。2.4 使用fromkeys方法快速创建默认字典当你需要创建一个新字典并且所有键都对应同一个初始值时fromkeys方法非常方便。# 为多个键设置相同的默认值 default_dict dict.fromkeys([key1, key2, key3], default_value) # 结果: {key1: default_value, key2: default_value, key3: default_value} # 如果不提供第二个参数默认值为None keys_only dict.fromkeys([a, b, c]) # 结果: {a: None, b: None, c: None}这个方法在初始化计数器、状态标记或者缓存结构时特别有用。但要注意如果默认值是一个可变对象如列表、字典所有键会共享同一个对象引用这通常不是你想要的会导致意外的数据污染。我们后面在“坑与技巧”部分会详细讨论这个问题。3. 核心操作增删改查的学问创建了字典接下来就是对它进行“增删改查”。这些操作看似基础但细节决定成败。3.1 访问元素安全第一访问字典元素最直接的方式是用方括号[]但如果键不存在会抛出KeyError异常。my_dict {a: 1, b: 2} value my_dict[a] # 正确得到 1 # value my_dict[c] # 错误KeyError: c在不确定键是否存在时更安全的做法是使用get(key, default)方法。如果键存在返回对应的值如果不存在则返回你指定的默认值默认为None而不会引发异常。value my_dict.get(c) # 返回 None value my_dict.get(c, 0) # 返回指定的默认值 0我的经验是在大多数从外部获取数据如解析JSON、读取数据库后访问字典的场景中优先使用get方法。这能有效避免因为数据格式意外变化而导致的程序崩溃。只有在你百分之百确定键一定存在时才使用[]直接访问。3.2 新增与修改元素本质相同向字典中添加新元素或修改现有元素语法是一样的dict[key] value。如果键已存在则更新其值如果不存在则创建新的键值对。my_dict {} my_dict[new_key] new_value # 新增 my_dict[new_key] updated_value # 修改3.3 删除元素多种方法各有用处删除操作有几种方式适用于不同场景del语句直接删除指定键的项。如果键不存在同样会引发KeyError。my_dict {a: 1, b: 2, c: 3} del my_dict[b] # 现在 my_dict 是 {a: 1, c: 3} # del my_dict[d] # KeyError!pop(key, default)方法删除指定键的项并返回被删除的值。这是del的“有返回值”版本。如果提供了default参数且键不存在则返回default而不抛异常否则抛KeyError。value my_dict.pop(a) # 删除键a并返回1 # my_dict 现在是 {c: 3} value my_dict.pop(d, None) # 键d不存在返回None不抛异常popitem()方法在Python 3.7中字典会保持插入顺序。popitem()会移除并返回最后插入的LIFO顺序键值对作为一个元组。对于空字典调用会引发KeyError。这个方法在实现栈或缓存淘汰如LRU Cache的简单实现时很有用。my_dict {a: 1, b: 2} key, value my_dict.popitem() # 移除并返回 (b, 2)clear()方法清空字典中的所有项使其变为空字典{}。my_dict.clear() # my_dict 现在是 {}选择建议如果你需要知道被删除的值用pop如果只是要删除用del如果想清空整个字典用clearpopitem则在特定顺序操作场景下使用。3.4 检查键是否存在in操作符判断一个键是否在字典中使用in操作符。这是最快、最Pythonic的方式。my_dict {a: 1, b: 2} if a in my_dict: print(键 a 存在) # 检查键不存在 if c not in my_dict: print(键 c 不存在)不要用my_dict.get(key)是否等于None来判断因为值本身可能就是None这样会产生歧义。4. 遍历与迭代高效获取数据字典的遍历有多种方式分别用于获取键、值或键值对。理解它们的区别和性能影响很重要。4.1 遍历键最直接的方式直接对字典进行迭代默认就是遍历它的键。my_dict {name: Alice, age: 25, city: London} for key in my_dict: print(key) # 输出: # name # age # city你也可以显式地使用keys()方法它返回一个视图对象dict_keys这个视图会动态反映字典的变化。for key in my_dict.keys(): print(key)在大多数情况下直接迭代字典和迭代my_dict.keys()效果一样。但keys()视图在某些需要集合操作的场景下更方便比如求两个字典键的交集my_dict.keys() other_dict.keys()。4.2 遍历值使用values()方法如果你只关心字典中的值可以使用values()方法它返回一个dict_values视图。for value in my_dict.values(): print(value) # 输出: # Alice # 25 # London4.3 遍历键值对使用items()方法这是最常用的遍历方式可以同时拿到键和值。items()返回一个dict_items视图其中每个元素是一个(key, value)元组。for key, value in my_dict.items(): print(f{key}: {value}) # 输出: # name: Alice # age: 25 # city: London重要技巧在遍历过程中修改字典如删除项是危险的可能会引发RuntimeError。安全的做法是先收集要处理的键遍历结束后再统一修改或者遍历其键或项的副本。# 错误示例在遍历时删除 # for key in my_dict: # if some_condition(key): # del my_dict[key] # 可能引发 RuntimeError # 正确做法1先记录要删除的键 keys_to_delete [] for key, value in my_dict.items(): if value 25: # 假设要删除值为25的项 keys_to_delete.append(key) for key in keys_to_delete: del my_dict[key] # 正确做法2Python 3遍历keys()的副本 for key in list(my_dict.keys()): if my_dict[key] 25: del my_dict[key]5. 字典的合并与更新在实际项目中我们经常需要将多个字典的内容合并到一起。Python提供了几种方法它们的行为有细微差别。5.1update()方法就地更新update()方法将一个字典或键值对序列中的所有项合并到当前字典中。如果键重复后者的值会覆盖前者的值。这个操作是就地修改原字典。dict1 {a: 1, b: 2} dict2 {b: 3, c: 4} dict1.update(dict2) print(dict1) # 输出: {a: 1, b: 3, c: 4} # dict1被修改了dict2不变update()也可以接受关键字参数或其他可迭代对象。dict1.update(d5, e6) # 使用关键字参数 # dict1 现在是 {a: 1, b: 3, c: 4, d: 5, e: 6}5.2 合并运算符|和|(Python 3.9)从Python 3.9开始引入了字典合并运算符让合并操作更直观。|合并运算符创建一个新字典包含两个字典的所有项。重复键的值来自右边的操作数。dict1 {a: 1, b: 2} dict2 {b: 3, c: 4} merged_dict dict1 | dict2 print(merged_dict) # 输出: {a: 1, b: 3, c: 4} print(dict1) # 输出: {a: 1, b: 2} (原字典未变)|更新运算符相当于update()的增强版就地更新左边的字典。dict1 {a: 1, b: 2} dict2 {b: 3, c: 4} dict1 | dict2 print(dict1) # 输出: {a: 1, b: 3, c: 4}如何选择如果你需要保留原始字典不变就使用|运算符或后面提到的字典解包。如果你明确要修改原字典使用update()或|。5.3 字典解包**(Python 3.5)在函数调用和字典字面量中可以使用**运算符来解包字典。这为合并多个字典提供了一种非常优雅的方式。dict1 {a: 1, b: 2} dict2 {b: 3, c: 4} dict3 {d: 5} # 合并多个字典到一个新字典 merged_dict {**dict1, **dict2, **dict3} print(merged_dict) # 输出: {a: 1, b: 3, c: 4, d: 5} # 注意dict2中的b覆盖了dict1中的b解包语法非常灵活你可以在创建新字典时混合使用解包和显式的键值对。base_config {host: localhost, port: 8080} user_config {port: 9090, debug: True} final_config {**base_config, **user_config, timeout: 30} # 结果: {host: localhost, port: 9090, debug: True, timeout: 30}6. 字典的常用方法进阶除了基础的增删改查字典还有一些内置方法能解决特定问题极大地提升代码效率。6.1setdefault(key, default)安全的“获取或设置”这个方法用于安全地获取一个值。如果键存在则返回其值如果键不存在则先将key: default插入字典再返回default。my_dict {a: 1} # 键存在直接返回值 value my_dict.setdefault(a, 100) print(value) # 输出: 1 print(my_dict) # 输出: {a: 1} (字典未变) # 键不存在插入并返回默认值 value my_dict.setdefault(b, 2) print(value) # 输出: 2 print(my_dict) # 输出: {a: 1, b: 2} (字典已更新)经典应用场景分组计数或初始化复杂值。比如统计一段文本中单词出现的频率text apple banana apple orange banana apple word_count {} for word in text.split(): # 如果word不在字典中将其计数初始化为0然后加1 # 如果word已在字典中获取当前计数然后加1 word_count[word] word_count.setdefault(word, 0) 1 print(word_count) # 输出: {apple: 3, banana: 2, orange: 1}在没有setdefault的情况下你需要写一个if...else判断代码会冗长一些。6.2 视图对象keys(),values(),items()前面提到这些方法返回的是“视图对象”而不是列表。这一点非常重要。视图对象是动态的它们会实时反映字典的变化并且支持集合操作。my_dict {a: 1, b: 2} keys_view my_dict.keys() values_view my_dict.values() items_view my_dict.items() print(keys_view) # 输出: dict_keys([a, b]) print(values_view) # 输出: dict_values([1, 2]) print(items_view) # 输出: dict_items([(a, 1), (b, 2)]) # 动态性演示 my_dict[c] 3 print(keys_view) # 输出: dict_keys([a, b, c]) 视图同步更新了视图对象还支持集合的交集()、并集(|)、差集(-)等操作values()视图除外因为值可能不唯一。dict1 {a: 1, b: 2, c: 3} dict2 {b: 20, c: 3, d: 4} # 找出两个字典中都有的键 common_keys dict1.keys() dict2.keys() print(common_keys) # 输出: {b, c} # 找出在dict1中但不在dict2中的键 unique_to_dict1 dict1.keys() - dict2.keys() print(unique_to_dict1) # 输出: {a}6.3copy()浅拷贝与深拷贝的陷阱copy()方法返回字典的一个浅拷贝shallow copy。这意味着它创建了一个新字典但新字典中的值是对原字典中值的引用对于可变对象。original {list: [1, 2, 3], num: 10} shallow_copied original.copy() # 修改浅拷贝字典中的不可变值不影响原字典 shallow_copied[num] 20 print(original[num]) # 输出: 10 (未变) # 修改浅拷贝字典中的可变值列表原字典也会被影响 shallow_copied[list].append(4) print(original[list]) # 输出: [1, 2, 3, 4] (被修改了)这就是一个常见的“坑”。如果你需要完全独立地复制一个字典包括它内部所有嵌套的可变对象你需要使用copy模块的deepcopy函数。import copy original {list: [1, 2, 3]} deep_copied copy.deepcopy(original) deep_copied[list].append(4) print(original[list]) # 输出: [1, 2, 3] (不受影响)经验法则如果字典的值只包含不可变对象数字、字符串、元组用copy()就够了。如果字典嵌套了列表、字典或其他可变对象并且你希望副本完全独立一定要用deepcopy()。7. 嵌套字典与复杂数据访问现实世界的数据很少是扁平的。我们经常要处理嵌套的字典比如从JSON API返回的数据。安全、优雅地访问嵌套数据是一项必备技能。7.1 链式访问与KeyError风险访问嵌套字典最直接的方式是链式使用[]。data { user: { name: Bob, address: { city: New York, zipcode: 10001 } } } city data[user][address][city] # 得到 New York但这种方式的缺点是如果中间任何一层键不存在就会立即抛出KeyError导致程序中断。7.2 使用get()方法进行安全链式访问我们可以通过多次调用get()来避免异常但代码会变得冗长。city data.get(user, {}).get(address, {}).get(city) # 如果任何一层缺失city将为None这虽然安全但写起来麻烦尤其是嵌套很深的时候。7.3 更优雅的解决方案collections模块的ChainMapChainMap可以将多个字典链接成一个逻辑上的单一映射。查找时它会按顺序在多个字典中搜索直到找到第一个匹配的键。这可以用来为嵌套访问提供默认值但它更适合管理多个上下文的配置而不是纯粹的深层访问。7.4 终极方案自定义递归访问函数或使用第三方库对于复杂的嵌套字典访问一个更健壮的方法是写一个辅助函数。def safe_get(dict_obj, keys, defaultNone): 安全地获取嵌套字典中的值。 :param dict_obj: 目标字典 :param keys: 键的列表或元组如 [user, address, city] :param default: 如果路径不存在返回的默认值 :return: 找到的值或默认值 current dict_obj for key in keys: if isinstance(current, dict) and key in current: current current[key] else: return default return current # 使用示例 city safe_get(data, [user, address, city]) # 返回 New York country safe_get(data, [user, address, country], USA) # 路径不存在返回默认值 USA对于大型项目你可能会考虑使用像glom或jmespath这样的第三方库它们提供了更强大、更声明式的语法来查询复杂的嵌套结构。8. 字典排序从无序到有序在Python 3.7之前字典是无序的你无法依赖其插入顺序。从Python 3.7开始字典会保持插入顺序这是一个官方保证的语言特性。这意味着你遍历字典时项的顺序就是你添加它们的顺序。但“保持插入顺序”不等于“按某种规则排序”。如果你需要按键或按值进行排序仍然需要额外的操作。8.1 按键排序使用内置的sorted()函数它可以对字典的键通过keys()获取进行排序然后你可以根据排序后的键来访问值。my_dict {banana: 3, apple: 4, pear: 1, orange: 2} # 按键升序排序 for key in sorted(my_dict.keys()): print(key, my_dict[key]) # 输出: # apple 4 # banana 3 # orange 2 # pear 1 # 按键降序排序 for key in sorted(my_dict.keys(), reverseTrue): print(key, my_dict[key])更简洁的方式是直接对items()返回的(key, value)元组进行排序。因为元组比较时默认先比较第一个元素即键。# 按键排序并直接获得排序后的键值对列表 sorted_items sorted(my_dict.items()) print(sorted_items) # 输出: [(apple, 4), (banana, 3), (orange, 2), (pear, 1)] for key, value in sorted_items: print(key, value)8.2 按值排序按值排序稍微复杂一点因为我们需要告诉sorted()函数根据每个键值对的第二个元素即值来排序。这可以通过key参数实现。my_dict {banana: 3, apple: 4, pear: 1, orange: 2} # 按值升序排序 sorted_by_value sorted(my_dict.items(), keylambda item: item[1]) print(sorted_by_value) # 输出: [(pear, 1), (orange, 2), (banana, 3), (apple, 4)] # 按值降序排序 sorted_by_value_desc sorted(my_dict.items(), keylambda item: item[1], reverseTrue) print(sorted_by_value_desc) # 输出: [(apple, 4), (banana, 3), (orange, 2), (pear, 1)]这里的lambda item: item[1]是一个匿名函数它接收一个元组item如(banana, 3)然后返回其第二个元素3sorted()函数就根据这个返回值进行排序。8.3 使用operator模块对于简单的按值排序使用lambda很清晰。但operator模块提供了更高效且可读性可能更好的方式。import operator # 按值排序使用itemgetter获取元组的第二个元素 sorted_by_value sorted(my_dict.items(), keyoperator.itemgetter(1)) # 按键排序使用itemgetter获取元组的第一个元素 sorted_by_key sorted(my_dict.items(), keyoperator.itemgetter(0))operator.itemgetter(1)会创建一个函数这个函数的作用和lambda x: x[1]一样但通常性能稍好一些尤其是在处理大量数据时。重要提示sorted()函数返回的是一个列表而不是字典。如果你需要一个保持排序顺序的字典Python 3.7的普通字典就可以因为你只需要按排序后的顺序插入项。或者你可以使用collections.OrderedDict在Python 3.7后与普通字典在有序性上功能重叠但仍有其特定方法。# 创建一个按键排序的新字典 (Python 3.7) sorted_dict dict(sorted(my_dict.items(), keylambda item: item[0])) print(sorted_dict) # 输出: {apple: 4, banana: 3, orange: 2, pear: 1} # 这个字典的遍历顺序就是按键排序的顺序9. 字典与JSON的亲密关系字典和JSONJavaScript Object Notation可以说是天生一对。JSON是一种轻量级的数据交换格式其结构对象、数组、字符串、数字、布尔值、null几乎可以直接映射到Python的数据类型字典、列表、字符串、int/float、True/False、None。9.1 字典转JSON字符串json.dumps()使用json模块的dumps()函数可以将字典序列化为JSON格式的字符串。import json data_dict { name: 小明, age: 20, courses: [数学, 物理], is_student: True, address: None } json_str json.dumps(data_dict, ensure_asciiFalse, indent2) print(json_str) # 输出格式化的JSON字符串 # { # name: 小明, # age: 20, # courses: [数学, 物理], # is_student: true, # address: null # }参数说明ensure_asciiFalse确保中文字符正常显示而不是被转义为\u形式。indent2使输出的JSON字符串带有缩进更易读。在生产环境为了节省空间通常不设置。sort_keysTrue可选参数对输出的键进行排序。9.2 JSON字符串转字典json.loads()反过来使用loads()函数可以将JSON字符串解析为Python字典。json_string {name: 小红, age: 22, city: 上海} parsed_dict json.loads(json_string) print(parsed_dict[name]) # 输出: 小红 print(type(parsed_dict)) # 输出: class dict9.3 处理文件更常见的场景是从文件读取JSON或写入JSON。# 将字典写入JSON文件 with open(data.json, w, encodingutf-8) as f: json.dump(data_dict, f, ensure_asciiFalse, indent2) # 从JSON文件读取到字典 with open(data.json, r, encodingutf-8) as f: loaded_dict json.load(f)踩坑提醒JSON和Python的数据类型并非完全一致。JSON中的true、false、null对应Python的True、False、None。JSON的数值都是浮点数但在Python中json.loads()会智能地解析为int或float。最重要的是JSON的键必须是字符串而Python字典的键可以是多种不可变类型。如果你尝试序列化一个键为元组的字典会得到TypeError。10. 性能考量与高级技巧字典以其O(1)的平均时间复杂度而闻名但了解其内部机制和边界条件能帮助你写出性能更好的代码。10.1 键的选择为什么必须是不可变类型字典的快速查找依赖于哈希表。每个键都会被计算出一个哈希值通过__hash__方法这个值用于确定键值对在内存中的存储位置。如果键是可变对象如列表、字典其内容变化会导致哈希值变化这就破坏了哈希表的完整性使得之前存储的值无法被正确找到。因此Python要求字典的键必须是“可哈希的”hashable即不可变对象如数字、字符串、元组仅当元组内所有元素也都是可哈希的。# 有效的键 valid_dict { 1: integer, # 整数 hello: string, # 字符串 (1, 2): tuple, # 元组元素都是不可变的 # [1,2]: list # 错误列表不可哈希 # {a:1}: dict # 错误字典不可哈希 }10.2 字典的大小与扩容字典在内部会维护一个哈希表。当字典中的项数增加使得负载因子已用槽位/总槽位超过某个阈值时字典会进行扩容resize即分配一个更大的内存空间并重新哈希rehash所有现有的键。这是一个相对昂贵的操作O(n)时间复杂度。这意味着如果你能提前知道字典的大致规模在创建时指定一个初始容量可以避免多次扩容提升性能。# 如果你知道大约要存1000个项可以这样创建 d dict.fromkeys(range(1000)) # 一种方式 # 或者更直接地虽然不能精确控制但给了提示 d {i: None for i in range(1000)} # Python解释器会根据这个规模进行优化10.3in操作符 vsget()vs[]的性能检查一个键是否在字典中in操作符是最快的方式因为它只进行哈希查找不涉及值的返回或异常处理。get()方法在键不存在时返回默认值比先用in检查再用[]访问要快因为get()只做一次哈希查找。而直接使用[]访问不存在的键会引发异常异常处理的开销是最大的。性能排序从快到慢in≈get()键存在时 []键存在时 get()键不存在但避免了异常 []键不存在引发异常。10.4 使用字典模拟Switch-Case语句Python没有switch-case语句但我们可以用字典巧妙地模拟使代码更简洁。def handle_operation(operation, a, b): # 定义一个操作到函数的映射字典 operations { add: lambda x, y: x y, subtract: lambda x, y: x - y, multiply: lambda x, y: x * y, divide: lambda x, y: x / y if y ! 0 else Division by zero } # 使用get方法并提供默认处理函数 func operations.get(operation, lambda x, y: fUnknown operation: {operation}) return func(a, b) print(handle_operation(add, 5, 3)) # 输出: 8 print(handle_operation(power, 2, 3)) # 输出: Unknown operation: power这种方法将逻辑分发dispatch从一长串if-elif-else语句中解放出来使代码更易于维护和扩展。10.5 使用字典进行缓存Memoization字典是实现缓存Memoization的绝佳数据结构常用于优化递归函数避免重复计算。def fibonacci(n, cache{}): 使用缓存计算斐波那契数列 if n in cache: return cache[n] if n 1: result n else: result fibonacci(n-1, cache) fibonacci(n-2, cache) cache[n] result return result print(fibonacci(50)) # 如果没有缓存这个计算会非常慢这里利用了一个技巧默认参数cache{}在函数定义时被求值并且对于每次函数调用如果调用者没有提供cache参数就会使用这个同一个默认字典对象。这相当于在函数外部隐藏了一个全局缓存但封装得更好。不过要小心这个缓存会在多次调用fibonacci时持续存在并累积。