ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python字符串格式化全解:从%到f-string的高级用法

Python字符串格式化全解:从%到f-string的高级用法

day02-python字符串格式化

很多人学Python,写代码写到第三天,就会遇到一个绕不开的坎:字符串格式化。这东西说大不大,说小不小,但几乎每段真实代码里都会用到。日志要拼消息、界面要展示数据、文件要写固定格式的文本,全得靠它。我见过不少写了两三年Python的人,还在用老旧的%拼接方式,不是说不能用,而是明明有更舒服、更省事的写法,却因为习惯一直没换,挺可惜的。

这篇文章就把Python字符串格式化这件事彻底讲透。我会从三种主流方式的历史演进开始聊,再逐个拆解格式化说明符的每个参数,然后重点说说f-string在真实项目里的高阶玩法,最后附上我踩过的一些坑和排查思路。无论你是刚接触Python的新手,还是写了一阵子想补补课的老手,这应该都能帮到你。

1. 三种格式化方式:从%到f-string的演进脉络

先看一段最简单的代码,体会一下三种方式的差异。我们要把一个用户名和分数拼成一条"玩家X得分Y"的消息:

name = "小北" score = 98 # 方式一:% 格式化(Python 2时代就开始用) message = "玩家%s得分%d" % (name, score) # 方式二:str.format()(Python 2.6+) message = "玩家{}得分{}".format(name, score) # 方式三:f-string(Python 3.6+) message = f"玩家{name}得分{score}"

三种方式都能跑,输出完全一样。但你看第三行,几乎就是"把变量直接写进字符串里",读代码的时候不需要来回对照括号位置,大脑的负担小很多。

1.1 %格式化:老当益壮,但该退居二线了

%格式化是Python从C语言的printf风格继承过来的,用%s代替字符串、%d代替整数、%f代替浮点数。年龄大的代码库里大量存在这种写法,所以你不能不认识它。

price = 5999 message = "价格是:%d元" % price ratio = 0.879 message = "占比:%.2f%%" % (ratio * 100) # 注意:输出百分号要用%%

这里有个特别容易栽跟头的点:如果你要在格式化结果里输出一个真实的百分号,必须写两个%。我第一次写日志、需要把成功率带百分号拼进去的时候,就因为这个%%卡了十几分钟。

%方式最大的问题是可读性差:占位符和后面的变量靠位置对应,一旦参数多了,你根本记不清哪个占位符对哪个变量,而且每个占位符必须和变量类型严格匹配,%s给了数字虽然大概率也能跑,但%d给了字符串直接TypeError。

1.2 str.format():过渡时代的产物

到了Python 2.6和3.0时代,引入了str.format()。它比%强的地方在于,可以用花括号{}做占位符,并且支持用序号或者名字引用变量:

# 按序号引用 message = "玩家{0}得分{1},排名第{0}".format(name, score) # 按名字引用 message = "玩家{name}得分{score}".format(name=name, score=score) # 字典也可以,加两个星号直接展开 player_info = {"name": "小北", "score": 98} message = "玩家{name}得分{score}".format(**player_info)

按序号引用有个很实用的价值:同一个变量可以在字符串里出现多次,不用像%那样反复传参。比如上面第一行里{0}出现了两次,format参数只用写一遍name。

但str.format()也有它的别扭之处。一个是字符串如果很长,format后面的参数列表会很冗长,视觉上和前面的字符串主体割裂。另一个是它需要额外多打一长串代码,写起来终究不如直接把变量塞进字符串里来得痛快。

1.3 f-string:Python 3.6之后的首选

先声明一个观点:只要是Python 3.6以上版本,新写的代码一律优先用f-string。这不是武断,而是事实:f-string不仅写法最简洁,执行速度也比前两者快,因为它在编译阶段就完成了字符串模板的解析,运行时不需要再做模板匹配,直接计算值再拼接即可。

name = "小北" score = 98 # 直接在花括号里写表达式、调用函数 message = f"玩家{name}得分{score}分,得分率:{score / 100 * 100:.1f}%" # 支持大小写转换 whisper = "hello" message = f"原文:{whisper},大写:{whisper.upper()}" # 支持三元表达式 status = "合格" if score >= 60 else "需加油" message = f"玩家{name}本次考试:{status}"

f-string里花括号中能写任意合法的Python表达式,这一点太强大了。我在实际项目里经常直接在里面调用方法、做计算、写条件判断,省掉了先算好一个中间变量再插进去的步骤。虽然行业里也有人认为f-string表达式别写太复杂、要保持可读性,但适度使用完全没问题。

三种方式我做了个简单对比,方便你心里有数:

对比维度% 格式化str.format()f-string
诞生版本Python 2初期Python 2.6 / 3.0Python 3.6
占位写法%s %d %f{ } {0} {name}{变量} {表达式}
可读性差,变量靠位置对应中等,长参数列表较累赘好,变量直接可见
执行效率较慢中等最快
表达式支持不支持不支持支持,任意合法表达式
适用建议只读老代码时用兼容旧版本时可考虑新代码优先选用

2. 格式化说明符:对齐、填充、宽度与精度的完整拆解

光会写f"玩家{name}得分{score}"还是皮毛。真实需求里,你常常需要控制数字小数位、让文本左对齐右对齐、给金额补零等等。这些统称为格式化说明符(format specifier),在f-string的花括号里,用冒号隔开后面的那部分就是。

格式语法可以概括为:

{内容:[[填充字符]对齐方式][宽度][,][.精度][类型]}

看着吓人,其实拆开非常好懂。我一点点说。

2.1 对齐和填充:控制排版的核心

默认情况下,字符串是左对齐的,数字是右对齐的。想要手动控制,用三个对齐符号:

  • >右对齐
  • <左对齐
  • ^居中对齐

填充字符写在对齐符号前面,不写则默认用空格填充。看几个例子:

name = "小北" # 宽度10,默认右对齐补空格 print(f"[{name:>10}]") # [ 小北] # 宽度10,左对齐 print(f"[{name:<10}]") # [小北 ] # 宽度10,居中 print(f"[{name:^10}]") # [ 小北 ] # 指定填充字符为星号 print(f"[{name:*^10}]") # [***小北***]

这个功能在打印报表、输出对齐的日志列、生成文本表格时特别常用。我做过一个数据检查工具,要把七七八八的字段对齐输出,方便肉眼比对,靠的就是^和>组合。比如:

headers = ["字段名", "期望值", "实际值", "偏差"] rows = [ ["交易笔数", 1000, 998, -2], ["成功金额", 50000.00, 49999.99, -0.01], ["失败率", 0.05, 0.052, 0.002], ] # 打印表头 print(f"{headers[0]:<10}{headers[1]:>12}{headers[2]:>12}{headers[3]:>12}") # 打印数据行 for row in rows: print(f"{row[0]:<10}{row[1]:>12}{row[2]:>12}{row[3]:>12}")

这样出来的结果每一列都是整整齐齐的,不需要手动数空格。注意我把第一列设成左对齐,数字列设成右对齐,这是排版的基本常识:中文和字符串通常左对齐好看,数字右对齐方便比大小。

2.2 数字格式化:精度、千分位与进制转换

数字格式化是日常用得最频繁的。先看最常见的精度控制:

pi = 3.141592653589793 # 保留两位小数 print(f"{pi:.2f}") # 3.14 # 保留四位小数 print(f"{pi:.4f}") # 3.1416 # 保留两位小数,同时总宽度占10 print(f"{pi:10.2f}") # 3.14(因为默认右对齐)

冒号后面.2f的意思是保留两位小数、按浮点数输出。注意这里有个隐藏的坑:如果你对整数使用.2f,它会强行把整数转成浮点并补零,f"{100:.2f}"输出的是100.00,而不是10.00,也不是1.00。这个看起来理所当然,但我真的见过有人误以为这是保留两位有效数字,结果算错数据。

再说千分位分隔符。处理金额、数量这类大数字时,加上逗号分隔能让可读性提升一个档次:

amount = 1234567890.5 # 加千分位 print(f"{amount:,}") # 1,234,567,890.5 # 加千分位并且保留两位小数 print(f"{amount:,.2f}") # 1,234,567,890.50

@--`这个后缀在Python 3.6+ES复杂一点的话,还会涉及进制转换:

number = 255 # 十六进制输出 print(f"Hex: {number:x}") # hex: ff,不能直接用于数据导出 # 十六进制,带上0x前缀 print(f"Hex: {number:#x}") # hex: 0xff # 二进制输出 print(f"Bin: {number:b}") # bin: 11111111 # 科学计数法 print(f"Sci: {number:e}") # sci: 2.550000e+02

进制转换在写底层协议、颜色值处理、权限位运算时会用到。比如前端要的颜色字符串需要转成十六进制,你就可以直接f"#{r:02x}{g:02x}{b:02x}"生成#00ffaa这种格式,一步到位,非常干净。

2.3 字符串的截断与组合技巧

除了数字,字符串本身也可以被格式化说明符操控。最有用的就是截断:用.精度来限制字符串长度。

long_text = "这是一段非常长的文本内容,可能需要截断显示" # 只取前6个字符 print(f"{long_text:.6}") # 这是一段非常 # 截断后左对齐,总宽度20,超出部分用省略号(需要手动拼) print(f"{long_text[:6]:<20}...")

注意:字符串截断是按字符个数算的,但如果你用.6s这种形式,效果和.6一模一样。另外,Python的切片截断不会自动加省略号,所以实际项目里我通常预先写好一个截断函数:

def truncate(text, limit=6): return text if len(text) <= limit else text[:limit] + "..." print(f"{truncate(long_text):<20}")

把截断逻辑收到自定义函数里,再用f-string去格式化,代码会干净很多。这其实是一个通用思路:f-string花括号里既然能写表达式,你就可以把复杂的变换逻辑放到函数里,字符串这边只保留展示规则。

2.4 组合使用示例:一条日志消息的诞生

把上面的知识串起来,模拟一个真实场景:生成订单详情日志,字段包括订单号、客户名、商品数量、总金额、折扣率、时间戳。

order_id = "NO20240518001" customer = "某开发者的测试店" quantity = 3 total_amount = 2999.999 discount = 0.8934 from datetime import datetime log_entry = ( f"[{datetime.now():%Y-%m-%d %H:%M:%S}] " f"订单号:{order_id:<15}" f"客户:{customer:<10}" f"数量:{quantity:>3}" f"金额:{total_amount:>10,.2f}" f"折扣:{discount:.1%}" ) # 输出示例: # [2025-01-12 17:22:45] 订单号:NO20240518001 客户:某开发者的测试店 数量: 3 金额: 2,999.999 折扣:89.3%

注意discount:.1%:把小数直接转成百分比格式,Python会替你把0.8934变成89.3%,并且自动带百分号。这个用法太常用也太容易被忽略,我后面专门再提一次。

3. f-string高阶玩法:表达式、调试、字典与datetime

基础语法掌握了,就能应付七成需求。剩下的两成半,靠f-string的进阶玩法解决。这一节全部是实战总结,每一条我都在真实代码里用过,踩过坑也享受过便利。

3.1 花括号内写表达式:算数、方法和条件分支

前面已经提到过,f-string花括号里可以放任意表达式,这里再系统展示一下。除了基本的算术运算和方法调用,它还可以做嵌套格式化:

items = [("苹果", 5, 3.5), ("香蕉", 2, 5.0)] for name, count, price in items: print(f"{name}: {count}个 x {price:.2f}元 = {count * price:.2f}元")

你注意,count * price:.2f这种情况下,:.2f修饰的是整个表达式的计算结果。这种"计算和格式化一步到位"的写法,能省掉中间变量,并且让数据来源一目了然。

条件分支也完全可行:

score = 72 print(f"考试成绩:{'通过' if score >= 60 else '补考'}")

还有嵌套字符串格式化。f-string的{}里如果还需要格式化,可以用嵌套的花括号:

width = 8 precision = 2 value = 3.14159 # 用变量控制格式参数 print(f"{value:{width}.{precision}f}") # 3.14 # 等价于直接写 print(f"{value:8.2f}")

这个嵌套格式化的需求比较少见,但在实现格式化模板可配置的功能时会用到。比如某个配置文件里允许用户自定义对齐宽度和精度,代码就能用这种动态格式参数的方式实现。

3.2 f-string的调试神器:=号语法

Python 3.8引入了一个特别好用的功能:在f-string的花括号里加=号,它会自动打印变量名和值:

name = "小北" score = 98 print(f"{name=} {score=}") # 输出:name='小北' score=98

如果你改成{score = :}加上格式化说明符,还能控制打印的精度等格式:

ratio = 0.8934 print(f"{ratio = :.1%}") # 输出:ratio = 89.3%

这个功能在调试时简直救星。以前排查问题,要么写print("score:", score),要么写print(f"score is {score}"),手忙脚乱。现在直接print(f"{score=}"),变量名和值一次性打出来,省时省力还不容易写错。

不过常用日志框架(比如logging)不支持f-string的=语法,因为日志系统的延迟求值机制和f-string的即时求值有点冲突。所以这招适合在print调试和交互环境里用,线上日志还是老老实实用%(name)s或者将f-string结果作为参数传入。

3.3 时间与日期格式化:直接内嵌datetime格式码

datetime对象可以直接放在f-string里,用%Y、%m、%d、%H等格式码控制输出。前面订单日志里已经演示过一次,这里展开看看:

from datetime import datetime now = datetime.now() print(f"{now:%Y-%m-%d %H:%M:%S}") # 2025-01-12 17:22:45 print(f"{now:%Y/%m/%d}") # 2025/01/12 print(f"{now:%A}") # Sunday(星期几,英文全名) print(f"{now:%j}") # 当天在一年中的第几天 # 结合业务场景:距离某个日期还有多少天 future = datetime(2026, 6, 1) remaining = future - now print(f"距离目标日期还剩 {remaining.days} 天")

%A是英文星期全名,如果你用%a得到缩写,%w得到数字(0-6,周日为0)。需求中经常遇到"显示上周一具体日期"之类的,用datetime对象配合timedelta处理完,最后统一用f-string格式化展示即可。它比strftime()方法更直观,因为格式码直接写在模板里,上下文一目了然。

3.4 复杂对象的格式化:字典、列表与自定义类

f-string默认调用对象的__format__方法。对字典、列表这类容器,直接格式化会得到它的repr字符串,像这样:

player = {"name": "小北", "score": 98} print(f"{player}") # 输出:{'name': '小北', 'score': 98}

如果想优雅地取出字典里的字段,可以自己写格式化逻辑:

player = {"name": "小北", "score": 98} print(f"玩家{player['name']}得分{player['score']}")

如果觉得每次['key']麻烦,可以用operator.itemgetter,也可以局部变量搞一下,我一般图省事直接在花括号里访问键。真正要处理复杂的对象展示,比如用户信息、订单信息等,更推荐给类自定义__format__方法。

class Player: def __init__(self, name, score): self.name = name self.score = score def __format__(self, spec): # spec 是冒号后面传来的格式化说明符 if spec == "brief": return f"{self.name}:{self.score}" if spec == "full": return f"玩家【{self.name}】当前得分:{self.score}" return str(self) p = Player("小北", 98) print(f"{p:brief}") # 小北:98 print(f"{p:full}") # 玩家【小北】当前得分:98

这个思路我强烈建议掌握。当对象本身就要经常被格式化成不同文案时,把格式逻辑放进__format__,f-string直接传说明符,调用方代码会非常干净。

4. 实战演练:一个登录日志系统的格式化改造

前面讲的都是知识点,这一节我带你走一遍完整的实操过程。假设你要写一个用户登录日志模块,记录用户ID、登录IP、登录时间、结果状态和耗时,并且要求输出对齐、可读、支持按级别过滤。

4.1 第一步:明确结构,确定字段与展示规则

梳理需求:每条日志需要包含时间戳(精确到秒)、用户ID(长度不固定)、IP(右对齐)、状态(成功/失败)、耗时(毫秒,保留一位小数)。我先把结构定出来,用一个数据类来承载:

from dataclasses import dataclass from datetime import datetime @dataclass class LoginRecord: user_id: str ip: str status: str elapsed_ms: float time: datetime = None def __post_init__(self): if self.time is None: self.time = datetime.now()

为什么不直接用字典?因为有了数据类,后续做类型提示、校验、扩展都会更舒服。真正到了格式化阶段,它的字段就是f-string的花括号变量来源。

4.2 第二步:编写格式化函数,控制展示细节

接下来写格式化函数。这里要注意几个决策:时间戳用%H:%M:%S还是完整日期?用户ID应该左对齐还是右对齐?状态信息要不要带颜色(终端输出场景)?我选择做成参数可配置的版本:

def format_login_record(record): time_str = f"{record.time:%Y-%m-%d %H:%M:%S}" user_part = f"{record.user_id:<12}" ip_part = f"{record.ip:>16}" status_part = f"{record.status:^6}" elapsed_part = f"{record.elapsed_ms:>8.1f}ms" return f"[{time_str}] user={user_part} ip={ip_part} status={status_part} cost={elapsed_part}"

跑一下:

r1 = LoginRecord("user_007", "192.168.1.100", "成功", 12.6) r2 = LoginRecord("admin_root", "10.0.0.55", "失败", 203.95) r3 = LoginRecord("guest", "172.16.3.90", "成功", 6.02) for r in (r1, r2, r3): print(format_login_record(r))

输出:

[2025-01-12 17:22:45] user=user_007 ip= 192.168.1.100 status= 成功 cost= 12.6ms [2025-01-12 17:22:45] user=admin_root ip= 10.0.0.55 status= 失败 cost= 204.0ms [2025-01-12 17:22:45] user=guest ip= 172.16.3.90 status= 成功 cost= 6.0ms

每一列都对得很齐。其中elapsed_ms:>8.1f的意思是:总宽度占8个字符(含小数点),保留1位小数,右对齐。三行数据的"ms"结尾也对齐了,视觉效果很好。

4.3 第三步:加入条件逻辑,输出分级信息

真实场景里日志通常有级别:INFO、WARNING、ERROR。我给格式化函数加一个级别判断:

def format_login_record(record, verbose=False): time_str = f"{record.time:%Y-%m-%d %H:%M:%S}" # 只有失败才输出详细IP;成功日志不展示IP可以降低噪声 ip_part = f"{record.ip:>16}" if verbose or record.status == "失败" else "" status_part = f"{record.status:^6}" elapsed_part = f"{record.elapsed_ms:>8.1f}ms" core = f"[{time_str}] user={record.user_id:<12} status={status_part} cost={elapsed_part}" if ip_part: core += f" ip={ip_part}" return core

这里用了一个经验:日志内容不是越多越好,默认展示必须关注的核心字段,细节信息在需要时再展开。

4.4 输出到文件和终端,处理编码与对齐问题

如果要把日志写入文件,编码要设置成UTF-8,否则中文状态字段会变成乱码:

with open("login.log", "w", encoding="utf-8") as f: for r in [r1, r2, r3]: f.write(format_login_record(r) + "\n")

然后可以在终端里more login.log查看对齐效果。注意一点:如果文件在Windows记事本打开,有些老版本对UTF-8没有BOM会显示乱码,但Python 3的读写配合encoding参数基本没有这个问题,多编码环境里最省心的方式就是统一UTF-8。

4.5 效果验证与后续扩展

最后验证一下各种边界情况:空字符串用户ID、很长的IP、负数耗时。你会发现格式化说明符的宽度控制不会截断超长内容,只是不补空格而已,这符合预期也符合安全原则。若需要强制截断,回到前面说的truncate函数思路即可。

这个模块后续可以扩展成:按级别过滤、JSON格式输出、敏感信息脱敏、批量统计失败率。而每一处输出字符串,都离不了格式化说明符和f-string的配合。学习字符串格式化,不是学完语法就结束,而是要落到这种真实的输出场景里反复打磨。

5. 常见问题与避坑实录

这部分是我自己在写Python过程中反复踩过的坑,整理了七条最有代表性的,每条都有事故现场和解决办法。

5.1 花括号{ }的转义问题

f-string里如果你想输出一个字面意义的花括号,比如生成JSON模板片段、写CSS或者正则表达式,必须用双花括号{{和}}:

# 想输出 {"key": "value"} print(f"{{'key': 'value'}}") # {'key': 'value'} # 想输出 "{name}",但name是变量 actual_name = "小北" print(f"{{name_val}}={actual_name}") # {name_val}=小北

最容易踩的坑就是正则表达式。正则里经常有{2,5}这种量词写法,如果字符串是f-string,花括号会被当作占位符解析,直接报错或输错。解决方法是正则部分先单独写成一个普通字符串,再用f-string去拼接:

import re pattern = r"\d{4}-\d{2}-\d{2}" # 普通字符串,花括号没事 text = "今天日期是2025-01-12" match_result = re.search(pattern, text) # 如果要和变量拼接再构成完整正则,用f-string但外层加双花括号 final_pattern = rf"(日期是){{4}}"

实际上,rf前缀同时开启raw和f-string时,花括号依然要双写。正则和f-string放一起,我建议尽量别混用,少一些隐患。

5.2 字典键是字符串时的引号嵌套

在f-string里访问字典时,最常见的是写f"{user['name']}",外层用双引号包整个字符串,内层用单引号访问键,没问题。但如果你整个字符串已经用了单引号,就直接会语法错误。

user = {"name": "小北"} print(f"{user['name']}") # 正确 # print(f'{user['name']}') # 报错:内层单引号与外层单引号冲突

解决办法很简单:外层字符串统一用双引号,内层字典键统一用单引号。这个看起来哪里都能查到的规矩,实操里真的有人因为不理会而浪费时间。

5.3 中文引号与填充宽度不一致

这是隐藏最深的一个问题。f-string的{:^10}等对齐功能,是按字符个数来填充的,不是按显示宽度。在中文环境下,中文是全角字符,在终端里通常占两个西文字符的宽度,但Python的计数只算1个。所以用{:^10}居中一个长度为4的中文字符串时,你期望两边补3个空格,实际两边各补3个,看起来右边总觉得缺了一点。

title = "登录日志" print(f"[{title:^10}]") # 显示效果:[ 登录日志 ],但在等宽字体终端里,中文占两格 # 看起来像:[ 登录日志 ](视觉上多出一块)

专业的解决办法是自己写一个按显示宽度对齐的函数,需要区分全角和半角字符。一般项目里不强求,但当你在终端输出中文表格、给运维看报表时,这个差异会很明显。坦白说我也没有在所有项目里统一处理,只是在需求明确要"严格对齐"时才去实现,因为过度精密会增加代码复杂度。

5.4 浮点数精度问题:格式化不代表精确计算

f"{0.1 + 0.2:.2f}"输出0.30,看起来很干净,但这只是格式化抹平了误差。二进制浮点本身有一个经典误差,0.1+0.2实际是0.30000000000000004。格式化前如果只做展示没问题,但如果你用格式化后的结果去做后续数值比较或者二次运算,就危险了。

value = 0.1 + 0.2 formatted = f"{value:.2f}" # "0.30" print(float(formatted) == 0.3) # True(通常成立,但别依赖) # 更稳妥的做法:涉及金额、利率等精确计算用Decimal from decimal import Decimal result = Decimal("0.1") + Decimal("0.2") print(f"{result:.2f}") # 0.30

我的原则是:展示用格式化,计算用数值类型。格式化输出前要想清楚这个字符串是不是还要流转回去参与计算,如果是,那就要格外小心。

5.5 bigint 与 超大数字性能损耗

大数字用千分位符会触发额外的字符串拼接?其实不影响。Python 3的整数可以无限大,f"{10**30:,}"轻松输出带逗号的大数字。真正影响性能的是循环里反复做复杂格式化,比如百万次级别的日志输出,f-string比str.format快,但仍然有开销。这种情况优先考虑批量拼接到一个列表,再一次性join写入文件,不要每条都直接print。

5.6 字符串为None时怎么办

f"{value}"如果value是None,输出字符串"None",一般不会报错。但如果你期望空字符串展示为"-"或"未知",就要自己处理:

nickname = None print(f"昵称:{nickname or '-'}" ) # 昵称:-

这个or技巧很容易被忽略,但在清洗数据、展示数据库可空字段时非常常用。空字符串、None、0都会走向或者分支,所以要与业务语义匹配,不是所有场景都合适。

5.7 多行字符串格式化时的缩进问题

用三引号写多行f-string时,每一行前面的空格会被原样保留。如果你想让每一行内容对齐,要么用textwrap.dedent去掉公共缩进,要么在每行单独用f-string再拼接。我一般喜欢用隐式连接加独立f-string行:

content = ( f"玩家:{name}\n" f"得分:{score}\n" f"结果:{'通过' if score >= 60 else '补考'}\n" )

这种方式既保留代码缩进,又不引入多余空白,是实战里最顺手的多行字符串构造方法。

6. 格式化的尽头:更规范的可维护性建议

写到这里,核心语法和坑你都见过了。最后聊几条我在维护长期项目时沉淀下来的规范,这些不属于语法范畴,但比语法更影响代码质量。

第一,不让f-string表达式过于复杂。虽然花括号里能写任意表达式,但一行里塞了三四个嵌套函数调用,除了当时写的人,别人很难读懂。我给自己定的红线是:表达式逻辑超过约15个字符,或者有嵌套引号时,就把计算逻辑提到外面用变量承载。

第二,日志系统尽量别用f-string直接传模板。标准logging库支持用%s做延迟格式化,因为它能等到日志输出等级确认后再格式化,避免无谓的性能损耗。f-string在传入的那一刻就执行了求值,即使这条日志后来被等级过滤掉,计算也已经发生了。高流量系统里,这种差异会被放大。

import logging logger = logging.getLogger("test") # 推荐:延迟格式化,日志等级不满足时零开销 logger.info("用户%s登录失败", user_id) # 不推荐:f-string立即求值,被过滤也会执行格式化 logger.info(f"用户{user_id}登录失败")

第三,统一输出格式的地方,封装成独立函数或类。比如公司日志规范要求所有日志都必须带request_id和时间戳,那就写一个fmt_log()函数,全项目调用,而不是每个人各自用f-string拼。这样后续要调整格式,只改一处。

第四,敏锐处理单复数、单位等国际化问题。某些格式化在英文里非常正常,但在中文环境会出现"1个用户"变成"1 个用户"之类的间距问题。这类细节没有统一解法,但你要有意识。

第五,对极端值做测试。浮点数很大(1e18)时,.0f会输出2000000000000000000这种很长一串,若想变得易读加上千分位符即可。负数、零、NaN、Infinity也要有预期,格式化不会报错,但展示规则最好提前定义。

我对字符串格式化这事的最终体会是:它不是一个需要背的语法点,而是一种"表达习惯"——你用什么方式把数据变成人可读的信息,直接决定了你写出来的代码是花十分钟就能维护,还是让人看半小时不知道在干什么。多在实际项目里用f-string,碰到需要模仿老代码库的时候再用%,再配合格式化说明符把版面、精度、单位这些细节都照顾到,你写的Python代码会明显上一个档次。

最后补一个实用小技巧:写f-string的时候,在IDE里可以把光标悬停在花括号内的变量上,看类型和值,也能检查拼写错误。如果你用的代码编辑器对变量高亮不敏感,可以考虑给f-string表达式加注释说明,尤其是用了缩写变量名的时候。这样做,代码在三个月后回头看依然舒服。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进