Python string.Template:缺失字段该报错,还是把占位符留到下一轮
一个自动生成说明文字的小工具,把文件名和价格放进模板。上线后却出现了未替换的字段,原因是调用 safe_substitute 时少传了参数:这个方法会保留无法替换的占位符。它可以保留待补内容,但后续处理需要明确设计,不能直接代表“最终文案已经完整”。
先写清输出契约:本文要求最终模板语法有效,引用的字段全部提供,而且传入字典不能有多余字段。最后一条是示例主动选择的严格政策,用于发现拼错的参数名;标准 substitute 本身并不要求额外字段必须报错。
AI概念配图,非真实界面:以抽象物件说明本文主题,不代表运行结果。
把模板语法与字段集合分别检查
下面程序保存为 demo.py,使用 Python 3.11 或更高版本运行,因为代码使用这一版本加入的 is_valid 和 get_identifiers。前者检查占位语法,后者列出有效标识符,示例再比较字段集合,最后才调用严格替换生成结果。
模板中美元符号有两种用途:一个美元加字段名表示替换,连续两个美元表示一个字面美元。价格前的写法因此同时包含转义美元和价格占位符。文件扩展名前使用花括号界定名称,也能让相邻文字与字段边界一眼可见。
from string import Template
def render_final(template_text, values):
template = Template(template_text)
if not template.is_valid():
raise ValueError("invalid template syntax")
required = set(template.get_identifiers())
supplied = set(values)
if required != supplied:
raise ValueError("template fields do not match")
return template.substitute(values)
text = "Build ${name}.txt costs $$${price}"
assert render_final(text, {"name": "demo", "price": 5}) == "Build demo.txt costs $5"
print(render_final(text, {"name": "demo", "price": 5}))
partial = Template(text).safe_substitute(name="demo")
assert partial == "Build demo.txt costs $${price}"
print(partial)
reparsed = Template(partial).substitute(price=5)
assert reparsed == "Build demo.txt costs ${price}"
assert reparsed != "Build demo.txt costs $5"
values = {"name": "demo"}
values["price"] = 5
assert render_final(text, values) == "Build demo.txt costs $5"
for template, values in [
(text, {"name": "demo"}),
(text, {"name": "demo", "price": 5, "extra": 1}),
("cost $", {}),
]:
try:
render_final(template, values)
except ValueError:
pass
else:
raise AssertionError("invalid final output accepted")
assert Template("$name_suffix").get_identifiers() == ["name_suffix"]
assert Template("${name}_suffix").substitute(name="demo") == "demo_suffix"
assert Template("$value").substitute(value="<b>literal</b>") == "<b>literal</b>"
try:
Template("$missing").substitute({})
except KeyError:
pass
else:
raise AssertionError("missing field accepted")
print("strict template contract checks passed")保留占位符是一种明确的中间状态
第一行应得到完整文字,第二行保留价格占位符。safe_substitute 没有凭空补出缺失值,也没有验证内容是否可交付,只是采用了更宽松的失败处理。若确实需要多轮替换,应为中间结果加上明确状态,避免被后续步骤误当成最终成品。
多轮处理不能无条件把上轮输出重新套进 Template。本例的 $${price} 再解析一次只会折叠美元转义,得到 ${price},不会变成期望的价格。代码用断言保留这个反例;需要逐轮补数据时,应保留原模板并补齐同一份映射,再从原模板生成结果,或另外设计明确的阶段语法。
示例还展示了下划线会继续属于标识符:$name_suffix 会查找整个 name_suffix 字段,而 ${name}_suffix 才表示 name 后面跟固定后缀。遇到“明明传了 name 还是缺字段”的情况,先检查模板边界,比给字典补一堆别名更容易维护。
is_valid 返回真,只表示模板语法没有明显错误;get_identifiers 能列出名称,也不代表这些值已经存在。反过来,字段齐全也不保证每个值符合业务要求。文件名不能为空、数量必须处于某个范围等条件,应在组装映射之前继续校验。
方法名字里的 safe 不表示自动防注入
最后一个断言故意把带尖括号的文本放进模板,结果仍然保留尖括号。Template 不会自动进行 HTML 转义、SQL 参数绑定或 shell 参数保护。安全替换中的“安全”主要涉及缺失或不规范占位符的宽松处理,不能据此宣布生成文本可以用于任意上下文。
如果结果要作为网页文本呈现,应采用该输出位置对应的转义规则或成熟模板引擎;如果结果要进入数据库查询或外部进程,应使用各自的参数接口。先替换完成再统一做一种字符清理,往往无法同时满足这些不同语法环境的要求。
也不要简单扫描最终结果中是否还含美元符号来判断替换完成,因为模板可以合法输出字面美元,字段值本身也可能包含美元。示例在替换前检查语法与所需字段,而不是根据输出的表面字符猜测是否存在遗留占位符。
维护一组模板时,可以把字段集合当作小型接口。模板新增字段后,相应调用方必须提供数据;调用方删除字段前,也应确认没有模板继续引用它。把语法错误、缺失字段、多余字段和业务值错误分别报告,排查时就能直接找到需要修改的一方。


