暗无天日

=============>DarkSun的个人博客

读:位掩码——用位运算打包多个标志位

最近读到 Martin Tournoij 的一篇文章 (Bitmasks explained),位掩码本身不难:用 0 和 1 开关某些标志位。但在代码里自如地运用 AND、OR、XOR 这些位运算,往往需要一个"突然开窍"的时刻。原文用一个终端着色库做案例,从需求出发讲到位运算原理,节奏很好。本文按原文思路整理,示例改用 Python。

问题:一堆 bool 字段的尴尬

假设你在设计一个 API token 的权限系统:

class TokenPermissions:
    count: bool
    export: bool
    site_read: bool
    site_create: bool
    site_update: bool

看着挺清爽。但想检查"只有 count 权限被开启",代码就变丑了:

if p.count and not p.export and not p.site_read and not p.site_create and not p.site_update:
    ...

检查"是否有任意一个权限被开启"也不好看。函数参数也一样,连续几个 bool 传进去,调用方根本分不清哪个是哪个:

do_something(False, False, True)  # 第三个参数是什么?

位掩码就是解决这类问题的:把多个标志压缩到一个整数里,用位运算来检查和组合。

位运算基础:AND、OR、XOR

先快速过一遍三种核心位运算。它们跟逻辑运算( andor )原理相同,只不过不是一个真假值,而是对整数的每一位分别做判断,拼出新的整数。

*AND(&)*:两个位都是 1,结果才是 1。

# 0b0011 & 0b0101 的逐位过程:
# 第3位: 0 AND 0 = 0
# 第2位: 0 AND 1 = 0
# 第1位: 1 AND 0 = 0
# 第0位: 1 AND 1 = 1
# 结果: 0b0001
print(bin(0b0011 & 0b0101))

AND 的核心用途是 检查某个标志是否被设置 。做法是用一个"掩码"去 AND,只保留你关心的那一位,其余位全部清零。结果不为 0,说明那一位是 1。

BOLD = 0b00000001  # 第 0 位
ITALIC = 0b00000100  # 第 2 位

# 只设置了 BOLD
attr = BOLD
print(f"BOLD 设置了吗? {attr & BOLD != 0}")   # True
print(f"ITALIC 设置了吗? {attr & ITALIC != 0}")  # False

# 同时设置 BOLD 和 ITALIC
attr = BOLD | ITALIC  # 稍后会讲 OR
print(f"BOLD 设置了吗? {attr & BOLD != 0}")   # True
print(f"ITALIC 设置了吗? {attr & ITALIC != 0}")  # True
BOLD 设置了吗? True
ITALIC 设置了吗? False
BOLD 设置了吗? True
ITALIC 设置了吗? True

注意 attr & BOLD ! 0= 也可以写成 attr & BOLD == BOLD ,两种写法等价。

**OR( | )**:任意一个位是 1,结果就是 1。

# 0b0011 | 0b0101
# 第3位: 0 OR 0 = 0
# 第2位: 0 OR 1 = 1
# 第1位: 1 OR 0 = 1
# 第0位: 1 OR 1 = 1
# 结果: 0b0111
print(bin(0b0011 | 0b0101))
0b111

OR 用来 组合多个标志 。这正是文章开头那个终端着色库的用法:

RED = 0b00000001
BOLD = 0b00000010
UNDERLINE = 0b00000100

# "红色加粗" = RED | BOLD
style = RED | BOLD
print(f"组合值: {bin(style)}")  # 0b11
组合值: 0b11

对比一下,不用位掩码的写法:

def apply_color(use_red=False, use_bold=False, use_underline=False):
    ...

# 调用方:这三个参数哪个是哪个?
apply_color(True, True, False)

用了位掩码之后:

apply_color(RED | BOLD)  # 一目了然

**XOR(^)**:两个位不同时结果为 1,相同时结果为 0。

# 0b0011 ^ 0b0101
# 第3位: 0 XOR 0 = 0
# 第2位: 0 XOR 1 = 1
# 第1位: 1 XOR 0 = 1
# 第0位: 1 XOR 1 = 0
# 结果: 0b0110
print(bin(0b0011 ^ 0b0101))

XOR 的用途是 翻转标志 :把 1 变 0、把 0 变 1。

BOLD = 0b00000010

attr = BOLD
print(f"初始: BOLD={attr & BOLD != 0}")  # True

attr = attr ^ BOLD  # 翻转 BOLD
print(f"翻转后: BOLD={attr & BOLD != 0}")  # False

attr = attr ^ BOLD  # 再翻转
print(f"再翻转: BOLD={attr & BOLD != 0}")  # True
初始: BOLD=True
翻转后: BOLD=False
再翻转: BOLD=True

清除标志和位移

**AND NOT**:清除某个标志位。Python 没有单独的 AND NOT 运算符,但可以用 AND + 取反 实现:

BOLD = 0b00000010

attr = BOLD | 0b00000001  # 设置了 BOLD 和最低位
print(f"清除前: {bin(attr)}")  # 0b11

attr = attr & ~BOLD  # 清除 BOLD 位
print(f"清除后: {bin(attr)}")  # 0b1,BOLD 位已清零
清除前: 0b11
清除后: 0b1

~ 是取反运算符(NOT),把所有位翻转。 ~BOLD 就是"除了 BOLD 那一位,其余全是 1"的掩码。用它去 AND,等于只清除 BOLD 位,其他位原样保留。

**位移(>>、<<)**:把所有位往右或往左移动若干位。

# 右移:所有位向右挪,右边溢出的位丢弃
print(bin(0b1010 >> 1))  # 0b101(右移1位)
print(bin(0b1010 >> 2))  # 0b10(右移2位,末尾的 0 丢了)

# 左移:所有位向左挪,右边补 0
print(bin(0b1010 << 1))  # 0b10100
0b101
0b10
0b10100

位移常用来从一个整数中提取某个区段的值。原文的终端着色库就是这么做的:一个 64 位整数,低 9 位存终端属性,中间 24 位存前景色,高位存背景色。要提取前景色的数值,先用 AND 掩码清除不相关的位,再右移把颜色值移到最低位。

打包一个辅助类

原文最后把位掩码操作封装成了几个通用方法。Python 版如下:

class Bitflag:
    def __init__(self, value: int = 0):
        self._value = value

    def has(self, flag: int) -> bool:
        return self._value & flag != 0

    def set(self, flag: int) -> None:
        self._value |= flag

    def clear(self, flag: int) -> None:
        self._value &= ~flag

    def toggle(self, flag: int) -> None:
        self._value ^= flag

    def __repr__(self) -> str:
        return bin(self._value)


# 使用
BOLD = 0b00000001
ITALIC = 0b00000010
UNDERLINE = 0b00000100

f = Bitflag()
f.set(BOLD)
f.set(ITALIC)
print(f)                # 0b11
print(f.has(BOLD))      # True
print(f.has(UNDERLINE))  # False
f.toggle(BOLD)
print(f.has(BOLD))      # False
f.clear(ITALIC)
print(f)                # 0b0
0b11
True
False
False
0b0

什么时候该用位掩码

不是所有场景都适合用位掩码。原文提到两个主要动机:

  1. API 更清晰 :组合标志时, RED | BOLDuse_red=True, use_bold=True 可读性好得多。检查"是否包含某些标志"时, attr & (EXPORT | READ)p.export and p.read 简洁。回到文章开头那个权限问题——检查"只有 count 被开启":

    # 位掩码写法:清除 count 位后,剩余位全为 0 就是"只有 count"
    if (perm & ~COUNT) == 0:
        ...
    

    对比原来的写法:

    if p.count and not p.export and not p.site_read and not p.site_create and not p.site_update:
        ...
    
  2. 内存效率 :一个整数能存几十个标志,而每个 bool 字段至少占一个字节。对大多数应用来说这不是关键因素,但在嵌入式或高频数据结构中会有实际意义。

反过来,如果你只是存一两个标志,或者不需要组合检查,普通 bool 字段更直观,不必强行用位掩码。

位运算 : 位掩码 : 编程基础 : Python