Python 的 namedtuple 有什么作用?怎么使用?
线上有个小脚本,跑了两个月都没事,某天导入订单文件突然把运费算进了实付金额。
代码看着也不复杂:
amount = float(row[5])
问题就出在这个 5。
这种代码我一眼就不太信。不是 Python 不靠谱,是下标不靠谱。今天字段顺序是这样,明天产品让运营多导出一列,后面全歪。脚本不一定报错,最烦的是它还能继续跑。
这时候 namedtuple 就有点用。
它干的事不玄乎:还是 tuple,但是能给每个位置起名字。
普通 tuple 是这样:
row = ("A1024", "paid", "2026-01-18", "Tom", "19.90", "3.00")pay = float(row[4])
fee = float(row[5])
过一周再看,row[4] 是啥? 不翻上游文档,基本靠猜。
换成 namedtuple:
from collections import namedtupleOrderLine = namedtuple(
"OrderLine",
"order_no status paid_at buyer amount freight"
)
row = OrderLine("A1024", "paid", "2026-01-18", "Tom", "19.90", "3.00")
pay = float(row.amount)
fee = float(row.freight)
print(row.order_no, pay + fee)
这段代码至少有个好处,读的人不用在脑子里数格子。
namedtuple 本质上还是 tuple,所以它有 tuple 的几个特点:轻量、不可变、可以解包、可以按下标取值。
print(row[0]) # A1024
print(row.order_no) # A1024order_no, status, *_ = row
print(order_no, status)
这里有个点要注意,不可变不是缺点。很多从文件、接口、SQL 查出来的行数据,本来就不应该在中间被随便改。
我平时比较喜欢把它放在“小型数据承载”场景里,比如日志解析、CSV 清洗、接口返回字段整理。
举个更像现场的例子。运营丢过来一个 CSV,让你筛出已支付但运费异常的订单。
原始写法一般长这样:
defbad_freight_rows(lines):
ret = []for line in lines:
cols = line.rstrip("\n").split(",")
if cols[1] != "paid":
continue
amount = float(cols[4])
freight = float(cols[5])
if amount > 0and freight <= 0:
ret.append(cols[0])
return ret
能跑,但味道不太好。cols[1]、cols[4]、cols[5] 多看几次就烦。
我会改成这样:
from collections import namedtupleCsvOrder = namedtuple(
"CsvOrder",
["order_no", "status", "paid_at", "buyer", "amount", "freight"]
)
defparse_order(line):
parts = line.rstrip("\n").split(",")
if len(parts) != 6:
raise ValueError(f"bad csv line, column_count={len(parts)}, raw={line[:80]}")
return CsvOrder(*parts)
deffind_bad_freight(lines):
bad_orders = []
for line_no, line in enumerate(lines, start=1):
try:
order = parse_order(line)
except ValueError as e:
print(f"[skip] line={line_no}, reason={e}")
continue
if order.status != "paid":
continue
amount = float(order.amount)
freight = float(order.freight)
if amount > 0and freight <= 0:
bad_orders.append(order.order_no)
return bad_orders
这段代码不高级,但排查起来舒服。
日志里看到 line=38,回头定位就行。业务判断也清楚,order.status、order.freight 比 cols[1]、cols[5] 强太多。
namedtuple 还有几个常用方法,平时能省不少事。
第一个是 _asdict(),把它转成字典。比如要打日志、转 JSON、临时对接接口时能用:
order = CsvOrder("A1024", "paid", "2026-01-18", "Tom", "19.90", "0")print(order._asdict())
输出大概是这样:
{
"order_no": "A1024",
"status": "paid",
"paid_at": "2026-01-18",
"buyer": "Tom",
"amount": "19.90",
"freight": "0"
}
第二个是 _replace()。
因为 namedtuple 不能直接改字段:
order.freight = "3.00"
这行会报错。
要改,只能生成一个新的:
fixed = order._replace(freight="3.00")
我反而喜欢这个设计。数据清洗的时候,旧数据和新数据分得开,不容易在半路被谁偷偷改掉。
还有一个细节,namedtuple 可以给字段设置默认值。比如有些老文件没有 coupon 字段,新文件才有:
PayLine = namedtuple(
"PayLine",
"order_no amount freight coupon",
defaults=["0"]
)p1 = PayLine("A1001", "30.00", "5.00")
p2 = PayLine("A1002", "30.00", "5.00", "2.00")
print(p1.coupon) # 0
print(p2.coupon) # 2.00
但默认值别滥用。字段缺了到底是合法兼容,还是上游导错了,这个要分清楚。导入脚本里我一般宁愿多校验一次,也不愿悄悄给默认值。
那 namedtuple 和普通 class 怎么选?
我的习惯是这样:只是承载一行数据,不需要复杂行为,用 namedtuple。字段多了、要校验、要方法、要类型提示更重一点,那就用 dataclass。
比如这个就不太适合 namedtuple:
# 字段会越来越多,还要做金额校验、状态流转、税费计算
# 这种别硬塞 namedtuple,后面会难受
namedtuple 最适合的位置,是替代那些“靠下标活着”的代码。
尤其是这种:
user_id = row[0]
city = row[3]
last_login = row[7]
看到就该警惕。不是现在一定有 bug,而是以后改字段时,很容易把 bug 藏进去。
改成这样,代码不会显得多牛,但少一点猜:
UserLogin = namedtuple(
"UserLogin",
"user_id nickname level city device ip last_login source"
)login = UserLogin(*row)
if login.city == "成都"and login.source == "app":
print(login.user_id, login.last_login)
namedtuple 的作用就这么朴素:让 tuple 里的位置有名字。
别把它当框架,也别把它当面向对象的替代品。它就是一个很轻的工具,专门收拾那些 row[3]、item[5]、data[0] 这种看着没错、改起来要命的代码。