前言
这个标题把两件事放在了一起:列表和位运算符。它们其实是两条独立的线——列表是容器, 位运算符操作的是整数。标题之所以会把它们绑在一起,多半是因为集合(set)和列表常在 同一段代码里出现,而集合的|、&、^看上去和位运算符一模一样,于是「|是 按位或还是求并集」就混了。本文先把位运算符本身的语义和优先级讲清,再专门点破这个 混淆:集合上的|/&不是位运算,字典在 3.9 之后也借用了|,那同样不是位运算。
先说版本与前提:Python 2.7 已于 2020 年 1 月 1 日停止维护;Python 2 里整数有int和long之分,Python 3 统一成任意精度的int,xrange也被移除。本文全部使用 Python 3 的写法,示例适用于 Python 3.8 及以上,涉及 3.12 的变更时会单独注明。
一个基础概念要先立起来:Python 文档明确说明,位运算的结果按「带有无限多个符号位的 二进制补码」来计算。也就是说,正数左边有无限个 0,负数左边有无限个 1。理解了这点,~0为什么等于-1、右移负数为什么越来越负,就都能推出来。
一、六个运算符的语义
| 运算符 | 名称 | 含义 | 示例 |
|---|
x & y | 按位与 | 两位都为 1 才得 1 | 6 & 3得2 |
| `x \ | y` | 按位或 | 有一位为 1 就得 1 |
x ^ y | 按位异或 | 两位不同才得 1 | 6 ^ 3得5 |
~x | 按位取反 | 每一位取反 | ~1得-2 |
x << n | 左移 | 左移 n 位,低位补 0 | 1 << 3得8 |
x >> n | 右移 | 右移 n 位 | 8 >> 2得2 |
# 适用于 Python 3.8+
# 用二进制形式看更直观:6 是 0b110,3 是 0b011
print(bin(6), bin(3)) # 0b110 0b011
print(6 & 3) # 2 0b010
print(6 | 3) # 7 0b111
print(6 ^ 3) # 5 0b101
print(~1) # -2
print(1 << 3) # 8
print(8 >> 2) # 2~x的结果有一个可记的恒等式:~x == -x - 1。所以~0是-1,~1是-2,~5是-6。
位移还有两条等价关系,文档里写得很明确:左移 n 位等价于乘以2 ** n;右移 n 位 等价于对2 ** n做向下取整的除法。右移用的是向下取整(floor),所以负数右移不会 像截断那样往零靠,而是越来越小:
# 适用于 Python 3.8+
print(7 // 2, 7 >> 1) # 3 3
print(-7 // 2, -7 >> 1) # -4 -4,两者一致,都是向下取整
print(-1 >> 1) # -1二、优先级:位运算夹在算术和比较之间
Python 官方文档对优先级的表述是:二元位运算的优先级低于所有数值运算(加减乘除等), 高于比较运算;一元~与一元+、-同级。按优先级从低到高排列大致是:
| 优先级(低到高) | 运算符 |
|---|
| 最低 | `x \ |
| ↑ | x ^ y |
| ↑ | x & y |
| ↑ | x << n、x >> n |
| ↑ | +、-(二元) |
| ↑ | *、/、//、% |
| 最高 | ~x、+x、-x(一元) |
这张表能解释一个特别容易踩的写法:因为加减法的优先级高于位移,1 << 2 + 1会被解析为1 << (2 + 1),结果是 8,而不是(1 << 2) + 1 = 5。
比较运算的优先级低于所有位运算,所以flags & MASK == 0会被解析成(flags & MASK) == 0,这通常正是你想要的。但正因为它和很多人的直觉(以及别的语言的 行为)不完全一致,在位运算表达式里主动加括号始终是划算的。
三、位移的两个边界行为
第一,移位数不能为负。文档写明「负的移位数是非法的,会抛出ValueError」。1 << -1和8 >> -1都会直接报错,不会悄悄取绝对值。
第二,Python 3 的整数是任意精度的。左移不会溢出成负数,而是按需变大:
# 适用于 Python 3.8+
print(1 << 70) # 一个很大的整数,Python 3 里没有 32/64 位溢出
# print(1 << -1) # ValueError: negative shift count这一点和 C 语言截然不同:在 C 里左移越界是未定义行为,在 Python 里只是位数变多。 但代价是内存和耗时都会随位数增长,别拿它去做无意义的超大位移。
四、和布尔值的纠缠
bool是int的子类,True相当于 1,False相当于 0。文档说得清楚:对两个布尔 值施加&、|、^,返回的仍是布尔值,效果等同于逻辑的与、或、异或;但逻辑运算 仍应优先使用and、or和!=,而不是位运算符。原因是位运算符不会短路:and、or在能确定结果时会跳过后面的求值,&、|一定会把两边都算完。
还有一个必须知道的现行变更:从 Python 3.12 起,对布尔值使用按位取反~已被弃用, 并将在 Python 3.16 中变成错误。所以~True、~False这类写法现在就不该再写。
# 适用于 Python 3.8+
print(True & False) # False
print(True | False) # True
print(True ^ True) # False
print(True | 2) # 3,一边是 int 就按整数算,返回 int 而不是 bool
# print(~True) # 3.12 起弃用,3.16 起报错,别写五、点破混淆:集合的|/&不是位运算
这是本文最想讲的一节。集合(set)上的|、&、-、^是集合运算,分别表示 并集、交集、差集、对称差集。它们和位运算长得一样,只是因为集合实现了同名的方法 (__or__、__and__、__sub__、__xor__)。运算符符号相同,语义由操作数的类型 决定,两件事不能混为一谈。
# 适用于 Python 3.8+
a = {1, 2, 3}
b = {2, 3, 4}
print(a | b) # {1, 2, 3, 4} 并集,不是「按位或」
print(a & b) # {2, 3} 交集,不是「按位与」
print(a - b) # {1} 差集
print(a ^ b) # {1, 4} 对称差集
# 对照:整数上的同名运算符才是位运算
print(6 | 3) # 7
print(6 & 3) # 2从 Python 3.9 起,字典也获得了|运算符,用来合并两个字典(PEP 584)。这同样不是 位运算,而是字典的合并(d1 | d2返回新字典,d1 |= d2原地更新左操作数)。
# 适用于 Python 3.9+
d1 = {"a": 1}
d2 = {"b": 2}
merged = d1 | d2
print(merged) # {'a': 1, 'b': 2}
print(d1) # {'a': 1},d1 没被改动
d1 |= d2 # 原地合并
print(d1) # {'a': 1, 'b': 2}记住一条:看到|,先看左右是什么类型。整数才是位运算,集合是集合运算, 字典是合并。三者共用符号,但不共用语义。
六、列表在这里是什么角色
列表本身不支持任何位运算符。[1, 2] & [2, 3]会直接抛TypeError,因为列表既 没有定义按位与,也不像集合那样把&解释成交集。要用集合运算,得先转换:
# 适用于 Python 3.8+
x = [1, 2, 3]
y = [2, 3, 4]
common = list(set(x) & set(y)) # 交集,顺序不保证
print(sorted(common)) # [2, 3]
union = list(set(x) | set(y)) # 并集
print(sorted(union)) # [1, 2, 3, 4]
diff = list(set(x) - set(y)) # 差集:在 x 不在 y
print(diff) # [1]转换的代价是O(len(x) + len(y)),而且结果没有顺序(集合不保插入顺序)。如果 既要集合语义又要保序,常规做法是遍历原列表、用一个集合记录已经见过的元素,按首次出现 的顺序把结果攒进新列表,用集合换掉顺序、用额外空间换回顺序。
常见坑点
坑点 1:以为^是乘方。
❌ 错误写法:
# 适用于 Python 3.8+
print(2 ^ 3) # 1,这是异或,不是 2 的 3 次方✅ 正确写法:
# 适用于 Python 3.8+
print(2 ** 3) # 8,乘方用 **
print(2 ^ 3) # 1,异或坑点 2:忘了加减法优先级高于位移。
❌ 错误写法:
# 适用于 Python 3.8+
print(1 << 2 + 1) # 8,被解析成 1 << (2 + 1)✅ 正确写法:
# 适用于 Python 3.8+
print((1 << 2) + 1) # 5,用括号明确意图坑点 3:用负数当移位数。
❌ 错误写法:
# 适用于 Python 3.8+
# print(1 << -1) # ValueError: negative shift count✅ 正确写法:
# 适用于 Python 3.8+
n = -1
if n < 0:
n = 0
print(1 << n) # 1坑点 4:把集合的|当成位运算来解释。
❌ 错误写法:
# 适用于 Python 3.8+
a, b = {1, 2}, {2, 3}
# 以为 a | b 是「按位或」,去解释为什么结果是 {1, 2, 3},越解释越乱✅ 正确写法:
# 适用于 Python 3.8+
a, b = {1, 2}, {2, 3}
print(a | b) # {1, 2, 3},这是并集;位运算只对整数有意义坑点 5:对布尔值用~。
❌ 错误写法:
# 适用于 Python 3.8+
flag = True
# print(~flag) # Python 3.12 起弃用,3.16 起报错✅ 正确写法:
# 适用于 Python 3.8+
flag = True
print(not flag) # False,逻辑取反用 not坑点 6:用&/|代替and/or,丢掉短路。
❌ 错误写法:
# 适用于 Python 3.8+
def check(x):
return x
# 两边都会被求值,check 一定会被调用
# result = cond() & check(value)✅ 正确写法:
# 适用于 Python 3.8+
def cond():
return False
def check(x):
print("被调用了")
return True
result = cond() and check(1) # 短路:cond() 为假,check 不会被调用
print(result) # False坑点 7:以为右移对负数等同于截断除法。
❌ 错误写法:
# 适用于 Python 3.8+
print(int(-7 / 2)) # -3,向零取整✅ 正确写法:
# 适用于 Python 3.8+
print(-7 >> 1) # -4,右移是向下取整除法,和 -7 // 2 一致坑点 8:拿列表直接做位运算。
❌ 错误写法:
# 适用于 Python 3.8+
# [1, 2] & [2, 3] # TypeError,列表不支持 &✅ 正确写法:
# 适用于 Python 3.8+
print(list(set([1, 2]) & set([2, 3]))) # [2],先转成集合总结
| 记不住的写法 | 正确认知 |
|---|
2 ^ 3 | 异或,得 1;乘方是2 ** 3 |
1 << 2 + 1 | 得 8,加减比位移优先 |
~True | 3.12 起弃用,3.16 起报错 |
| `{1,2} \ | {2,3}` |
| `{'a':1} \ | {'b':2}` |
[1] & [2] | 列表不支持,先set() |
-7 >> 1 | 得-4,向下取整除法 |
1 << -1 | ValueError,移位数不能为负 |
位运算本身语义很干净:按无限位补码算、优先级夹在算术和比较之间、移位数不能为负。 真正容易让人栽跟头的是同一个符号被多种类型借用——整数是位运算,集合是集合运算, 字典是合并。写代码时看到|和&,先确认操作数类型,比背优先级表更管用。