拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Python数据类型核心梳理:可变与不可变、哈希、深浅拷贝与性能

先说明一个判断Python 的数据类型如果只当作“int、float、str、list、tuple、dict、set”这张表来背后面写代码基本会被深浅拷贝、可变默认参数和哈希问题绊住。这篇博客按“内存里怎么存—能不能改—能不能哈希—运行开销有多大”这条线把 Python 类型系统过一遍再配合可直接运行的代码片段验证结果。全文不需要安装额外第三方库只要本机有 Python 3 环境就能跟着测。先给一个总览Python 是动态类型语言也是强类型语言。说“动态”是因为变量不需要声明类型解释器根据赋值推断说“强”是因为字符串和数字不会隐式互相转换1 2会直接抛TypeError而不会像部分脚本语言那样自动转成字符串或数字。正文会覆盖数值、字符串、列表、元组、字典、集合另外重点讲可变与不可变区别、显式类型转换、类型判断、深浅拷贝、容易踩的坑以及不同数据结构的性能差异。我建议你把文章里的代码段粘贴到 Python 交互环境或者 VSCode 里逐段运行。看类型知识最有效的办法不是“背结论”而是观察同一个变量名在修改前后的内存地址变化。1. Python 数据类型体系一览Python 的官方文档习惯把内置类型分成“基本类型”和“容器类型”实际编码时更实用的分类方法是按“数据类型在内存中的存在方式”来划分。分类类型名称是否可变是否可哈希典型用途数值类型int否是整数运算、计数、索引数值类型float否是浮点数运算、科学计算数值类型complex否是复数运算布尔类型bool否是条件判断、开关状态文本类型str否是文本处理二进制类型bytes、bytearraybytes 不可变bytearray 可变bytes 可哈希bytearray 不可哈希文件读写、网络传输、编码处理序列容器list是否有序、可修改的动态数组序列容器tuple否内部元素全部可哈希时可哈希固定结构、函数多返回值映射容器dict是否键值映射、缓存、配置集合容器set是否去重、集合运算集合容器frozenset否是可作为字典键的不可变集合空值NoneType否是表示“无值”这里有个容易忽略的细节bool是int的子类。也就是说True和1在比较时是相等的hash(True)和hash(1)也相同。后面讲字典键冲突时会再次遇到这个点。Python 官方文档把int、float、complex归为数值类型把str、list、tuple、range、bytes归为序列类型。从编码角度我更建议你先记住“可变对象”和“不可变对象”的分类因为几乎所有的引用、拷贝、函数传参问题都出在这条线上。a 42 print(type(a)) # class int print(isinstance(a, int)) # True2. 可变与不可变对象Python 类型体系的核心分界线先启动 Python 交互环境运行下面这段代码python3 -V然后逐行执行s hello print(id(s)) # 记录原始内存地址 s world print(id(s)) # 地址变了说明生成了新对象 lst [1, 2, 3] print(id(lst)) # 记录原始内存地址 lst.append(4) print(id(lst)) # 地址不变说明修改发生在原对象内部id()返回对象的内存地址是观察“是否创建了新对象”最直接的手段。字符串是不可变对象每次拼接都会创建新字符串然后让变量指向新对象列表是可变对象append、pop、sort、索引赋值都会直接修改原列表不会重新创建对象。这个差异对函数传参有直接影响def add_item_to_list(target): target.append(99) num_list [1, 2, 3] add_item_to_list(num_list) print(num_list) # [1, 2, 3, 99]函数内部修改了外部列表如果传递的是不可变对象函数内部重新赋值只会让局部变量指向新对象外部变量不受影响。如果是可变对象并且函数内部调用了修改方法外部数据会被“就地”改动。实际工程中这既是方便也是隐患。理解可变性之后再看可哈希性就会清晰很多。一个对象能否作为字典的键或放入集合取决于它是否可哈希而可哈希的前提通常是对象不可变。不可变并不保证一定可哈希因为元组内部如果包含 list它照样不能哈希。hash((a, 1)) # 可以返回整数 hash(([a], 1)) # 抛 TypeError: unhashable type: listset内部本质是一个哈希表所以它不能存放 list 或 dict。试着执行set().add([1, 2])会直接报错。3. 数值类型详细拆解int、float、complex、bool3.1 int任意精度与大整数Python 的int不是固定 32 位或 64 位整数它是任意精度的。即使计算2 ** 10000也能直接得到完整结果不会溢出这是对比 Java 和 C 语言时感知最明显的一个点。print(2 ** 100) print(10 ** 300)整数的进制写法需要会认print(0x10) # 16十六进制 print(0b101) # 5二进制 print(0o12) # 10八进制也可以通过int()函数转换进制字符串print(int(101, 2)) # 5 print(int(ff, 16)) # 255整数运算中有三个运算符容易混/永远做浮点除法即使除得尽结果也是float。//做地板除结果向下取整。%做取余。print(7 / 2) # 3.5 print(7 // 2) # 3 print(-7 // 2) # -4注意是向下取整 print(int(-7 / 2)) # -3int() 截断到零方向//和int()对负数的处理不同前者是向下取整后者是直接丢弃小数位。笔试和面试题里经常拿这个点做陷阱编码时先想清楚需求。3.2 float二进制浮点的精度问题float对应 C 语言里的 double占用 64 位遵循 IEEE 754 标准。它的问题不在于“数不够大”而在于“小数精度不精确”。print(0.1 0.2) # 0.30000000000000004 print(0.1 0.2 0.3) # False0.1 在二进制里是无限循环小数计算机只能用近似值保存所以直接比较浮点数结果时经常出问题。业务代码里处理金额、税率、单价这一类十进制数值更稳妥的做法是使用decimal.Decimalfrom decimal import Decimal, getcontext getcontext().prec 28 result Decimal(0.1) Decimal(0.2) print(result) # 0.3注意Decimal构造时最好传字符串。如果直接传Decimal(0.1)它先读取二进制浮点近似值再转换成 Decimal精度问题照样存在。getcontext().prec控制全局有效数字位数需要按实际业务精度设置。另一个隐蔽陷阱是round()采用“银行家舍入”也就是四舍六入五取偶print(round(2.5)) # 2 print(round(3.5)) # 4 print(round(4.5)) # 4 round(2.675, 2) # 2.67这个例子并非总是可复现但能看出精度问题如果业务需要“四舍五入”不要直接用round()处理浮点金额尤其是对外展示的数据优先用 Decimal再配合 quantize 控制精度。3.3 complex复数类型Python 原生支持复数这在数据分析、信号处理场景中很有用。复数用j表示虚部单位c 3 4j print(c.real) # 3.0 print(c.imag) # 4.0 print(abs(c)) # 5.0模长复数比较大小在数学上并不存在唯一顺序Python 也不允许对复数直接比较大小。如果遇到排序需求需要先取实部或模长再排序。3.4 bool不是独立数值体系bool只有True和False两个值但它继承自int所以可以参与整数运算print(True 1) # 2 print(False * 100) # 0 print(isinstance(True, int)) # True print(isinstance(1, bool)) # False在判断条件时Python 有自己的真值规则None、False、0、0.0、0j、空字符串、空列表[]、空元组()、空字典{}、空集合set()都视为假其他对象默认视为真。空字符串、空容器在布尔判断中为 False这是 Python 语法风格里非常重要的一点。if not []: print(空列表是 False)4. 字符串 str不可变但有丰富的处理手段字符串是 Python 中使用频率最高的数据类型。它本质是 Unicode 字符序列不可变。字符串的创建可以用单引号、双引号或三引号s1 hello s2 world s3 多行 文本单双引号本身没有功能差异只要规范统一即可。三引号常用于多行文本和 docstring。索引和切片是字符串最重要的操作s python print(s[0]) # p print(s[-1]) # n print(s[1:4]) # yth print(s[::-1]) # nohtyp反转字符串切片语法[start:end:step]对字符串和列表通用。注意切片是左闭右开区间s[1:4]包含索引 1、2、3不包含 4。写出s[::-1]时倒序会在很多算法题里看到这实际上新建了字符串对象因为str不可变。字符串拼接有一个经典工程问题。用拼接多个字符串看起来很自然但在循环里连续拼接 N 次时间复杂度可能退化到接近 O(N^2)因为每次拼接都会重新分配内存并生成新字符串。可靠做法是收集到列表中最后用joinparts [Python, , 数据类型, , 实战] result .join(parts) print(result)join只遍历一次是处理大量字符串拼接的首选。业务日志拼接如果用循环拼接几万条字符串能明显感受到性能差异。字符串的索引和切片依赖于字符位置计算因此查找子串、替换子串都有专门方法s hello, python print(len(s)) # 13 print(s.find(python)) # 7 print(s.replace(python, world)) print(s.split(,)) # [hello, python]关于编码必须理解str和bytes的关系。Python 3 的字符串一定是 Unicode 字符序列写入文件或发送到网络时需要编码成bytes读取时再解码回strs 中文 b s.encode(utf-8) print(b) # b\xe4\xb8\xad\xe6\x96\x87 print(b.decode(utf-8)) # 中文最常见的UnicodeDecodeError就是因为文件或网络数据不是 UTF-8 编码却用 UTF-8 去解码。读取文本文件时明确指定encodingutf-8是常规做法with open(data.txt, r, encodingutf-8) as f: content f.read()字符串格式化现在优先用 f-stringname Python version 3.12 print(f语言: {name}, 版本: {version})f-string 支持格式控制这在输出对齐、数值精度控制时很实用price 12.3456 print(f{price:.2f}) # 12.35 print(f{price:10}) # 右对齐宽度 10 print(f{name}) # namePython变量调试f{name}这种写法在 Python 3.8 之后可以使用会把变量名和值一起打印非常适合临时调试。注意不同 Python 版本对 f-string 内部引号复用限制不同老版本不要嵌套使用相同引号否则会报语法错误。5. 容器类型list、tuple、dict、set5.1 list动态数组与切片list是有序的可变容器可以存放任意类型的元素。nums [1, 2, 3] nums.append(4) nums.insert(0, 0) nums.pop() print(nums) # [0, 1, 2, 3] print(nums[1:3]) # [1, 2]列表推导式是 Python 风格的代表写法之一squares [x * x for x in range(10) if x % 2 0] print(squares) # [0, 4, 16, 36, 64]它比手写 for 循环加 append 结构更紧凑。新工程师阅读代码时看到列表推导式需要知道前半部分是结果表达式后半部分是循环来源和过滤条件。列表底层是动态数组或者说“分离式数组”连续内存块加上额外的整体容量。这决定了它的性能特征append平均时间复杂度是 O(1)摊还下来很稳定。在头部insert(0, x)或删除pop(0)的时间复杂度是 O(n)因为需要移动后续所有元素。随机索引访问lst[i]是 O(1)。如果日常代码需要频繁在头部插入数据比如实现队列用 list 不是最佳选择可以考虑collections.dequefrom collections import deque d deque([1, 2, 3]) d.appendleft(0) print(d) # deque([0, 1, 2, 3])deque在左右两端增删都是 O(1)。不过它中间索引访问比 list 慢具体用哪个要看操作模式。这里体现了“选择数据结构的前提是知道操作频率分布”。5.2 tuple不可变序列与解包tuple与list的区别只有一个核心点不可变。创建后无法添加、删除或替换元素。t (1, 2, 3) t[0] 10 # TypeError创建单元素元组时容易踩坑t1 (1) t2 (1,) print(type(t1)) # class int print(type(t2)) # class tuple(1)只是一个整数 1加不加括号对元组的定义都没有影响真正决定元组身份的是逗号。理解元组之后交换变量这种操作可以写得很简洁a, b 10, 20 a, b b, a print(a, b) # 20 10右侧b, a会被打包成元组对象然后左侧解包赋值给a和b。我经常建议初学阶段至少手写一个“用临时变量交换两个数”的版本再背这个一行交换版本能直观体会到元组打包与解包的机制。元组相比列表有两个工程优势数据固定不可变防止函数内部意外修改业务数据。如果元组内部所有元素都可哈希元组本身可哈希因此可以作为字典键或集合元素。point (10, 20) locations {point: 原点} print(locations[(10, 20)])但要注意([1, 2],)这种元组内部包含列表它不能哈希。5.3 dict键值映射的底层规则字典是 Python 工程中使用频率极高的映射容器。自 Python 3.7 起字典按插入顺序保存键值这是一个语言规范不是巧合实现。d {} d[name] Python d[version] 3.x d[age] 30 print(list(d.keys())) # [name, version, age]字典的键必须可哈希所以 list、dict、set 不能作为字典键bad_dict {[key]: value} # TypeError: unhashable type: list如果实际开发中需要“列表作为键”正确做法是把它转换为元组。字典读取时推荐用get而不是直接d[key]避免 KeyErrorconfig {host: 127.0.0.1} print(config.get(host, localhost)) print(config.get(port, 8000))setdefault可以完成“取不到就设默认值”的一次性操作但更 Pythonic 的写法是使用collections.defaultdictfrom collections import defaultdict counts defaultdict(int) for word in [apple, banana, apple]: counts[word] 1 print(dict(counts)) # {apple: 2, banana: 1}defaultdict(int)表示访问不存在的键时自动调用int()得到 0然后参与加法运算。对比原始写法你不再需要写if key not in counts: counts[key] 0。统计可哈希元素频率时直接用collections.Counterfrom collections import Counter text abracadabra counter Counter(text) print(counter.most_common(2)) # [(a, 5), (b, 2)]字典的合并有多种写法注意版本差异d1 {a: 1, b: 2} d2 {b: 3, c: 4} merged {**d1, **d2} # Python 3.5 print(merged) # {a: 1, b: 3, c: 4} merged2 d1 | d2 # Python 3.9 print(merged2)当两个字典出现相同键时后写的字典值会覆盖前面的值。字典深层嵌套的场景中修改值默认是就地修改。5.4 set无序去重与集合运算set与dict的底层实现类似也是哈希表但它只存储键不存储值。它的核心用途是去重和集合运算。words [a, b, a, c] unique set(words) print(unique) # {a, b, c}创建空集合只能使用set()不能使用{}因为{}创建的是空字典empty_set set() empty_dict {} print(type(empty_set)) # class set print(type(empty_dict)) # class dict集合运算包括交集、并集、差集、对称差集a {1, 2, 3} b {3, 4, 5} print(a b) # {3}交集 print(a | b) # {1, 2, 3, 4, 5}并集 print(a - b) # {1, 2}差集 print(a ^ b) # {1, 2, 4, 5}对称差集set不支持索引和切片因为元素顺序不受开发者控制。需要遍历集合时通常不需要在意顺序如果业务逻辑对顺序有强要求遍历前先sorted()一次更稳妥。frozenset是不可变集合可以哈希因此能作为字典键或另一个集合的元素。业务里如果配置项本身需要被缓存并作为复合键可以考虑 frozenset。6. 类型转换显式转换、隐式转换与最容易出错的细节Python 的强类型特性决定了它不会把字符串自动转换成数字。所谓“类型转换”指的是通过内置构造函数显式创建新对象。6.1 常用显式转换函数转换目标转换函数示例注意整数int()int(42)字符串必须是数字形式浮点数float()float(3.14)可以转inf、nan字符串str()str(100)任意对象可转字符串列表list()list(abc)字符串转成字符列表元组tuple()tuple([1, 2])可迭代对象都可转集合set()set([1, 1, 2])去重字典dict()dict([(a, 1)])需要键值对序列或映射数值转字符串和字符串转数值是两个最常见方向print(int(42)) print(float(3.14)) print(str(100))但如果字符串格式不合法转换会直接抛ValueErrorprint(int(3.14)) # ValueError: invalid literal for int() print(int(abc)) # ValueErrorint(3.14)之所以报错是因为int()期望的参数是整数字符串3.14 不是合法整数而float(3.14)可以成功之后如果需要整数再自己做取整。这里有一个细节值得记住字符串两端的空格在转换时通常会被忽略但中间出现空格不会。6.2 隐式转换数值提升是例外Python 在数值运算中会自动把int提升为floatprint(1 2.0) # 3.0结果是 float print(type(1 2.0))bool在参与数值运算时也会被当作 0 和 1print(True True) # 2但字符串与数字之间的运算不会隐式转换print(1 2) # TypeError: can only concatenate str这就是强类型语言的优势类型错误尽早暴露缺点是写代码时需要显式处理边界场景。日常编码中为了减少隐式转换带来的意外我建议对输入值先做类型校验或显式转换不要依赖“刚好能跑”。6.3 float 转 int 的截断行为int(3.99)会把小数部分直接截掉得到 3而不是四舍五入。需要四舍五入时先调用round()print(int(3.99)) # 3 print(int(-3.99)) # -3向零截断 print(round(3.99)) # 4从 Python 3.11 开始int()对字符串的解析支持更多下划线分隔写法比如int(1_000_000)代码可读性可以提高不过这与普通业务代码关系不大基础阶段不必深挖。6.4 布尔值转换的坑bool(False)的结果是True因为非空字符串永远是 Truebool()才是 False。判断用户输入的False时如果直接if bool(user_input)会得到错误逻辑需要先比较字符串再做转换user_input False flag user_input True print(flag) # False这种错误在配置文件和前端表单解析中很常见。想清楚“容器的空与非空”和“业务字符串的字面含义”是两码事不要混在一起。7. 类型判断与类型标注type()、isinstance()、typingPython 是动态语言但在工程代码里不能放任类型随便变化。需要的不是变量声明而是运行时的类型检查以及给 IDE 和团队同事看的静态类型注释。type()返回对象所属的类isinstance()判断对象是否是指定类型或其子类。绝大多数类型判断场景应该用isinstance()因为它支持继承关系这也是 isinstance(True, int) 返回 True 的原因。print(type(10) is int) # True print(isinstance(10, int)) # True print(isinstance(True, int)) # True print(type(True) is int) # Falsetype(True) 是 bool不是 inttype(True) is int与isinstance(True, int)结果不同这是子类判断和精确类型判断的区别。代码里判断数字时如果用了type(x) is int传 True 会被排除大多数业务逻辑反而希望把 bool 当作数字处理所以要看你到底要排查什么。只判断内置类型的写法def process_number(value): if isinstance(value, (int, float)): return value * 2 raise TypeError(expected number)类型标注用于静态检查工具和 IDE 提示。Python 3.9 之后可以直接用list[int]、dict[str, int]Python 3.10 之后可以用int | str表示联合类型from typing import Optional, Union def parse_name(name: str | None None) - str: return name or 默认名老项目如果运行在 Python 3.8 或更早版本需要从typing导入List、Dict、Optional、Unionfrom typing import List, Dict, Optional, Union def process(config: Optional[Dict[str, int]]) - List[str]: if config is None: return [] return [str(v) for v in config.values()]类型标注不会改变运行行为只服务于代码检查和 IDE使用mypy或pyright可以做静态检查。只要项目运行版本允许我建议新代码直接用内建泛型语法减少typing模块的导入噪音。在实际工程里还有一个“鸭子类型”场景只要对象拥有所需的方法或属性就认为它是目标类型。比如只需要传入有.items()方法的映射对象不一定非得是dict。def format_mapping(mapping): for key, value in mapping.items(): print(f{key}: {value})使用collections.abc里的抽象基类做结构类型判断而不是强制判断某个具体类能让函数更通用。8. 引用、深浅拷贝与对象内存细节可变对象在赋值时并没有复制数据只是把变量指向同一块内存。a [1, 2, 3] b a b.append(4) print(a) # [1, 2, 3, 4]很多新手以为b a创建了新列表实际上没有。需要独立副本时必须显式调用拷贝方法或切片。切片lst[:]返回新对象但只做了浅拷贝。列表内部如果嵌套了列表切片复制的是嵌套列表的引用内层元素仍然共享。matrix [[1, 2], [3, 4]] copy_matrix matrix[:] copy_matrix[0][0] 99 print(matrix) # [[99, 2], [3, 4]]内层变化影响到了原始数据浅拷贝只复制最外层容器深拷贝会递归复制所有层级的对象import copy original [[1, 2], [3, 4]] deep copy.deepcopy(original) deep[0][0] 0 print(original) # [[1, 2], [3, 4]]实际业务中配置字典、模板列表如果要在后续流程里被修改并且原始数据还需要复用建议根据嵌套深度选择浅拷贝或深拷贝。如果到处都是copy.deepcopy要警惕其不是没有成本的深度很深的嵌套对象递归复制会消耗较多内存和时间。另一种思路是尽量设计成不可变结构比如用元组替代内部列表或使用frozenset。CPython 还有一个内存细节是“小整数缓存”a 256 b 256 print(a is b) # True c 257 d 257 print(c is d) # 可能在部分环境下为 FalseCPython 通常会缓存 -5 到 256 这个范围内的小整数便于频繁复用。注意这是实现细节不是语言规范业务代码不要依赖is判断两个整数是否相等应该用。is只适合与None比较因为None是单例对象。字符串技术上有时也会做驻留优化但同样不代表可以依赖is比较字符串内容。一律使用是比较安全可靠的原则。9. Python 数据类型踩坑清单9.1 可变对象作为函数默认参数这是面试中出现频率很高的坑。默认参数只在函数定义时创建一次不会每次调用都重新创建def add_item(item, target[]): target.append(item) return target print(add_item(1)) # [1] print(add_item(2)) # [1, 2] print(add_item(3)) # [1, 2, 3]正确写法是用None作为默认值函数内部再创建新列表def add_item(item, targetNone): if target is None: target [] target.append(item) return target这个问题的本质是默认参数是函数定义阶段创建的对象当对象可变并且函数内原地修改时修改会被累积到同一个对象上。任何可变对象出现在默认参数位置都会存在这个风险。9.2 列表乘法的引用共享[[]] * 3会创建包含三个相同空列表引用的大列表而不是三个独立空列表matrix [[]] * 3 matrix[0].append(1) print(matrix) # [[1], [1], [1]]如果需要一个嵌套列表结构应该使用列表推导式matrix [[] for _ in range(3)] matrix[0].append(1) print(matrix) # [[1], [], []][0] * 5没有问题因为整数是不可变对象有问题的场景是内部元素本身是可变对象乘法只是复制了外层引用。9.3 遍历时修改列表或字典遍历列表时删除元素会导致索引错位产生隐蔽 bug。与其在现场调试为什么“某些元素没被删掉”不如直接遍历原列表副本或者用列表推导式构造新列表numbers [1, 2, 3, 4, 5] even_removed [num for num in numbers if num % 2 ! 0] print(even_removed)遍历字典时删除键值则可能触发RuntimeError: dictionary changed size during iteration。正确的做法是先收集要删除的键data {a: 1, b: 2, c: 3} for key in [k for k in data if data[k] 2]: del data[key]9.4True与字典键冲突前面的小节提到bool是int的子类所以True 1hash(True) hash(1)。把它们同时作为字典键时会发生覆盖d {} d[True] yes d[1] no print(d) # {True: no}同理False与0也不适合同时作为字典键。设计哈希键时需要考虑该键对应的类型语义业务数据中如果存在多种可能的类型值建议拆成不同字段或者把所有值统一转成字符串避免哈希碰撞带来的数据覆盖。9.5 浮点数金额不要直接比较0.1 0.2 0.3的结果是 False这在数据分析、报表计算、接口交互中会反复出现。需要精确十进制运算时使用 Decimal不要试图在浮点数基础上写“足够接近”这种临时判断因为不同业务对误差容忍度不同项目后期修改成本很高。9.6与is混用判断值是否相等is判断内存地址是否指向同一个对象。大部分业务比较应该使用只有与None比较时必须用isif x is None: pass运行时把is用于判断x 1即使在小整数区间偶然正确代码到了处理大整数或者其他对象时也会变得不可靠。10. 资源占用与性能观察数据结构需要按场景选不同数据类型的性能不能靠直觉判断下面给出一套可复现的测量方法实际数字以本机运行为准。使用timeit可以比较不同数据结构的开销import timeit # 列表尾部插入 print(timeit.timeit(lst.append(1), setuplst [], number100000)) # 列表头部插入 print(timeit.timeit(lst.insert(0, 1), setuplst [], number100000)) # deque 头部插入 print(timeit.timeit(d.appendleft(1), setupfrom collections import deque; d deque(), number100000))从实测经验看列表头部插入和deque头部插入的性能差异会随数据量增大而变大。核心不是记住哪个容器快而是先判断业务主要是“头尾操作”还是“中间随机访问”。另一个常见的性能对比是“用列表查找元素”还是“用集合去重”。Python 的列表是顺序结构查找某个元素需要线性扫描集合底层是哈希表通常可以在 O(1) 时间完成查找。数据规模到几千以上时x in list与x in set的差异会非常明显。big_list list(range(10000)) big_set set(big_list) print(9999 in big_list) # 线性查找 print(9999 in big_set) # 哈希查找内存占用方面字典比列表多存储哈希信息会占用更多内存元组比列表少一部分预留容量和动态扩容元数据所以通常更省内存。对性能没有明显要求的内层结构不必强行优化到极致但明确需要海量数据驻留内存时列表和元组优先于字典。如果处理数值型大数据例如百万级浮点数数组纯 Pythonlist的内存占用和计算速度都不够理想。这时候应该转向array模块或 NumPy它们能提供更紧凑的底层存储和向量化计算。这个过程实际上并没有改变 Python 语言本身的特性而是把数据从“Python 对象列表”转换成了“连续内存数值数组”。生成器和迭代器也是“类型选择”的一部分。虽然它们本身不是基础数据类型但它们代表惰性求值的数据序列思路。处理超大文件或无限序列时用列表一次加载全部数据会占用大量内存改用生成器逐次生成结果会让内存占用大幅降低。def integers(): i 0 while True: yield i i 1使用内置函数range()时它返回的是一个区间对象不是完整列表。for i in range(10 ** 9)能够启动迭代因为每次只生成一个整数不会把十亿个整数全部放进内存。这也是 Python 数据类型学习里容易被忽略的重要性质很多容器类和迭代对象在设计上并不要求一次性把所有元素加载到内存。日常编码时可以用sys.getsizeof()查看单个对象的大概内存占用用tracemalloc做分配追踪import sys print(sys.getsizeof([])) # 空列表的基础大小 print(sys.getsizeof(())) # 空元组的基础大小 print(sys.getsizeof({})) # 空字典的基础大小 print(sys.getsizeof(set())) # 空集合的基础大小不过sys.getsizeof()只统计对象本身不统计内部元素。观察整体内存占用更可靠的方式是使用tracemalloc测量执行前后的差值。11. 中大型项目里的数据类型使用规范项目代码越复杂越需要把数据类型的使用约束成固定规范否则后期排查成本会很高。以下几个建议来自实际工程经验可以直接套用到 Python 项目里。约定容器内元素类型。如果一个列表设计出来只存放订单号不要在业务代码里往里面塞字符串描述或嵌套字典。类型标注明确写出list[str]或list[int]能让 IDE 在编码阶段提示错误也能减少调用方误传数据。输入边界优先做显式转换。读取配置、解析 HTTP 参数、读取 CSV 时拿到的字段经常是字符串。要在入口处统一转换成目标类型不要散落在各个函数里。业务缓存优先使用frozenset或元组。设计复合键时把临时可变列表转换为元组再入字典避免键值在后续流程中悄悄变化导致数据丢失。批量任务日志里要对变量类型做记录。写日志时使用 f-string 本身不会报错但把自定义对象直接塞进日志时如果该对象没有友好的__str__实现日志会难以阅读。基础类型通常没问题复杂对象要显式调用字段方法。如果需要把多个键值动态写入同一份字典优先用setdefault或defaultdict初始化避免重复写“判断键是否存在”的模板代码。类似地做分组时以字典的键为分组标识值为列表或元组做去重时优先用集合然后用sorted()保证输出顺序稳定。高并发或受限环境下不要随意使用copy.deepcopy。深拷贝的安全性强但代价较大。更合理的做法是系统入口处做深度防御式拷贝内部函数间传递数据时尽量遵守“谁拥有数据、谁负责修改”的约束。如果只想防止外部函数原地修改列表把列表转换成元组或传入副本也是一种轻量方案。12. 从数据类型扩展到 Python 后续学习路线数据类型是 Python 代码设计的基础。先把不可变对象与可变对象的分界线记清楚再看深浅拷贝、集合去重、字典嵌套就会通顺很多。判断一个 Python 初学者是否真正掌握了这部分知识不看能不能背出多少个类型名称只看四个问题作用于字符串和列表时的内存行为是否一致。函数默认参数使用空列表后为什么会产生累计效果。为什么dict键不能用 list 但可以用 tuple。为什么0.1 0.2 0.3是 False。这四道题如果都能不假思索解释出来说明数据类型的底层逻辑已经通了。接下来推荐按这个顺序继续深入先掌握collections模块中的defaultdict、Counter、deque、namedtuple再学习enum.Enum和dataclasses把“类型”从基础容器提升到领域建模层。之后如果接触数据处理重点会比较list与 numpyndarray的差异如果接触网络编程重点理解str与bytes的边界。Python 数据类型本身只是一个起点但把这个起点铺扎实后面的坑会少很多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门