Python 库
Pytuck:为受限 Python 留一座数据库
一个面向 Ren’Py 等受限环境的纯 Python 嵌入式数据库:默认零外部依赖,也保留模型、查询与多后端能力。
并不是每一个运行着 Python 的地方,都拥有一个完整的 Python 世界。
在普通的后端项目里,需要保存结构化数据时,我大可以选择 SQLite、SQLAlchemy,或者一套更完整的数据库服务。但在 Ren’Py 这样的运行环境中,Python 只是作品运行时的一部分:解释器版本、标准库、二进制扩展和第三方依赖都可能受到限制。一个在开发机上理所当然的方案,到了玩家设备上未必还能成立。
Pytuck 就是从这道缝隙里长出来的。
它是一个纯 Python 实现的轻量级嵌入式数据库。核心安装不依赖任何第三方包,却仍然提供模型定义、Session、条件查询、索引、关系与事务等能力。它并不试图在正常环境中取代成熟数据库;它想做的,是在选择很少的地方,仍然留下一种足够清晰的数据组织方式。
从游戏存档之外开始
游戏当然可以把状态塞进字典,再用 JSON 保存。数据很少时,这种方法简单而直接。但当角色、物品、任务、对话状态与玩家行为逐渐彼此关联,字典会慢慢变成一张没有被承认的表,散落的读写逻辑也会形成一套没有名字的查询系统。
我希望这些数据能够被明确地描述:
- 哪些字段属于一条记录;
- 哪个字段是主键,哪些字段需要索引;
- 查询、修改和删除采用同一套表达;
- 存储格式可以更换,而上层模型不必随之重写;
- 最重要的是,这一切在缺少额外依赖时仍然能够运行。
于是,Pytuck 选择了一套接近 SQLAlchemy 2.0 的 Pythonic API,但没有要求使用者编写 SQL:
from typing import Type
from pytuck import (
Column,
PureBaseModel,
Session,
Storage,
declarative_base,
insert,
select,
)
db = Storage(file_path="archive.pytuck")
Base: Type[PureBaseModel] = declarative_base(db)
class Entry(Base):
__tablename__ = "entries"
id = Column(int, primary_key=True)
title = Column(str, nullable=False, index=True)
category = Column(str, nullable=False)
session = Session(db)
session.execute(
insert(Entry).values(
title="沿着地平线",
category="writing",
)
)
session.commit()
entry = session.execute(
select(Entry).where(Entry.title == "沿着地平线")
).first()
db.flush()
db.close()
模型负责描述数据,Session 负责组织对象的变化,select()、insert()、update() 与 delete() 负责表达操作,Storage 决定数据最终落在哪里。对于小型项目,也可以改用 Active Record 风格,让模型直接完成常见的增删改查。
同一套模型,落入不同的容器
Pytuck 把数据操作与持久化后端拆开。目前同一套模型可以写入 Pytuck、JSON、JSONL、CSV、SQLite、DuckDB、Excel 与 XML。
这些格式并不是为了追求“支持得越多越好”。它们对应的是不同的去处:
- 默认的
.pytuck单文件适合随应用分发和本地持久化; - JSON 便于阅读、调试和手工检查;
- JSONL 与 CSV 适合交换或归档;
- SQLite 与 DuckDB 适合需要原生 SQL 路径或更大数据量的场景;
- Excel 与 XML 则面向办公交付和标准化交换。
Pytuck、JSON、JSONL、CSV 和 SQLite 都可以在基础安装中使用;DuckDB、Excel、XML 与 orjson 加速被放在可选扩展里。这个边界是刻意保留的:第三方能力可以向外生长,却不能反过来成为核心读取、查询和写入的前提。
一个文件里的表与索引
默认的 Pytuck 引擎使用自定义的 PTK7 单文件格式。重新打开文件时,它会先恢复表结构与索引目录,记录内容在真正需要时才被读取。多表数据只改动其中一部分时,未变化的延迟加载数据块可以直接沿用,不必先把所有记录解码进内存再重新写回。
这个格式也支持可选加密。新写入的加密文件使用独立的加密与认证子密钥,并通过 HMAC-SHA256 检查完整性;不需要加密时,则不会为核心路径引入额外依赖。
我更在意的并不是创造一种神秘的新文件格式,而是让它承担几件朴素的事:单文件、可迁移、能保留 Python 类型,并且在受限环境中可以由标准库独立完成读写。
它刻意没有成为另一种东西
纯 Python 带来了可移植性,也划定了 Pytuck 的上限。
它适合单进程、中小规模、本地嵌入式的数据;万级以内的热点表是更舒适的范围。当单表接近十万条,或需求开始涉及高并发、复杂联表、分析计算和大型数据集时,SQLite、DuckDB、PostgreSQL 以及成熟 ORM 往往是更合适的选择。Pytuck 支持 Relationship 与预取,但不假装自己拥有完整的 SQL JOIN。
这是我希望项目始终保持诚实的一点:它不是“用 Python 重写一个更好的 SQLite”,也不是要与 SQLAlchemy 争夺正常的应用开发。它服务的是那些无法自然带上这些工具的地方。
仍在生长
写下这篇文章时,Pytuck 的公开版本是 1.4.0。它已经能够在八种存储引擎之间保持一套统一的模型与查询方式,并支持索引、关系预取、跨 Storage 关联、类型校验、数据迁移和可插拔后端。
但对我而言,继续维护它的方向并不是不断堆叠功能。比功能数量更重要的,是跨后端语义能否一致,单文件能否可靠地再次打开,数据格式能否长期演进,以及下一次把它放进一个不完整的 Python 环境时,它是否仍然不需要解释自己的依赖。
Pytuck 的名字最终指向的,正是这样一小块被妥善收起的数据空间。
BUILD / ENTRY
Pytuck:为受限 Python 留一座数据库
一个面向 Ren’Py 等受限环境的纯 Python 嵌入式数据库:默认零外部依赖,也保留模型、查询与多后端能力。