
我最早对Python进制转换产生兴趣是帮朋友调一段LED灯带程序。他对着文档填了一个“#FF6A00”的颜色值结果灯珠亮出来是橙红色和他想要的浅黄色差了十万八千里。排查半天最后发现他压根没意识到“FF”“6A”“00”这三个字节分别对应红、绿、蓝三种颜色分量更没搞懂“FF”其实是十六进制的255而不是某种需要特殊转义的字符。这件事特别典型进制转换在教科书里往往只占一小节老师给三个函数——bin、oct、hex——就完事了。但真到了实际开发里颜色值、文件权限、串口报文、内存地址、抓包数据哪一样不和进制打交道所以这篇我打算把这个话题彻底展开从Python内置函数到手动实现算法从负数补码到浮点精度从位运算到高频实战场景把“Python进制转换”这个看似基础、实则很容易翻车的主题一五一十讲透。不管你是在Python入门阶段刚学到基础语法还是已经开始处理底层数据的工程师这篇都会有你能直接抄走的东西。1. 别把进制转换当考试题它藏在日常开发的各个角落很多人觉得进制转换是初中数学内容和写代码关系不大。但只要你离开纯业务逻辑向下接触一点底层就会发现计算机世界里到处都是进制的身影。最直观的例子是网页颜色。“#FF6A00”这种色值本质就是三个十六进制字节红色分量是0xFF255绿色分量是0x6A106蓝色分量是0x000。你在CSS里写颜色在Pillow里调像素在matplotlib里设线条颜色这些库的内部表示全是RGB整数你只是没看见它们的十六进制长相而已。再看Linux权限。你在命令行敲chmod 755755这个数其实是八进制数展开成二进制是111 101 101每一位都对应“读、写、执行”三个权限位。如果你不懂八进制和二进制的映射看644、600这种权限数字就永远只能靠死记硬背不懂为什么4是读、2是写、1是执行。还有更底层的场景串口调试助手返回一串十六进制数据抓包工具里显示的MAC地址是3C:22:FB:54:6D:8A调试器里内存地址是0x7fff5c2f1a40。这些数据的本质全是二进制十六进制只是它的简洁书写形式。1.1 计算机只有二进制其他进制都是给人看的缩写为什么计算机内部只有二进制因为电路只有两种稳定状态高电平和低电平对应1和0。这是物理层面的限制没有任何商量余地。但二进制对人类极不友好写一个十进制255二进制要写成11111111八位还不算长写一个65535二进制要写成16位眼睛直接看花。于是我们创造了缩写规则十六进制一位恰好对应四个二进制位八进制一位恰好对应三个二进制位。为什么不是十二进制因为4和3都是2的幂分组方便。所以“十六进制其实是把二进制从右往左每4位分组后的简写”——这句话比背任何转换公式都有用。二进制10111101从右往左分组1011 1101对应十六进制BD。二进制111101101从右往左分组111 101 101对应八进制755。理解了这一层你就明白为什么内存地址不直接用十进制显示——因为十六进制和二进制在结构上天然对应调试时一眼就能看出内存布局。1.2 哪些人最需要认真读这篇文章给读者一个简单的自测看到0xFF能脱口而出是255吗看到0b1010能瞬间反应出是10吗如果还要在脑海里转个弯甚至需要打开计算器那你需要把下面的内容看完。我按读者类型把重点章节划了一下Python入门阶段的新手重点看第2章和第3章。内置函数是日常最常用的工具手写算法是为了让你理解原理不至于换个语言就抓瞎。做脚本自动化、数据处理的人第4章和第5章是重点。解析日志、处理字节、读串口数据随时会遇到负数和浮点数的二进制问题。准备面试的程序员第3章的算法一定要能手写出来第5章的位运算也是高频考点。2. 内置函数三板斧int、bin、oct、hex与format的正确打开方式Python在这件事上的内置支持可以说相当全。你可能已经用过其中一两个但很容易忽略一些细节比如int()的base参数到底能干什么比如format()比bin()强在哪里。先看一张速查表把常用函数和结果放在一起对比操作写法结果十进制转二进制带前缀bin(10)0b1010十进制转八进制带前缀oct(10)0o12十进制转十六进制带前缀hex(255)0xff十进制转二进制不带前缀format(10, b)1010十进制转十六进制大写format(255, X)FF按指定进制解析字符串int(1010, 2)10自动识别前缀解析字符串int(0x1f, 0)31这个表里的每一行都有讲究下面逐个拆。2.1 int() 按进制还原整数最容易忽略的 base 细节先说int(s, base)。它把一个字符串按指定进制解析成十进制整数第二个参数base是可选的不传的时候默认是10。这看起来简单但有几个细节值得注意。第一base的取值范围是0和2到36。如果你传一个超出范围的值Python直接抛TypeError。比如int(101, 5)是合法的三进制数字里没有4以上数字必须小于基数但int(201, 2)会报ValueError因为二进制里不允许出现数字2。第二字符串可以带正负号。int(-1010, 2)返回-10int(1f, 16)返回31。这个特性在解析外部输入时很有用但要注意字符串内部不能有空格或下划线除非是Python 3.6以后支持的int(1_000, 10)这种写法。第三最容易被忽略的玩法是base0。当你传0时Python会看字符串的前缀来猜测进制int(0x1f, 0) # 31识别十六进制前缀0x int(0b1010, 0) # 10识别二进制前缀0b int(0o17, 0) # 15识别八进制前缀0o int(123, 0) # 123没有前缀按十进制处理这个特性在写通用解析工具时非常方便你不需要自己去strip前缀Python自动搞定。但要注意如果字符串带了错误的前缀比如int(0b1010, 16)结果不是报错而是按十六进制解析最终得到一个很大的数。我见过有人在这里吃过亏日志里解析出的数据完全对不上排查半天才发现是把带前缀的二进制字符串错传成了16。第四Python的int还能直接处理数字字面量。int(0b1010)结果是10但这里的0b1010本身已经是整数10了再转一次没意义纯属脱裤子放屁。你真正会用到的是把字符串转整数或者把带进制的字符串规整成统一数值。2.2 bin、oct、hex从整数到进制字符串这三个函数是一族作用都是把整数转成对应进制的字符串并且带上明确的前缀bin(10) # 0b1010 oct(10) # 0o12 hex(255) # 0xff注意两个容易混淆的输出格式。第一Python 3里八进制的前缀是0o数字0和字母o不是数字0后面跟一个字母O也不是单纯的0。很多老教程里写017这种八进制字面量在Python 3里已经不能用了必须写成0o17。第二十六进制字母默认是小写hex(255)返回0xff如果你需要大写0xFF得用upper()或者直接用下一节要讲的format。此外这三个函数的参数可以是负数结果会在最前面保留负号bin(-10)返回-0b1010。注意这里的负号不是二进制本身的一部分而是Python语言层面表示“这个数是负数”的方式。这个细节到第4章讲补码时还会再提。2.3 format 更灵活大小写、前缀、补齐一网打尽如果你只需要一个不带前缀的二进制字符串或者想控制输出宽度format()比bin()、oct()、hex()好用得多。它的格式说明符才是真正的完整工具。format(255, x) # ff小写十六进制 format(255, X) # FF大写十六进制 format(255, #X) # 0XFF带前缀的大写十六进制 format(10, b) # 1010不带前缀的二进制 format(10, #b) # 0b1010带前缀的二进制 format(5, 08b) # 000001018位补零 format(5, #08b) # 0b000101总宽8位前缀也算在内这里有个大坑format(5, #08b)的结果是0b000101很多新手以为它会把二进制数字部分补到8位结果发现只补到了6位数字加2位前缀总长度才是8。因为格式说明符里的08指的是总宽度包括0b这两个字符。如果你真的想让二进制数字部分占满8位写format(5, 010b)或者干脆先得到不带前缀的字符串再手动补零。format在写固定宽度日志、生成表格、拼接字符串时尤其好用。比如你要把三通道颜色值输出成二进制调试信息r, g, b 255, 106, 0 print(format(r, 08b), format(g, 08b), format(b, 08b)) # 11111111 01101010 00000000这种输出在调试图像算法时比纯十进制直观得多——你一眼就能看出哪些像素位是连在一起的。3. 手动实现进制转换算法看穿原理才能举一反三内置函数用起来方便但如果你不懂底层原理换个场景就抓瞎。比如面试让你手写一个十进制转二进制的函数再比如你在Java里也想做进制转换Python的bin()帮不上忙。所以我一直建议不管哪个语言都值得自己实现一遍进制转换算法。这一章不只有“抄答案”更重要的是讲清楚算法为什么这么写。3.1 除2取余法与“为什么余数要逆序”十进制整数转二进制标准方法是“除2取余倒序排列”。以十进制29为例完整的除法过程是被除数商余数291411470731311101从下往上读余数11101这就是29的二进制。这个过程几乎所有教材都会写但很少解释为什么余数要倒序。我用自己的话来拆一下。二进制数的每一位从右往左权重依次是1、2、4、8、16……当你对一个数除以2取余数时得到的余数实际上是这个数“除以2后剩没剩1”——换句话说就是它的最低位。接下来商14继续除以27除以2直到商为0每一步都产生下一位。所以第一次除得到的余数是最低位最后一次除得到的余数才是最高位。倒序输出其实是把“先算出来的低位”放到右边、把“后算出来的高位”放到左边的自然结果。用代码实现时如果每次都把余数插到结果前面时间复杂度是O(n^2)级别的更好的做法是先用append收集最后反转def decimal_to_binary(n): if n 0: return 0 bits [] while n 0: n, remainder divmod(n, 2) bits.append(str(remainder)) return .join(reversed(bits)) decimal_to_binary(29) # 11101用递归写更简洁也更适合展示原理def decimal_to_binary(n): if n 2: return str(n) return decimal_to_binary(n // 2) str(n % 2)递归版本和循环版本本质一样每一步都在做“剥掉最低位继续处理剩下的高位”。理解了这点二进制转十进制就是反向操作从最高位开始每往右移动一位当前结果乘2再加上这一位的值。3.2 任意进制转换一套代码搞定2到36进制二进制只是特例把“除2”换成“除base”你就有了一个通用的十进制转任意进制的函数。核心是准备一个够长的映射表DIGITS 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ def to_base(n, base): if n 0: return 0 if n 0: return - to_base(-n, base) result [] while n 0: n, remainder divmod(n, base) result.append(DIGITS[remainder]) return .join(reversed(result))divmod(n, base)返回商和余数两个值写的时候注意顺序是n, remainder divmod(...)不是remainder, n divmod(...)。我一开始写这个函数时就是顺序搞反调试了好久。反向操作——任意进制的字符串转十进制——用“按权展开”def from_base(s, base): s s.upper() value 0 for ch in s: value value * base DIGITS.index(ch) return value这个函数的逻辑是从左往右扫描字符串每读到一个字符就把之前的结果乘以base然后加上当前字符代表的数值。十六进制的2F转十进制就是2 * 16 15 47完全符合直觉。如果追求效率可以把字符到数值的映射预计算成一个字典避免每次index线性查找CHAR_TO_VAL {ch: i for i, ch in enumerate(DIGITS)} def from_base_fast(s, base): s s.upper() value 0 for ch in s: value value * base CHAR_TO_VAL[ch] return value有了这一对函数你在任何语言里都可以手写进制转换不依赖Python的bin()和hex()。3.3 二进制加法的直观理解进制转换和二进制运算是一家人。你理解了“逢二进一”就理解了为什么二进制的加减法那么接近直觉。以0b1101 0b1011为例从最低位开始加最低位1 1 10记0进1第二位0 1 进位1 10记0进1第三位1 0 进位1 10记0进1最高位1 1 进位1 11记1进1额外产生一个最高位1最终结果是0b11000也就是十进制的24和13加11完全一致。理解二进制的进位规则对后面讲位运算很有帮助因为你马上会看到“左移一位”和“乘以2”在二进制里是同一件事。4. 负数与小数的二进制底层理解补码和浮点精度前两章讲的都是正整数代码看起来清爽。但实际工作中你一定会遇到负数和小数这两块才是进制转换里最容易翻车的地方。4.1 负数用补码存储bin()返回的只是“带符号的字符串”你运行bin(-13)得到的结果是-0b1101。这个负号是Python帮你在字符串最前面加的它不代表内存里的真实状态。在计算机内部-13是以二进制的补码形式存储的。补码是什么一句话取反加一。以8位二进制为例13是00001101把它所有位取反得到11110010再加1得到11110011这就是-13在8位下的补码。验证一下11110011 00001101 100000000丢掉进位后是00000000。也就是说-13加上13恰好等于0。这正是补码设计的精髓——把减法统一成加法CPU只需要加法电路不需要单独的减法电路。补码还有一个规律值得记住在N位下负数x的补码等于2^N x。对-13和N8来说就是256 - 13 243243的二进制正好是11110011。这给了我们一个非常方便的转换函数def to_signed_binary(n, width8): if n 0: return format(n, f0{width}b) else: return format((1 width) n, f0{width}b) to_signed_binary(-13, 8) # 11110011这个函数在处理串口协议、Modbus报文、传感器负温度数据时特别有用。设备返回的是一串十六进制字节你想知道它代表的负数值就需要反过来把补码转成十进制def from_signed(bits, width8): if bits (1 (width - 1)): return bits - (1 width) return bits bits int(11110011, 2) from_signed(bits, 8) # -13判断思路是看最高位符号位是不是1。是1说明这是负数减掉2^width就是真实值。这比“取反加一”的机械操作直观多了。下面给一个4位补码的对照表你把它背下来负数转换的直觉就有了十进制4位补码-11111-21110-31101-41100-51011-61010-71001-81000注意到没有补码从-1到-8看起来像从全1开始往下数。这是“取反加一”带来的自然结果。4.2 小数转二进制的乘2取整法以及0.1的无限循环小数转二进制的算法是“乘2取整”。以0.625为例0.625 × 2 1.25取整数1剩下0.25。0.25 × 2 0.5取整数0剩下0.5。0.5 × 2 1.0取整数1剩下0。从上往下读整数部分101所以0.625的二进制是0.101。验证没有问题0.5 0.125 0.625。但这个算法有个残酷的现实很多十进制小数在二进制下是无限循环小数。比如0.10.1 × 2 0.2 → 00.2 × 2 0.4 → 00.4 × 2 0.8 → 00.8 × 2 1.6 → 10.6 × 2 1.2 → 10.2 × 2 0.4 → 0开始循环0.1的二进制是0.00011001100110011001100110011...无限循环。这就像十进制里1/3等于0.333...永远写不完一样二进制里0.1也永远写不完。这就是为什么你运行0.1 0.2得到的是0.30000000000000004而不是精确的0.3。Python的float是IEEE 754双精度浮点数只有52位二进制尾数存不下无限循环只能近似。任何语言都一样这不是Python的bug。如果你要写一个小数转二进制的工具可以用这样的循环但必须设置最大迭代次数防止死循环def decimal_to_binary_frac(frac, precision10): result [] while frac 0 and len(result) precision: frac * 2 bit int(frac) result.append(str(bit)) frac - bit return 0. .join(result) if result else 0.0 decimal_to_binary_frac(0.625) # 0.101 decimal_to_binary_frac(0.1, 10) # 0.0001100110做浮点二进制转换时永远记得精度是有限的。如果要做金额计算别用float直接用decimal.Decimal如果要做精确的定点小数可以考虑fractions.Fraction。但无论用哪种类型二进制无法精确表示某些十进制小数这个数学事实不会消失。5. 位运算与进制转换互为表里的底层操作很多人把位运算和进制转换当成两门课来学其实它们是同一件事的两个侧面。进制转换是“看得见”的二进制位运算是“算得动”的二进制。5.1 左移右移在二进制里做乘除2速算先看最直观的两个操作左移和右移。13 1 # 26 13 1 # 6为什么左移一位等于乘以2因为二进制的每一位权重都是2的整数次幂把所有1向左挪一格相当于每位的权重都乘以2整体数值自然翻倍。右移一位正好相反相当于整除2。13 1是6而不是6.5因为是整数右移低位被直接丢弃。这个运算在底层编程里非常常见。比如解析协议时两个4位十六进制数要拼成一个字节就会用到左移和或操作。更基础的是很多性能敏感场景用x * 2和x 1互换来微优化虽然现代编译器早就帮你做了这件事但理解这种等价关系依然重要。5.2 与、或、异或用二进制位做状态管理位运算中最实用的场景是状态位管理。用一个整数来保存多个布尔状态这在Linux权限、文件打开模式、网络协议标志位里到处可见。举个例子假设我们要管理三个权限位READ 0b100 # 4 WRITE 0b010 # 2 EXEC 0b001 # 1合并权限用或运算perm READ | WRITE # 0b110十进制6读写检查是否有某个权限用与运算has_read perm READ ! 0 # True has_exec perm EXEC ! 0 # False如果只有八进制权限数字怎么反推它代表什么权限0o644就是二进制110 100 100依次拆成三组每组三位分别表示所有者、组、其他人的读/写/执行权限。这正是下一章实战要讲的chmod数字的底层逻辑。异或运算^也有不少妙用。比如x ^ 1可以用来翻转二进制最低位a ^ b ^ b等于a可以用于无临时变量交换两个数虽然Python里写a, b b, a就够了但位运算的思路在加密、校验算法里非常常见。5.3 掩码实战从一个协议字节里拆出信息假设你从设备读到一个字节0x3A在二进制里是00111010。如果协议规定高4位是设备状态低4位是通道号你怎么拆分data 0x3A high (data 4) 0x0F # 高4位3 low data 0x0F # 低4位10即A这里 0x0F的作用是掩码把高于4位的部分全部清零。为什么要先右移再与操作因为如果不右移data 0x0F只能取到低4位0xA拿不到高4位。右移4位后原来的高4位变成了新的低4位再用掩码取出来就是3。反向拼接也很简单combined (high 4) | low # 0x3A这种“取高位、取低位、拼回去”的操作在解析I2C设备寄存器、串口指令、网络协议标志位时每天都会用到。你不需要背协议表只要掌握了二进制位拆分逻辑任何字节都能快速拆开。6. 高频场景实战颜色值、权限位、协议数据与老代码兼容最后用四个真实场景把这篇文章的内容串起来。这些代码你基本可以直接抄进项目里。6.1 RGB与十六进制颜色值互相转换网页和图像处理里最常见的需求。给一个RGB元组输出#RRGGBB字符串给一个颜色字符串拆回三个整数。def rgb_to_hex(r, g, b): return #{:02X}{:02X}{:02X}.format(r, g, b) def hex_to_rgb(hex_str): hex_str hex_str.lstrip(#) return tuple(int(hex_str[i:i2], 16) for i in (0, 2, 4)) rgb_to_hex(255, 106, 0) # #FF6A00 hex_to_rgb(#FF6A00) # (255, 106, 0){:02X}的意思是转成十六进制大写固定占2位不足补0。这个格式串在颜色处理里几乎是标配。如果你在用OpenCV或Pillow读像素拿到的是(R, G, B)或(B, G, R)元组想把它输出成可以粘贴到网页里的颜色值这两个函数就能省不少事。注意OpenCV默认是BGR用的时候先转为RGB再调用。6.2 chmod 755八进制权限的一次解析Linux的权限数字是八进制每一位代表一组权限。755展开是7、5、5三位八进制数字每一位再拆成三个二进制位。八进制二进制权限含义0000无权限1001执行2010写3011写执行4100读5101读执行6110读写7111读写执行写一个解析八进制权限字符串的小函数def oct_to_permission(s): mapping { 0: ---, 1: --x, 2: -w-, 3: -wx, 4: r--, 5: r-x, 6: rw-, 7: rwx, } return .join(mapping[ch] for ch in s) oct_to_permission(755) # rwxr-xr-x oct_to_permission(644) # rw-r--r--这样你看到644不用再查表所有者可读写组和其他人只读。Python里给文件设置权限也是直接用八进制字面量import os os.chmod(script.sh, 0o755)6.3 协议字节的拼接与大小端处理串口、网络、传感器数据常以字节流形式返回。假设你收到两个字节0x1A和0x2B按大端高字节在前拼接成一个16位整数value int.from_bytes(b\x1a\x2b, big) # 0x1A2B 6699反过来把一个整数转回字节流value 6699 data value.to_bytes(2, big) # b\x1a\x2bbig是“大端”低地址存高位little是“小端”低地址存低位。哪种顺序取决于设备协议。用int.from_bytes和to_bytes比手动(value 8) 0xFF拼字节要省事得多。如果你要处理的数据不是8位对齐就用第5章的掩码思路。比如两个4位十六进制数拼成一个字节high, low 0x3, 0xA combined (high 4) | low # 0x3A6.4 为什么老教程代码跑不通Python版本带来的语法坑很多初学者下载了最新的Python跟着网上的老教程敲代码结果一运行就报错。这里有几个和进制转换有关的常见坑提前预警。Python 2里oct(10)返回012到Python 3返回0o12。如果你在解析某个历史系统生成的八进制字符串先确认数据来源的格式否则会解析失败或得到错误结果。Python 2里5 / 2结果是2整除Python 3里结果是2.5。如果你看到老代码里用/做整数除法并依赖它最好改成//。我们在讨论手动实现进制转换算法时用到的divmod和//在Python 3下行为是稳定的但老教程里大量使用/的算法移植过来会变味。还有最经典的print语法。Python 2可以写print helloPython 3必须写print(hello)。如果你下载的是3.12照着2的教程敲第一行就报SyntaxError。判断教程是否过时先看开头的print是不是带括号带括号的通常是Python 3写法。最后说点个人体会我把这套内容反反复复教过不少人也手写实现过不知道多少遍。最有价值的体会是不要死背bin、oct、hex的返回值而是建立一个“二进制直觉”。什么是直觉看到0x3A立刻知道它是58并且能拆成高4位3、低4位A看到755立刻反应出所有者rwx、其他人r-x看到0.1 0.2的精度问题不再骂Python而是知道这是二进制浮点数的数学宿命。这种直觉不需要天赋靠的就是多写、多拆、多看。如果你愿意可以把第3章的手写算法、第4章的补码函数、第6章的三个实战工具合在一起拼成一个小模块收进你的工具库。以后遇到进制相关的需求直接用自己封装好的函数比每次重新查文档舒服太多。最后分享一个我在实际项目里常用的技巧解析串口报文时先用int.from_bytes把原始字节变成整数再用format(x, 08b)打印成二进制最后用第5章的掩码拆分字段。三步走任何协议报文都能快速看懂。这套流程我试了很多年稳得很。