大家好,我是 SiKi老师。技能配置表里有一列「是否启用」,策划填的是「是」和「否」。用 Python 读进来以后,明明写了「否」的技能,if row['enabled']:还是走进了“启用”分支,看起来像是整列都变成了真。先别怀疑判断写反了,第一步要看的是:读进来的值到底是什么类型。
本文在 Linux 上的 Python 3.12.15 中运行,另用 3.13.5 复跑同一脚本,下面引用的输出两边一致。表格样例全部自拟,只在内存和临时目录中演示,不读写真实游戏配置,也没有在任何引擎里验证技能开关效果。官方在线文档页面标注的小版本是 3.12.16,与本机安装的 3.12.15 差一个补丁号,正文以本机实测为准。
之前我写过 JSON 里false和"false"的区别,这篇只讲 CSV 配置表里用中文「是/否」当开关的情况,重点是怎样写一个说得清楚的转换规则。
一 先把读进来的值和类型打出来
自拟一份四行的技能表。第 3 行「是否启用」留空,第 4 行在「否」前面多了一个半角空格、后面多了一个全角空格(从表格软件复制时常见):
importcsvimportio sample="id,name,enabled\n1,火球,是\n2,冰箭,否\n3,雷击,\n4,治疗, 否\u3000\n"rows=list(csv.DictReader(io.StringIO(sample)))forrinrows:print(r["id"],repr(r["enabled"]),type(r["enabled"]).__name__)本地输出:
1 '是' str 2 '否' str 3 '' str 4 ' 否\u3000' str四个值全是str。官方文档写得很直接:csv 读出来的每一行都是字符串列表,不会自动做类型转换。也就是说,表格里的「是/否」读进来只是两个汉字,Python 并不知道它们表示开关。
二 为什么「否」也是真
接着用最常见的写法判断:
print({r["id"]:bool(r["enabled"])forrinrows})print(bool("否"),bool("False"),bool("0"),bool(""))本地输出:
{'1': True, '2': True, '3': False, '4': True} True True True False原因是 Python 的真值规则:字符串只要不是空串就是真,bool()不会去理解字面意思。所以「否」「False」「0」这三个非空字符串都是真,只有第 3 行的空串是假。表里全是「是/否」时,读进来就会“全是真”。
再顺手排除两个常见误解:
"否" == False的结果是False,"是" == True也是False,字符串和布尔值比较不会自动转换。int("否")会直接报ValueError: invalid literal for int() with base 10: '否',不能靠先转数字再转布尔。
三 写一个显式映射,认不出就报错
我的做法是:先strip()去掉首尾空白,再转小写,然后去两张“白名单”里查;两张表都查不到就报错,并带上行号和列名,方便回头改表。
TRUE_WORDS={"是","true","1","yes","y"}FALSE_WORDS={"否","false","0","no","n"}defparse_flag(text,*,row_id,column):word=text.strip().lower()ifwordinTRUE_WORDS:returnTrueifwordinFALSE_WORDS:returnFalseraiseValueError(f"第{row_id}行{column}列无法识别为布尔:{text!r}")parsed,errors={},[]forrinrows:ifr["enabled"].strip()=="":errors.append(f"第{r['id']}行 enabled 列为空")continueparsed[r["id"]]=parse_flag(r["enabled"],row_id=r["id"],column="enabled")print(parsed)print(errors)本地输出:
{'1': True, '2': False, '4': False} ['第 3 行 enabled 列为空']第 4 行虽然带了半角和全角空格,strip()后仍识别为「否」(本地实测"\u3000".isspace()为True,所以全角空格也会被去掉)。再试一个表里不该出现的词:parse_flag("开", row_id="5", column="enabled")会抛出第 5 行 enabled 列无法识别为布尔: '开'。
这里有两个需要你按项目协议决定的点:
- 空值怎么算:本文把空值单独记成错误,不偷偷当成
False。如果你们约定“留空=关闭”,可以改成返回默认值,但要写进表头说明。 - 白名单收多宽:本文同时接受中文和英文写法,是为了兼容下面要讲的回写。团队只用「是/否」的话,就把集合收窄,宁可报错也不要猜。
四 回写再读回来,同样要过映射
工具有时会把解析后的结果再写回 CSV。csv.writer写布尔值时会把它转成字符串:
buf=io.StringIO()w=csv.writer(buf,lineterminator="\n")w.writerow(["id","enabled"])w.writerow(["2",False])print(repr(buf.getvalue()))back=next(csv.DictReader(io.StringIO(buf.getvalue())))print(bool(back["enabled"]),parse_flag(back["enabled"],row_id="2",column="enabled"))本地得到'id,enabled\n2,False\n',再读回来:直接bool()是True,过映射才是False。所以“写回去的是 False,读回来又变真”也是同一个原因。上面白名单里放了false,再配合lower(),就能接住False这种写法。
落到真实文件时,我同样按官方建议用open(path, newline='', encoding='utf-8')交给 csv 模块。临时文件实测与内存一致:第 2 行读出来是'否',bool()仍为True,跑完临时目录里只有自拟的skills.csv。
五 一步步排查清单
遇到“开关怎么改都是开”,我会按这个顺序查:
- 打印
repr(值)和type(值).__name__,确认是不是字符串。 - 看判断写法,是不是直接
if 值:或bool(值)。 - 看单元格里有没有首尾空格、全角空格。
- 用显式映射转换,认不出的值报错并带行号,空值按协议单独处理。
- 把「是」「否」「空」「带空格的否」「不认识的词」「回写的 False」写进回归样例,改代码后先跑一遍。
本次独立验证脚本覆盖了上面每一条,在 3.12.15 和 3.13.5 上都通过。
你们的配置表开关列是怎么填的,「是/否」「1/0」,还是 TRUE/FALSE?可以贴一行自拟的样例和现在的判断写法,我们一起看映射该收多宽。
依据:Python 3.12 csv 模块(docs.python.org/3.12/library/csv.html)关于读取结果为字符串、不做自动类型转换的说明,以及内置类型文档(docs.python.org/3.12/library/stdtypes.html)中的真值测试规则。示例和表达原创,不引用具体课程信息。