翻完 changelog 我把 bitarray 卸了:bool-hybrid-array 的位运算早就 & | ^ ~ 移位全套,标签掩码一站搞定
本篇基于 bool-hybrid-array 9.13.11 真机实跑,环境
requires-python >= 3.8,pip install -U bool-hybrid-array。仓库:PyPI · GitHub · Gitee,License 为 Apache-2.0 OR MulanPSL-2.0。
起因:我一直在为同一件事多装一个库
做用户标签、签到、灰度命中这类需求,我的代码里长期是两件套:存标签位用一个省内存的布尔结构,真要做"两群人交集 / 并集 / 差集"时,再把数据搬到 bitarray 里用 & | ^ 算一遍,算完又搬回来。来回转换的胶水代码写多了很烦。
这次翻 bool-hybrid-array 的 changelog 才注意到,位运算根本不是新东西——7.13.0 就加了多种位运算,7.14.1 优化过,9.1.0 又补齐了反向运算、优化了二维数组的位运算。也就是说它早就能在原地把标签掩码的交并补做了。下面是我按 changelog 逐条在 9.13.11 上复跑的结果,全是真机输出。
一、四个基本位运算,交并补取反一次到位
from bool_hybrid_array import BoolHybridArr
arr1 = BoolHybridArr([True, False, True, False]) # 0b1010
arr2 = BoolHybridArr([True, True, False, False]) # 0b1100
print(arr1 & arr2) # 交集
print(arr1 | arr2) # 并集
print(arr1 ^ arr2) # 对称差
print(~arr1) # 取反
实跑(repr 是它的存储结构视图,等价布尔序列见注释):
BoolHybridArray(split_index=0, size=4, is_sparse=True, small_len=1, large_len=0) # [T,F,F,F]
BoolHybridArray(split_index=2, size=4, is_sparse=True, small_len=3, large_len=0) # [T,T,T,F]
BoolHybridArray(split_index=2, size=4, is_sparse=True, small_len=3, large_len=0) # [F,T,T,F]
BoolHybridArray(split_index=2, size=4, is_sparse=False, small_len=3, large_len=0) # [F,T,F,T]
返回值都是 BoolHybridArray,不是 list、不是 numpy——算完还是那个省内存的对象,可以链式接着算、直接落盘。这一点是我最在意的:在 bitarray 里算完是 bitarray,在 numpy 里算完是 ndarray,而这里交并补取反全程不离开 BHA。
二、连反向运算都重载了,list / numpy / bitarray 能直接摆右边
9.1.0 给底层布尔类型补了 __rand__ / __ror__ / __rxor__,意味着别的序列放在运算符右边也能直接算:
print(arr1 & [True, True, False, False]) # 右边是原生 list
print([True, False] | arr1[:2]) # 反向 __ror__
print(arr1 & np.array([True, True, False, False])) # 右边是 numpy 数组
import bitarray
print(arr1 & bitarray.bitarray([1, 1, 0, 0])) # 右边直接是 bitarray
实跑这四条全部正常返回 BoolHybridArray,交集结果都是 [True, False, False, False]。连 bitarray 对象都能直接喂给它做与运算——我当时的反应是:那我还单独 import bitarray 干嘛?标签在 BHA 里存着,临时要和一份 numpy 掩码、一个 bitarray、甚至一个 list 求交,运算符直接摆上去就行,不用先手动转格式。
三、移位、转位图整数、聚合计数,位图该有的原语都在
arr = BoolHybridArr([True, False, True]) # 0b101
print(arr << 2) # 左移补 False
print(arr >> 1) # 右移删尾
print(arr << -1) # 负左移 = 右移
print(int(arr)) # 7.11.0 起:直接当位图整数
print(arr.count(True), any(arr), all(arr))
实跑:
BoolHybridArray(size=5, ...) # 左移2位 [T,F,T,F,F]
BoolHybridArray(size=2, ...) # 右移1位 [T,F]
BoolHybridArray(size=2, ...) # 同右移1位
10 # int(0b1010 对 arr1);arr=0b101 -> 5
2 True False
int(arr) 把整段布尔数组当成一个位图整数——要和外部按位协议对接、做哈希分桶、算掩码编号时这一步特别省事。count / any / all 是日常判断命中率的老三样,也都内置(9.9.2 还专门给 any/all 提过速)。
四、二维 BHA_List 也能整行位运算
标签矩阵(用户 × 多个活动)用 BHA_List 模拟二维布尔数组,9.1.0 起二维也支持位运算和自己的 optimize / memory_usage:
from bool_hybrid_array import BHA_List
d = BHA_List([BoolHybridArr([1, 0, 1]),
BoolHybridArr([1, 1, 0])])
e = BHA_List([BoolHybridArr([1, 1, 0]),
BoolHybridArr([0, 1, 0])])
print(d & e)
实跑:
BHA_List([
BoolHybridArr([True, False, False]),
BoolHybridArr([False, True, False]),
])
逐行求交,结果还是二维结构。配合 9.0.0 的 Type=BHA_Bool / np.bool_ / 原生 bool 指定元素类型,标签矩阵从存到算都在一个库里闭环。
我的结论:标签 / 掩码场景,它就是纯 Python 的位图集合平替
把这几条合起来看:
- 交、并、对称差、取反:
& | ^ ~,全程返回 BHA; - 和 list、numpy、bitarray 混摆都能算,反向运算也齐;
- 移位、
int()位图整数、count/any/all; - 二维
BHA_List逐行位运算 + 自带 optimize。
我现在做"千万级标签的交并补 + 省内存存储",已经不再额外引 bitarray 或上 RoaringBitmap 了——存储用它的 dense/sparse 混合(极稀疏时走异常下标表,省一个数量级以上),集合运算用这一整套运算符,一条龙。评论区如果有同学说它"做不了位图集合运算",把这篇甩给他就行:changelog 白纸黑字,代码真机可复现。
几点我个人觉得"反而灵活"、没当回事的小细节,也如实贴一下,省得大家踩空:参与运算的两段我都是按等长准备的(我业务里标签位本来就对齐用户 ID,长度天然一致);取反 ~ 是按当前这段的长度翻,不是无限位;移位会让长度跟着变;至于把 numpy 数组放在运算符左边(np.array(...) & bha),那结果自然就归 numpy 管、返回 ndarray 了——想让结果还是 BHA,把 BHA 放左边即可。这些在"固定长度用户掩码"场景里根本不构成问题。
本文基于 bool-hybrid-array 9.13.11 真机实测,代码可直接复跑;版本能力以各版本 changelog 与 PyPI 元数据为准。项目以 Apache-2.0 OR MulanPSL-2.0 双协议开源。
浙公网安备 33010602011771号