记一个python中按位取反的问题

众所周知,python中只有一种整数类型 int,并没有“无符号”的概念。导致有时我们需要使用对一些数据进行取反,python自动使用有符号的形式输出了,不符合预期。怎么解决这个问题呢?

问题现象

>>> bin(~0b00)
'-0b1'
>>> bin(~0b01)
'-0b10'
>>> bin(~0b10)
'-0b11'
>>> bin(~0b11)
'-0b100'

其实我只期望输出类似 0b11 0b10 之类的,并不需要这个符号,python这个自动以有符号数输出的转换就显得太多余了,这样的结果完全没法直接使用。

问题解决

问题的主要矛盾点就在于,不让python以负数补码形式展示。但 f-string 等方法都没法以无符号形式展示[1],因此无法通过自带方式解决。

尝试

那么,本质上是因为反码补码运算导致的显示问题,那我们可不可以直接通过相反操作来抵消这个补码运算呢?(死去的微机原理又开始攻击我)

举个例子,来说明负数运算原理:

5 = 4'b0101
取反得反码:4'b1010
加一得补码:4'b1011
得 -5的补码:4'b1011

那我想获取 bin(~5),实际上我获取 bin(~(5-1)) 不就可以了?

然而也不行,咱定睛一看,啊,原来之前举的例子不充分,python的显示逻辑是这样的:

>>> bin(5)
'0b101'
>>> bin(-5)
'-0b101'

所以实际显示的是负数的二进制数。想想也合理,怎么可能展示补码呢。

通过ctypes解决

既然python会自动转换,那么需要找一个机制,让它老老实实接收并输出原来的值,不要执行转换。自然就可以使用ctypes来中转一下解决。

>>> import ctypes
>>> ctypes.c_uint32(~0b11)
c_ulong(4294967292)
>>> ctypes.c_uint32(~0b11).value
4294967292
>>> bin(ctypes.c_uint32(~0b11).value)
'0b11111111111111111111111111111100'

这样就相当于强制将负数转化成了无符号数,然后再通过value方法取出,因为32位带了符号,python自动使用longlong类型存储这个数据,所以输出了4294967292。然后再调用bin函数,就能输出取反后的数据了。

封装如下:

def unsigned_bitwise_not(v: int):
    return  bin(ctypes.c_uint32(~v).value)

通过numpy解决

>>> from numpy import uint32
>>> bin(~uint32(0b111))
'0b11111111111111111111111111111000

注意要先uint32再取反,否则会报错:

>>> bin(uint32(~0b111))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OverflowError: Python integer -8 out of bounds for uint32

通过字符串解决

简单粗暴,直接上代码,实现0和1字符的互转

convert_dict = {'0': '1', '1': '0'}
def unsigned_bitwise_not(v: int):
    [_, value] = bin(v).split('b')
    return '0b' + ''.join([convert_dict[c] for c in value])

或:

def unsigned_bitwise_not(v: int):
    [_, value] = bin(v).split('b')
    return '0b' + ''.join(['1' if c == '0' else '0' for c in value])

或:

def unsigned_bitwise_not(v: int):
    [_, value] = bin(v).split('b')
    return '0b' + ''.join(list(map(lambda c: '1' if '0' == c else '0', value)))

通过与运算解决

通过与运算直接mask掉符号位即可,这也是最方便的方法

>>> bin(~0b011001 & 0xFFFF)
'0b1111111111100110'
>>>
>>> bin(~0b011001 & 0xFFF)
'0b111111100110'

就算是用完了所有可用位数也无所谓,这应该和python内部自动类型转换机制有关,会直接拓展到运算两侧最大的位数(包括符号位),所以符号位永远都被mask掉。(猜测)

>>> bin(~0b011001 & 0xFFFFFFFF)
'0b11111111111111111111111111100110'

关于这个问题,到时扒拉cpython源码看看。

[1] Python 文档 https://docs.python.org/3/library/string.html#format-specification-mini-language

发表评论