大家好,我是 SiKi老师。关卡工具读出生点坐标,代码里一句x, y, z = pos,跑到一半抛出not enough values to unpack,却看不出是哪一条记录出了问题。先别急着在外面套try,我会先在拆包之前检查长度和类型,让报错直接带上记录编号、实际分量个数和原始值。
本文在 Linux 上的 Python 3.12.15 中运行,另用 3.13.5 复跑同一脚本,下面引用的输出两边一致。坐标样例全部自拟,只在内存中检查,不读写真实关卡文件,也没有测试任何引擎的向量类型或场景导入。讨论的是工具端的数据协议,不是引擎规定。官方在线文档标注的小版本是 3.12.15,与本机一致。
一 先复现拆包报错缺了哪些信息
用几个自拟坐标直接拆包:
forposin[(1.0,2.0),(1,2,3,4),None]:try:x,y,z=posexcept(ValueError,TypeError)asexc:print(type(exc).__name__,exc)本地输出三行:ValueError not enough values to unpack (expected 3, got 2)、ValueError too many values to unpack (expected 3)、TypeError cannot unpack non-iterable NoneType object。
这些信息说明了“期望 3 个”,少的时候还给出实际个数,但不会告诉你是哪个出生点、原始值长什么样。多出分量时这两个版本的消息里也没有实际个数。另外缺值是ValueError,整条记录是None时却是TypeError,只捕获一种异常会漏掉另一种。
官方语言参考在赋值语句一节写明:目标列表不带星号时,右侧对象必须是可迭代对象,并且元素个数与目标个数相同,然后从左到右依次赋值。所以拆包本身只关心“能不能迭代、个数对不对”,并不检查每个分量是不是数字。
二 有些坏数据拆包不会报错
更麻烦的是,个数刚好是 3 的错误数据会直接“通过”:
x,y,z="123"print(x,y,z)x,y,z={"x":1,"y":2,"z":3}print(x,y,z)本地输出1 2 3和x y z。第一行看起来像数字,实际是三个字符'1'、'2'、'3';第二行拿到的是字典的三个键名,不是坐标值。这类数据要到后面做加减时才报类型错误,或者更糟,被当成文本悄悄写进下一步。
还有一个容易忽略的细节:右侧是迭代器时,拆包失败也会消耗元素。
it=iter([1,2,3,4,5])try:a,b,c=itexceptValueErrorasexc:print(exc,list(it))本地输出too many values to unpack (expected 3) [5],也就是失败后迭代器只剩最后一个元素。如果工具一边读流一边拆包,出错后不要假设还能从原位置重试;先把一条记录取成列表或元组再检查,会稳妥得多。
三 拆包前先检查长度、类型和数值
我的做法是写一个小函数,先检查再返回三元组,报错消息里带上记录编号:
importmathfromcollections.abcimportSequencedefcheck_vec3(value,where):ifisinstance(value,(str,bytes))ornotisinstance(value,Sequence):raiseValueError(f"{where}: 需要长度为3的列表或元组,实际是{type(value).__name__}")iflen(value)!=3:raiseValueError(f"{where}: 需要3个分量,实际{len(value)}个:{value!r}")out=[]foraxis,iteminzip("xyz",value):iftype(item)notin(int,float):raiseValueError(f"{where}.{axis}: 需要数字,实际是{type(item).__name__}")ifnotmath.isfinite(item):raiseValueError(f"{where}.{axis}: 不是有限数{item!r}")out.append(float(item))returntuple(out)x,y,z=check_vec3([4,0,2],"spawn_02")print(x,y,z)本地输出4.0 0.0 2.0。这里有几处是刻意的选择:字符串和字节虽然也是序列,但先排除,避免"123"混进来;字典不是Sequence,会被拒绝;分量用type(item) in (int, float)判断,这样True不会被当成1;math.isfinite拦住nan和无穷大。整数分量统一转成浮点,是本文自拟协议的约定,你的工具如果要求坐标必须是整数格子,应改成对应规则。
检查通过后再拆包,拆包这一步就不会再出现个数错误,出错时也只会出现在检查函数里,消息格式统一。
四 一次报告所有坏记录
关卡表里可能不止一条坏数据。遇到第一条就抛异常,修一条跑一次,效率很低。我会把检查放在循环里,收集所有错误后一起输出:
rows=[("spawn_01",[0.0,1.5,-3.0]),("spawn_03",[1.0,2.0]),("spawn_04",[1.0,2.0,3.0,4.0]),("spawn_05","123"),("spawn_07",[1.0,True,2.0]),("spawn_09",[float("nan"),0.0,0.0]),("spawn_10",None),]good,errors={},[]forrow_id,valueinrows:try:good[row_id]=check_vec3(value,row_id)exceptValueErrorasexc:errors.append(str(exc))print(good)forlineinerrors:print(line)本地输出第一行是{'spawn_01': (0.0, 1.5, -3.0)},随后六行错误依次为:
spawn_03: 需要3个分量,实际2个: [1.0, 2.0] spawn_04: 需要3个分量,实际4个: [1.0, 2.0, 3.0, 4.0] spawn_05: 需要长度为3的列表或元组,实际是 str spawn_07.y: 需要数字,实际是 bool spawn_09.x: 不是有限数 nan spawn_10: 需要长度为3的列表或元组,实际是 NoneType每条都能直接定位到记录和分量。是否在有错误时整批拒绝,还是只跳过坏行,属于工具的业务决定,本文不替你选;但无论哪种,都应该把错误列表完整打印或写进报告,而不是只留第一条异常。
五 用回归样例锁住维数边界
本次独立验证脚本覆盖:直接拆包的三种报错消息、字符串和字典“误通过”、迭代器失败后被消耗、长度不符报告实际个数、整条记录类型错误、分量为布尔或字符串、非有限数、批量收集全部错误,以及检查前后原始数据未被修改。全过程只用内存中的自拟数据,不读写真实关卡,也不调用引擎接口。
接手坐标相关工具时,可以先准备二维、四维、字符串"123"、含True的三元组和None这五个样例,分别喂给现有读取代码。如果有任何一个没有报错,或报错里看不出是哪条记录,就说明检查还停在拆包这一层。
你遇到的坐标问题是少了一个分量,还是类型混进了字符串?可以给出一条自拟样例和当前的读取写法,我们一起看应该在哪一步加检查。
依据:Python 3.12 语言参考 赋值语句中关于目标列表拆包时元素个数必须与目标个数相同的说明。示例和表达原创,不引用具体课程信息。