Python int.to_bytes 的有符号边界:同一个 ff,为什么既能读成 255 又能读成负一

昨天 3阅读

设备报文里收到一个ff,日志工具显示二百五十五,业务解析器却显示负一。字节没有变,差别来自字段约定:无符号整数把全部八位当作非负数值,有符号整数则按补码解释。同一份二进制数据不能脱离位宽和符号规则单独确定含义。

本例在Linux、CPython 3.12.14实跑。保存为demo.py后执行python3 demo.py。程序只构造短字节串,固定使用big字节序,集中检查signed与length;不连接设备,也不读写任何真实报文文件。

Python int.to_bytes 的有符号边界:同一个 ff,为什么既能读成 255 又能读成负一

AI生成的概念示意图:相同容量在单向计数与双向数值范围中采用不同解释;图中开关仅作抽象比喻,不代表程序的具体位模式。

完整程序与本地结果

完整可运行程序

packet = (-1).to_bytes(1, "big", signed=True)
assert packet == b"\xff"
print("minus one byte:", packet.hex())
unsigned = int.from_bytes(packet, "big", signed=False)
signed = int.from_bytes(packet, "big", signed=True)
assert (unsigned, signed) == (255, -1)
print("same byte decoded:", unsigned, signed)

for value in (-128, -1, 0, 127):
    encoded = value.to_bytes(1, "big", signed=True)
    assert int.from_bytes(encoded, "big", signed=True) == value
    print("signed roundtrip:", value, encoded.hex())

for value in (-129, 128):
    try:
        value.to_bytes(1, "big", signed=True)
    except OverflowError:
        print("signed one-byte rejected:", value)
    else:
        raise AssertionError("out-of-range value accepted")

positive = (128).to_bytes(2, "big", signed=True)
negative = (-129).to_bytes(2, "big", signed=True)
assert positive.hex() == "0080" and negative.hex() == "ff7f"
print("two-byte encodings:", positive.hex(), negative.hex())
assert (255).to_bytes(1, "big", signed=False) == packet
try:
    (-1).to_bytes(1, "big", signed=False)
except OverflowError:
    print("unsigned negative: OverflowError")
else:
    raise AssertionError("negative unsigned value accepted")

本次实际输出(以下为结果,不是程序)

minus one byte: ff
same byte decoded: 255 -1
signed roundtrip: -128 80
signed roundtrip: -1 ff
signed roundtrip: 0 00
signed roundtrip: 127 7f
signed one-byte rejected: -129
signed one-byte rejected: 128
two-byte encodings: 0080 ff7f
unsigned negative: OverflowError

字节内容相同,解码约定可以不同

程序把负一编码成一个有符号字节,十六进制输出为ff。随后用两种signed设置读取同一个packet,依次得到二百五十五和负一。这个实验刻意没有重新编码数据,因而可以直接把差异定位到解码规则,而不是传输途中被修改。

排错日志最好同时保存原始十六进制、字段长度和符号解释。只写一串十进制数字,会丢掉判断所需的背景;只写ff也不足以说明业务值。对于来自不同协议版本的字段,还应记录对应版本,避免把旧版无符号定义套在新版有符号字段上。

一字节的正数上界会随着signed改变

有符号一字节能表示负一百二十八到一百二十七。循环对两个边界、负一和零逐项编码再解码,输出分别是80、ff、00、7f,断言保证每个结果回到原值。这里的范围属于八位字段,不是Python整数本身只能存这些数。

一百二十八虽然是正数,却已经超出一字节有符号范围。程序将它与负一百二十九一起送入编码,两次均得到OverflowError。不要只验证“值是不是整数”或“是否小于二百五十六”;字段是否允许负数会改变两端的可接受范围。

扩到二字节后,必须保留符号解释

一百二十八用二字节有符号编码得到0080,负一百二十九得到ff7f。前面的字节参与表示范围,不能因为看起来重复就随意删除。尤其是正数0080,删掉开头零再按有符号一字节读取,就会从正数变成另一个负数。

如果协议明确规定字段宽度,应始终使用那个宽度,越界就报告错误。不要在遇到异常时自动加一个字节发送,否则接收端后面的字段位置会全部偏移。只有协议本身支持可变长整数,并定义长度信息时,改变宽度才是完整方案的一部分。

无符号模式也有自己的边界

二百五十五在无符号一字节下可以成功编码,并且恰好等于前面的ff;负一在无符号模式下则被拒绝。signed的默认值为False,因此处理负数时不能只写length和byteorder,再假定函数会根据正负自动选择格式。

对固定n字节字段,无符号范围是零到二的八n次方减一;有符号范围是负二的八n减一次方到二的八n减一次方减一。落地时可把上下界作为字段定义的一部分,与单位、缩放倍数一起验证,避免同一个字段在多个入口使用不同判断。

越界检查应发生在组成完整报文之前

本例每次只转换一个值,错误很容易对应到具体输入。真正组装多个字段时,应先验证各字段,再合并字节,错误信息指出字段名、预期位宽和允许范围。这样不会把一个字段的失败误报为整段报文长度异常。

不要为了绕过OverflowError,随手与255做按位与后再发送。那等于截取低八位,会把许多不同整数折叠成同一字节;除非协议明确要求模运算,否则接收端无法恢复原值。是否截断是业务规则,不是编码器应当替你猜测的默认修复。

本文固定为big,目的是隔离符号与容量问题;多字节字段还必须另外约定字节序。编码和解码参数一致只是往返的必要条件,单位与有效值约束仍要由协议定义。断言通过说明这些有限样本成立,并不替代整份协议的验证。

参考资料

资料核验日期:2026年10月2日。以上输出来自固定输入的本地实跑,退出码为0。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。