想象你刚被任命为一间“Python 数据仓库”的管理员。仓库里有六种风格迥异的收纳容器:有的是可以无限伸长刻度的量杯,有的是有细微误差的电子秤,有的是印上就不能涂改的钢印标签,有的是按编号摆放的货架,有的是带门牌号的储物柜,还有一只会自动去重的收纳袋。数据就是那些需要入库的物品——数字、文字、一组编号、一条记录。你要做的,就是为每一类物品挑对容器。挑对了,仓库高效整齐;挑错了,轻则慢如蜗牛,重则直接扔出 TypeError。 学完这趟“仓库参观之旅”,你将彻底掌握 int、float、str、list、dict、set 这六大内置类型的脾气:谁能精确计量、谁有近似误差;谁能原地修改、谁只能重新打印;谁按序号找、谁按名字找、谁自动去重。再遇到“该用列表还是集合”“为什么浮点数会算错”“字典的键为什么不能是列表”这类问题,你都能一眼看穿。

岗位关联

对 AI 从业者而言,内置类型是所有算法与数据处理脚本的“地基砖块”。理解它们的可变性、哈希规则与时间复杂度,才能写出又快又稳的 Python 代码。

目录
  1. 可视化演示
  2. 类型总览
  3. 数值家族
  4. 文本不可变
  5. 动态数组
  6. 哈希字典
  7. 去重集合
  8. 选型指南
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示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(a)=id(b)\displaystyle \text{id}(a) = \text{id}(b)
“id 相同”意味着两个变量名指向同一个对象。想真正复制容器,得用 copy,否则一个货架被搬动,另一张便签也会看到变化。
  1. id 函数id 返回对象在内存中的唯一身份编号,相当于仓库发给每个容器的专属编号。
    id(a)\text{id}(a)
  2. 等号赋值= 不是“复制容器”,而是“贴便签”,让 a 也指向 b 所指的那个对象。
    a=ba = b
  3. 整体含义赋值后两个名字拥有同一身份编号,所以对可变容器的修改会同时反映在两个名字上。
    id(a)=id(b)\text{id}(a) = \text{id}(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 电子秤。两者结合,数值的仓库才能既无限又高效。

253−1=9007199254740991\displaystyle 2^{53} - 1 = 9007199254740991
在 IEEE 754 双精度结构里,有效数字只有 53 位,所以超过 2^53 - 1 的整数不一定能精确表示。遇到超大整数,请相信 int 的量杯,而不是 float 的电子秤。
  1. 底数 2浮点数内部使用二进制科学计数法,每一位有效数字都是 2 的幂次组合,而不是 10 的幂次。
    2532^{53}
  2. 减 153 位二进制能表示的状态从 0 开始数,最大连续可精确表示的整数是 2^53 - 1。
    253−12^{53} - 1
  3. 计算结果这个值约等于 900 万亿,日常金额和普通计数很少超过它,但大数据场景中要警惕。
    90071992547409919007199254740991

文本不可变

仓库里有一台“钢印机”,它负责制作 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]=s[i] s[i+1]⋯s[j−1]\displaystyle s[i:j] = s[i]\,s[i+1]\cdots s[j-1]
s[i:j] 从索引 i 开始,到索引 j 之前结束,包含 i,不包含 j。这个半开区间设计,让 s[:2] + s[2:] 永远能拼回完整的 s,像钢印标签完整切开又接上。
  1. 左边界 i切片从第 i 个字符开始,包含它。
    s[i]s[i]
  2. 右边界 j切片在第 j 个字符前停止,不包含它。
    s[j−1]s[j-1]
  3. 连续取字符用省略号表示从 s[i] 一路取到 s[j-1] 的连续序列。
    s[i]s[i+1]⋯s[j−1]s[i]s[i+1]\cdots 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 里查找元素”。

末尾追加与头部插入的时间代价,用数学语言写出来非常直观:

货架胜在灵活和有序,但要记住它的“软肋”在头部插入和成员查找。当仓库任务变成“频繁在开头加货”时,就该考虑换一种容器了。

Tappend(n)=O(1)均摊,Tinsert(0,x)(n)=O(n)\displaystyle T_{\text{append}}(n) = \mathcal{O}(1)_{\text{均摊}}, \qquad T_{\text{insert}(0, x)}(n) = \mathcal{O}(n)
均摊 O(1) 的意思是:偶尔需要扩容搬货,但平均到每次 append 上,代价仍然接近常数。头部插入则每次都要让所有元素往后挪,所以正比于列表长度。
  1. append 复杂度在末尾追加只需要写入当前末尾位置,偶尔扩容均摊后为 O(1)。
    Tappend(n)=O(1)T_{\text{append}}(n) = \mathcal{O}(1)
  2. insert 复杂度在头部插入时,所有已有元素都要向后移动一个位置,因此是 O(n)。
    Tinsert(0,x)(n)=O(n)T_{\text{insert}(0, x)}(n) = \mathcal{O}(n)
  3. 对比记忆末尾是加货,头部是“塞货”——塞得越满,代价越高。
    O(1) vs O(n)\mathcal{O}(1) \ \text{vs} \ \mathcal{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 当钥匙。

h(k) mod m→槽位\displaystyle h(k) \bmod m \to \text{槽位}
哈希函数 h 把任意不可变对象 k 映射成一个整数,再对桶数量 m 取模,得到目标槽位。只要哈希函数分得够均匀,平均每次查找就是常数时间。
  1. 哈希函数 hh 接收键 k,输出一个看起来没有规律的整数。
    h(k)h(k)
  2. 取模运算对桶数 m 取模,把大整数压缩到有限的槽位编号范围内。
    h(k) mod mh(k) \bmod m
  3. 槽位结果最终箭头指向的槽位,就是存值的位置。
    →槽位\to \text{槽位}

去重集合

set 是一只神奇的“自动去重袋”。你把一串货物倒进去,袋子会自动识别并合并重复项,最后袋子里的每件货都只保留一份。它底层和 dict 一样使用哈希表,所以成员判断平均只需要 O(1)。同时,set 是“无序”的:袋子里没有 0 号位、1 号位的概念,你无法像 list 那样用下标取第几个元素。

去重的威力在于“一句代码解决问题”:list(set(你的列表)) 能快速去掉重复项。但要注意,去重后的顺序不保证和原来一致,因为袋子本就不在意顺序。如果既要保序又要去重,可以手写一个循环,用 set 记录已见过的元素,再用 list 保存顺序。

set 不只去重,还能做数学上的集合运算:A | B 是并集,A & B 是交集,A - B 是差集。这些运算在数据分析里非常常见,例如“两个用户群里共同的用户”就是取交集。

交集的定义可以用集合语言精确表达:

去重袋是仓库里的“净味神器”:它能瞬间告诉你某件货在不在袋子里,也能一键过滤重复。代价是放弃顺序。当你需要的只是“有没有”和“不重复”时,set 就是最优解。

A∪B={x:x∈A 或 x∈B}\displaystyle A \cup B = \{x : x \in A \text{ 或 } x \in B\}
并集把两个袋子里所有不重复的货合并在一起。对应地,A & B 是交集,只有同时出现在两个袋子里的货才会被选中。
  1. 花括号花括号表示“集合”,其中的元素不重复、无顺序。
    A∪BA \cup B
  2. 条件 x ∈ Ax 来自 A 集合。
    x∈Ax \in A
  3. 或 x ∈ B并集允许元素来自 A 或 B,两者满足其一即可。
    x∈Bx \in B

选型指南

走到仓库最后一间调度室,你终于要面对终极问题:这堆数据到底该放哪个容器?规则其实很简单。需要保持顺序、允许重复,用 list 编号货架;需要按键快速取值,用 dict 门牌储物柜;只需要判断是否存在、自动去重,用 set 去重袋;纯文本内容,用 str 钢印标签;精确整数,用 int 量杯;带小数的近似值,用 float 电子秤。

性能上最值得记住的是“查找速度”的差距。list 查找一个元素是否在货架上,需要从头扫到尾;而 dict 和 set 凭借哈希表,直接一步定位。一个 O(n),一个 O(1),数据量越大,差距越夸张。

仓库管理员的经验法则还有:不要用可变对象当 dict 键或放进 set;不要依赖 set 的打印顺序;不要拿 float 当钱的精确计算工具。容器没有绝对的优劣,只有合不合适的场景。挑对了,你的 Python 仓库会像流水线一样顺畅运转。

Tdict/set=O(1)≪Tlist=O(n)\displaystyle T_{\text{dict/set}} = \mathcal{O}(1) \quad \ll \quad T_{\text{list}} = \mathcal{O}(n)
这行公式专门用于“判断某元素是否存在”的场景。若你反复执行 x in ...,把数据放进 set 或 dict 的键中,能省下大量时间。
  1. dict/set 复杂度哈希表让存在性判断平均一步完成。
    Tdict/set=O(1)T_{\text{dict/set}} = \mathcal{O}(1)
  2. list 复杂度线性扫描让存在性判断随长度增长。
    Tlist=O(n)T_{\text{list}} = \mathcal{O}(n)
  3. 远小于号n 越大,O(1) 与 O(n) 之间的鸿沟越深。
    ≪\ll

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 演示

python23 行
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()
目的:用 str 做文本清洗,用 list 保存排序后的词条,用 dict 统计词频,用 set 去重,并演示 int 总量与 float 平均值的类型差异。
  1. words = text.lower().replace(',', ' ').replace('.', ' ').split()把原始一行文本切成单词列表,这就是 str 的“钢印加工”。
  2. counts = {}开启一个空 dict 储物柜,准备记录每个词的出现次数。
  3. counts[word] = counts.get(word, 0) + 1按键取值,得不到就返回 0,再加 1 存回去,完成计数。
  4. unique_words = set(counts)把 dict 的键直接丢进去重袋,得到不重复词集合。
  5. 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. 下列哪个是可变类型?

单选题 · easy

  • A.整数 int
  • B.字符串 str
  • C.列表 list← 正确答案
  • D.浮点数 float

答案:C。list 是编号货架,可以在原容器上增删改,属于可变类型。int、str、float 都是不可变对象,任何“修改”都会产生新对象。记住“货架能挪货,钢印不能改”即可。

Q2. 查看变量类型的函数是?

单选题 · easy

  • A.type()← 正确答案
  • B.len()
  • C.print()
  • D.range()

答案:A。type(变量) 像给数据容器扫条形码,返回它的类型;len() 返回长度,print() 负责输出,range() 生成整数序列。所以扫型号选 type。

Q3. 0.1 + 0.2 == 0.3 的结果是?

单选题 · easy

  • A.True
  • B.False← 正确答案
  • C.None
  • D.Error

答案:B。float 电子秤用二进制近似表示小数,0.1 + 0.2 得到的近似值不完全等于 0.3,因此比较结果是 False。判断浮点数相等应使用误差允许范围,而不是 ==。

Q4. 下列哪个对象可以作为 dict 的键?

单选题 · mid

  • A.列表 list
  • B.字典 dict
  • C.字符串 str← 正确答案
  • D.集合 set

答案:C。dict 的键必须可哈希,而可哈希对应不可变性。str 是钢印标签,天然可哈希;list、dict、set 都是可变容器,会因内容变化导致哈希失效,不能当键。

Q5. 执行 int(12.7) 会得到什么?

单选题 · mid

  • A.返回 12← 正确答案
  • B.返回 13
  • C.返回 12.0
  • D.直接报错

答案:A。把 float 转成 int 时,Python 会朝零方向截断,直接丢弃小数部分,所以 int(12.7) 是 12,而不是四舍五入到 13。只有字符串转 int 时,字符串本身必须是整数写法。

Q6. 执行 s = 'abc'; s[1] = 'x' 会发生什么?

单选题 · mid

  • A.得到 axc
  • B.得到 xbc
  • C.直接报错← 正确答案
  • D.返回新串

答案:C。str 是不可变的钢印标签,不能修改已有标签上的某个字符,s[1] = 'x' 会抛出 TypeError。想得到新字符串,只能通过切片或方法重新生成。

Q7. 判断元素是否在 set 中的平均时间复杂度是?

单选题 · mid

  • A.O(1)← 正确答案
  • B.O(n)
  • C.O(log n)
  • D.O(n^2)

答案:A。set 底层是哈希表,查找元素时先算哈希再直接定位,平均只需要常数时间 O(1),不需要像 list 那样从头扫到结尾。

Q8. 在 list 头部插入元素的最坏时间复杂度是?

单选题 · hard

  • A.O(1)
  • B.O(log n)
  • C.O(n)← 正确答案
  • D.O(n^2)

答案:C。list 是连续编号的货架,在 0 号位插入会让后面所有格子整体后移,因此最坏需要 O(n) 次移动。频繁头部插入时,应改用 deque 等专门结构,或反转后末尾追加。

Q9. 关于 list 和 str,下列说法正确的有?

多选题 · mid

  • A.都能用索引访问← 正确答案
  • B.都支持切片← 正确答案
  • C.都是可变类型
  • D.都能被 for 遍历← 正确答案

答案:ABD。list 和 str 都是序列,支持索引、切片、循环遍历,所以 A、B、D 正确。但 list 是可变货架,str 是不可变钢印,C 错误。序列的通用能力并不等于可变性也相同。

Q10. 关于 dict 与 set,下列说法正确的有?

多选题 · extreme

  • A.都是可变容器← 正确答案
  • B.都基于哈希表← 正确答案
  • C.都保留插入顺序
  • D.都要求成员唯一← 正确答案

答案:ABD。dict 和 set 都靠哈希表实现,都是可变容器,A、B 正确。dict 的键唯一,set 的元素唯一,D 正确。dict 在 Python 3.7+ 保证插入顺序,但 set 规范上不保证顺序,因此 C 错误。