内存翻车半个月,我在 PyPI 上捡到“布尔救星”

1. 踩坑:百万级布尔数组,内存直接“爆炸”

本文关键词:Python 布尔数组、内存优化、稀疏存储、numpy 替代方案、bool-hybrid-array、BoolHybridArray、混合数组、位运算、OOM 排查

事情要从一个不起眼的“用户画像标签”说起。当时我在做一个推荐系统的用户分群逻辑,需要对几百万个用户打上布尔标签,判断他们是否命中某个兴趣圈层,True / False 简单得不行,但量一大就出事了。

我最先直接用了 Python 的 list,几百万个 True / False 往里面一塞,内存直接飙破 100MB,当时还觉得“就这点用户数据不至于吧”。事实证明,很至于——后来切到千万级用户,光这一个标签列表就干掉了将近 200MB 内存,整个推荐服务直接 OOM 重启循环。

当时我天真地以为“Python 的布尔值不就 1 个字节吗”,直到我看了眼内存占用才傻眼:

import sys

# 100 万个布尔值,用 list 存
tags = [True] * 1_000_000
print(sys.getsizeof(tags))  # 8000056,约 8MB 只是指针数组
# 每个 True 还是一个独立的 Python 对象,实际占用远不止这些

没错,list 里每个元素都是一个独立的 Python 对象,光指针数组就 8MB,再加上每个布尔对象的开销,百万级直接奔着 100MB 去了。我当时盯着这个数字,感觉自己在用“金锄头挖地”。

然后我换 numpy,用了 np.bool_,内存是省了不少,但一碰到需要动态更新的场景又开始头疼:numpy 一改就新建实例,大数组频繁 append / del 的开销高得离谱。更崩溃的是,有时候数据极度稀疏(比如 1% 的 True),有时候又极度密集(99% 的 True),没有一种存储方式能两头兼顾
你以为这就完了?还有更阴间的:我试过 array('b'),省内存但慢得跟蜗牛爬;试过 bitarray,快是快,但一遇到动态增删就各种别扭;还试过 bytearray 自己手动按位编码,结果代码写得像天书,三个月后自己都看不懂。每个方案都有它的“阿喀琉斯之踵”,我就像在玩“打地鼠”,按下一个坑,冒出三个新坑。

我把当时试过的方案整理成了一张表,方便你直观感受什么叫“没有一个能打的”:

方案 内存占用 动态更新 稀疏场景 我的评价
list 💥 爆炸 ✅ 灵活 ❌ 全存 百万级直接 OOM
numpy.ndarray ✅ 省 ❌ 改即新建 ❌ 全存 频繁改开销离谱
array('b') ✅ 省 ⚠️ 一般 ❌ 全存 慢得跟蜗牛爬
bitarray ✅ 极省 ❌ 增删别扭 ✅ 稀疏友好 动态场景劝退
bytearray 手写位 ✅ 极省 ❌ 天书代码 ✅ 稀疏友好 上线第二天翻车

最离谱的是有一次,我为了省内存把布尔数组塞进 bytearray 里手动按位操作,写了个 get_bit / set_bit 函数,自我感觉良好。结果上线第二天,线上数据全乱了——因为我把“第 3 位”和“第 3 个字节”搞混了,整整 200 万用户的标签全部错位。那天晚上我盯着监控面板,感觉自己在给全公司表演“如何用一行代码毁掉一个推荐系统”。

2. 构想:自适应“密集 + 稀疏”混合数组

当时熬夜画了张脑图,觉得既然稀疏场景用位图或区间存储最省内存,密集场景直接用连续数组最快,那为什么不让数据结构自己判断、自己切?

我甚至给这个“理想中的数组”起了个名字,叫 ChameleonArray(变色龙数组)——因为它应该像变色龙一样,环境一变就自动换皮肤。当时我还认真画了张架构图,看起来简直完美:

flowchart LR A["输入数据"] --> B{"密度判断"} B -->|"稀疏(True 占比低)"| C["稀疏模式:array.array 存索引"] B -->|"密集(True 占比高)"| D["密集模式:numpy.ndarray"] C --> E["自动切换器"] D --> E E --> F["对外统一接口"]

我还专门写了个 PPT 给同事讲这个构想,讲到“自动切换”的时候,同事问了一句:“那切换的阈值怎么定?数据一直在变,会不会来回抖动?”我当场愣住了,支支吾吾说“这个……我还没想好”。现在回头看,那个 PPT 就是标准的“画饼三件套”:概念图、流程图、以及一个我自己都答不上来的关键问题。

我的核心思路其实很简单:

  • 数组前半段密集存 True,就继续用紧凑的 ndarray;
  • 后半段零零散散几个 False,切成稀疏存储;
  • 这两段还能根据数据变化自动重新切割

当时觉得这个思路太优雅了,当晚就开工。我先是设计“怎么判断当前该用哪种模式”,再写“怎么在两种模式之间无缝切换”,最后还要保证“切片、赋值、遍历这些操作在两种模式下行为完全一致”。我越写越上头,然后就掉进了“写 10 行,调 3 天 Bug”的循环。

3. 自己做,做了十几天,疼到怀疑人生

这十几天基本是这样度过的:

  • 第一天:写了个能跑的数组类,能用,开心。
  • 第二天:切片返回的类型不对,重写底层逻辑。
  • 第三天:内置方法没覆盖,一调用就卡死。
  • 第四天:稀疏模式下数据静默写错位置,排查了一整天。
  • 第五天:for 循环和 len() 行为打架。
  • 第六天:复制对象直接报错,忘了实现对应协议。
  • 第七天:边界条件没处理好,成功制造出“数组越界不报错、静默返回错误结果”的深海巨坑。
  • 第八天:比较两个数组,返回的居然是一个数组而不是布尔值,吓得我以为自己穿越到了 numpy。
  • 第九天:想写个“统计当前占了多少内存”的小工具,结果数字忽大忽小,比股票还刺激。
  • 第十天:自动优化函数写出来了,但那个内存统计工具的实现反复炸了好几次,看着更新日志里那一排“尝试修复…×N”想笑又想哭。
  • 第十一天:序列化直接崩,内部结构太复杂,协议没写对。
  • 第十二天:我盯着自己写的 2000 多行代码,发现还有一堆边界条件没处理,心态彻底崩了。

到了第十二天,我盯着自己的代码仓库,从“模式切换的边界条件”到“数组扩容的时机”一大堆待修问题,心态彻底崩了——这玩意儿逻辑太细了,从零锤一个生产可用的混合布尔数组,真不是一个人两个月的事

那几天我连做梦都在调 bug:梦里那个内存统计工具终于返回了正确数字,我激动得笑醒,结果一睁眼发现是假的。我甚至开始怀疑人生——我到底是在写代码,还是在给 Python 的 C 扩展打工? 一个“小小的布尔数组”,居然能让我体验到从入门到放弃的全流程。

最崩溃的是第十三天早上,我打开 GitHub 想看看有没有人遇到过类似问题,结果搜到一条 Stack Overflow 回答,底下有人评论:“为什么不直接用 bool-hybrid-array?”我当时心想:这是什么野鸡包?点进去一看,好家伙,这不就是我梦寐以求的东西吗

4. 峰回路转:PyPI 上的“布尔救星”

已经快放弃了,随手在 PyPI 上搜 bool array sparse,翻了几页突然看到一个包:bool-hybrid-array

点进去一看介绍,我整个人都愣住了:

一个专门为布尔值优化的数组类,能够根据数据特征自动在密集存储和稀疏存储模式间切换,兼顾性能和内存效率。

这不就是我想做的那个东西吗?!下载量 14 万+,GitHub 星标 0(是的你没看错,全网下载量超高但星标还是个位数,笑死),我赶紧试了一下。

python -m pip install bool-hybrid-array

三行代码就把我之前苦战十几天的问题全解决了:

from bool_hybrid_array import BoolHybridArr

# 10 万个元素,只有 1% 是 True,自动走稀疏存储
big_arr = BoolHybridArr([i % 100 == 0 for i in range(100000)])
print(big_arr.memory_usage(detail=True))
# 输出:对比原生 list 节省 99%+,对比 numpy 节省 80%+

而且你根本不用关心什么时候切稀疏、什么时候切密集,BoolHybridArray 自己全搞定,修改元素速度还不比原生 list 慢。

5. 如果你也被卡住,你会经历什么

先别急着往下看,我问你几个问题,你心里默默回答就行:

  • 你是不是也处理过几百万、几千万的布尔标签,然后看着内存监控一路飙红,最后 OOM 重启?
  • 你是不是也试过 numpy,结果每次增删一个元素就新建整个数组,改着改着就想摔键盘?
  • 你是不是也想过「要不自己写一个」,然后写了两天就放弃了,因为边界条件实在太多?

如果你中了任何一条,那接下来的内容就是写给你的。如果你一条都没中,那你现在就可以关掉这篇文章,省一分钟。

我当初就是三条全中,然后花了十几天自己造轮子,最后在 PyPI 上翻到了 bool-hybrid-array。装完之后,我那个 1% 稀疏的场景,内存从 list 的几百 MB 降到了个位数 MB。100 万元素,从 800MB 干到 8MB,省了 99%。

来,直接看数据,这是我实际跑出来的对比:

场景(100 万布尔元素) 原生 list numpy.ndarray BoolHybridArray
内存占用 ~800 MB ~100 MB ~8 MB
稀疏场景(1% True) ❌ 全存 ❌ 全存 ✅ 自动稀疏
密集场景(99% True) ❌ 全存 ✅ 紧凑 ✅ 自动密集
动态修改 ✅ 快 ❌ 改即新建 ✅ 快
位运算 ❌ 不支持 ⚠️ 需手动 ✅ 原生支持

而且它用起来跟 list 一样,索引、切片、赋值、遍历,零学习成本。& / | / ^ / ~ 这些位运算直接链式写,不用自己写一堆工具函数。memory_usage(detail=True) 直接告诉你当前占了多少、有没有优化空间,不用再靠「感觉」估内存。

  • 它太省内存了。100 万元素从 800MB 干到 8MB,省了 99%。省到啥程度?我那个「内存不够就换电脑」的申请报告,写了三遍都被打回来了——它让我失去了正当的换机理由,这算不算缺点?
  • 它 API 太多了。80+ 个接口,从 BoolHybridArrBHA_Queue,连 C++ 的 cin / coutendlsetfill 都给你搬过来了。我本来想「三天学完」,结果学了一周还没摸完,严重高估了我的学习进度,这算不算缺点?
  • 它太聪明了。什么时候切稀疏、什么时候切密集,它自己全搞定,我根本插不上手。我本来想「自己写个切换逻辑秀一波操作」,结果它全自动了,让我毫无用武之地,这算不算缺点?
  • 它太好用了。索引、切片、赋值、遍历,跟 list 一模一样,零学习成本。我本来想「装完先研究半天文档」,结果三行代码就跑通了,让我连「研究文档」的仪式感都没了,这算不算缺点?
  • 它太能打了& / | / ^ / ~ 位运算直接链式写,memory_usage(detail=True) 直接告诉你占了多少内存。我本来想「自己写一堆工具函数练练手」,结果它全包了,让我连写工具函数的机会都没有,这算不算缺点?

但话说回来:如果你只是偶尔处理小规模布尔数据,用 list 就够了,它对你确实没什么用;可如果你跟我一样,被海量布尔数组的内存问题折磨到 OOM、被 numpy 的「改即新建」气到摔键盘,那它就是你缺的那块拼图——缺点再多,也架不住它正好长在你的痛点上

6. 你只需要花一分钟判断

我知道你看到「星标 0」心里会犯嘀咕:这包靠谱吗?会不会明天就没人管了?说实话,我一开始也这么想,但用下来之后,我的看法是这样的:

  • 星标少 ≠ 不好用。它下载量 14 万+,说明用的人其实不少,只是大家都不太习惯点星标。就像小区门口那家天天排队的苍蝇馆子,大众点评上就 3 条评论,但每天翻台 8 次。
  • 作者确实在认真维护。更新日志里那一排「尝试修复…×N」,看着像翻车记录,但反过来想,一个愿意反复修 bug 的作者,比那种「半年一更、更完就跑」的包靠谱多了。

生态小?你怕是没数过它的 API。我对着 bool_hybrid_array.__dict__ 一个个数了一遍,截止 9.11.38 版本,暴露在库顶层作用域的 API 有 80+ 个——从 BoolHybridArrBoolHybridArrayTruesArrayFalsesArray 这些核心数组类,到 BHA_ListBHA_Queue 这类容器,再到 BHA_FunctionProtectedBuiltinsDictAsk_BHACreate_BHAnumba_opt 这些工具,一应俱全。更离谱的是,它连 C++ 那套流操作都搬过来了——cin / coutendlsetfillsetwofstream / ifstreamC++ 里有的操纵算子它基本都有,就差给你写个 #include <iostream> 了。

80+ 个 API 是什么概念? 很多号称「生态成熟」的库,核心 API 也就十几个。它一个「0 星小透明」,愣是给你塞了 80 多个接口,这哪是小生态,分明是「全家桶中的全家桶」。所以它和 numpy 不是替代关系,而是互补关系:需要完整数据科学生态的人继续用 numpy,被布尔数组内存问题卡住的人,在 numpy 旁边加一个它就够了。

所以我的建议很直接:别因为它「0 星」就一票否决,也别把它当成 numpy 的「平替」——它压根不是来替代谁的,而是来给 numpy 补位的。numpy 管「全量数据科学」,它管「布尔数组这一亩三分地」,两者是「主力 + 特种兵」的关系:你该用 numpy 的地方继续用 numpy,被布尔数组内存问题卡住的地方,在 numpy 旁边加一个它,等于给主力配了个专门攻坚的尖刀连。你正好被这个问题卡住,它就是为你准备的;你没有被卡住,那它对你确实没什么用。这两种情况不冲突,你只需要花一分钟判断自己属于哪一种。

怕它和 numpy 是「二选一」?恰恰相反。我一开始也担心:我的项目已经大量依赖 numpy 生态了,用了它是不是就得把 numpy 数组全换成它的数组?结果用下来发现,它俩是「并肩作战」的关系,不是「你死我活」——你该用 numpy 的地方继续用 numpy,被布尔数组内存问题卡住的地方,在 numpy 旁边加一个它就够了。它甚至能直接和 numpy 数组互相转换、无缝衔接,你现有的 numpy 代码一行都不用改。它不是来替代 numpy 的,而是来给 numpy 生态「补漏」的**——你不需要在「numpy 全家桶」和「它」之间做选择,两个都要,各干各的活。怎么判断?很简单,回想一下你最近一次处理布尔数组的场景:

怎么判断?很简单,回想一下你最近一次处理布尔数组的场景:

  • 如果你当时用的是 list,内存飙到几百 MB,那你就是「被卡住」的人,装它,一分钟的事。
  • 如果你当时用的是 numpy,但每次改长度都新建整个数组,改得想骂人,那你也是「被卡住」的人,装它,一分钟的事。
  • 如果你只是偶尔处理几千个布尔值,list 完全够用,那你不是「被卡住」的人,划走就行,不亏。

花一分钟装一下试试,成本几乎为零,装错也能卸。万一它正好解决了你的问题,那这一分钟就是你这周最值的投资;就算用不上,你也只是花了一分钟确认「这玩意儿不适合我」,不亏。觉得好用的话,顺手去 GitHub 点个星,就当给作者加个油 🥲。

# 建议提前先安装 cython,使用时更快!
python -m uv pip install cython

# 安装方式(推荐用 uv,超快)
python -m uv pip install bool-hybrid-array --upgrade
posted @ 2026-08-19 14:21  贝壳bkshell  阅读(4)  评论(0)    收藏  举报