ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python数据存储与运算:从内存对象到分布式存储的完整实践指南

Python数据存储与运算:从内存对象到分布式存储的完整实践指南 前阵子帮一个刚学Python的朋友调月度账单统计脚本逻辑一眼扫过去毫无问题价格、数量、总价、列表求和每一步都挺正常可跑出来的结果在小数位上总是差那么几分钱。排查到最后问题不是出在写法上而是出在浮点数在内存里的存储方式上。这让我意识到Python学习者刚开始最容易忽略、却最值得花时间搞清楚的两件事就是数据怎么存、数据怎么算。存储和运算看起来是两个词实际上在编程里是同一枚硬币的两面存储结构决定了你能用什么方式运算运算结果反过来又会影响你要不要把数据放到另一种结构里去。这篇文章不打算讲多高深的理论就把Python里数据存储与运算这条线从变量、内存、文件一路撸到数据库和分布式存储把我这些年踩过的坑和验证过的方法一并写出来。1. Python中“存储”的第一步变量到内存对象别把引用当容器1.1 变量名的本质只是贴到对象上的标签我刚学Python那会儿最根深蒂固的一个误解是把变量当盒子。以为a 1是把数字1装进a这个盒子b a是复制了一份1给b。后来看内存地址才反应过来Python的变量压根不是盒子而是贴在对象上的标签。a 10086 b a print(id(a), id(b), a is b) # 两个id完全相同is返回Truea和b指向的是同一个对象id一样。真正的“复制”得用copy模块或者切片c [1, 2, 3] d c[:] # 切片复制出一份新的列表 d.append(4) print(c) # 输出[1,2,3]c不受影响这个“标签式”的存储设计让Python的赋值天生轻量因为只是把一个名字挂到已有对象上不复制底层数据。但也正是这个设计初学阶段最容易踩坑。有个典型的坑叫“默认参数陷阱”def add_item(item, container[]): container.append(item) return container print(add_item(1)) # [1] print(add_item(2)) # [1, 2] 第二次调用没有传container按直觉应该拿到一个空列表结果还是上一个列表。因为默认参数在函数定义时就被创建并固定下来了每次调用共用的是同一个列表对象。这个问题的本质就是引用共享。解决办法是用None做默认值函数体里再新建。顺带说一下is和的区别is比的是“是不是同一个对象”也就是存储地址是否一致比的是“值是否相等”。小整数[-5, 256]在Python里做了缓存所以a1; b1时a is b是True但大整数不缓存a257; b257时a is b是False。很多人第一次在这里被搞晕其实就是存储层面做了优化。注意排查数据问题时先打印type()和id()把存储层面看清楚再往下查运算逻辑能省掉大量瞎猜的时间。1.2 可变与不可变内存里的两种性格根据对象能不能原地修改Python内置类型划分成两大阵营类型可变性典型操作int, float, str, tuple不可变任何修改都会生成新对象list, dict, set, bytearray可变可以原地增删改这一点直接决定了运算方式。字符串不可变想做拼接、替换只能生成新字符串所以大量循环里做str text这种操作性能很差因为每次都重新申请内存、复制旧内容复杂度接近O(n²)。更合理的做法是收集到一个list里最后用.join()一次性合并join是C层面实现速度能差几十倍。列表可变所以list.append()是原地操作速度快list.insert(0, x)看着也是原地但底层要把后面所有元素往后挪数据量大时很慢复杂场景建议用collections.deque。不夸张地说你在选存储结构的那一刻就已经决定了后续运算的效率和写法。后面讲NumPy、讲文件存储都是同一个逻辑。1.3 看清存储开销用sys.getsizeof实际测一测光听概念不够我习惯直接量一下。sys.getsizeof可以返回一个对象占用的内存字节数import sys print(sys.getsizeof(1)) # 28 print(sys.getsizeof(hello)) # 54 print(sys.getsizeof([1, 2, 3])) # 88注意这只是列表本身容易忽略的是列表的getsizeof只算它自身存放指针的空间不算元素对象本身。真要算整体占用得把元素一个个加进去如果元素是大的对象内存开销可能远比你想象大。这也是为什么处理几千万行数据用纯Python list经常把内存吃满——因为每个元素是一个独立对象各自带着类型信息、引用计数这些头部元数据。下一节讲NumPy连续存储就是针对这个痛点的解药。2. 浮点数运算为什么总和直觉拧着来二进制存储的精度账2.1 0.1 0.2 不等于 0.3 的根因直接在Python里跑一下print(0.1 0.2) # 0.30000000000000004很多人把锅甩给Python其实Python没错是绝大多数编程语言共用的浮点数存储标准IEEE 754的问题。这个标准用“符号位 指数位 尾数位”的二进制科学计数法来存储小数类似十进制里用有限位数去写1/3只能写成0.3333。十进制0.1转成二进制是个无限循环小数的形态但存储空间有限只能在某个精度截断截断后存下来的数值就已经不是严格的0.1了。可以看一下实际存储值print(format(0.1, .20f)) # 0.10000000000000000555 print(format(0.2, .20f)) # 0.200000000000000011100.1和0.2各自保存时都有微小误差相加后误差累积结果就是0.30000000000000004。2.2 哪些场景会真的被浮点误差坑到不是所有场景都需要管这个误差。比如画图、做图像处理、做科学计算误差在1e-15量级根本不影响结论。但下面几类场景必须重视金额计算差一分钱都是事故比如账单、订单、工资。循环累积比如每笔0.1元累加1000笔误差会被不断放大。边界判断if total 100.0这种判断在浮点世界里非常危险大概率不成立。涉及比较排序如果两个浮点值理论相等但存储不同排序和去重会出现莫名其妙的结果。2.3 处理浮点偏差的几种实用手段我实际项目里用到的方案就这几类按适用场景选方案适用场景示例直接用整型金额把“元”换成“分”存储5.23元存成523decimal.Decimal需要高精度的十进制运算Decimal(0.1) Decimal(0.2)fractions.Fraction需要精确分数比较Fraction(1,3)容忍度比较浮点数值比较abs(a-b) 1e-9重点讲Decimal注意要用字符串传参。from decimal import Decimal, getcontext getcontext().prec 28 print(Decimal(0.1) Decimal(0.2)) # 0.3 # 不要这样写 print(Decimal(0.1) Decimal(0.2)) # 还是0.3000000000000000166533453694Decimal(0.1)是先拿到浮点数那个已经失真的值再转成Decimal失真被继承。只有Decimal(0.1)这种字符串创建才真正精确。提示金额如果能控制数据源优先用整数存最小单位如果一定要浮点显示层再统一转字符串处理。千万别在显示层用float相加后再round那是把误差藏到后面。2.4 类型转换也是存储格式的转换热词里有“python类型转换”我觉得它本质上是存储表示的转换。常见规则int(42) # 42 float(3.14) # 3.14 str(10086) # 10086 int(3.99) # 3注意是截断不是四舍五入int(3.99)的结果会让不少人意外它的行为是向零截断不是四舍五入。想要四舍五入先用round或math.floor/ceil根据业务决定。另外一个坑是int处理带小数的字符串会报错int(3.14) # ValueError正确做法是先转float再转int。这类错误在读取CSV、Excel数据时特别常见因为单元格里存的是“3.14”这种字符串文本。遇到的时候别慌先明确原始数据的存储格式再决定转换链条。3. 位运算从二进制存储直接落地的最底层计算3.1 先看整数在内存里的二进制形态位运算听起来唬人实际上就是直接对二进制位做运算。Python里可以用bin()查看整数的二进制表示print(bin(5)) # 0b101 print(bin(6)) # 0b110 print(5 6) # 4二进制101 110 100 print(5 | 6) # 7二进制101 | 110 111 print(5 ^ 6) # 3二进制101 ^ 110 011关于有符号和无符号热词里“c语言中有符号整数和无符号整数和位运算”提到这个问题。Python的int是任意精度没有传统的固定位宽所以也没有无符号整数类型。但某些场景比如解析网络协议、二进制文件格式需要按32位无符号整数处理时可以用 0xFFFFFFFF来和掩码求与把超过32位的部分丢掉value 4294967295 # 2^32 - 1 value_32 value 0xFFFFFFFF3.2 位运算的六个基本操作Python的位运算符有六个基本成员按位与、|按位或、^按位异或、~按位取反、左移、右移。其中按位取反在Python里由于int无限精度结果容易出乎意料比如~5得到的是-6这是补码表示带来的用之前要有心理准备。异或有个很实用的性质同一个数异或两次会还原。这个性质可以用来做简单的数据交换不需要临时变量a, b 5, 9 a ^ b b ^ a a ^ b print(a, b) # 9 5按位左移一位等于乘2右移一位等于整除2向下取整print(5 1) # 10 print(5 1) # 2相当于5 // 23.3 位运算在实际项目里的典型用途判断奇偶n 1结果为1是奇数0是偶数。比n % 2稍快一点不过现代Python里区别微乎其微主要是写法简洁。判断是否是2的整数次幂n 0 and (n (n - 1)) 0。权限组合用不同bit表示不同权限例如读1(0b001)写2(0b010)执行4(0b100)一个整数就能同时表达多个权限。二进制协议解析从字节里抽取特定bit需要掩码和移位配合。颜色打包/解包RGBA每个通道8bit可以用移位和与运算一次性打包成32位整数。列一个权限组合的小例子READ 1 WRITE 2 EXEC 4 user1 READ | WRITE # 0b011 has_write user1 WRITE # 2非0表示有写权限 has_exec user1 EXEC # 0没有执行权限这种用整数做存储、用位运算做判断的写法在配置项、状态位、特征开关这类场景非常常见存储成本低判断效率高。3.4 延伸说明机器视觉里的“开闭运算”不是同一件事热词里有“机器视觉开闭运算参数原理”为了避免混淆说一下图像处理里的开运算、闭运算属于形态学操作主要在灰度图或二值图上做膨胀和腐蚀的组合用来去噪、连接断开区域或填充小空洞OpenCV的cv2.morphologyEx是常用入口。它跟Python的整数位运算不是一回事但它底层同样是逐像素的数值运算理解矩阵和数值运算的机制对学习它很有帮助。把两者都归到“数据的运算”这个大框架下就没毛病。4. 批量数据的存储与运算从列表到NumPy矩阵4.1 纯Python列表存批量数据的问题用list存一千个数很轻松但存一千万个呢内存和速度都会很难看。前面说过list存的是对象引用每个浮点数都是独立对象分散在内存各处还会附带额外头部信息。在做运算时Python解释器还要频繁做类型检查、创建新对象、回收旧对象性能自然上不去。统计里常说的“向量化运算”“矩阵运算”其实就是想摆脱这种逐对象遍历模式改成底层用C/C实现、对连续内存块统一处理的方式。这就是NumPy存在的意义。4.2 NumPy数组连续内存里的一整块数据NumPy的ndarray核心特征是类型统一、存储连续可以近似理解成一块连续内存加上元数据形状、类型、步长import numpy as np arr np.array([1, 2, 3, 4]) print(arr.dtype) # int64 print(arr.shape) # (4,) matrix np.array([[1, 2], [3, 4]]) print(matrix.shape) # (2, 2)用np.zeros、np.ones、np.arange这类方式生成数组底层是一次性分配一整块内存比逐元素append快得多。4.3 矩阵运算的基本逻辑矩阵运算在机器学习、图像处理里是家常便饭。NumPy里最核心的是点积和元素级运算的区别。元素级是形状一致的对应位置相乘点积是线性代数里的定义行列相乘后求和A np.array([[1, 2], [3, 4]]) B np.array([[5, 6], [7, 8]]) print(A * B) # 元素级乘法[[5,12],[21,32]] print(A B) # 矩阵乘法[[19,22],[43,50]] print(A.dot(B)) # 等价于 实际项目里最容易出错的是形状不匹配。比如两个二维数组做矩阵乘法要遵守(A,B) (B,C)这样的维度规则中间维度必须相等。理解shape的规则能省下大量用np.reshape凑维数的时间。还有广播机制形状不同但满足规则时NumPy会自动扩展运算。比如给整个二维数组减去每列平均值可以直接把向量和矩阵做运算NumPy会自动把向量“铺”到每一行上。4.4 数据集场景MNIST和训练之前的数据形态热词里有“mnist数据集”“kitti数据集下载”“yolov8训练自己的数据集”。训练深度学习模型之前数据基本都要转成数值矩阵。MNIST就是典型例子每张图28x28像素展开就是784维向量整个数据集可以组织成(样本数, 784)的矩阵标签是一个(样本数,)的向量。训练前的归一化操作把像素从0~255映射到0~1就是对整个矩阵做一个除法运算一行代码但背后是几十万个元素的批量化处理这正是矩阵运算的优势。如果不用NumPy或者DataLoader这类工具纯Python去遍历几十万张图做预处理时间成本完全不是一个量级。4.5 一个直观的实测对比写个简单的性能对比让大家有个直观感受import time import numpy as np n 1_000_000 py_list list(range(n)) start time.time() py_sum sum(py_list) py_time time.time() - start np_arr np.arange(n) start time.time() np_sum np_arr.sum() np_time time.time() - start print(fPython sum: {py_time:.5f}s) print(fNumPy sum: {np_time:.5f}s)在我普通笔记本上纯Python的sum大概在0.03秒左右NumPy在0.001秒左右差距有一个数量级。数据量越大差距越大。这就是“存储方式决定运算效率”最直观的证明。5. 数据落盘与持久化文件、数据库、备份与分布式存储5.1 文件存储文本和二进制怎么选内存里的数据一关机就没了要长久保存就得落盘。最简单的是文本文件和JSONimport json data {name: 张三, scores: [88, 92, 95]} with open(data.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse) with open(data.json, r, encodingutf-8) as f: loaded json.load(f)文本存储可读性好但体积大、解析慢。二进制文件体积小、读写快但可读性差。Python的pickle可以把任意对象序列化到文件很方便但要注意它只在Python之间通用版本兼容性也有问题不适合作为跨语言、跨版本的长期存档格式。CSV也是文本存储的常见形态数据分析场景经常需要从CSV读数据。读取时需要注意编码、表头、空值这些问题。推荐直接用pandas的read_csv省心很多。5.2 数据库存储MySQL和SQLite的日常数据量变大、需要频繁条件查询时文件和JSON就不够用了得交给数据库。SQLite适合单机轻量使用零配置、单文件MySQL适合多客户端并发、服务端部署。热词里有“mysql可以存储整数数值的是”和“mysql数据库命令大全”说明很多人学数据库时第一个搞不清的就是字段类型。MySQL里存储整数常见类型有TINYINT、SMALLINT、INT、BIGINT占用的字节分别是1、2、4、8能表示的整数范围也不同。写表的时候要根据数据范围选不是所有整数都无脑用INT。如果只是存年龄TINYINT就够存订单金额考虑到最小单位可能得用BIGINT或DECIMAL。Python连MySQL的标准做法是用连接器比如PyMySQLimport pymysql conn pymysql.connect( host127.0.0.1, userroot, passwordxxx, databasedemo, charsetutf8mb4, ) cursor conn.cursor() cursor.execute(SELECT COUNT(*) FROM orders) print(cursor.fetchone()) conn.close()其实我更推荐直接用ORM框架比如SQLAlchemy或Django ORM减少手写SQL的语法错误。但无论如何字段类型设计、索引创建这些基本功要懂否则数据量上来后查询慢得惊人是必然的。再往上走大数据环境里还有DolphinScheduler这类调度工具做数据库数据抽取本质上就是把存储在不同库里的数据取出来做运算和流转属于ETL的范畴。5.3 备份与恢复没有备份的存储等于没有存储热词里有“数据备份与恢复”和“win10 存储感知设置步骤”“硬盘存储查看工具”都在围绕“存储空间和数据安全”打转。我的经验是存储方案必须配套备份方案否则数据一旦丢失之前的效率全白搭。几个基本规则一个完整备份至少包含数据文件、配置文件、环境说明三部分。自动化优于手动用cron或定时任务定期备份。恢复演练比备份本身重要。备份了但恢复不了等于白备份。至少每季度选一个干净环境做一次恢复测试。异地备份不要把鸡蛋放在同一个筐里本地一份、云端对象存储再放一份。数据库备份的核心命令也很简单MySQL用mysqldumpPostgreSQL用pg_dump把数据库导出成SQL文件。恢复时用source或导入命令。关键是把备份文件按日期命名便于回溯。提示写完备份脚本之后第一件事不是看备份文件生成没生成而是老老实实跑到一个干净的机器上做一次恢复测试。恢复不了备份就是一堆没用的字节。5.4 对象存储、NAS和分布式存储怎么理解前面说的都是单机或传统文件存储热词里还有“对象存储服务”“分布式存储”“nas存储”。这几个概念和Python的数据存储运算也有关联因为跑Python服务时经常要把数据放到这类存储里。对象存储面向海量非结构化数据不依赖目录层级每个对象有唯一标识key比如云服务商提供的对象存储服务适合存图片、视频、日志、模型文件。NAS网络附属存储多台机器通过网络共享适合局域网内共享文件、办公协作、家庭影音。热词里“萤石摄像头通过easynvr docker接入飞牛nas实现大容量视频存储”就是这个场景。分布式存储数据被切分并冗余存放在多台服务器上优点是容量弹性扩展、数据高可靠。HDFS、Ceph、MinIO都是典型代表。对Python开发者来说最常用的是这些存储服务的SDK把文件上传到一个bucket或共享目录然后在代码里通过URL或SDK读取。理解一致性、可用性、容量的取舍选型时才不会拍脑袋。5.5 顺带说说“删除文件后存储还在”的现象热词里“小米平板删除文件后为什么存储还在”挺生活化。这种现象大多是因为文件系统里的删除只是移除了目录项标记底层数据块还在在被新数据覆盖前有可能通过碎片扫描找回部分内容也可能是还有进程占用、缩略图缓存、回收站机制。在Python做文件处理时也有类似经验想真正释放存储不仅要把文件删掉还要确保文件对象已经close否则数据可能还在缓存里。写代码时用with open()就能在退出块时自动关闭这就是标准姿势。6. 把存储和运算串起来一个完整的小案例为了把前面的内容串起来我写一个模拟案例记录一周的步数和体重计算日均步数、周均体重保存到JSON再复制一份做备份。import json import shutil import numpy as np records [ {date: 2026-01-05, steps: 8234, weight: 68.6}, {date: 2026-01-06, steps: 10120, weight: 68.4}, {date: 2026-01-07, steps: 6548, weight: 68.7}, {date: 2026-01-08, steps: 9207, weight: 68.5}, {date: 2026-01-09, steps: 11302, weight: 68.2}, {date: 2026-01-10, steps: 7312, weight: 68.3}, {date: 2026-01-11, steps: 9860, weight: 68.1}, ] steps np.array([r[steps] for r in records]) weights np.array([r[weight] for r in records]) avg_steps steps.mean() avg_weight weights.mean() print(f日均步数: {avg_steps:.0f}) print(f周均体重: {avg_weight:.2f}) with open(health_records.json, w, encodingutf-8) as f: json.dump(records, f, ensure_asciiFalse, indent2) shutil.copy(health_records.json, health_records.json.bak)这个例子看似简单但里面的每个步骤都在呼应前面的知识点用列表和字典存原始记录用NumPy数组做批量计算浮点体重做格式化输出JSON做持久化文件复制做备份。真实项目无非是把这些环节做得更规范、更大型而已。我个人的核心体会是两条第一存储方案先行运算才会顺。决定用什么结构存数据等于决定你能用什么方式处理它。第二任何存储方案都要考虑精度和可靠性两个边界精度决定结果对不对可靠性决定数据会不会丢。把这条主线捋顺了后续学文件IO、数据库、数据分析和机器学习都会轻松很多。最后分享一个调试小技巧遇到“计算出来不对”的问题时不要急着改运算逻辑先打印每个变量的类型和id把存储层面看清楚。绝大多数诡异bug最后都出在存储和类型上而非表面的算术上。
返回列表