
1. 从“Hello World”到格式化字符串为什么我们需要 f-string如果你写过Python那第一个接触的函数大概率就是print()。从经典的print(Hello World)开始我们用它来输出变量、调试代码、查看程序运行状态。但当你需要把变量和文字信息拼接在一起输出时是不是也写过下面这种代码name 张三 age 25 print(我的名字是 name 我今年 str(age) 岁。)或者你可能用过更“古老”一点的%格式化print(我的名字是%s 我今年%d岁。 % (name, age))又或者用过str.format()方法print(我的名字是{} 我今年{}岁。.format(name, age))这些方法都能用但代码看起来总有点啰嗦尤其是当变量多、格式复杂的时候拼接字符串就像在玩“找位置”的游戏既容易出错可读性也差。直到 Python 3.6 引入了f-string格式化字符串字面值这一切才变得简单优雅。print(f...)正是print()函数与 f-string 的黄金组合它让你能在字符串里直接嵌入变量和表达式用最直观的方式构建输出内容。简单来说print(f...)就是在print()函数里使用一个以f或F为前缀的字符串。在这个字符串内部你可以用花括号{}包裹任何有效的Python表达式运行时这些表达式会被求值并自动替换为它们的字符串表示形式。它的核心价值在于将数据与展示逻辑无缝融合极大提升了代码的简洁性、可读性和编写效率。对于日常的数据查看、调试日志、报告生成等场景它几乎成了我的首选。接下来我们就深入拆解它的具体用法、高级技巧以及那些容易踩的“坑”。2. f-string 基础语法与核心操作理解print(f...)首先要彻底掌握 f-string 的基础语法。它的形式非常简单在普通的字符串引号前加上字母f或F。2.1 变量嵌入最直接的用法最基本的操作就是在花括号{}里直接放入变量名。name 李四 score 95.5 print(f学生{name}的考试成绩是{score}) # 输出学生李四的考试成绩是95.5这里Python 会自动去查找当前作用域内的变量name和score将它们转换成字符串并填入对应位置。你不需要手动调用str()也无需关心变量是字符串、整数还是浮点数。注意花括号{}里的内容会被当作表达式来求值。这意味着你不仅可以放变量还可以进行简单的运算。a 10 b 3 print(f{a} {b} {a b}, {a} / {b} {a / b:.2f}) # 输出10 3 13, 10 / 3 3.33在上面的例子中{a b}执行了加法运算{a / b:.2f}则先进行除法运算然后应用了格式说明符后面会详细讲保留两位小数。这种在字符串内部直接计算的能力让print(f...)在快速验证计算结果时非常方便。2.2 调用函数与方法花括号内几乎可以执行任何有效的Python表达式自然也包括函数和方法调用。# 调用内置函数 items [1, 2, 3, 4, 5] print(f列表的长度是{len(items)}) print(f最大值是{max(items)}) # 调用对象方法 text Hello, World! print(f大写{text.upper()}) print(f反转{text[::-1]}) # 甚至可以使用切片 # 调用自定义函数 def get_greeting(name): return f你好{name} user 王五 print(f{get_greeting(user)} 欢迎回来。) # 输出你好王五 欢迎回来。这个特性非常强大尤其是在调试时。你可以直接输出一个经过处理后的结果而无需先创建一个临时变量。data {a: 1, b: 2} # 调试时直接查看JSON格式化的字典 import json print(f数据内容{json.dumps(data, indent2)})2.3 访问字典、列表等容器元素对于字典、列表、元组等数据结构f-string 也能直接访问其元素。person {name: 赵六, age: 30, city: 北京} print(f姓名{person[name]} 城市{person[city]}) # 注意访问字典键时字符串键需要用引号推荐使用单引号与外层双引号区分。 fruits [苹果, 香蕉, 橙子] print(f我最喜欢的水果是{fruits[0]})这里有一个重要的细节当在 f-string 的{}内访问字典的键时如果键是字符串你必须使用与外部字符串引号不同的引号。通常外部字符串用双引号内部字典键就用单引号反之亦然。这是为了避免语法解析错误。# 错误示例引号冲突会导致 SyntaxError # print(f姓名{person[name]}) # 错误 # 正确示例 print(f姓名{person[name]}) # 外部双引号内部单引号 print(f城市{person[city]}) # 外部单引号内部双引号3. 格式化控制让输出更专业仅仅输出值还不够我们经常需要控制输出的格式比如对齐、填充、小数位数、千位分隔符等。f-string 通过在花括号内的表达式后面添加一个冒号:然后跟上格式说明符来实现语法为{expression:format_spec}。3.1 数字格式化数字格式化是日常开发中最常用的功能之一。控制小数位数使用:.nf来指定浮点数保留n位小数。pi 3.1415926535 print(f圆周率{pi:.2f}) # 保留2位小数 print(f圆周率{pi:.4f}) # 保留4位小数 # 输出 # 圆周率3.14 # 圆周率3.1416指定总宽度和对齐使用:width来指定最小总宽度。默认是右对齐字符串左对齐。number 42 print(f|{number:5}|) # 宽度5右对齐默认 print(f|{number:5}|) # 宽度5左对齐 print(f|{number:^5}|) # 宽度5居中对齐 # 输出 # | 42| # |42 | # | 42 |填充字符在对齐符号前可以加一个填充字符。print(f|{number:*5}|) # 宽度5右对齐用*填充左边 print(f|{number:_5}|) # 宽度5左对齐用_填充右边 print(f|{number:.^5}|) # 宽度5居中对齐用.填充两边 # 输出 # |***42| # |42___| # |.42..|千位分隔符使用:,可以为整数或浮点数添加千位分隔符。big_num 1000000 print(f金额{big_num:,}) # 整数 print(f金额{big_num:,.2f}) # 浮点数同时保留小数和千位分隔 # 输出 # 金额1,000,000 # 金额1,000,000.00百分比显示使用:.n%可以将小数自动转换为百分比格式并保留n位小数。ratio 0.875 print(f完成度{ratio:.1%}) # 转换为百分比保留1位小数 print(f完成度{ratio:.0%}) # 转换为百分比不保留小数 # 输出 # 完成度87.5% # 完成度88%3.2 字符串格式化字符串同样可以控制宽度和对齐。text Python print(f|{text:10}|) # 宽度10左对齐默认 print(f|{text:10}|) # 宽度10右对齐 print(f|{text:^10}|) # 宽度10居中对齐 print(f|{text:*^10}|) # 宽度10居中对齐用*填充 # 输出 # |Python | # | Python| # | Python | # |**Python**|3.3 综合示例一个简单的报表结合以上所有功能我们可以用几行代码生成一个格式清晰的简单报表。products [ {name: 笔记本, price: 12.5, quantity: 120}, {name: 钢笔, price: 3.8, quantity: 250}, {name: 橡皮, price: 1.2, quantity: 500}, ] print(f{商品名称:10} | {单价:8} | {数量:6} | {小计:10}) print(- * 45) for p in products: subtotal p[price] * p[quantity] print(f{p[name]:10} | {p[price]:8.2f} | {p[quantity]:6} | {subtotal:10.2f})输出结果商品名称 | 单价 | 数量 | 小计 --------------------------------------------- 笔记本 | 12.50 | 120 | 1500.00 钢笔 | 3.80 | 250 | 950.00 橡皮 | 1.20 | 500 | 600.004. 高级特性与底层原理掌握了基础用法你已经能解决95%的问题。但要成为高手还需要了解一些高级特性和背后的原理这能帮你避免一些隐蔽的陷阱。4.1 在 f-string 中使用引号和花括号本身有时我们需要在 f-string 里输出花括号{}字符本身或者处理复杂的引号嵌套。输出花括号如果需要输出字面意义上的花括号需要将其双写。# 输出一个空的花括号对 print(f这是一个空的花括号{{}}) # 输出这是一个空的花括号{} # 在花括号内使用表达式同时输出外围花括号 value 100 print(f值 {{{value}}} 被包裹在花括号里。) # 输出值 {100} 被包裹在花括号里。引号嵌套如前所述处理字典键时要注意引号。更通用的原则是外层和内层使用不同的引号类型。quote Knowledge is power. print(f弗朗西斯·培根说过{quote}) # 输出弗朗西斯·培根说过Knowledge is power. # 如果字符串本身包含单双引号可以考虑使用三引号 multi_line f这是一个多行字符串。 它内部可以包含单引号和双引号。 变量值{value}。 print(multi_line)4.2 表达式求值顺序与副作用f-string 中的表达式在运行时求值并且从左到右依次进行。这意味着后面的表达式可以依赖前面已经求值并在概念上“替换”后的结果吗不它们依赖的是当前作用域中变量的实时值。一个需要警惕的点是避免在 f-string 中使用有副作用的表达式比如修改变量的值因为这会让代码难以理解和调试。x 1 # 不推荐在表达式中修改变量可读性差 print(f{x} {x 1} {x}) # 输出1 2 1 x并没有被修改 # 但如果这样做 print(f{(x:x1)} {x}) # 输出2 2 使用了海象运算符修改了x上面第二行使用了 Python 3.8 引入的海象运算符:它允许在表达式内进行赋值。虽然语法上允许但在print(f...)中这样用会让代码逻辑变得晦涩强烈不推荐。调试信息输出应该是清晰、无副作用的。4.3 f-string 与__format__()方法当 f-string 对花括号内的表达式求值后是如何将其转换为字符串的呢答案是调用该对象的__format__()方法。对于内置类型int,float,str等Python 已经实现了标准的格式化逻辑。对于自定义类你可以通过重写__format__()方法来定义它在 f-string 中的行为。class Person: def __init__(self, name, age): self.name name self.age age def __format__(self, format_spec): 定义Person对象在f-string中的格式化方式 if format_spec short: return self.name elif format_spec long: return f{self.name}({self.age}) else: # 默认行为 return fPerson(name{self.name}, age{self.age}) p Person(孙七, 28) print(f简短介绍{p:short}) print(f详细资料{p:long}) print(f默认输出{p}) # 输出 # 简短介绍孙七 # 详细资料孙七(28) # 默认输出Person(name孙七, age28)这个特性让你能完全控制自定义对象在print(f...)中的显示格式非常强大。5. 性能考量与最佳实践在大多数情况下f-string 的性能和可读性都是最优的。但了解其细微差别有助于你在特定场景做出最佳选择。5.1 与其他格式化方式的对比Python 主要有三种字符串格式化方式%格式化、str.format()和 f-string。我们来简单对比一下特性%格式化str.format()f-string引入版本Python 2.7 之前Python 2.6 / 3.0Python 3.6可读性较差位置对应容易错较好支持命名参数优秀直观嵌入性能一般较慢最快表达式支持不支持有限支持通过__format__完全支持动态格式较难可以可以但稍复杂性能测试简单拼接次数多时差异明显import timeit # % 格式化 t1 timeit.timeit(%s %d % (name, 10), number1000000) # str.format() t2 timeit.timeit({} {}.format(name, 10), number1000000) # f-string (需要在函数内测试因为定义时即求值) t3 timeit.timeit(f{name} {age}, setupnamename; age10, number1000000) print(f% 格式化: {t1:.3f}秒) print(fstr.format: {t2:.3f}秒) print(ff-string: {t3:.3f}秒)通常情况下f-string 的执行速度会明显快于其他两种方式因为它是在编译时就被部分解析运行时直接进行值替换而%和format需要在运行时解析格式字符串。5.2 何时使用何时避免强烈推荐使用 f-string 的场景调试输出快速查看变量值和表达式结果代码最简洁。日志记录在需要格式化的日志信息中注意logging模块本身支持%和format风格但你可以先用 f-string 构建字符串再传入。生成报告或消息需要将多个变量和固定文本组合成友好提示时。任何 Python 3.6 的新项目作为默认的字符串格式化选择。可能需要谨慎或避免的场景需要兼容 Python 3.6 以下版本这是最硬性的限制。如果你的代码需要在旧版本Python上运行就不能使用 f-string。格式字符串是动态生成或来自外部f-string 在定义时就会被求值它的格式是固定的。如果你需要根据运行时条件动态决定格式f-string 可能不太方便。# 动态格式用 .format() 更自然 format_template {name} is {age} years old. # 或者从配置文件读取 template print(format_template.format(nameAlice, age30))虽然你可以用eval()动态执行 f-string但这会带来严重的安全风险绝对禁止。复杂的国际化i18n场景一些国际化框架可能对%或format有更好的支持。5.3 可读性维护技巧避免过长的内联表达式虽然 f-string 支持复杂表达式但为了可读性如果表达式超过一行或逻辑复杂最好先计算出来赋给一个临时变量。# 不推荐过于冗长 print(f结果{some_list[0] if len(some_list) 0 and some_list[0] is not None else N/A}) # 推荐先计算后输出 first_item some_list[0] if len(some_list) 0 and some_list[0] is not None else N/A print(f结果{first_item})统一格式化风格在一个项目或模块中尽量保持一致的格式化风格。如果主要使用 f-string就不要再混杂大量的%格式化代码。注释复杂格式对于使用了复杂格式说明符如复杂的数字格式、自定义对象的__format__的 f-string添加简要注释说明其意图。6. 常见“坑”与调试技巧即使是最优雅的工具使用不当也会带来问题。下面是我在长期使用中总结的几个常见陷阱和解决方法。6.1 NameError变量未定义这是最常见的错误。f-string 在求值时会从当前作用域查找变量。如果变量名写错或尚未定义就会抛出NameError。# 假设我们只定义了 user_name user_name Tom # 错误变量名拼写错误 print(fHello, {user_nmae}!) # NameError: name user_nmae is not defined调试方法仔细检查花括号内的变量名是否与定义完全一致包括大小写。使用 IDE 的代码提示和语法高亮功能可以有效避免此类问题。6.2 字典键引号导致的 SyntaxError如前所述在 f-string 中访问字典的字符串键时引号使用不当会导致语法错误。my_dict {key: value} # 错误引号嵌套错误 # print(fValue: {my_dict[key]}) # SyntaxError # 正确使用不同类型的引号 print(fValue: {my_dict[key]})最佳实践养成习惯在 f-string 内部访问字典键时统一使用单引号如果外部字符串用双引号。6.3 在 f-string 内部使用反斜杠f-string 的{}内不能包含使用反斜杠\的转义字符。如果需要可以先将转义操作放在变量或函数中。# 错误花括号内不能有反斜杠转义 # print(f换行符{\n}) # SyntaxError # 正确将需要转义的内容放在变量或外部处理 newline \n print(f换行符{newline}) # 或者对于简单的字符使用其Unicode值或chr函数 print(f换行符的Unicode值{ord(\n)}) print(f换行符{chr(10)})6.4 处理包含花括号的文本如 JSON、代码当你需要输出本身就包含大量花括号的文本例如 JSON 字符串、代码片段时双写花括号的规则会使得字符串变得难以阅读和维护。# 输出一个JSON字符串模板这看起来很混乱 json_template f{{name: {name}, age: {age}}}更好的做法对于复杂的、结构化的文本输出考虑使用专门的模板引擎如string.Template、Jinja2或者先构建好数据对象再用json.dumps()等方法转换成字符串。import json data {name: name, age: age} json_str json.dumps(data) print(f生成的JSON{json_str}) # 或者直接打印 print(json.dumps(data, indent2))6.5 调试复杂的 f-string当一个复杂的print(f...)语句没有输出预期结果时可以尝试分步调试拆解表达式将花括号内复杂的表达式拆分成多个简单的子表达式分别打印看是哪一部分出了问题。# 复杂表达式 result some_func(a, b) another_func(c) print(f结果{result}) # 拆解调试 temp1 some_func(a, b) print(ftemp1: {temp1}) temp2 another_func(c) print(ftemp2: {temp2}) print(f结果{temp1 temp2})检查类型确保表达式求值结果的类型与你期望的格式化方式匹配。例如试图对一个字符串使用:.2f格式会抛出ValueError。value 123.456 # 错误字符串不能使用浮点数格式 # print(f{value:.2f}) # ValueError # 正确先转换为浮点数 print(f{float(value):.2f})使用repr()进行原始查看在调试时有时你不仅想看值还想看它的类型和原始表示。可以在格式说明符中使用!r转换标志来调用repr()函数。s Hello\nWorld print(f值{s}) # 输出值Hello World (换行符生效) print(frepr{s!r}) # 输出reprHello\nWorld (看到原始字符串和换行符)!r对应repr()!s对应str()默认!a对应ascii()。这在调试时非常有用。7. 举一反三在日志记录与数据报告中的实战理解了print(f...)的核心我们可以将其思想应用到更广泛的场景。虽然print()常用于临时输出和调试但其背后的 f-string 格式化能力在正式项目中同样价值巨大。7.1 与logging模块结合在生产环境中我们通常使用logging模块而非print来记录日志。logging支持传统的%格式化和str.format()风格但我们可以利用 f-string 先构建好日志信息。import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) user_id 1001 action login status success ip 192.168.1.1 # 使用 f-string 构建清晰、信息丰富的日志消息 log_message f用户 {user_id} 执行 {action} 操作状态{status}来源IP{ip} logger.info(log_message) # 对于更复杂的结构可以这样 detail {user_id: user_id, action: action, status: status} logger.info(f操作详情{detail})需要注意的是logging模块本身为了性能和安全延迟求值更推荐使用%或format风格因为它只在消息需要被真正输出时才对参数进行格式化。但对于大多数应用先使用 f-string 构建字符串再传给logger也是完全可接受的代码可读性更高。7.2 生成结构化的数据报告或消息模板假设你需要定期生成一段文本报告内容来自数据库查询结果。# 模拟从数据库获取的数据 sales_data [ {region: 华东, q1: 150, q2: 180, q3: 220, q4: 190}, {region: 华南, q1: 120, q2: 160, q3: 185, q4: 210}, {region: 华北, q1: 90, q2: 110, q3: 135, q4: 160}, ] def generate_report(data): 生成销售报告文本 report_lines [] report_lines.append( * 50) report_lines.append(年度销售数据报告) report_lines.append( * 50) report_lines.append(f{地区:6} | {Q1:8} | {Q2:8} | {Q3:8} | {Q4:8} | {总计:10}) report_lines.append(- * 65) for item in data: total item[q1] item[q2] item[q3] item[q4] line f{item[region]:6} | {item[q1]:8,d} | {item[q2]:8,d} | {item[q3]:8,d} | {item[q4]:8,d} | {total:10,d} report_lines.append(line) # 计算总计 grand_total sum(d[q1]d[q2]d[q3]d[q4] for d in data) report_lines.append(- * 65) report_lines.append(f{总计:6} | {:8} | {:8} | {:8} | {:8} | {grand_total:10,d}) report_lines.append( * 50) return \n.join(report_lines) report_text generate_report(sales_data) print(report_text) # 也可以将 report_text 写入文件、发送邮件等这个例子展示了如何利用 f-string 强大的格式化能力将原始数据快速转换为对齐美观、带千位分隔符的文本报表。代码逻辑清晰修改和维护都很方便。7.3 在 Web 框架模板中的思想延伸虽然像 Django、Flask 这样的 Web 框架有自己更强大的模板引擎如 Jinja2但其核心思想与 f-string 有相通之处将动态数据嵌入到静态文本结构中。理解 f-string 能帮助你更好地理解模板渲染的概念。例如一个简单的类 f-string 模板渲染函数仅用于演示思想非生产级def simple_template(template: str, **context) - str: 一个极其简单的模板渲染模拟 f-string 思想有安全风险勿用于生产 # 警告直接使用 eval 或 exec 执行用户提供的模板是极度危险的 # 这里仅用于演示 f-string “求值替换”的思想。 # 生产环境请使用 jinja2 等经过安全处理的模板引擎。 try: # 这里用一个非常不安全的方式模拟实际绝不能这么做 return eval(ff{repr(template)}, {}, context) except Exception as e: return f[Template Error: {e}] # 使用方式注意template 必须来自可信来源 name 访客 count 5 template_str 您好{name}您有 {count} 条新消息。 # 安全警告如果 template_str 来自用户输入这将导致严重的安全漏洞 result simple_template(template_str, namename, countcount) print(result) # 输出您好访客您有 5 条新消息。再次强调绝对不要自己用eval()或exec()去解析用户提供的模板字符串这会带来代码注入的致命风险。专业的模板引擎Jinja2, Mako等在提供类似 f-string 的便利语法时都做了严格的安全沙箱处理。理解这一点能让你明白 f-string 的便利性背后其实也需要对“执行动态代码”保持警惕。在日常的print(f...)中因为字符串是你自己写的所以是安全的。