发现一个有趣的结论。
在 Python 中读取 jsonl 文件,读取为一个 list[dict]
,最佳实践是怎么样?
刚学 python 的人会这么写:
def read_jsonl(jsonl_path: str):
content = Path(jsonl_path).read_text()
results = []
for i in content.splitlines():
results.append(json.loads(i))
return results
这个人写的已经够好了,如果是学业不精的人还能写出更千奇百怪的写法,比如用 while 找\n
,或者用 range 来迭代,然后用索引来访问一个个元素什么的。如果我是老师,我觉得上面这个已经可以打 60 分了。加上它用 Path 而不是 open 我觉得可以额外加 10 分。
但熟悉 micro optimization 的人一眼就能看出各种问题,比如存在不应该的数据复制、列表不如迭代器、append 和 json.loads 反复地对象取属性,优化之后大概长这样:
def read_jsonl(jsonl_path: str):
with open(jsonl_path) as f:
return list(map(json.loads, f))
这样写感觉很 pythonic,比如用到了 python 中的上下文管理器,用到了 file 对象是可以按行迭代的迭代器,用到了 map 函数式编程等特性。可读性也 up up up
如果一个学生写到这种程度,我觉得已经可以打 100 分了,甚至还有点炫技的嫌疑,我可能会让 Ta 上台来给大家讲一讲 哈哈哈
但是!
这不是最快的办法。假设你只是读一个几 MB 大的 jsonl 数据文件,不关心内存(几 MB 也没啥好优化内存的),这样是最快的
def read_jsonl(path: str):
content = Path(jsonl_path).read_text().rstrip().replace("\n", ",")
return json.loads(f"[{content}]")
看懂了吗?是不是很 hack,这其实是把整个 jsonl 用字符串替换的方式变成一个 json 列表,然后直接一次性 loads
快多少
在我的电脑上,一个 3MB 的 jsonl 文件,上面两种办法分别是 49ms 和 42ms,后面这种 hack 是 19ms
可以认为是从 3 帧的卡顿变成了 1 帧,这个优化效果还是非常惊艳的
为什么
Python 性能之禅,能避免用 python 就避免用 python(比如避免循环),即使这会让你的代码更绕弯路
本故事源于我前天开的一个 PR:
思考题
如果是大文件呢?比如 GB 级的,肯定不能一次性读到内存中的
参考答案
我觉得得分类讨论,如果是每行都很大的,比如一行都是 MB 级的,那可能可以一行一行做:
def iter_jsonl_data(jsonl_path: str):
with open(jsonl_path) as f:
yield from map(json.loads, f)
但如果是行数很多,但每行都不大的情况(比如日志流),那我觉得就麻烦了,可能得分 chunk,然后每个 chunk 内用上面第三种方法来 hack
当然,这些方法都是戴着镣铐跳舞,如果条件允许,最好的办法当然是拿低级语言写个 pyd import 进来,效率绝对比这些都快
这个故事还是教学意义比实践意义大