从零开始学python | Python 中的按位运算符 I
目录
计算机将各种信息存储为称为位的二进制数字流。无论您是处理文本、图像还是视频,它们都归结为 1 和 0。Python 的按位运算符让您可以在最细粒度的级别上操作这些单独的数据位。
您可以使用按位运算符来实现压缩、加密和错误检测等算法,以及控制Raspberry Pi 项目或其他地方的物理设备。通常,Python 将您与具有高级抽象的底层位隔离开来。在实践中,您更有可能发现按位运算符的重载风格。但是,当您以它们的原始形式与它们合作时,您会惊讶于它们的怪癖!
在本教程中,您将学习如何:
- 使用 Python按位运算符操作单个位
- 以与平台无关的方式读取和写入二进制数据
- 使用位掩码将信息打包到单个字节上
- 在自定义数据类型中重载Python 按位运算符
- 在数字图像中隐藏秘密信息
Python 的按位运算符概述
Python 附带了几种不同类型的运算符,例如算术、逻辑和比较运算符。您可以将它们视为利用更紧凑的前缀和中缀语法的函数。
注意: Python 不包括后缀运算符,如C 中可用的增量 ( i++
) 或减量 ( i--
) 运算符。
按位运算符在不同的编程语言中看起来几乎相同:
操作符 | 例子 | 意义 |
---|---|---|
& |
a & b |
按位与 |
| |
a | b |
按位或 |
^ |
a ^ b |
按位异或(异或) |
~ |
~a |
按位非 |
<< |
a << n |
按位左移 |
>> |
a >> n |
按位右移 |
如您所见,它们用看起来很奇怪的符号而不是单词来表示。这使它们在 Python 中脱颖而出,比您可能习惯看到的要少一些冗长。您可能无法仅通过查看它们来弄清楚它们的含义。
注意:如果您来自另一种编程语言,例如Java,那么您会立即注意到 Python 缺少由三个大于号 ( )表示的无符号右移运算符>>>
。
这与 Python 在内部如何表示整数有关。由于 Python 中的整数可以有无数位,因此符号位没有固定位置。事实上,Python 中根本没有符号位!
大多数按位运算符是binary,这意味着它们期望使用两个操作数,通常称为左操作数和右操作数。按位非 ( ~
) 是唯一的一元按位运算符,因为它只需要一个操作数。
所有二元按位运算符都有一个相应的复合运算符来执行扩充赋值:
操作符 | 例子 | 相当于 |
---|---|---|
&= |
a &= b |
a = a & b |
|= |
a |= b |
a = a | b |
^= |
a ^= b |
a = a ^ b |
<<= |
a <<= n |
a = a << n |
>>= |
a >>= n |
a = a >> n |
这些是用于更新左操作数的速记符号。
这就是 Python 的按位运算符语法的全部内容!现在,您已准备好仔细查看每个运算符,以了解它们最有用的地方以及如何使用它们。首先,在查看两类按位运算符之前,您将快速复习一下二进制系统:按位逻辑运算符和按位移位运算符。
五分钟了解二进制系统
在继续之前,花点时间复习一下二进制系统的知识,这对于理解按位运算符至关重要。如果您已经对它感到满意,请继续并跳转到下面的位逻辑运算符部分。
为什么使用二进制?
有无数种方法来表示数字。自古以来,人们就发展出不同的符号,如罗马数字和埃及象形文字。大多数现代文明都使用位置符号,它高效、灵活,非常适合进行算术运算。
任何位置系统的一个显着特征是它的基数,它表示可用的位数。人们自然有利于基地十数字系统,也被称为十进制系统,因为它与手指计数起着很好。
另一方面,计算机将数据视为一组以二为基数的数字系统(通常称为二进制系统)表示的数字。此类数字仅由两位数字组成,即零和一。
注意:在数学书籍中,数字文字的基数通常用略低于基线的下标表示,例如 42 10。
例如,二进制数 10011100 2相当于十进制中的 156 10。因为十进制中有十个数字——从零到九——用十进制写出相同的数字通常比用二进制写出的数字少。
注意:您不能仅通过查看给定数字的数字来判断数字系统。
例如,十进制数 101 10恰好只使用二进制数字。但它代表的值与其二进制对应物 101 2完全不同,后者相当于 5 10。
二进制系统比十进制系统需要更多的存储空间,但在硬件中实现起来要简单得多。虽然您需要更多的积木,但它们更容易制作,而且种类较少。这就像将您的代码分解为更模块化和可重用的部分。
然而,更重要的是,二进制系统非常适合将数字转换为不同电压电平的电子设备。由于电压喜欢因各种噪声而上下漂移,因此您需要在连续电压之间保持足够的距离。否则,信号可能会失真。
通过仅采用两种状态,您可以使系统更加可靠和抗噪。或者,您可以升高电压,但这也会增加功耗,这是您绝对想要避免的。
二进制如何工作?
想象一下,您只有两个手指可以依靠。你可以数一个零、一个一和一个二。但是当你用完手指时,你需要记下你已经数到 2 的次数,然后重新开始,直到再次数到 2:
十进制 | 手指 | 八人制 | 四人制 | 二人组 | 那些 | 二进制 |
---|---|---|---|---|---|---|
010 | ✊ | 0 | 0 | 0 | 0 | 02 |
110 | ☝️ | 0 | 0 | 0 | 1 | 12 |
210 | ✌️ | 0 | 0 | 1 | 0 | 102 |
310 | ✌️+☝️ | 0 | 0 | 1 | 1 | 112 |
410 | ✌️✌️ | 0 | 1 | 0 | 0 | 1002 |
510 | ✌️✌️+☝️ | 0 | 1 | 0 | 1 | 1012 |
610 | ✌️✌️+✌️ | 0 | 1 | 1 | 0 | 1102 |
710 | ✌️✌️+✌️+☝️ | 0 | 1 | 1 | 1 | 1112 |
810 | ✌️✌️✌️✌️ | 1 | 0 | 0 | 0 | 10002 |
910 | ✌️✌️✌️✌️+☝️ | 1 | 0 | 0 | 1 | 10012 |
1010 | ✌️✌️✌️✌️+✌️ | 1 | 0 | 1 | 0 | 10102 |
1110 | ✌️✌️✌️✌️+✌️+☝️ | 1 | 0 | 1 | 1 | 10112 |
1210 | ✌️✌️✌️✌️+✌️✌️ | 1 | 1 | 0 | 0 | 11002 |
1310 | ✌️✌️✌️✌️+✌️✌️+☝️ | 1 | 1 | 0 | 1 | 11012 |
每次你写下另一对手指时,你还需要按 2 的幂对它们进行分组,这是系统的基础。例如,要数到 13,您必须同时用两个手指六次,然后再用一根手指。你的手指可以排列成一八、一四、一一。
这些 2 的幂对应于二进制数中的数字位置,并准确地告诉您要打开哪些位。它们从右到左增长,从最低有效位开始,这决定了数字是偶数还是奇数。
位置符号就像汽车中的里程表:一旦某个特定位置的数字达到其最大值(二进制系统中的 1),它就会滚动到零,然后向左移动。如果数字左侧已经有一些,这可能会产生级联效果。
计算机如何使用二进制
现在你知道了双星系统的基本原理和为什么计算机使用它,你就可以学会如何,他们代表了它的数据。
在以数字形式复制任何信息之前,您必须将其分解为数字,然后将它们转换为二进制系统。例如,纯文本可以被认为是一串字符。您可以为每个字符分配一个任意数字或选择现有的字符编码,例如ASCII、ISO-8859-1或UTF-8。
在 Python 中,字符串表示为Unicode代码点数组。要显示它们的顺序值,请调用ord()
每个字符:
>>> [ord(character) for character in "€uro"]
[8364, 117, 114, 111]
生成的数字唯一标识 Unicode 空间中的文本字符,但它们以十进制形式显示。你想用二进制数字重写它们:
特点 | 十进制码点 | 二进制代码点 |
---|---|---|
€ | 836410 | 100000101011002 |
u | 11710 | 11101012 |
r | 11410 | 11100102 |
o | 11110 | 11011112 |
请注意,位长度(二进制数字的数量)在字符之间变化很大。欧元符号 ( €
) 需要 14 位,而其余字符可以轻松容纳 7 位。
注意:以下是在 Python 中检查任何整数的位长的方法:
>>> (42).bit_length()
6
如果数字周围没有一对括号,它将被视为带有小数点的浮点文字。
可变位长是有问题的。例如,如果您将这些二进制数并排放置在光盘上,那么您最终会得到一长串没有字符之间明确界限的位流:
100000101011001110101111001011011112
了解如何解释此信息的一种方法是为所有字符指定固定长度的位模式。在现代计算中,最小的信息单位称为八位字节或字节,由八位组成,可以存储 256 个不同的值。
您可以用前导零填充二进制代码点以用字节表示它们:
特点 | 十进制码点 | 二进制代码点 |
---|---|---|
€ | 836410 | 00100000 101011002 |
u | 11710 | 00000000 011101012 |
r | 11410 | 00000000 011100102 |
o | 11110 | 00000000 011011112 |
现在每个字符需要两个字节,或 16 位。总的来说,您的原始文本大小几乎翻了一番,但至少它的编码可靠。
您可以使用霍夫曼编码为特定文本中的每个字符找到明确的位模式,或者使用更合适的字符编码。例如,为了节省空间,UTF-8 特意使用拉丁字母而不是您不太可能在英文文本中找到的符号:
>>> len("€uro".encode("utf-8"))
6
根据 UTF-8 标准编码,整个文本占用六个字节。由于 UTF-8 是 ASCII 的超集,因此字母u
、r
和o
各占一个字节,而欧元符号在此编码中占用三个字节:
>>> for char in "€uro":
... print(char, len(char.encode("utf-8")))
...
€ 3
u 1
r 1
o 1
其他类型的信息可以与文本类似地数字化。光栅图像由像素组成,每个像素都有将颜色强度表示为数字的通道。声音波形包含与给定采样间隔的气压相对应的数字。三维模型是根据由其顶点定义的几何形状构建的,等等。
归根结底,一切都是数字。
按位逻辑运算符
您可以使用按位运算符对单个位执行布尔逻辑。这是类似于使用逻辑运算符,如and
,or
和not
,但有点水平。按位运算符和逻辑运算符之间的相似之处不止于此。
可以使用按位运算符而不是逻辑运算符来计算布尔表达式,但通常不鼓励这种过度使用。如果您对详细信息感兴趣,则可以展开下面的框以了解更多信息。
使用位运算符计算布尔表达式显示隐藏
除非您有充分的理由并且知道自己在做什么,否则您应该仅将按位运算符用于控制位。否则很容易出错。在大多数情况下,您需要将整数作为参数传递给按位运算符。
按位与
按位与运算符 ( &
)对其操作数的相应位执行逻辑与操作。对于在两个数字中占据相同位置的每一对位,只有当两个位都打开时,它才返回一个:
结果位模式是运算符参数的交集。它在两个操作数都是 1 的位置打开了两个位。在所有其他地方,至少有一个输入具有零位。
在算术上,这相当于两个位值的乘积。您可以通过在每个索引i处乘以它们的位来计算数字a和b的按位与:
这是一个具体的例子:
表达 | 二进制值 | 十进制值 |
---|---|---|
a |
100111002 | 15610 |
b |
1101002 | 5210 |
a & b |
101002 | 2010 |
一乘以一给出一,但任何乘以零的结果总是为零。或者,您可以取每对中两位中的最小值。请注意,当操作数的位长不相等时,较短的操作数会自动在左侧填充零。
按位或
按位 OR 运算符 ( |
) 执行逻辑分离。对于每一对对应的位,如果其中至少一个被打开,它会返回一个:
结果位模式是运算符参数的联合。它打开了五个位,其中任何一个操作数都有一个 1。只有两个零的组合在最终输出中才会给出零。
它背后的算术是位值的总和和乘积的组合。要计算数字a和b的按位或,您需要将以下公式应用于它们在每个索引i处的位:
这是一个切实的例子:
表达 | 二进制值 | 十进制值 |
---|---|---|
a |
100111002 | 15610 |
b |
1101002 | 5210 |
a | b |
101111002 | 18810 |
它几乎就像两位的总和,但被限制在较高端,因此它永远不会超过 1 的值。您还可以取每对中两位的最大值来获得相同的结果。
按位异或
与按位AND、OR和NOT 不同,按位异或运算符 ( ^
) 在 Python 中没有逻辑对应物。但是,您可以通过在现有运算符之上构建来模拟它:
def xor(a, b):
return (a and not b) or (not a and b)
它评估两个相互排斥的条件,并告诉您是否正好满足其中之一。例如,一个人可以是未成年人也可以是成年人,但不能同时是两者。相反,一个人不可能既不是未成年人也不是成年人。选择是强制性的。
名称 XOR 代表“异或”,因为它对位对执行异或。换句话说,每个位对必须包含相反的位值才能产生一个:
从视觉上看,这是运算符参数的对称差异。结果中打开了三个位,其中两个数字具有不同的位值。其余位置的位相互抵消,因为它们是相同的。
与按位 OR 运算符类似,XOR 的算术涉及求和。然而,虽然按位 OR 将值钳位为 1,但 XOR 运算符使用求和模 2将它们包裹起来:
模数是两个数字的函数——被除数和除数——执行除法并返回其余数。在 Python 中,有一个用百分号 ( )表示的内置模运算符%
。
再一次,您可以通过查看示例来确认公式:
表达 | 二进制值 | 十进制值 |
---|---|---|
a |
100111002 | 15610 |
b |
1101002 | 5210 |
a ^ b |
101010002 | 16810 |
两个 0 或两个 1 的总和除以 2 会产生一个整数,因此结果的余数为零。但是,当您将两个不同位值的总和除以2 时,您会得到一个余数为 1 的分数。XOR 运算符的一个更直接的公式是每对中两个位的最大值和最小值之间的差。
按位非
最后一个按位逻辑运算符是按位非运算符 ( ~
),它只需要一个参数,使其成为唯一的一元按位运算符。它通过翻转所有位对给定数字执行逻辑否定:
反转位是 1 的补码,它将零变成 1,将 1 变成零。它可以在算术上表示为从 1 中减去单个位值:
这是一个示例,显示了之前使用的数字之一:
表达 | 二进制值 | 十进制值 |
---|---|---|
a |
100111002 | 15610 |
~a |
11000112 | 9910 |
虽然按位 NOT 运算符似乎是所有运算符中最直接的,但在 Python 中使用它时需要格外小心。到目前为止,您阅读的所有内容都基于数字用无符号整数表示的假设。
注意:无符号数据类型不允许您存储负数,例如 -273,因为常规位模式中没有用于符号的空间。尝试这样做会导致编译错误、运行时异常或整数溢出,具体取决于所使用的语言。
尽管有多种方法可以模拟无符号整数,但 Python 本身并不支持它们。这意味着无论您是否指定一个数字,所有数字都会附加一个隐式符号。这显示当您对任何数字进行按位 NOT 操作时:
>>> ~156
-157
您得到的不是预期的 99 10,而是负值!一旦您了解了各种二进制数表示法,原因就会变得很清楚。目前,快速修复解决方案是利用按位 AND 运算符:
>>> ~156 & 255
99
这是位掩码的一个完美示例,您将在接下来的部分中对其进行探讨。
按位移位运算符
按位移位运算符是另一种位操作工具。它们让您可以移动位,这对于稍后创建位掩码非常方便。过去,它们经常被用来提高某些数学运算的速度。
左移
按位左移运算符 ( <<
) 将其第一个操作数的位向左移动其第二个操作数中指定的位数。它还负责插入足够的零位以填充新位模式右边缘出现的间隙:
将单个位向左移动一个位置会使其值加倍。例如,该位将指示移位后的 4,而不是 2。将它向左移动两个位置将使结果值翻四倍。当您将给定数字中的所有位相加时,您会注意到它也会随着每个位置的移动而加倍:
表达 | 二进制值 | 十进制值 |
---|---|---|
a |
1001112 | 3910 |
a << 1 |
10011102 | 7810 |
a << 2 |
100111002 | 15610 |
a << 3 |
1001110002 | 31210 |
通常,向左移动位对应于将数字乘以2的幂,指数等于移动的位数:
左移曾经是一种流行的优化技术,因为位移是一条指令,并且比指数或乘积的计算成本更低。然而,今天,编译器和解释器,包括 Python 的,非常有能力在幕后优化你的代码。
注意:不要在 Python 中使用位移运算符作为过早优化的手段。您不会看到执行速度的差异,但绝对会降低代码的可读性。
在纸面上,左移产生的位模式会随着您移位的位置而变长。由于 Python 处理整数的方式,一般来说这对 Python 也是如此。但是,在大多数实际情况下,您需要将位模式的长度限制为 8 的倍数,这是标准字节长度。
例如,如果您正在处理单个字节,则将其向左移动应该丢弃超出其左边界的所有位:
这有点像通过固定长度的窗口查看无限的比特流。有一些技巧可以让你在 Python 中做到这一点。例如,您可以使用按位 AND 运算符应用位掩码:
>>> 39 << 3
312
>>> (39 << 3) & 255
56
将 39 10向左移动三位会返回一个高于您可以存储在单个字节上的最大值的数字。它需要九位,而一个字节只有八位。要砍掉左边的一个额外位,您可以应用具有适当值的位掩码。如果您想保留更多或更少的位,则需要相应地修改掩码值。
右移
按位右移运算符 ( >>
) 类似于左移运算符,但不是将位向左移动,而是将它们向右推指定的位数。最右边的位总是被丢弃:
每次向右移动一个位置,它的潜在价值就会减半。将相同的位向右移动两位会产生原始值的四分之一,依此类推。当您将所有单个位相加时,您会看到相同的规则适用于它们所代表的数字:
表达 | 二进制值 | 十进制值 |
---|---|---|
a |
100111012 | 15710 |
a >> 1 |
10011102 | 7810 |
a >> 2 |
1001112 | 3910 |
a >> 3 |
100112 | 1910 |
将诸如 157 10 之类的奇数减半会产生一个分数。为了摆脱它,自动向右移位运算符楼层的结果。它实际上与按 2 的幂进行的楼层划分相同:
同样,指数对应于向右移动的位数。在 Python 中,您可以利用专用运算符来执行楼层划分:
>>> 5 >> 1 # Bitwise right shift
2
>>> 5 // 2 # Floor division (integer division)
2
>>> 5 / 2 # Floating-point division
2.5
按位右移运算符和地板除法运算符的工作方式相同,即使对于负数也是如此。但是,地板除法让您可以选择任何除数,而不仅仅是 2 的幂。使用按位右移是提高某些算术除法性能的常用方法。
注意:您可能想知道当您用完要移位的位时会发生什么。例如,当您尝试推入比数字中位数更多的位置时:
>>> 2 >> 5
0
一旦没有更多位打开,您就会被困在零值上。零除以任何东西将始终返回零。但是,当您右移负数时,事情会变得更加棘手,因为隐式符号位会妨碍您:
>>> -2 >> 5
-1
经验法则是,无论符号如何,结果都将与以 2 的某个幂进行地板除法相同。一个小的负分数的底总是负一,这就是你会得到的。继续阅读以获得更详细的解释。
就像左移运算符一样,位模式在右移后改变其大小。虽然向右移动位会使二进制序列更短,但这通常无关紧要,因为您可以在不更改值的情况下在位序列前面放置任意数量的零。例如, 101 2与 0101 2相同,00000101 2也是如此,前提是您处理的是非负数。
有时您会希望在右移后保留给定的位长以将其与另一个值对齐或适合某处。您可以通过应用位掩码来做到这一点:
它只雕刻出您感兴趣的那些位,并在必要时用前导零填充位模式。
Python 中负数的处理与传统的按位移位方法略有不同。在下一节中,您将更详细地研究这一点。
算术与逻辑移位
您可以将按位移位运算符进一步分类为算术和逻辑移位运算符。虽然 Python 只允许您进行算术移位,但了解其他编程语言如何实现按位移位运算符以避免混淆和意外是值得的。
这种区别来自他们处理符号位的方式,符号位通常位于有符号二进制序列的最左边缘。实际上,它仅与右移运算符相关,这会导致数字翻转其符号,从而导致整数溢出。
注意: 例如,Java和JavaScript使用附加的大于号 ( >>>
)来区分逻辑右移运算符。由于左移运算符在两种移位中的行为一致,因此这些语言没有定义逻辑左移对应物。
通常,打开的符号位表示负数,这有助于保持二进制序列的算术特性:
十进制值 | 有符号二进制值 | 符号位 | 符号 | 意义 |
---|---|---|---|---|
-10010 | 100111002 | 1 | - | 负数 |
2810 | 000111002 | 0 | + | 正数或零 |
从左边看这两个二进制序列,您可以看到它们的第一位携带符号信息,而其余部分由幅度位组成,这两个数字相同。
注意:具体的十进制值将取决于您决定如何以二进制表示有符号数。它因语言而异,在 Python 中变得更加复杂,因此您可以暂时忽略它。一旦你进入下面的二进制数表示部分,你就会有一个更好的画面。
逻辑右移,也称为无符号右移或零填充右移,移动整个二进制序列,包括符号位,并用零填充左边的结果间隙:
请注意有关数字符号的信息是如何丢失的。不管原始符号如何,它总是会产生一个非负整数,因为符号位被零替换。只要您对数值不感兴趣,逻辑右移在处理低级二进制数据时就很有用。
但是,由于在大多数语言中,有符号二进制数通常存储在固定长度的位序列上,因此它可以使结果环绕极值。您可以在交互式 Java Shell 工具中看到这一点:
jshell> -100 >>> 1
$1 ==> 2147483598
结果数字的符号从负变为正,但它也会溢出,最终非常接近 Java 的最大整数:
jshell> Integer.MAX_VALUE
$2 ==> 2147483647
这个数字乍一看似乎是任意的,但它与 Java 为Integer
数据类型分配的位数直接相关:
jshell> Integer.toBinaryString(-100)
$3 ==> "11111111111111111111111110011100"
它采用32位来存储有符号整数的二进制补码表示。去掉符号位后,剩下 31 位,其最大十进制值等于 2 31 - 1 或 2147483647 10。
另一方面,Python 存储整数就好像有无数位可供您使用一样。因此,在纯 Python 中无法很好地定义逻辑右移运算符,因此语言中缺少它。不过,您仍然可以模拟它。
这样做的一种方法是利用通过内置模块公开的C中可用的无符号数据类型ctypes
:
>>> from ctypes import c_uint32 as unsigned_int32
>>> unsigned_int32(-100).value >> 1
2147483598
它们让您传入一个负数,但不给符号位附加任何特殊含义。它被视为其余的幅度位。
虽然 C 中只有几种预定义的无符号整数类型,它们的位长不同,但您可以在 Python 中创建一个自定义函数来处理任意位长:
>>> def logical_rshift(signed_integer, places, num_bits=32):
... unsigned_integer = signed_integer % (1 << num_bits)
... return unsigned_integer >> places
...
>>> logical_rshift(-100, 1)
2147483598
这会将有符号位序列转换为无符号位序列,然后执行常规算术右移。
但是,由于 Python 中的位序列长度不固定,因此它们实际上没有符号位。此外,它们不像在 C 或 Java 中那样使用传统的二进制补码表示。为了缓解这种情况,您可以利用模运算,这将保留正整数的原始位模式,同时适当地环绕负整数。
算术右移 ( >>
),有时称为有符号右移运算符,通过在向右移动位之前复制其符号位来保持数字的符号:
换句话说,它用符号位填充左边的空白。结合有符号二进制的二进制补码表示,这会产生算术上正确的值。无论数字是正数还是负数,算术右移都相当于地板除法。
正如您即将发现的那样,Python 并不总是以简单的二进制补码形式存储整数。相反,它遵循自定义自适应策略,其工作方式类似于具有无限位数的符号大小。它在数字的内部表示和二进制补码之间来回转换数字,以模仿算术移位的标准行为。
二进制数表示
在使用按位求反 ( ~
) 和右移运算符 ( >>
)时,您已经亲身体验到 Python 中缺少无符号数据类型。您已经看到有关在 Python 中存储整数的不寻常方法的提示,这使得处理负数变得棘手。要有效地使用按位运算符,您需要了解二进制数字的各种表示形式。
无符号整数
在 C 等编程语言中,您可以选择是使用给定数字类型的有符号还是无符号风格。当您确定永远不需要处理负数时,无符号数据类型更合适。通过分配一个额外的位(否则将用作符号位),您实际上可以将可用值的范围扩大一倍。
通过在溢出发生之前增加最大限制,它还可以使事情变得更安全。但是,溢出只发生在固定位长的情况下,因此它们与没有此类约束的 Python 无关。
在 Python 中体验无符号数字类型的最快方法是使用前面提到的ctypes
模块:
>>> from ctypes import c_uint8 as unsigned_byte
>>> unsigned_byte(-42).value
214
由于此类整数中没有符号位,因此它们的所有位都表示数字的大小。传递负数会强制 Python 重新解释位模式,就好像它只有幅度位一样。
有符号整数
一个数的符号只有两种状态。如果您暂时忽略零,那么它可以是正数或负数,这很好地转换为二进制系统。然而,有几种替代方法可以用二进制表示有符号整数,每种方法都有其优点和缺点。
可能最直接的一个是sign-magnitude,它自然地建立在无符号整数之上。当二进制序列被解释为符号大小时,最高有效位扮演符号位的角色,而其余位的工作方式与往常相同:
二进制序列 | 符号幅度值 | 无符号值 |
---|---|---|
001010102 | 4210 | 4210 |
101010102 | -4210 | 17010 |
最左边位的零表示正 ( +
) 数,一表示负 ( -
) 数。请注意,符号位对符号幅度表示中的数字绝对值没有贡献。它的存在只是为了让您翻转剩余位的符号。
为什么是最左边的一点?
它使位索引保持完整,这反过来又有助于保持用于计算二进制序列十进制值的位权重的向后兼容性。然而,并不是关于符号幅度的一切都那么好。
注意:有符号整数的二进制表示只对固定长度的位序列有意义。否则,您无法分辨符号位在哪里。然而,在 Python 中,你可以用任意多的位来表示整数:
>>> f"{-5 & 0b1111:04b}"
'1011'
>>> f"{-5 & 0b11111111:08b}"
'11111011'
无论是四位还是八位,符号位总是会出现在最左边的位置。
可以在符号大小位模式中存储的值范围是对称的。但这也意味着你最终有两种表达零的方式:
二进制序列 | 符号幅度值 | 无符号值 |
---|---|---|
000000002 | +010 | 010 |
100000002 | -010 | 12810 |
零在技术上没有符号,但没有办法不在符号大小中包含一个。虽然在大多数情况下有一个模糊的零并不理想,但这并不是故事中最糟糕的部分。这种方法的最大缺点是繁琐的二进制算术。
当您将标准二进制算术应用于以符号大小存储的数字时,它可能不会给您预期的结果。例如,将两个大小相同但符号相反的数字相加不会使它们相消:
表达 | 二进制序列 | 符号幅度值 |
---|---|---|
a |
001010102 | 4210 |
b |
101010102 | -4210 |
a + b |
110101002 | -8410 |
42 和 -42 的总和不会产生零。此外,残留比特有时可以从大小传播到符号位,反转符号并产生了意想不到的结果。
为了解决这些问题,一些早期的计算机采用了补码表示。这个想法是改变十进制数映射到特定二进制序列的方式,以便它们可以正确相加。要更深入地了解一个人的补语,您可以展开以下部分。
尽管如此,现代计算机不使用补码来表示整数,因为有一种更好的方法称为二进制补码。通过应用一个小的修改,您可以一次性消除双零并简化二进制算术。要更详细地探索二进制补码,您可以展开以下部分。
补码显示隐藏
有符号数表示还有其他一些变体,但它们并不那么受欢迎。
浮点数字
在IEEE 754标准定义了由所述的实数的二进制表示符号,指数和尾数位。在不涉及太多技术细节的情况下,您可以将其视为二进制数的科学记数法。小数点“浮动”以容纳不同数量的有效数字,但它是一个二进制小数点。
广泛支持符合该标准的两种数据类型:
- 单精度: 1 个符号位、8 个指数位、23 个尾数位
- 双精度: 1 个符号位、11 个指数位、52 个尾数位
Python 的float
数据类型相当于双精度类型。请注意,某些应用程序需要更多或更少的位。例如,OpenEXR 图像格式利用半精度以合理的文件大小表示具有高动态颜色范围的像素。
数字 Pi (π) 在四舍五入到小数点后五位时具有以下单精度二进制表示:
符号 | 指数 | 尾数 |
---|---|---|
02 | 100000002 | .100100100001111110100002 |
符号位的工作方式与整数类似,因此零表示正数。然而,对于指数和尾数,可以根据一些边缘情况应用不同的规则。
首先,您需要将它们从二进制转换为十进制形式:
- 指数: 128 10
- 尾数: 2 -1 + 2 -4 + … + 2 -19 = 299261 10 /524288 10 ≈ 0.570795 10
指数存储为无符号整数,但为了说明负值,它通常在单精度下具有等于 127 10的偏差。您需要减去它以恢复实际指数。
尾数位代表一个分数,因此它们对应于 2 的负幂。此外,您需要向尾数添加一个,因为在这种特殊情况下,它假定小数点之前有一个隐式前导位。
综上所述,您可以得出以下公式将浮点二进制数转换为十进制数:
当您用变量替换上例中的实际值时,您将能够破译以单精度存储的浮点数的位模式:
就是这样,假设 Pi 已四舍五入到小数点后五位。稍后您将学习如何以二进制显示这些数字。
定点数
虽然浮点数非常适合工程用途,但由于精度有限,它们在货币计算中失败。例如,一些以十进制表示法具有有限表示的数字在二进制中只有无限表示。这通常会导致舍入误差,随着时间的推移会累积:
>>> 0.1 + 0.2
0.30000000000000004
在这种情况下,最好使用 Python 的decimal
模块,该模块实现定点算术并允许您指定在给定位长度上放置小数点的位置。例如,您可以告诉它要保留多少位数字:
>>> from decimal import Decimal, localcontext
>>> with localcontext() as context:
... context.prec = 5 # Number of digits
... print(Decimal("123.456") * 1)
...
123.46
但是,它包括所有数字,而不仅仅是小数。
注意:如果您正在处理有理数,那么您可能有兴趣查看fractions
模块,它是 Python 标准库的一部分。
如果您不能或不想使用定点数据类型,可靠地存储货币值的一种直接方法是将金额缩放到最小单位,例如美分,并用整数表示它们。
Python中的整数
在过去的编程时代,计算机内存非常宝贵。因此,语言可以让您非常精细地控制为数据分配多少字节。让我们快速浏览一下 C 中的一些整数类型作为示例:
类型 | 尺寸 | 最小值 | 最大值 |
---|---|---|---|
char |
1 字节 | -128 | 127 |
short |
2 字节 | -32,768 | 32,767 |
int |
4字节 | -2,147,483,648 | 2,147,483,647 |
long |
8 字节 | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
这些值可能因平台而异。但是,如此丰富的数字类型允许您在内存中紧凑地排列数据。请记住,这些甚至不包括无符号类型!
另一方面是诸如 JavaScript 之类的语言,它们只有一种数字类型来统治它们。虽然这对初学者来说不太容易混淆,但代价是增加了内存消耗、降低了处理效率和降低了精度。
在谈论按位运算符时,必须了解 Python 如何处理整数。毕竟,您将主要使用这些运算符来处理整数。Python 中有几种完全不同的整数表示形式,它们取决于它们的值。
实习整数
在CPython中,在-5非常小的整数10和256 10被拘留在全局高速缓存来获得一些性能,因为在该范围内被广泛使用。在实践中,无论何时引用这些值之一,它们是在解释器启动时创建的单例,Python 将始终提供相同的实例:
>>> a = 256
>>> b = 256
>>> a is b
True
>>> print(id(a), id(b), sep="\n")
94050914330336
94050914330336
这两个变量具有相同的标识,因为它们引用内存中完全相同的对象。这是典型的引用类型,但不是不可变的值,例如整数。但是,当超出缓存值的范围时,Python 将在变量赋值期间开始创建不同的副本:
>>> a = 257
>>> b = 257
>>> a is b
False
>>> print(id(a), id(b), sep="\n")
140282370939376
140282370939120
尽管具有相同的值,但这些变量现在指向不同的对象。但不要让那个愚弄你。Python 偶尔会在幕后介入并优化您的代码。例如,它会缓存在同一行多次出现的数字,而不管其值如何:
>>> a = 257
>>> b = 257
>>> print(id(a), id(b), sep="\n")
140258768039856
140258768039728
>>> print(id(257), id(257), sep="\n")
140258768039760
140258768039760
变量a
和b
是独立的对象,因为它们驻留在不同的内存位置,而字面上使用的数字print()
实际上是同一个对象。
注意: Interning 是CPython 解释器的一个实现细节,在未来的版本中可能会发生变化,所以不要在你的程序中依赖它。
有趣的是,Python 中也有类似的字符串实习机制,它适用于仅由 ASCII 字母组成的短文本。它允许通过内存地址或C 指针而不是单个字符串字符来比较它们的键,从而有助于加快字典查找速度。
定精度整数
您最有可能在 Python 中找到的整数将利用 Csigned long
数据类型。它们在固定位数上使用经典的二进制补码二进制表示。确切的位长取决于您的硬件平台、操作系统和 Python 解释器版本。
现代计算机通常使用64 位体系结构,因此这将转换为 -2 63和 2 63 - 1之间的十进制数。 您可以通过以下方式检查 Python 中固定精度整数的最大值:
>>> import sys
>>> sys.maxsize
9223372036854775807
它超大!大约是我们银河系中恒星数量的 900 万倍,所以它应该足够日常使用了。虽然您可以从unsigned long
C中的类型中挤出的最大值更大,但在 10 19的数量级上,Python 中的整数没有理论限制。为此,不适合固定长度位序列的数字在内存中的存储方式不同。
任意精度整数
您还记得 2012 年成为全球热门的流行 K-pop 歌曲“江南 Style”吗?在YouTube的视频是第一个打破十亿意见。不久之后,观看视频的人太多了,以至于观看计数器都溢出了。YouTube别无选择,只能将他们的计数器从 32 位有符号整数升级到 64 位。
这可能会为视图计数器提供足够的空间,但在现实生活中,尤其是在科学世界中,还有更大的数字并不少见。尽管如此,Python 可以毫不费力地处理它们:
>>> from math import factorial
>>> factorial(42)
1405006117752879898543142606244511569936384000000000
这个数字有五十二位十进制数字。用传统方法用二进制表示它至少需要 170 位:
>>> factorial(42).bit_length()
170
由于它们远远超过了任何 C 类型必须提供的限制,因此这些天文数字被转换为符号幅度位置系统,其基数为 2 30。是的,你没看错。虽然你有十个手指,但 Python 有超过十亿个!
同样,这可能会因您当前使用的平台而异。如有疑问,您可以仔细检查:
>>> import sys
>>> sys.int_info
sys.int_info(bits_per_digit=30, sizeof_digit=4)
这将告诉您每个数字使用多少位以及底层 C 结构的字节大小。要在 Python 2 中获得相同的命名元组,您可以改为引用sys.long_info
属性。
虽然固定精度和任意精度整数之间的这种转换在 Python 3 的幕后无缝完成,但有一段时间事情变得更加明确。有关更多信息,您可以展开下面的框。
int
而long
在Python 2显示隐藏
这种表示消除了整数溢出错误并给出了无限位长的错觉,但它需要更多的内存。此外,执行bignum 算法比固定精度更慢,因为它不能在没有中间仿真层的情况下直接在硬件中运行。
另一个挑战是在替代整数类型之间保持按位运算符的一致行为,这对于处理符号位至关重要。回想一下,Python 中的固定精度整数使用 C 中的标准二进制补码表示,而大整数使用符号大小。
为了减轻这种差异,Python 会为您进行必要的二进制转换。在应用按位运算符之前和之后,它可能会改变数字的表示方式。这是来自CPython源代码的相关注释,它更详细地解释了这一点:
负数的按位运算就像在二进制补码表示上一样。因此,将参数从符号大小转换为二进制补码,并在最后将结果转换回符号大小。(来源)
换句话说,当您对负数应用按位运算符时,负数被视为二进制补码位序列,即使结果将以符号大小形式呈现给您。不过,有一些方法可以模拟Python 中的符号位和一些无符号类型。
Python 中的位串
欢迎您在本文的其余部分使用笔和纸。它甚至可以作为一个很好的锻炼!但是,在某些时候,您需要验证您的二进制序列或位字符串是否与 Python 中的预期数字相对应。就是这样。
转换int
为二进制
要在 Python 中显示组成整数的位,您可以打印格式化的字符串文字,它可以让您选择指定要显示的前导零的数量:
>>> print(f"{42:b}") # Print 42 in binary
101010
>>> print(f"{42:032b}") # Print 42 in binary on 32 zero-padded digits
00000000000000000000000000101010
或者,您可以bin()
使用号码作为参数调用:
>>> bin(42)
'0b101010'
这个全局内置函数返回一个由二进制文字组成的字符串,它以前缀开头,0b
后跟一和零。它始终显示没有前导零的最小位数。
您也可以在代码中逐字使用此类文字:
>>> age = 0b101010
>>> print(age)
42
Python 中可用的其他整数文字是十六进制和八进制文字,您可以分别使用hex()
和oct()
函数获取它们:
>>> hex(42)
'0x2a'
>>> oct(42)
'0o52'
请注意十六进制系统如何利用字母A
直通F
来扩充可用数字集。其他编程语言中的八进制文字通常以纯零作为前缀,这可能会造成混淆。Python 明确禁止此类文字以避免出错:
>>> 052
File "<stdin>", line 1
SyntaxError: leading zeros in decimal integer literals are not permitted;
use an 0o prefix for octal integers
您可以使用任何提到的整数文字以不同的方式表达相同的值:
>>> 42 == 0b101010 == 0x2a == 0o52
True
选择在上下文中最有意义的一项。例如,习惯上用十六进制表示法来表示位掩码。另一方面,现在很少看到八进制文字。
Python 中的所有数字文字都不区分大小写,因此您可以使用小写或大写字母作为前缀:
>>> 0b101 == 0B101
True
将二进制转换为 int
准备好位字符串后,您可以通过利用二进制文字来获取其十进制表示:
>>> 0b101010
42
这是在交互式 Python 解释器中工作时进行转换的一种快速方法。不幸的是,它不会让您转换在运行时合成的位序列,因为所有文字都需要在源代码中进行硬编码。
注意:您可能会想用 来评估Python 代码eval("0b101010")
,但这是一种危及程序安全的简单方法,所以不要这样做!
int()
在动态生成的位串的情况下,使用两个参数调用会更好:
>>> int("101010", 2)
42
>>> int("cafe", 16)
51966
第一个参数是一串数字,而第二个参数确定数字系统的基数。与二进制文字不同,字符串可以来自任何地方,甚至是用户在键盘上打字。要更深入地了解int()
,您可以展开下面的框。
的其他用途 int()
显示隐藏
到现在为止还挺好。但是负数呢?
模拟符号位
当你调用bin()
一个负整数时,它只是在从相应的正值获得的位串前面加上减号:
>>> print(bin(-42), bin(42), sep="\n ")
-0b101010
0b101010
更改数字的符号不会影响 Python 中的底层位串。相反,在将位串转换为十进制形式时,允许在位串前加上减号:
>>> int("-101010", 2)
-42
这在 Python 中是有意义的,因为在内部,它不使用符号位。您可以将 Python 中整数的符号视为与模数分开存储的一条信息。
但是,有一些变通方法可以让您模拟包含符号位的固定长度位序列:
- 位掩码
- 模运算 (
%
) ctypes
模块array
模块struct
模块
您从前面的部分知道,为了确保数字的某个位长度,您可以使用漂亮的位掩码。例如,要保留一个字节,您可以使用恰好由八个开启位组成的掩码:
>>> mask = 0b11111111 # Same as 0xff or 255
>>> bin(-42 & mask)
'0b11010110'
掩码强制 Python 暂时将数字的表示从符号幅度更改为二进制补码,然后再返回。如果您忘记了结果二进制文字的十进制值(等于 214 10 ),那么它将以二进制补码形式表示 -42 10。最左边的位将是符号位。
或者,您可以利用之前使用的模运算来模拟 Python 中的逻辑右移:
>>> bin(-42 % (1 << 8)) # Give me eight bits
'0b11010110'
如果这对您的口味来说看起来过于复杂,那么您可以使用标准库中的一个模块来更清楚地表达相同的意图。例如,使用ctypes
将产生相同的效果:
>>> from ctypes import c_uint8 as unsigned_byte
>>> bin(unsigned_byte(-42).value)
'0b11010110'
您以前见过它,但作为提醒,它会搭载 C 中的无符号整数类型。
另一个可用于 Python 中此类转换的标准模块是array
模块。它定义了一个类似于a 的数据结构,list
但只允许保存相同数字类型的元素。声明数组时,需要用对应的字母在前面指明其类型:
>>> from array import array
>>> signed = array("b", [-42, 42])
>>> unsigned = array("B")
>>> unsigned.frombytes(signed.tobytes())
>>> unsigned
array('B', [214, 42])
>>> bin(unsigned[0])
'0b11010110'
>>> bin(unsigned[1])
'0b101010'
例如,"b"
代表一个 8 位有符号字节,而"B"
代表它的无符号等效字节。还有一些其他预定义类型,例如带符号的 16 位整数或 32 位浮点数。
在这两个数组之间复制原始字节会改变位的解释方式。然而,它需要两倍的内存量,这是相当浪费的。要执行这样的位重写,您可以依赖struct
模块,它使用一组类似的格式字符进行类型声明:
>>> from struct import pack, unpack
>>> unpack("BB", pack("bb", -42, 42))
(214, 42)
>>> bin(214)
'0b11010110'
打包允许您根据给定的 C 数据类型说明符将对象放置在内存中。它返回一个只读bytes()
对象,其中包含结果内存块的原始字节。稍后,您可以使用一组不同的类型代码读回这些字节,以更改它们被转换为 Python 对象的方式。
到目前为止,您已经使用了不同的技术来获得以二进制补码表示的整数的固定长度位串。如果你想将这些类型的位序列转换回 Python 整数,那么你可以试试这个函数:
def from_twos_complement(bit_string, num_bits=32):
unsigned = int(bit_string, 2)
sign_mask = 1 << (num_bits - 1) # For example 0b100000000
bits_mask = sign_mask - 1 # For example 0b011111111
return (unsigned & bits_mask) - (unsigned & sign_mask)
该函数接受由二进制数字组成的字符串。首先,它将数字转换为普通的无符号整数,不考虑符号位。接下来,它使用两个位掩码来提取符号位和幅度位,其位置取决于指定的位长。最后,它使用常规算术将它们组合起来,知道与符号位关联的值是负数。
您可以针对前面示例中可靠的旧位字符串进行尝试:
>>> int("11010110", 2)
214
>>> from_twos_complement("11010110")
214
>>> from_twos_complement("11010110", num_bits=8)
-42
Pythonint()
将所有位视为量级,因此没有任何意外。但是,默认情况下,这个新函数假定一个 32 位长的字符串,这意味着对于较短的字符串,符号位隐式等于 0。当您请求与您的位字符串匹配的位长度时,您将获得预期的结果。
虽然在大多数情况下整数是最适合使用按位运算符的数据类型,但有时您需要提取和操作结构化二进制数据的片段,例如图像像素。该array
和struct
在这个主题模块暂时触摸,所以你会更详细地探索它旁边。
从零开始学python | Python 中的按位运算符 II
- 点赞
- 收藏
- 关注作者
评论(0)