Python技术迷

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 namedtuple

OrderLine = 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)   # A1024

order_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 namedtuple

CsvOrder = 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] 这种看着没错、改起来要命的代码。