ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python %-formatting 完全指南:从基础语法到避坑实战

Python %-formatting 完全指南:从基础语法到避坑实战 如果你在Python代码里看到%s、%d、%(name)s这些写法那它就是在用 %-formatting。这是Python里历史最悠久的一种字符串格式化方式比f-string早了差不多二十年。很多新教程都在推f-string但我在维护老项目和读第三方库源码时遇到的还是大量这种旧式写法——它并没有完全退场。这篇文章就把%-formatting从基础语法到实战应用再到底层那些容易踩的小坑全部过一遍适合刚入门Python的同学也适合天天跟历史代码打交道的开发者。1. 先搞清楚它是什么%-formatting 的设计初衷与存在价值1.1 从“字符串拼接”到“模板填充”在没有%-formatting的日子里Python写字符串拼接只能靠号硬拼。比如name 张三 ip 192.168.1.100 print(用户 name 登录成功IP: ip)这样写的问题很明显第一容易漏掉空格输出可能变成“用户张三登录成功”第二如果你想拼接一个数字还得先调用str()转一下第三代码一旦变长满屏的加号和引号让人看得头皮发麻想改其中一个字段的位置得小心翼翼地移动好几个月牙引号。%-formatting 的解决方案是把“模板”和“数据”分开print(用户 %s 登录成功IP: %s % (name, ip))看到模板的开头基本就能预判输出长什么样这在设计上和后来的str.format()、f-string 一脉相承。它最大的意义不是省几个字符而是把“格式”抽离成一种可复用的表达让代码的可读性和可维护性上了一个台阶。1.2 到2024年它依然存在的三个场景很多人觉得%-formatting已经过时了我可以理解这种想法但实际业务里它并没有完全退出。我遇到的典型场景有三个第一个是历史代码。f-string是Python 3.6才引入的在那之前运行了十年以上的业务系统尤其是Web后端和自动化脚本几乎全是用%写的。你去翻一个五六年前的项目里面随便一个文件都能看到%s、%d的身影。看不懂这种语法等于看不懂这些项目的一半逻辑。第二个是logging模块。logging的API在设计上就是模仿printf风格的官方文档至今推荐logger.info(value is %s, value)这种延迟格式化写法。如果用f-string先把字符串拼好再传给logger虽然也能跑但这种做法无法避免不必要的字符串构造在日志量大的场景下性能差异是肉眼可见的。第三个是数据库操作。早期DB-API风格的数据库驱动文档里大量示例都是用%s作为占位符。虽然现在安全最佳实践是参数化查询但你看老代码、看ORM底层实现、看运维脚本时不懂%就会非常难受。2. 语法拆解格式说明符的每一块是什么2.1 最常用的类型符%s、%d、%f、%%%-formatting 的基本结构是模板 % 数据。如果只有一个值可以直接写在%后面如果有多个值右边必须用元组包起来print(%s 今年 %d 岁 % (小明, 18)) print(圆周率约等于 %f % 3.14159)%s是“什么都能填”的万能占位符它会对变量调用str()所以任何对象都能塞进去。%d要求传入整数%f用于浮点数默认保留6位小数。还有一个特殊的%%它表示转义后的百分号因为%本身是格式符的起始标记想输出字面的“%”就必须写两个print(进度100%%) # 输出进度100%下面这张表是我平时最常用到的几种类型符类型符作用示例输出%s字符串自动调用str()%s % 3.143.14%d十进制整数%d % 4242%f浮点数默认6位小数%f % 3.143.140000%x十六进制%x % 255ff%o八进制%o % 810%e科学计数法%e % 12345.6781.234568e04%c字符按ASCII码或单个字符%c % 65A%%转义百分号100%% % ()100%2.2 宽度、对齐、补零、正负号让输出变成表格格式符的完整结构是%[标志][宽度][.精度]类型。宽度和标志的组合是%-formatting 能输出整齐表格的关键。%10s表示这个字段最少占10个字符宽不够就在左侧补空格也就是默认右对齐%-10s加了负号标志变成左对齐%010d则是在宽度不足时用数字0填充左侧。这三个我在写对齐报表时经常用到print(|%10s| % hello) # | hello| print(|%-10s| % hello) # |hello | print(|%010d| % 42) # |0000000042|还有一个很容易被忽略的标志是。%d会让正数也显示正号这在做正负数对齐时特别有用。% d会在正数前留一个空格让正数和负数的个位对齐我在格式化温度、浮点对比数据时常用到。2.3 精度不只是浮点数的小数位说到精度很多人第一反应是%.2f保留两位小数。这没错但它只是其中一半。对字符串来说.精度限定了最大字符数相当于截断print(%.2f % 3.14159) # 3.14 print(%.5s % hello world) # hello浮点数的宽度和精度可以同时出现比如%8.3f表示整个数字包括小数点和所有数字字符最少占8个字符宽其中小数点后保留3位。这个数字的布局规则是“先精度后宽度”初学者容易搞反记住一点.后面的数字管小数部分.前面的数字管整体占位。2.4 字典传参%(name)s 与更清晰的可读性当模板里的占位符非常多时按位置一个个填很容易记错顺序。这时候就可以用字典风格info { name: 张三, ip: 192.168.1.100, cost: 0.2358, } print(用户 %(name)s 从 %(ip)s 登录耗时 %(cost).2f 秒 % info)这里的写法格式是%(键名)格式符键名必须和字典里的key一一对应右边直接传一个字典而不是元组。字典里多了多余的key没影响但如果模板里引用了字典里不存在的键会直接抛出KeyError。字典风格最大的好处是模板里已经写明了字段名后续调整字段顺序时不用去数“第几个占位符”改起来非常省心。它在配置文件、日志格式模板这类“模板与代码分离”的场景里尤其好用。3. 动手实操用 %-formatting 格式化一份登录记录3.1 需求先想清楚再动手写模板我以一个实际需求来演示。假设现在要输出一份用户登录记录表字段有用户名、IP地址、耗时毫秒浮点数、状态。预期效果是每行对齐看起来像一张规整的报表而不是一堆乱糟糟的字符串。这个需求其实很典型。写脚本输出审计日志、爬虫运行记录、批量任务汇总时“对齐”永远是刚需。如果只是简单拼一下出来的东西自己看得费劲别人更看不懂。3.2 第一版能跑但不精致先来一个最直接的版本username zhangsan ip 192.168.1.100 cost 0.2358 status success print(用户%sIP%s耗时%sms状态%s % (username, ip, cost, status))这个版本能跑但有几个问题耗时直接输出了0.2358没有体现毫秒精度不同用户名的长度不同IP地址段长短不一导致每一行的结尾对不齐如果记录多了整屏输出非常乱。说白了这只是在用%替换原来的字符串拼接并没有发挥出格式化语法的真正威力。3.3 第二版用宽度和对齐让输出“成表”既然要对齐就得给每个字段设置一个合理的显示宽度line %-15s | %-18s | %8.2f ms | %s % (username, ip, cost, status) print(line)这里我做了三件事用户名左对齐占15个字符IP地址左对齐占18个字符耗时右对齐占8个字符并保留2位小数。竖线|作为分隔符让每一列在视觉上独立。输出效果大概是这样zhangsan | 192.168.1.100 | 0.24 ms | success lisi | 10.10.3.5 | 1.02 ms | success wangwu | 172.16.20.88 | 12.36 ms | timeout这样一对比每一列都是对齐的扫码人眼扫过去能快速定位耗时异常的行。宽度的选择也有讲究不能随便拍脑袋用户名最长的记录是多少宽度就比它大2到3个字符IP地址最长是IPv4的15字符包括点我给了18个字符是留出余量以后如果要存IPv6能直接扩展。3.4 第三版字典传参 统一模板带来的维护性提升如果我们有几十行记录并且每个字段来自不同的变量、字典、甚至数据库查询结果第二版的位置参数写法会慢慢变得不好维护。这时可以改用字典风格把模板单独抽出来record { user: zhangsan, ip: 192.168.1.100, cost: 0.2358, status: success, } fmt 用户%(user)-10s | IP%(ip)-16s | 耗时%(cost)8.2f ms | 状态%(status)s print(fmt % record)注意看%(user)-10s这种写法它表示“用字典里的user键左对齐占10个字符”。字典风格和宽度标志可以无缝结合。这样做的好处是模板可以全局复用数据库里查出来的每一条记录都能用同一个模板输出改格式只需要改fmt这一行。如果记录是列表循环输出非常自然records [ {user: zhangsan, ip: 192.168.1.100, cost: 0.2358, status: success}, {user: lisi, ip: 10.10.3.5, cost: 1.02, status: success}, ] for r in records: print(fmt % r)这套思路在写自动化巡检报告、批量任务结果展示、数据导出前的预览时都能直接用。4. 避坑实录我在实践中踩过的格式化深坑4.1 忘记转义字面量百分号这是新手最容易犯的错。你想输出“进度100%”结果写成print(进度100%) # ValueError: incomplete formatPython看到%后面没有类型符直接报错。正确的写法是把一个%写成两个print(进度100%%) # 进度100%这个坑在前端模板、SQL LIKE语句、batch进度条输出里尤其常见。我自己有一次写SQL的模糊查询在传参时没注意%转义导致查询语句直接崩了排查了很久才发现是这里的问题。4.2 单个值不包元组导致参数不匹配%s % hello是合法的所以很多人以为一个值不需要元组这是对的。但问题出在模板里有两个及以上占位符时# 错误 %s %s % hello # TypeError: not enough arguments for format string # 正确 %s %s % (hello, world)还有一种反向的坑就是你想格式化一个元组对象本身。比如t (1, 2) print(%s % t) # 输出 (1, 2)这没问题 print(%s %s % t) # 输出 1 2元组被展开两个行为看着像结果完全不一样原因在于%右边的对象是元组时会被打包成参数序列。如果真想让整个元组作为单个%s的参数要写成(%s % (t,))右边的(t,)是一个只有一个元素元组t的元组。这个细节在传递动态参数时很容易让人迷糊。4.3 %d 不是万能的“数字转换器”很多人以为%d会把浮点数转成整数实际上它没那么智能%d % 3.14 # TypeError: %d format: a real number is required, not float %d % 3 # TypeError: %d format: a number is required, not str在Python 3里字符串和数字类型区分得很严格%d只能接收整数。如果你想把浮点数按整数输出要么先int(3.14)要么用%.0f。后者还有个好处是它遵循四舍六入五成双的规则而int()是直接截断小数部分两者在3.7这类值上结果不同%.0f % 3.7 # 4 int(3.7) # 3这个差异在数据处理任务里关乎成败取值前要先想清楚到底要“四舍五入”还是“向下取整”。4.4 浮点舍入不是你想的那种“四舍五入”字符格式化的浮点舍入遵循的是“银行家舍入”round-half-even而不是小学数学里的“四舍五入”。直接看例子%.2f % 0.125 # 0.12因为第二位小数2是偶数 %.2f % 0.135 # 0.14因为0.135在二进制里存得比实际略大相信很多人第一次看到0.125格式化出来是0.12时都会懵。原因有两层第一%.2f的舍入规则和内置round()一致当小数部分恰好是5的时候会向最近的偶数舍入第二部分值比如0.135在二进制浮点数中无法精确表示存储值比名义值略大结果就“错位”四舍五入了。如果你的业务比如金额计算要求严格的四舍五入建议直接用decimal.Decimal处理不要依赖%f。这个坑在格式化金额、百分比、费率时非常关键。4.5 logging 里的 % 和 %-formatting 根本不是一回事logging模块里经常看到这样的配置logging.basicConfig(format%(asctime)s [%(levelname)s] %(message)s)很多初学者以为这里的%(asctime)s是%-formatting就尝试用字典传参去控制它结果一脸懵。实际上logging的format参数是自己定义的一套字段语法%(asctime)s指的是LogRecord对象的属性由logging内部负责替换和Python字符串格式化没有任何关系。logging真正和%相关的是消息参数的延迟拼接logger.info(用户 %s 登录耗时 %.2f ms, username, cost)这个写法是logging推荐的好处是当日志级别不满足输出条件时格式化根本不会执行省去不必要的字符串构造。但要注意logger.info(%(user)s, {user: x})是行不通的因为logging的*args会作为一个元组传给%模板遇到了元组而不是字典直接抛TypeError。想用字典只能先手动格式化好再传消息但那就失去了延迟格式化的意义。4.6 数据库操作不要把 % 拼进 SQL最后这条特别重要。在数据库代码里经常能看到这种写法cursor.execute(SELECT * FROM users WHERE name %s % name)这是把用户输入直接拼到SQL语句里非常危险。这里的%虽然长得像Python的%但它其实是数据库接口DB-API定义的占位符两者不是同一个体系。正确处理方式是参数化查询cursor.execute(SELECT * FROM users WHERE name %s, (name,))把%s留在SQL模板里数据通过第二个参数传入由数据库驱动负责安全的转义和引用。别把Python的%格式化用在后端SQL里这个习惯养成了能避免一类非常严重的安全问题。如果确实需要在SQL里拼接动态表名、排序字段这类无法参数化的部分只对白名单字段做映射绝对不要直接拼接用户输入。5. 对比与迁移%, format(), f-string 到底怎么选5.1 同一句话的三种写法同一个字符串需求用三种方式写出来对比一下name Python version 3.12 # %-formatting msg version: %s %s % (name, version) # str.format() msg version: {} {}.format(name, version) # f-string msg fversion: {name} {version}三种方式输出完全一样但风格差异明显。%-formatting最老基于C语言的printf风格str.format()更灵活支持位置参数、关键字参数对齐方式有、、^还支持千分位、进制转换等f-string 是3.6引入的直接在字符串字面量里写表达式性能最好写法最直观。我一直跟团队里新人说新代码直接写f-string不用犹豫。但“新代码用f-string”不等于“旧代码必须马上改”。贸然重构历史代码容易引入隐藏的兼容性问题改完还要重新测试收益没那么高。5.2 我的选择建议新代码、老代码、日志分别处理具体场景我给个明确的建议表场景用什么原因新写业务逻辑、新脚本f-string可读性最好执行效率最高维护老项目只改一个输出跟着原文件风格走改动最小review成本低logging消息%s参数传递logging原生支持延迟格式化性能好日志格式配置format串logging自己的字段语法这是logging的配置规则和字符串格式化无关需要动态生成格式化模板str.format()或%f-string的模板在编译期就固定了无法从配置读取f-string模板极老环境Python 2.7或无第三方依赖%老环境对f-string根本不支持补充一点f-string有一个天然局限它的模板在代码编译时就已经确定了没法从配置文件里读取一段带有占位符的字符串再动态填充。如果你做的是模板系统比如根据用户配置的格式输出报告用str.format()或%反而更合适。5.3 迁移老代码时的实操技巧如果决定把老代码改成f-string不建议手工一行行改。我分享一个我自己的流程先用grep全局搜出所有%格式化的位置grep -n % your_project/*.py然后按文件逐个review优先处理那些逻辑简单、测试覆盖较好的模块。改的时候注意几点%右边是元组的直接把元组拆成变量展开%右边是单个变量的直接写成{var}如果模板里同时有多个%且其中一个是%%转义改成f-string后要把%%改回%。# 改前 print(进度%d%% % progress) # 改后 print(f进度{progress}%)这里特别容易出现残留的%%因为%d%%里的%是格式符改成f-string后就不需要转义了。还有logging相关的调用尽量不要为了“统一风格”去改成f-string拼接保持logger.info(...%s, value)这种参数传递形式性能更好也符合logging的设计初衷。个人经验小结我在实际项目里处理这类代码时最深的体会是语法本身不难难的是判断“该不该动它”。老代码里用%的地方往往和业务逻辑纠缠很深牵一发动全身。与其追求所有代码风格统一不如先在读代码时打通%这条线把历史代码看懂、跑通、测试好再决定要不要改。新代码的话直接f-string省心省力。最后再分享一个小技巧如果你只是临时调试想在输出里同时打印变量名和值Python 3.8以后的f{x}是最好用的但如果你不懂%遇到logging配置和数据库占位符时依然会卡壳。所以这项老手艺还是值得花半小时吃透的。
返回列表