想象你刚被任命为一间“Python 数据仓库”的管理员。仓库里有六种风格迥异的收纳容器:有的是可以无限伸长刻度的量杯,有的是有细微误差的电子秤,有的是印上就不能涂改的钢印标签,有的是按编号摆放的货架,有的是带门牌号的储物柜,还有一只会自动去重的收纳袋。数据就是那些需要入库的物品——数字、文字、一组编号、一条记录。你要做的,就是为每一类物品挑对容器。挑对了,仓库高效整齐;挑错了,轻则慢如蜗牛,重则直接扔出 TypeError。 学完这趟“仓库参观之旅”,你将彻底掌握 int、float、str、list、dict、set 这六大内置类型的脾气:谁能精确计量、谁有近似误差;谁能原地修改、谁只能重新打印;谁按序号找、谁按名字找、谁自动去重。再遇到“该用列表还是集合”“为什么浮点数会算错”“字典的键为什么不能是列表”这类问题,你都能一眼看穿。
对 AI 从业者而言,内置类型是所有算法与数据处理脚本的“地基砖块”。理解它们的可变性、哈希规则与时间复杂度,才能写出又快又稳的 Python 代码。
可视化演示
展示list/dict/set三大核心类型的操作时间复杂度对比、哈希表实现原理与动态数组扩容机制,切换模式查看类型选用策略。
本节配有交互动效(进入视口后自动加载,约 3KB)。
list/dict/set操作复杂度对比 紫色为O(1)操作,红色为O(n)操作,绿色为最优选择 查找:dict O(1) > list O(n) dict和set基于哈希表实现O(1)查找,list基于动态数组需O(n)遍历查找
类型总览
推开 Python 数据仓库的大门,你眼前是六口形态各异的收纳容器:量杯、电子秤、钢印机、货架、储物柜和去重袋。它们看似毫无关联,其实共同构成了一套完整的“数据收纳体系”。作为管理员,第一课不是急着存数据,而是先把每种容器的属性写在胸口的名牌上,做到心中有数。
从大类上看,int、float、str 是“标量容器”,每次只装一件物品;list、dict、set 是“集合容器”,能装下一批物品。更关键的是“可变性”:你可以在货架上随意挪动货物,这叫可变;但你不可能在已经印好的钢印上改字,这叫不可变。Python 把是否可变刻进了每个类型的骨子里,而这又决定了它能不能被放进 dict 的键位或 set 的袋口。
type() 和 isinstance() 是仓库里的两把“查容器型号”的扫码枪。调用 type(变量) 能看到这个变量背后到底连着什么容器,isinstance(变量, 类型) 则像在问“这是不是某个容器的款式”。对初学者来说,看到这两个函数,就能在乱糟糟的数据堆里迅速定位物品该归哪一类。
还有一个容易被忽略的真相:Python 变量更像贴在容器上的便签,而不是容器本身。当你执行 a = b,并没有复制一份数据,而是给同一个数据物品多贴了一张便签。
回到仓库门口,你终于明白:选容器之前,先看它可变不可变,再看它是标量还是集合。这六个容器各有分工,接下来的每一节,就是打开它们的使用手册。
- id 函数
id返回对象在内存中的唯一身份编号,相当于仓库发给每个容器的专属编号。 - 等号赋值
=不是“复制容器”,而是“贴便签”,让a也指向b所指的那个对象。 - 整体含义赋值后两个名字拥有同一身份编号,所以对可变容器的修改会同时反映在两个名字上。
数值家族
仓库里最常入库的货物是数字。整数用“无限量杯”来装,量杯的刻度可以无限向上延伸,Python 的 int 因此拥有任意精度:只要内存够大,21000 也能完整装下,不会像许多编程语言那样发生“溢出”。而小数用“电子秤”来装,float 是 IEEE 754 双精度数,内部只用 64 位二进制表示,大约能称出 15~17 位有效数字,超出部分只能四舍五入。
正因为电子秤的刻度有限,所以浮点数天生有误差。最经典的例子是 0.1 + 0.2,在二进制里 0.1 是一个无穷循环小数,电子秤只能截取近似值,结果自然不等于 0.3。这不是 Python 的 bug,而是所有用二进制表示小数的计算机的通病。
数值转换像是“换容器”:int("12") 可以把字符串数字转成整数,float("3.14") 可以把字符串小数转成浮点数。但要注意,int("3.14") 会报错,因为量杯只认识完整整数;而 int(12.7) 会得到 12,电子秤上的小数放进量杯时,小数点后的部分会被直接丢掉,而不是四舍五入。
双精度浮点能精确表示的整数范围,其实有一道清晰的天花板:
回到货架上:需要精确计量的大整数,一定用 int 量杯;需要科学计算或小数近似,再用 float 电子秤。两者结合,数值的仓库才能既无限又高效。
2^53 - 1 的整数不一定能精确表示。遇到超大整数,请相信 int 的量杯,而不是 float 的电子秤。- 底数 2浮点数内部使用二进制科学计数法,每一位有效数字都是 2 的幂次组合,而不是 10 的幂次。
- 减 153 位二进制能表示的状态从 0 开始数,最大连续可精确表示的整数是 2^53 - 1。
- 计算结果这个值约等于 900 万亿,日常金额和普通计数很少超过它,但大数据场景中要警惕。
文本不可变
仓库里有一台“钢印机”,它负责制作 str 标签。钢印一旦打下去,字符序列就永远固定了。你无法擦掉第 2 个字符重写,正如 s = "abc" 之后,s[1] = "x" 会直接报错。str 是不可变类型,任何“修改”其实都是制作一张全新的钢印标签,再把变量便签贴到新标签上。
钢印上最重要的操作是“索引”和“切片”。索引是精确读取第几个字符,例如 s[0] 得到第一个字符;切片则是按区间裁出一段标签,例如 s[1:4] 从第 2 个字符取到第 4 个字符。Python 的序号从 0 开始,这就像仓库货架从 0 号位开始编号,第 1 个位置其实是 0 号。
字符串方法同样遵循“钢印法则”:.upper() 返回大写新字符串,.split() 返回切好的碎片列表,.join() 把一堆碎片重新粘成完整标签。但无论方法怎么折腾,原字符串对象都不变,只是返回了新对象。如果没有人接住返回值,那这张新标签就会被丢弃。
切片操作本质上可以看作字符序列的一个区间表达式:
钢印标签的好处是安全:无论你如何调用方法,原始字符串都不会被意外篡改。想要改变内容?重印一张就好。这就是 str 的不可变哲学。
s[i:j] 从索引 i 开始,到索引 j 之前结束,包含 i,不包含 j。这个半开区间设计,让 s[:2] + s[2:] 永远能拼回完整的 s,像钢印标签完整切开又接上。- 左边界 i切片从第 i 个字符开始,包含它。
- 右边界 j切片在第 j 个字符前停止,不包含它。
- 连续取字符用省略号表示从 s[i] 一路取到 s[j-1] 的连续序列。
动态数组
list 是仓库里最常用的“编号货架”。货架上的每个格子都有顺序编号,你可以在格子里放任何类型的数据,可以允许重复,也可以随时取走或补上新货。它是可变容器,这意味着你可以原地操作:lst.append(x) 在末尾上货,lst.pop() 从末尾取货,lst.insert(0, x) 则直接把新货插到 0 号位。
但货架有物理限制:底层是连续内存的“动态数组”。当容量不够时,Python 会申请一块更大的新货架,把旧货物全部搬过去。因此,平均来看在末尾追加货物非常快,属于“均摊 O(1)”;可如果你总是往 0 号位插货,后面的每个格子都得往后挪一格,代价随货架长度线性上升。
list 的索引访问是瞬间完成的:lst[3] 像直接报出 3 号货位,不需要从头数格子。而 x in lst 则不同,它是从头到尾挨个扫货架,最坏要扫完整个列表。理解这层区别,才能解释“为什么列表这么好用,却仍需要在 set 里查找元素”。
末尾追加与头部插入的时间代价,用数学语言写出来非常直观:
货架胜在灵活和有序,但要记住它的“软肋”在头部插入和成员查找。当仓库任务变成“频繁在开头加货”时,就该考虑换一种容器了。
- append 复杂度在末尾追加只需要写入当前末尾位置,偶尔扩容均摊后为 O(1)。
- insert 复杂度在头部插入时,所有已有元素都要向后移动一个位置,因此是 O(n)。
- 对比记忆末尾是加货,头部是“塞货”——塞得越满,代价越高。
哈希字典
dict 是仓库里一排带门牌的储物柜。每个柜子上写着一个“键”,柜子里装着对应的“值”。你要取货时,不需要挨个打开柜门找,只要报出键,管理员用哈希函数算一下键,就能直接走到正确的柜门前。这正是 dict 查找平均 O(1) 的秘密。
既然要用门牌定位,门牌就必须能用哈希函数计算。Python 规定:只有不可变对象才能被哈希。所以 str、int、float 可以当键,而 list、dict、set 这些可变容器不能当键。试想,如果门牌被人用白板笔随手涂改,哈希结果变了,管理员就再也找不到原来的柜子了。
dict 的常用操作非常顺手:d.get(k) 安全取值,d.setdefault(k, v) 只在键不存在时写入,d.items() 把键值对成对取出来遍历。for k in d 遍历的是键,而不是值。记住这些方法,等于给储物柜配了自动寻物系统。
哈希定位的过程可以简化为一个公式:
储物柜的哲学是“用门牌换速度”。只要键不可变、哈希均匀,你就能在数据海洋里瞬间找到想要的那件货。代价也很明确:门牌不能变,所以别拿 list 当钥匙。
- 哈希函数 hh 接收键 k,输出一个看起来没有规律的整数。
- 取模运算对桶数 m 取模,把大整数压缩到有限的槽位编号范围内。
- 槽位结果最终箭头指向的槽位,就是存值的位置。
去重集合
set 是一只神奇的“自动去重袋”。你把一串货物倒进去,袋子会自动识别并合并重复项,最后袋子里的每件货都只保留一份。它底层和 dict 一样使用哈希表,所以成员判断平均只需要 O(1)。同时,set 是“无序”的:袋子里没有 0 号位、1 号位的概念,你无法像 list 那样用下标取第几个元素。
去重的威力在于“一句代码解决问题”:list(set(你的列表)) 能快速去掉重复项。但要注意,去重后的顺序不保证和原来一致,因为袋子本就不在意顺序。如果既要保序又要去重,可以手写一个循环,用 set 记录已见过的元素,再用 list 保存顺序。
set 不只去重,还能做数学上的集合运算:A | B 是并集,A & B 是交集,A - B 是差集。这些运算在数据分析里非常常见,例如“两个用户群里共同的用户”就是取交集。
交集的定义可以用集合语言精确表达:
去重袋是仓库里的“净味神器”:它能瞬间告诉你某件货在不在袋子里,也能一键过滤重复。代价是放弃顺序。当你需要的只是“有没有”和“不重复”时,set 就是最优解。
A & B 是交集,只有同时出现在两个袋子里的货才会被选中。- 花括号花括号表示“集合”,其中的元素不重复、无顺序。
- 条件 x ∈ Ax 来自 A 集合。
- 或 x ∈ B并集允许元素来自 A 或 B,两者满足其一即可。
选型指南
走到仓库最后一间调度室,你终于要面对终极问题:这堆数据到底该放哪个容器?规则其实很简单。需要保持顺序、允许重复,用 list 编号货架;需要按键快速取值,用 dict 门牌储物柜;只需要判断是否存在、自动去重,用 set 去重袋;纯文本内容,用 str 钢印标签;精确整数,用 int 量杯;带小数的近似值,用 float 电子秤。
性能上最值得记住的是“查找速度”的差距。list 查找一个元素是否在货架上,需要从头扫到尾;而 dict 和 set 凭借哈希表,直接一步定位。一个 O(n),一个 O(1),数据量越大,差距越夸张。
仓库管理员的经验法则还有:不要用可变对象当 dict 键或放进 set;不要依赖 set 的打印顺序;不要拿 float 当钱的精确计算工具。容器没有绝对的优劣,只有合不合适的场景。挑对了,你的 Python 仓库会像流水线一样顺畅运转。
x in ...,把数据放进 set 或 dict 的键中,能省下大量时间。- dict/set 复杂度哈希表让存在性判断平均一步完成。
- list 复杂度线性扫描让存在性判断随长度增长。
- 远小于号n 越大,O(1) 与 O(n) 之间的鸿沟越深。
AI 应用场景
处理手机号去重与保序
阶段:数据清洗
解决的问题:用户提交的号码名单常有重复,传统做法是双层循环去重,慢且易错。AI 数据处理中先用 set 判断是否见过,再用 list 保留原始顺序。
具体案例:广告投放团队拿到 10 万条用户手机号,用 list(dict.fromkeys(phone_list)) 去重并保序,再用 set 快速剔除黑名单号码,几毫秒完成清洗。
构建热词统计字典
阶段:文本特征提取
解决的问题:把一段对话变成词频统计,需要同时用 str 做分词、list 保存结果、dict 做键值计数、set 去停用词。传统手工数组无法应对动态增长的词表。
具体案例:聊天机器人上线前,工程师统计客服语料中出现最多的业务词。用 word_counts[word] = word_counts.get(word, 0) + 1 完成计数,再用 set 过滤口语词,最终把 top10 喂给关键词抽取模型。
大整数 ID 的精确传递
阶段:数据存储与接口对接
解决的问题:订单号、日志 ID 常常超过 JS 安全整数范围,如果用 float 接收会丢失精度。Python 的 int 任意精度让它成为后端解析大 ID 的安全容器。
具体案例:跨系统推送的用户 ID 是 20 位数字,后端用 Python 接收后先 int(raw_id) 保存,再交给数据库,保证全程无精度丢失。
Python 演示
def analyze_words(text, stopwords): words = text.lower().replace(',', ' ').replace('.', ' ').split() counts = {} for word in words: if word in stopwords: continue counts[word] = counts.get(word, 0) + 1 unique_words = set(counts) total = sum(len(word) * n for word, n in counts.items()) avg = total / len(words) if words else 0.0 return unique_words, counts, total, avg def main(): text = "apple banana apple cherry, banana apple." stops = {"a", "the"} unique, counts, total, avg = analyze_words(text, stops) print("unique:", unique) print("top3:", sorted(counts.items(), key=lambda kv: kv[1], reverse=True)[:3]) print("total chars:", total, type(total).__name__) print("avg length:", avg, type(avg).__name__) if __name__ == "__main__": main()
words = text.lower().replace(',', ' ').replace('.', ' ').split()把原始一行文本切成单词列表,这就是 str 的“钢印加工”。counts = {}开启一个空 dict 储物柜,准备记录每个词的出现次数。counts[word] = counts.get(word, 0) + 1按键取值,得不到就返回 0,再加 1 存回去,完成计数。unique_words = set(counts)把 dict 的键直接丢进去重袋,得到不重复词集合。avg = total / len(words) if words else 0.0整数除以整数得到 float,类型转换在 Python 中自动发生。
常见误区
误区浮点数就是带小数点的数,电脑算 `0.1 + 0.2` 应该等于 `0.3`,结果却是 `0.30000000000000004`,Python 是不是坏了?
正解float 用有限的二进制位表示数字,而很多十进制小数在二进制里是无限循环的。它更像一把有刻度限制的电子秤,只能给近似值。比较浮点数应该用 abs(a - b) < 1e-9,不能用 ==。
为何常见:十进制生活经验让人默认小数都能精确表示,忽略了计算机内部是二进制这一层。
误区我明明写了 `s = s + "!"`,字符串不是变长了吗?怎么还说它不可变?
正解变量只是贴在对象上的便签。s = s + "!" 创建了一个全新的字符串对象,再把 s 这张便签贴到新对象上,原来的字符串对象从未被修改。str 的“不可变”指对象内容不可变,不是变量不可以重新指向。
为何常见:初学者容易把“变量”和“对象”混为一谈,误以为变量变了就是对象变了。
误区list 是 Python 最重要的容器,按索引访问那么快,那 `x in list` 应该也很快吧?
正解list 按索引访问确实是 O(1),但 in 是“成员查找”,必须从头到尾逐个比对字符串或数字,最坏 O(n)。只有基于哈希表的 set 和 dict 能保证平均 O(1) 的成员判断。
为何常见:把“随机访问”和“成员查找”两个概念混在了一起,看到索引快就以为所有操作都快。
误区dict 那么灵活,键应该想放什么就放什么,list 我也想放进去当键。
正解键必须可哈希,而 list、dict、set 都是可变对象,会被 Python 判定为 unhashable type 并抛出 TypeError。可变对象的哈希值会随内容变化,就像门牌号会被涂改,储物柜就再也定位不了了。
为何常见:只看到了 dict 的“键值对应”形象,没看到它底层哈希表对键的硬性要求。
误区我多次打印同一个 set,每次顺序都一样,甚至看起来像从小到大的,set 应该是有序容器。
正解set 规范上不保证任何顺序,打印结果受哈希值、插入历史、解释器实现等多种因素影响。CPython 的某些表现只是“碰巧稳定”,不代表语言承诺。需要保序去重时,请使用 dict.fromkeys() 的键来辅助。
为何常见:观察到的实现细节容易让人误以为是语言规范,忽略“有序”必须由 list 这类序列容器来保证。
面试题
Q1. 下列哪个是可变类型?
- A.整数 int
- B.字符串 str
- C.列表 list← 正确答案
- D.浮点数 float
答案:C。list 是编号货架,可以在原容器上增删改,属于可变类型。int、str、float 都是不可变对象,任何“修改”都会产生新对象。记住“货架能挪货,钢印不能改”即可。
Q2. 查看变量类型的函数是?
- A.type()← 正确答案
- B.len()
- C.print()
- D.range()
答案:A。type(变量) 像给数据容器扫条形码,返回它的类型;len() 返回长度,print() 负责输出,range() 生成整数序列。所以扫型号选 type。
Q3. 0.1 + 0.2 == 0.3 的结果是?
- A.True
- B.False← 正确答案
- C.None
- D.Error
答案:B。float 电子秤用二进制近似表示小数,0.1 + 0.2 得到的近似值不完全等于 0.3,因此比较结果是 False。判断浮点数相等应使用误差允许范围,而不是 ==。
Q4. 下列哪个对象可以作为 dict 的键?
- A.列表 list
- B.字典 dict
- C.字符串 str← 正确答案
- D.集合 set
答案:C。dict 的键必须可哈希,而可哈希对应不可变性。str 是钢印标签,天然可哈希;list、dict、set 都是可变容器,会因内容变化导致哈希失效,不能当键。
Q5. 执行 int(12.7) 会得到什么?
- A.返回 12← 正确答案
- B.返回 13
- C.返回 12.0
- D.直接报错
答案:A。把 float 转成 int 时,Python 会朝零方向截断,直接丢弃小数部分,所以 int(12.7) 是 12,而不是四舍五入到 13。只有字符串转 int 时,字符串本身必须是整数写法。
Q6. 执行 s = 'abc'; s[1] = 'x' 会发生什么?
- A.得到 axc
- B.得到 xbc
- C.直接报错← 正确答案
- D.返回新串
答案:C。str 是不可变的钢印标签,不能修改已有标签上的某个字符,s[1] = 'x' 会抛出 TypeError。想得到新字符串,只能通过切片或方法重新生成。
Q7. 判断元素是否在 set 中的平均时间复杂度是?
- A.O(1)← 正确答案
- B.O(n)
- C.O(log n)
- D.O(n^2)
答案:A。set 底层是哈希表,查找元素时先算哈希再直接定位,平均只需要常数时间 O(1),不需要像 list 那样从头扫到结尾。
Q8. 在 list 头部插入元素的最坏时间复杂度是?
- A.O(1)
- B.O(log n)
- C.O(n)← 正确答案
- D.O(n^2)
答案:C。list 是连续编号的货架,在 0 号位插入会让后面所有格子整体后移,因此最坏需要 O(n) 次移动。频繁头部插入时,应改用 deque 等专门结构,或反转后末尾追加。
Q9. 关于 list 和 str,下列说法正确的有?
- A.都能用索引访问← 正确答案
- B.都支持切片← 正确答案
- C.都是可变类型
- D.都能被 for 遍历← 正确答案
答案:ABD。list 和 str 都是序列,支持索引、切片、循环遍历,所以 A、B、D 正确。但 list 是可变货架,str 是不可变钢印,C 错误。序列的通用能力并不等于可变性也相同。
Q10. 关于 dict 与 set,下列说法正确的有?
- A.都是可变容器← 正确答案
- B.都基于哈希表← 正确答案
- C.都保留插入顺序
- D.都要求成员唯一← 正确答案
答案:ABD。dict 和 set 都靠哈希表实现,都是可变容器,A、B 正确。dict 的键唯一,set 的元素唯一,D 正确。dict 在 Python 3.7+ 保证插入顺序,但 set 规范上不保证顺序,因此 C 错误。