在硬件层面,晶体管只有开和关两种状态,我们通常用1和0来表示。
这就是binary digits,简称bits。
对于使用底层语言的系统程序员来说,了解bits和binary尤其重要。
然而,在计算技术的大部分历史中,编程的努力方向一直是寻找越来越精巧的方法,来抽象掉这种二进制的现实。
在 Python(以及许多其他高级编程语言)中,我们使用int、float、string以及其他定义好的类型,甚至还包括音频和视频格式。
我们让 Python 内部(最终)负责把一切转换成比特。
不过,使用位运算符和位运算,有时能在速度和内存效率上带来显著优势,即使是在 Python 这样的高级语言中也是如此。
不出所料,Python 与用户交互时使用的是十进制数,但程序员可以覆盖这一默认设置。
事实上,Python 可以轻松接受binary、hexadecimal或octal格式的int,并乐意在它们之间进行数学运算。
想了解更多细节,可以复习binary-octal-hexadecimal这个概念。
二进制数以0b前缀输入,就像0x可用于十六进制(hex 数是一种简洁地表示 4 个比特一组的方式),而oct可用于八进制数。
把整数转换成二进制字符串有多种方式,区别在于是否包含0b前缀,以及是否支持用零进行左填充:
# Binary entry.
>>> 0b10111
23
# Converting an int display to binary string, with prefix.
>>> bin(23)
'0b10111'
>>> number = 23
# Binary without prefix, padded to 8 digits.
>>> format(number, '08b')
'00010111'
# Same format, but using an f-string.
>>> f"{number} in decimal is {number:08b} in binary and {number:x} in hex"
'23 in decimal is 00010111 in binary and 17 in hex'
Bitwise Logic在bools概念中,我们讨论过用于布尔值(True和False)的逻辑运算符and、or和not。
处理比特时,适用的逻辑规则是一样的。
不过,逻辑运算符&(and)、|(or)、~(not)和^(XOR)对应的位运算版本,会作用于二进制表示中的每一个比特,把1当作True(“开”),把0当作False(“关”)。
用一个位运算&的例子来说明会更清楚:
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x & y, '08b')
'00100010'
只有当两个输入数字在某一位上_都_是1时,输出中这一位才会被设为1。
位运算&常用来在一组压缩存储的True/False值中分离出单个比特,比如应用里用户可配置的设置。
这样就可以用单个比特的值来控制程序逻辑:
>>> number = 0b0110
>>> number & 0b0001 > 0
False
>>> number & 0b0010 > 0
True
对于位运算|(or),只要两个输入中_任意一个_在某一位上是1,输出中这一位就会被设为1:
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x | y, '08b')
'01101110'
^运算符表示位运算异或(xor),只有当某一位在输入中_恰好一个为1、而不是两个都_为1时,输出中这一位才会被设为1。
这个符号你可能在sets概念中见过,那里它用于set的对称差,这与应用于集合的 xor是一回事。
如果 xor 的^看起来很奇怪,那么要知道,它至今仍是密码学中最常见的运算。
>>> x = 0b01100110
>>> y = 0b00101010
>>> format(x ^ y, '08b')
'01001100'
最后是~运算符(也就是波浪号字符),它是一种位运算not,只接受一个输入,并_反转所有比特_,得到的结果可能出乎你的意料!
表示中的每一个1都会变成0,反之亦然。
详情见下一节。
在十进制表示中,我们用数字左边的+或-号来区分正数和负数。
但在二进制层面使用这些符号,对数字计算来说效率低下,还带来了+0与-0并不相同的问题。
现代计算机没有使用-和+,而是用two's complement表示法来表示负数,一直到硅芯片层面都是如此。
这意味着所有比特都会被反转,如果最左边的一位(也称为“最高有效位”,即 MSB)是1,这个数就_会被解释为负数_。
正数的最高有效位是0。
这种表示法的优点在于零只有一种形式,程序员不必再去处理-0和+0。
这种表示正负数的方式给 Python 带来了一个麻烦:核心语言内部没有int32或int64这类有限整数概念。
在“现代” Python 中,int的大小不受限制(只受硬件容量限制),一个负数或按位取反后的数,其左边有(理论上)无穷多个1,就像正数有无穷多个0一样。
这使得很难给出一个有实际意义的bitwise not示例:
>>> x = 0b01100110
>>> format(x, '08b')
'01100110'
# This is a negative binary (not twos-complement display).
>>> format(~x, '08b')
'-1100111'
# Decimal representation.
>>> x
102
# Using the Bitwise not, with an unintuitive result.
>>> ~x
-103
这并不是我们在使用固定大小整数的语言中会看到的0b10011001。
~运算符只有在_无符号_字节或整数类型上,或者在使用固定大小整数类型时,才能按预期工作。
这些数值类型在NumPy、pandas和sympy等第三方包中受支持,但核心 Python 中不支持。
实践中,Python 程序员经常只用正数来使用&、|、^以及下面介绍的移位运算符。
对负数进行位运算要少见得多。
一种技巧是给负值加上2**32 (or 1 << 32)来把int变成无符号数,但这很难管理。
另一种策略是使用ctypes模块,采用 C 风格的整数类型,但这同样笨拙。
Shift operators左移运算符x << y会把x中的所有比特向左移动y位,空出的位置用零填充。
注意,这在算术上等同于把一个数乘以(2**y)。
右移运算符x >> y则相反。
这在算术上等同于整数除法x // (2**y)。
在 Python 中对负数移位时,请记住上一节关于负数及其坑的内容。
>>> x = 8
>>> format(x, '08b')
'00001000'
# A left bit shift.
>>> x << 2
32
>>> format(x << 2, '08b')
'00100000'
# A right bit shift.
>>> format(x >> 2, '08b')
'00000010'