>>> x = -4
>>> print("{} {:b}".format(x, x))
-4 -100
>>> mask = 0xFFFFFFFF
>>> print("{} {:b}".format(x & mask, x & mask))
4294967292 11111111111111111111111111111100
>>>
>>> x = 0b11111111111111111111111111111100
>>> print("{} {:b}".format(x, x))
4294967292 11111111111111111111111111111100
>>> print("{} {:b}".format(~(x ^ mask), ~(x ^ mask)))
-4 -100
我无法弄清楚Python如何表示负整数,以及因此位操作如何工作。我的理解是Python试图模拟两个补码,但是有任意数量的位。因此,通常使用32位掩码强制Python在位操作之前在整数上设置标准大小。
正如您在我的示例中所看到的,-4 & 0xFFFFFFFF
会产生一个大的正数。为什么Python似乎将其读作无符号整数,而不是2的补码负数?稍后,操作~(x ^ mask)
应该产生与大正数相同的二进制补码位模式,而不是-4
。导致转换为signed int的原因是什么?
谢谢!
答案 0 :(得分:1)
TLDR; CPython整数类型将符号存储在结构的特定字段中。当执行按位运算时,CPython将负数替换为它们的二进制补码,并且有时(!)执行相反的操作(即,将负数替换为两个补数)。
整数的内部表示是一个PyLongObject
结构,其中包含一个PyVarObject
结构。 (当CPython创建一个新的PyLong
对象时,它为结构分配内存,并为数字分配尾随空间。)这里重要的是PyLong
的大小:ob_size
字段PyVarObject
嵌入结构的_包含整数的大小(以数字为单位)(数字是15或30位数字)。
如果整数为负数,则此大小为减去个位数。
(参考:https://github.com/python/cpython/blob/master/Include/object.h和https://github.com/python/cpython/blob/master/Include/longobject.h)
如您所见,整数的内部CPython表示形式实际上与通常的二进制表示形式相去甚远。但是CPython必须为各种目的提供按位操作。让我们看一下the code中的评论:
static PyObject *
long_bitwise(PyLongObject *a,
char op, /* '&', '|', '^' */
PyLongObject *b)
{
/* Bitwise operations for negative numbers operate as though
on a two's complement representation. So convert arguments
from sign-magnitude to two's complement, and convert the
result back to sign-magnitude at the end. */
/* If a is negative, replace it by its two's complement. */
/* Same for b. */
/* Complement result if negative. */
}
要在按位运算中处理负整数,CPython使用二进制补码(实际上,这是一个二进制补码,但我不赘述)。但是请注意“符号规则” (名称是我的):结果的符号是应用于数字符号的按位运算符。更准确地说,如果nega <op> negb == 1
为负({negx
= 1
代表负,0
代表正),则结果为负。 Simplified code:
switch (op) {
case '^': negz = nega ^ negb; break;
case '&': negz = nega & negb; break;
case '|': negz = nega | negb; break;
default: ...
}
另一方面,格式化程序甚至不执行二进制补码,即使是以二进制表示形式:[format_long_internal](https://github.com/python/cpython/blob/master/Python/formatter_unicode.c#L839)
调用[long_format_binary](https://github.com/python/cpython/blob/master/Objects/longobject.c#L1934)
并删除两个前导字符,但保留符号。参见the code:
/* Is a sign character present in the output? If so, remember it
and skip it */
if (PyUnicode_READ_CHAR(tmp, inumeric_chars) == '-') {
sign_char = '-';
++prefix;
++leading_chars_to_skip;
}
long_format_binary
函数不执行任何二进制补码:仅输出以2为底的数字,preceeded by the sign。
if (negative) \
*--p = '-'; \
我将按照您的REPL顺序进行操作
>>> x = -4
>>> print("{} {:b}".format(x, x))
-4 -100
毫不奇怪,考虑到格式中没有两个补码,而是一个符号。
>>> mask = 0xFFFFFFFF
>>> print("{} {:b}".format(x & mask, x & mask))
4294967292 11111111111111111111111111111100
数字-4
为负数。因此,在逻辑和之前,用二进制补码替换它。您期望结果将变为负数,但请重新考虑“签名规则”:
>>> nega=1; negb=0
>>> nega & negb
0
因此:1.结果没有负号; 2.结果不取二。即使该规则看起来不太直观,您的结果也符合“签名规则”。
现在,最后一部分:
>>> x = 0b11111111111111111111111111111100
>>> print("{} {:b}".format(x, x))
4294967292 11111111111111111111111111111100
>>> print("{} {:b}".format(~(x ^ mask), ~(x ^ mask)))
-4 -100
同样,-4
为负数,因此用二进制补码0b11111111111111111111111111111100
代替,然后与0b11111111111111111111111111111111
进行异或。结果为0b11
(3
)。一元补码,即再次为0b11111111111111111111111111111100
,但这次的符号为负:
>>> nega=1; negb=0
>>> nega ^ negb
1
因此,结果得到了补充,并得到了负号,正如您期望的那样。
结论:我想没有一个完美的解决方案来拥有任意长的带符号的数字并且提供按位运算,但是文档对所做出的选择并不是很冗长。