想象你正在整理自家冰箱:番茄放进保鲜盒,鸡蛋站进蛋格,酱油贴上标签摆上调味架,剩菜封进密封罐。为什么这么讲究?因为每种食材需要不同的“容器”和“保存方式”——有的怕压、有的怕串味、有的要一眼就能找到。Python 里的数据也完全一样:数字、文字、列表、字典,每一种数据类型都是不同材质的“收纳容器”,各有各的脾气。变量不是容器本身,只是贴在容器上的标签纸;赋值更不是把食材复制一份,而是多贴了一张标签。这篇文带你像整理冰箱一样,把 Python 最核心的“收纳体系”——数据类型与结构——彻底摸透。搞懂这套体系,你就知道为什么有人用字典换来了“一眼取物”的速度,为什么有人因为误用了“共享保鲜盒”而改坏了数据,以及什么时候该用列表、什么时候该用元组。准备好你的冰箱贴纸,我们开工。
数据类型和结构是 Python 一切代码的地基,也是 AI 脚本里组织训练数据、配置参数、缓存结果的“收纳系统”。掌握它,你能少写一半调试时间,并真正读懂他人的工程代码。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
四种数据结构的底层实现 List 底层是动态数组,扩容时超额分配使 append 均摊 O(1) Dict 和 Set 均基于哈希表,通过哈希映射实现平均 O(1) 的查找与判重 Tuple 是不可变序列,创建后内存布局固定,无法增删改元素 T_lookup(avg) = O(1), T_lookup(worst) = O(n) List/Dict/Set 靠哈希与动态数组提效,Tuple 以不可变性保安全
对象模型
打开冰箱门,你会看到番茄、鸡蛋、酱油瓶——它们形态各异,但归根结底都是“物品”。物品可以摸、可以看、可以打标签。Python 世界也一样:整数是物品、字符串是物品、列表是物品、连函数和类也是物品。这就是“一切皆对象”的第一层含义——万物都有共同的身份规则,就像冰箱里的每样东西都有生产日期、品类和营养价值表。
既然什么都是物品,那我们怎么区分“同一个物品”和“两个长一样的物品”?Python 给每件物品盖了三个邮戳:身份(id)、类型(type)和值(value)。身份是物品的唯一编号,类型是它的“品类”,值才是它真正的内容。你拿起一盒番茄说“这是我的番茄”,这个动作不是把番茄变出来,而是把一张写着“我的”的标签贴了上去。变量,本质上就是这种标签纸。
a 和 b 贴在同一件物品上,所以 a is b 为 True;a 和 c 内容相同但物品不同,a == c 为 True 而 a is c 为 False。用 id 函数可以查看物品编号,Python 正是靠这个编号判断“是不是同一个”。在写 AI 脚本时,最坑的地方往往不是算法,而是你以为是复制、其实是贴标签,导致改一处数据、全盘变量跟着变。
回到冰箱的场景:你在番茄盒上贴“早餐用”,又在同一盒上贴“沙拉用”,两个标签指向同一盒番茄。有人从“沙拉用”拿走一颗,你的早餐番茄也少了。理解“变量是贴纸、赋值是贴标签”,就是理解 Python 数据世界的起点。
- 比较符号 isis 在问“两个标签是否贴在同一件物品上”。
- 身份函数 id向对象要它的专属编号,这个编号终身不变。
- 编号对照只有双方编号完全一致,答案才为 True。
a = [1, 2, 3]b = a # 不是复制盒子,只是多贴一张标签c = [1, 2, 3] # 新买了一盒一模一样的番茄
数值与布尔
冰箱里有两类“数字信息”:一类是温度计的实时读数——可能是 4 摄氏度,也可能精确到 4.2;另一类是保质期天数——永远是整数天数,不会出现 3.7 天。Python 的数值类型也是这样分工:int 管整数,float 管小数,complex 管复数(AI 里的频域信号会用到)。
Python 的整数非常“贪吃”,想多大就多大,不受 32 位或 64 位限制;float 则遵循双精度标准,能表示很大或很小的数,但精度有限——就像秤只能称到克,称不出纳克。特别要注意的是除法:一条斜杠 / 永远返回浮点数,两条斜杠 // 才做向下取整的整数除法。百分比 % 求余数,相当于把食材按固定分量打包后剩下的零头。
说到布尔,Python 里 True 和 False 其实是 int 的“特制版”—— True 等于 1,False 等于 0。这在统计场景里特别有用,比如 sum([True, False, True]) 直接算出 2 个 True。数值类型的选型逻辑就像冰箱分区:保质期用 int,温度用 float,开关状态用 bool,各归其位,代码读起来才干净。
就像把 7 个鸡蛋按每盒 2 个装,能装 3 盒还剩下 1 个。数字类型选对了,后续的 AI 特征工程、数据归一化才不会在精度上翻车。
- 输入两个数设定两个整数,作为被除数和除数。
- 向下取整除法// 先算商,再往负无穷方向取整。
- 取余数% 算出整除后剩余的部分。
print(7 / 2) # 3.5print(7 // 2) # 3print(7 % 2) # 1print(True + 1) # 2,因为 bool 是 int 的子类
序列家族
保鲜盒、密封罐、包装袋——冰箱里凡是能按顺序摆放的容器,都有“第一个”“第二个”的概念。Python 的序列家族(str、list、tuple)就共享这样一套规则:支持索引、支持切片、支持 len() 数长度。索引从 0 开始,这常让新手愣一下——就像数楼层时从 0 层开始数一样。
列表是“能加菜的保鲜盒”:买东西回来随时 append 塞进去,做饭时 remove 拿出去,盒子的容量和内容都可以变。元组是“定型密封罐”:出厂时放好三样东西,之后不能再加不能减——这种“锁死”让元组特别适合存固定配置,比如一次训练任务的超参数组合。字符串则是“贴好标签的包装条”,它也是不可变的,任何修改操作都会生成一条新包装条。
负数索引是序列家族的贴心设计:s[-1] 表示最后一个元素,s[-2] 表示倒数第二个。它的本质是一条简单的数学公式:倒数第 k 个,就是正数第 n-k 个。序列家族选择法则很简单——内容要变就选列表,内容定了就用元组,纯文本就用字符串。
保鲜盒能改内容所以灵活,密封罐不能改但能保证“送出去的不会中途变质”。理解了这个,你在用元组做函数返回值、用列表做动态数据集时,就会自然选对容器。
- 正数索引从容器头部开始数,第 k 个元素。
- 负数索引从容器尾部倒着数,倒数第 k 个。
- 换算关系倒数第 k 个恰好等于正数第 n-k 个(n 是长度)。
fruits = ["apple", "banana"]fruits.append("cherry") # 保鲜盒加菜coords = (10.0, 20.5, 3) # 固定三维坐标name = "bot"print(name[0], name[-1]) # b t,负数索引倒着数
哈希家族
冰箱里有一类“带名字的储物柜”:打开柜门,标签上写着“鸡蛋”,里面躺着 12 颗蛋——这就是字典。还有一类“只放同款一件的收纳篮”,同一件食材重复放进去只会留下一个——这就是集合。字典和集合的共同靠山,叫哈希表。哈希表像一台智能分拣机:你把食材名字交给它,它瞬间算出一个数字,然后直接走到对应格子前,把食材放进或取出。
之所以能“瞬间”,是因为字典内部不是挨个翻找,而是用哈希函数把键(key)算成一个数字,再取模落到内部格子上。所以字典的键必须“可哈希”——也就是键在生命周期内不能变。列表是可变容器,不能当键;元组如果里面全是不可变元素,就可以当键。集合本质上是“只有键没有值的字典”,它利用同一套分拣机制保证元素不重复。
你可能会想:哈希分拣机偶尔会不会算重?会,这叫哈希冲突。Python 的字典和集合内部有成熟的冲突处理策略,把冲突元素放进邻近格子继续找,所以对使用者完全透明。哈希家族把“查找”变成了“直取”——写 AI 脚本时,用字典存特征名到特征值的映射、用集合存已见样本的 ID 去重,都是秒级体验。
把字典想象成酒店前台:报上房客名字,前台一秒翻出房卡,而不是挨个房间敲门。集合则是“打卡处”,同一个名字重复打卡也只算一次。哈希家族是 Python 性能神话的最大功臣。
- 键的名字字典里每个食材都有唯一名字,即 key。
- 哈希指纹哈希函数把任意名字转成一串整数数字。
- 取模定位数字指纹除以格子总数 m,余数就是放哪格。
user = {"name": "小冰", "age": 3}user["name"] # 按标签取物,一步到位tag_set = {"早餐", "快手菜", "早餐", "低卡"}print(tag_set) # {'快手菜', '低卡', '早餐'},重复被自动过滤
可变不可变
有些容器天生“软”:塑料保鲜盒可以捏、可以掰、可以往里加东西——列表、字典、集合都属于这一类,叫可变类型。有些容器天生“硬”:玻璃密封罐打开时就定好了形状,你想塞新东西进去只能重新买一个——int、float、str、tuple 都属于这一类,叫不可变类型。这个“软硬”差异,直接决定了数据能不能被安全共享。
“硬”容器的好处是可信赖:元组可以在多个字典键里安全使用,字符串可以在多线程里无锁读取,因为谁都没法中途改掉里面的内容。“软”容器的好处是灵活:你可以在循环里不停 append 到列表,最后得到完整的结果集。但灵活也带来陷阱——当两个变量指向同一个“软”容器时,通过其中一个变量修改内容,另一个看到的也变了。
这个“赋值陷阱”是 Python 新手最常踩的坑。要真正复制一个可变容器,必须显式调用 copy() 或 copy.deepcopy(),就像把盒子的内容倒进一个新盒子。不可变类型没有这个烦恼:对字符串做任何操作,都会生成新对象,原来的对象纹丝不动。
把共享可变容器想成“同事共用的一个冰箱抽屉”:你往里放了一盒酸奶,同事打开抽屉也看到了,大家面对的是同一个物理空间。设计 AI 训练管线时,要明确哪些数据是共享的、哪些是私有副本,否则一次“顺手修改”可能悄悄污染整批训练数据。
- 创建盒子列表
[1]是一个可变保鲜盒,变量 a 是它的标签。 - 贴第二张标签
b = a不创建新盒子,只复制标签纸。 - 修改穿透通过 b 改第一个元素,a 看到的同一个盒子内容跟着变。
box = ["一层"] # 一个保鲜盒alias = box # 多贴一张标签,并没有复制alias.append("二层") # 从 alias 往里加菜print(box) # ['一层', '二层'] ,原标签也看到了新菜
按需选型
好的收纳师不买一堆同样的盒子,而是按食材选容器:酱油放调味架、鸡蛋进蛋格、剩菜用密封罐、每天要翻的零食放最外层。Python 选型也是同一个逻辑——先想你要做哪些操作,再决定用哪种结构。最核心的判断标准就两个字:快慢。
如果你频繁按“名字”查东西,比如通过特征名查特征值、通过样本 ID 查标签,字典是最优解,平均 O(1) 一步到位。如果你需要保持“先后顺序”,比如事件日志、训练样本的排列顺序,列表是稳妥之选,但它的查询是 O(n)——挨个翻找。如果目的是去重,比如清洗重复的 URL,集合最合适。如果数据定了就不再改,比如模型配置项、坐标三元组,元组既省内存又防误改。
这是初学者到工程师的思维跃迁:不再问“Python 能存什么”,而是问“我的场景该用哪个”。选错容器,数据量小的时候感觉不到,一旦进入 AI 场景中几十万样本的规模,O(n) 和 O(1) 的差距就是“数分钟”和“瞬间”的差距。
选型就像给不同食材安排冰箱层位:单价高、常取的放门口;笨重、少用的塞冷冻层。提前想清楚“查、改、序、去重”四个动作的频率,你写出的代码自然又快又稳。
- 线性查找列表查找最坏情况要遍历全部 n 个元素。
- 哈希直查字典通过哈希分拣直接跳转到目标格。
- 复杂度偏好n 越大,常数级的 O(1) 越远胜线性的 O(n)。
# 坏味道:用列表当查询表features = [("learning_rate", 0.001), ("batch_size", 32)]value = [v for k, v in features if k == "learning_rate"][0] # O(n) # 好味道:用字典,一步查到config = {"learning_rate": 0.001, "batch_size": 32}value = config["learning_rate"] # O(1)
底层实现
你有没有好奇过:保鲜盒是怎么造出来的?列表和字典背后是两种截然不同的“工厂流水线”。列表的底层是动态数组:工厂先给你一个小盒子,装满了就默默换一个更大的盒子,把所有菜搬过去。因为搬一次很费劲,Python 不会每次加菜都扩容,而是预留余量——这次扩容后留出空位,后面好几次 append 都不用搬家。
这就引出一个精妙的结论:虽然扩容那一次的开销是 O(n),但把搬家成本平摊到每一次追加操作上,平均下来每次 append 仍然是 O(1)。就像你为了装年货换了台大冰箱,之后半年每天放一两样东西都不愁地方——那年货的搬家费摊到半年里,几乎可以忽略。字典的底层则是哈希表:前面讲过靠哈希函数定位,但格子总有撞车的时候,所以哈希表还要准备“冲突处理”,通常用开放寻址法,在冲突时找下一个空位。
理解底层不是为了背八股,而是为了解释现象:为什么列表在中间插入比在尾部追加慢?因为在中间插入要让后面的元素全部“往后挪一位”。为什么字典占用内存比列表大?因为哈希表要预留空槽来减少冲突。你不需要手写这些结构,但了解“盒子是怎么造的”,就能预判不同操作的成本分布,写出更贴合实际的数据处理代码。
把底层实现想成“收纳盒工厂的工艺参数”:塑料盒可以挤压扩容,玻璃罐一次成型。懂了工艺,你就能解释为什么某些操作贵、某些操作便宜,进而在大规模 AI 数据处理中避开隐藏的性能杀手。
- 单次追加一般情况下,往列表尾部放一个元素只需 O(1)。
- 扩容搬家数组满时要申请新空间并搬运全部 n 个元素,成本 O(n)。
- 均摊到每次把一次搬家成本平分到 n 次追加中,平均仍是常数。
# 动态扩容的宏观表现:列表可以无限 appendnums = []for i in range(10000): nums.append(i) # 内部自动扩容,使用者无感
类型判断
收纳师清点冰箱时,会看两种信息:一是物品包装上的“品类标签”,二是这个物品“真正是什么做的”。Python 判断类型也有两套工具:type(x) 看实际类型,isinstance(x, C) 看类型继承链。两者看起来差不多,但结果可能不同——就像一盒“素肉”,包装上写着“肉”,实际原料却是豆制品。
这两个工具的区别在继承场景下非常明显。bool 是 int 的子类,所以对 x = True 来说,type(x) == int 是 False(实际类型是 bool),但 isinstance(x, int) 是 True(bool 属于 int 家族)。写 AI 代码时,优先用 isinstance,因为它更宽容,能正确识别子类,而且对鸭子类型更友好。所谓鸭子类型,就是“走路像鸭子、叫起来像鸭子,那就是鸭子”——Python 不强制要求参数必须是某个精确类型,只要它支持你需要的方法就行。
这种“看行为不看标签”的哲学,让 Python 代码非常灵活:同一个函数既能处理数,也能处理序列。类型判断的另一个常见需求是显式转换:int("3") 把字符串变成整数,float("2.5") 变成浮点数,str(42) 变成字符串。就像把保鲜盒里的食材换到另一个更适合的容器里,转换本身生成了新的对象,原对象不动。
清点冰箱时,如果你只认“包装上的品类名”,可能会把素肉当肉;如果你看“它能不能吃、怎么吃”,就不会错。Python 的类型判断也要兼顾“标签”和“族谱”两个视角,才能在灵活与严谨之间找到平衡。
- type 查实际类型type(x) 返回对象最精确的类,不看继承。
- isinstance 查族谱isinstance 沿继承链从子类一路查到祖先。
- 两者分岔子类实例的 type 不等于父类,但 isinstance 判定属于父类家族。
def double(x): return x * 2 print(double(4)) # 8,整数走乘法print(double("ab")) # abab,字符串走复制print(double([1])) # [1, 1],列表也照样工作
AI 应用场景
数据预处理管线
阶段:数据准备与清洗
解决的问题:传统方式下手动管理样本去重、特征索引和标签映射非常低效,且容易因误用可变结构导致数据污染。用 dict 存特征名到列索引的映射、set 去重重复样本 ID、tuple 固定样本元数据,能把清洗逻辑写得清晰又安全。
具体案例:某推荐系统团队每天处理 200 万条用户行为日志。他们用 set 对用户 ID 去重,用 dict 建立“用户ID -> 行为序列列表”的缓存,用 tuple 封装不可变的样本元组进入训练队列。结果去重耗时从原来的 6 分钟降到 20 秒,且不再出现“误改共享列表导致训练集被污染”的事故。
模型配置管理
阶段:训练配置与实验管理
解决的问题:配置文件如果用列表保存超参,容易在实验过程中被意外修改;用字典则能按名字快速读取和覆盖默认值。元组的不可变性保证“实验参数一旦确定就不再被篡改”,dict 的键名访问让代码可读性更高。
具体案例:一位算法工程师用 dict 维护默认配置,用 config.get("learning_rate", 0.001) 安全读取,再用 tuple 保存“模型版本号、数据路径、超参摘要”三元组作为实验标识。每次实验结束,把标识存进 set 自动去重,避免重复跑相同配置的模型训练。
Python 演示
# -*- coding: utf-8 -*-"""冰箱收纳系统:用四种数据结构管理每日食材"""from collections import defaultdict def main(): # 列表:可增删的保鲜盒,适合需要不断变化的清单 shopping = ["鸡蛋", "牛奶", "面包"] shopping.append("番茄") shopping.remove("面包") # 元组:不可变的密封罐,适合固定不变的营养标牌 egg_label = ("鸡蛋", 13, "高蛋白") # 字典:带标签的储物柜,按名字直接存取 fridge = {"鸡蛋": 12, "牛奶": "2升", "番茄": 4} fridge["鸡蛋"] -= 2 # 集合:只留一份的去重收纳篮 tags = {"早餐", "快手菜", "早餐", "低卡"} assert len(tags) == 3, "重复元素被自动去重" # 赋值陷阱:alias 只是多贴一张标签,不是复制盒子 box = ["一层"] alias = box alias.append("二层") assert box == ["一层", "二层"], "共享同一个列表对象" # 选型示例:用 defaultdict 统计食材出现次数 counter = defaultdict(int) for item in shopping: counter[item] += 1 print("购物清单:", shopping) print("营养标签:", egg_label) print("库存:", fridge) print("去重标签:", tags) print("统计:", dict(counter)) print("别名陷阱验证通过: box =", box) if __name__ == "__main__": main()
shopping.append("番茄")列表是可变保鲜盒,随时往末尾加菜shopping.remove("面包")按内容删除元素,体现列表动态增删能力egg_label = ("鸡蛋", 13, "高蛋白")元组一旦创建不可变改,适合固定标签fridge["鸡蛋"] -= 2字典按键名直接修改值,类似按标签改库存tags = {"早餐", "快手菜", "早餐", "低卡"}集合自动去重,重复的“早餐”只保留一个alias = box赋值只是多贴一张标签,并没有创建新盒子assert box == ["一层", "二层"]断言验证了“通过别名修改会穿透到原变量”
常见误区
误区我写 `b = a` 后,把 b 叫作“列表 a 的副本”,然后 `b.append(1)` 居然把 a 也改了!Python 是不是有 bug?
正解b = a 从头到尾没有复制任何数据。变量是贴纸,赋值是贴标签:a 和 b 只是同一张冰箱盒上的两张标签。想要真正的副本,必须显式做 b = a.copy() 或 b = list(a),就像把食材倒进一个新盒子。嵌套列表还需要 deepcopy。
为何常见:生活里“把东西给另一个人保管”通常意味着对方可以得到一份新的,但 Python 的等号语义是“绑定引用”,初学者带着日常直觉写代码,自然踩坑。
误区字典键不是随便什么值都能放吗?我想用列表记录一组特征,然后拿它当键去查缓存,结果报错“unhashable type: list”,凭什么?
正解字典和集合的底层是哈希表,键必须“可哈希”,也就是在生命周期内值不可变。列表可变,你把它当键放进字典后,如果列表内容变了,哈希值跟着变,字典就再也找不到原来那个键了。所以 Python 直接禁止列表当键;元组则可以。
为何常见:初学者只看到字典“键值对应”的表面,没看到“哈希定位”的底层机制,遇到 unhashable 报错时容易一头雾水。
误区我就是想比较两个变量“相不相等”,用 is 不是更快吗?老师说 `is` 是身份比较,`==` 是值比较,但这俩对我来说没区别。
正解== 比的是内容,is 比的是身份。a == b 可能为 True 但 a is b 为 False,比如两个内容相同但不同对象。反过来,小整数在 Python 里有缓存,1 is 1 碰巧为 True,让初学者误以为两者等价。判断 None 时应该用 is None,比较内容用 ==。
为何常见:日常语言中“相等”往往意味着“同一个”,加上小整数缓存的巧合,掩盖了两者的本质差异。
误区元组不是“不可变的密封罐”吗?可我发现里面装的列表居然能 append,那元组到底可不可变?
正解元组的不可变是指“元素引用不可变”——不能增、删、替换元素。但当元素本身是一个可变对象(如列表)时,这个列表的内部内容仍然可以变化。元组只是“不能从罐子里拿出或换掉这个列表”,但列表自己内部怎么折腾,元组管不着。
为何常见:将“不可变”误解为“深层完全冻结”,忽略了它只约束容器的结构,不约束嵌套可变对象的内部状态。
误区我打印 `{1, 2, 3}` 每次都显示 `{1, 2, 3}`,集合是不是内部排好序了?那我用集合当有序列表行不行?
正解集合在概念上是无序的,底层由哈希表实现,元素的遍历顺序由哈希值和槽位决定。小整数碰巧哈希值等于自身,所以打印时看似有序。换成字符串混合元素,顺序就会“飘忽不定”。任何依赖集合顺序的代码都是隐患,顺序问题请用列表。
为何常见:日常实验里小整数集合恰好有序,形成了“集合会排序”的错觉,直到某天换了一组数据顺序大变才意识到问题。
面试题
Q1. 执行 b = [1, 2]、c = b、c.append(3) 后,变量 b 的值是什么?
- A.[1, 2]
- B.[1, 2, 3]← 正确答案
- C.[1, 2, 3, 3]
- D.引发异常
答案:B。变量是贴纸,赋值是贴标签。c = b 没有复制列表,只是让 c 和 b 指向同一个列表对象,所以 c.append 修改的就是 b 看到的那个盒子,b 变成 [1, 2, 3]。
Q2. 下列哪种对象可以作为字典的键?
- A.列表 [1, 2]
- B.可变集合 set
- C.包含列表的元组
- D.元组 (1, 2)← 正确答案
答案:D。字典键必须可哈希,即生命周期内不可变。元组本身不可变,只要内部元素也都不可变,就可以当键。列表和集合可变,不可哈希;包含列表的元组因为含可变元素,也不可哈希。
Q3. 以下哪些是 Python 的不可变类型?
- A.int← 正确答案
- B.list
- C.tuple← 正确答案
- D.str← 正确答案
答案:ACD。int、tuple、str 都是不可变的“定型容器”,对它们做任何修改都会生成新对象。列表是“可变保鲜盒”,可以原地增删元素,因此不属于不可变类型。
Q4. 对于 x = True,type(x) == int 和 isinstance(x, int) 的结果分别是?
- A.True / True
- B.False / True← 正确答案
- C.True / False
- D.False / False
答案:B。bool 是 int 的子类。type(x) 看“实际标签”,True 的实际类型是 bool,所以不等于 int;isinstance 看“族谱”,bool 属于 int 家族,所以为 True。这正体现了 type 与 isinstance 的差异。
Q5. len({1, 2, 2, 3}) 的返回值是多少?
- A.2
- B.3← 正确答案
- C.4
- D.报错
答案:B。集合是“只留一份的去重篮”,重复的 2 只保留一个,所以集合里实际是 {1, 2, 3},长度为 3。这个特性常用于数据清洗中的去重。
Q6. 关于元组,下列说法正确的是?
- A.空元组可以用 () 表示← 正确答案
- B.元组支持 append 方法
- C.元组可以作为字典的键← 正确答案
- D.元组的元素引用不可变← 正确答案
答案:ACD。() 就是空元组;元组不可变所以没有 append 方法;元组可哈希所以能做字典键;元组的“不可变”特指元素引用不可变。B 是典型错误,把列表方法安到了元组上。
Q7. 字典按键查找元素的平均时间复杂度是多少?
- A.O(n)
- B.O(log n)
- C.O(1)← 正确答案
- D.O(n^2)
答案:C。字典底层是哈希表,通过哈希函数直接计算槽位,平均一步到位,复杂度为 O(1)。它不像列表那样要逐个遍历,这正是“查得快用字典”的根本原因。
Q8. 关于 Python 的 is 运算符,最准确的说法是?
- A.判断两个变量的值是否相等
- B.判断两个变量的类型是否一致
- C.判断两个引用是否指向同一个对象← 正确答案
- D.判断两个对象是否都不可变
答案:C。is 比较的是“身份”,即是否同一个对象,等价于 id(a) == id(b)。== 才比较值。初学者常因为小整数缓存造成 1 is 1 为真,误以为两者等价。
Q9. 下列哪些类型可以直接作为集合的元素?
- A.整数 5← 正确答案
- B.字符串 "ai"← 正确答案
- C.列表 [1]
- D.布尔值 True← 正确答案
答案:ABD。集合元素必须可哈希。int、str、bool 都是不可变类型,可以直接放入。列表是可变容器,哈希值会变化,因此不能被集合接受。
Q10. 对于 Python 列表的 append 操作,其均摊时间复杂度约为多少?
- A.O(1)← 正确答案
- B.O(n)
- C.O(log n)
- D.O(n log n)
答案:A。列表底层是动态数组,大部分追加操作直接写在末尾是 O(1);只有当容量满时才扩容并搬运数据,成本 O(n),但把搬家成本平摊到多次追加后,平均仍为 O(1)。这就是“摊还常数时间”。