Metadata-Version: 2.4
Name: nltcache
Version: 2.0.1
Summary: Lightweight function caching decorators for memory and disk
Author-email: niuliangtao <farfarfun@qq.com>
License: MIT License
        
        Copyright (c) 2026 farfarfun
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/farfarfun/nltcache
Project-URL: Repository, https://github.com/farfarfun/nltcache
Project-URL: Issues, https://github.com/farfarfun/nltcache/issues
Keywords: cache,caching,decorator,memoize,lru,ttl,diskcache,pickle
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cachebox<6,>=5.0.1
Requires-Dist: diskcache<6,>=5.6.3
Provides-Extra: dev
Requires-Dist: coverage>=7.4; extra == "dev"
Requires-Dist: mypy>=1.9; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Dynamic: license-file

# nltcache

一个简洁的 Python 函数缓存装饰器库，提供多种缓存策略，涵盖内存缓存、磁盘缓存和 Pickle 文件缓存。

## 安装

```bash
pip install nltcache
```

要求 Python >= 3.10。

## 快速开始

```python
from nltcache import lru_cache

@lru_cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(50))
```

## 内存缓存

基于 [cachebox](https://github.com/awolverp/cachebox) 实现，提供多种淘汰策略。所有装饰器都支持带括号和不带括号两种写法，并且原生支持 `async def` 函数。

包装后的函数暴露底层缓存对象：`f.cache` 可用于 `len(f.cache)` 查看条目数，`f.cache_clear()` 清空。

### cache

最简单的 LRU 缓存装饰器，默认 maxsize=1000。

```python
from nltcache import cache

@cache
def add(a, b):
    return a + b
```

### lru_cache

**LRU (Least Recently Used)** — 淘汰最久未被访问的缓存条目。

```python
from nltcache import lru_cache

@lru_cache(maxsize=500)
def query(sql):
    ...
```

### ttl_cache

**TTL (Time To Live)** — 缓存条目在超过指定时间后自动过期。

```python
from nltcache import ttl_cache

@ttl_cache(maxsize=1000, ttl=300)  # 300 秒后过期
def get_config(key):
    ...
```

### vttl_cache

**VTTL (Virtual TTL)** — 与 TTL 类似，但采用惰性淘汰策略，仅在访问时检查并移除过期条目。

```python
from nltcache import vttl_cache

@vttl_cache(maxsize=1000, ttl=60)
def get_status(service):
    ...
```

### lfu_cache

**LFU (Least Frequently Used)** — 淘汰访问次数最少的缓存条目。

```python
from nltcache import lfu_cache

@lfu_cache(maxsize=1000)
def translate(word):
    ...
```

### fifo_cache

**FIFO (First In First Out)** — 淘汰最早进入缓存的条目。

```python
from nltcache import fifo_cache

@fifo_cache(maxsize=1000)
def process(data):
    ...
```

### rr_cache

**RR (Random Replacement)** — 随机淘汰一个缓存条目。

```python
from nltcache import rr_cache

@rr_cache(maxsize=1000)
def compute(x):
    ...
```

## 持久化缓存

`disk_cache` 与 `pkl_cache` 共享同一套语义：按**指定的参数**计算缓存键，把结果写到磁盘，跨进程和重启后依然有效。两者都支持 `async def` 函数。

### 选择要作为缓存键的参数

```python
from nltcache import disk_cache

# 单个参数
@disk_cache(cache_key="query")
def search(query):
    ...

# 多个参数
@disk_cache(cache_key=["query", "top_k"])
def search(query, top_k=10):
    ...

# 全部参数（省略 cache_key）
@disk_cache()
def search(query, top_k=10, lang="zh"):
    ...
```

> **⚠️ 只有被列入 `cache_key` 的参数会参与缓存键的计算。**
> 未列入的参数即使改变，也会命中同一条缓存并返回旧结果——这是静默的错误结果，不会报错。
> 如果函数的输出依赖多个参数，请把它们全部列出，或直接省略 `cache_key` 以全部参数为键。

### 缓存键的稳定性

缓存键由参数值的**规范化编码**计算得出，而不是直接 pickle：

- 集合与字典会先排序再编码，因此不受 `PYTHONHASHSEED` 影响，跨进程稳定；
- 类型参与键的计算，`1`、`"1"`、`1.0`、`True` 互不冲突；
- 相等的值共享同一条缓存，`{"a": 1, "b": 2}` 与 `{"b": 2, "a": 1}` 命中同一项；
- 无法稳定序列化的值（文件句柄、锁、socket 等）会抛出 `UnstableKeyError`。
  在全参数模式下则降级为"不缓存"并打一条 warning 日志，不会中断调用。

### 运行时开关与跳过

```python
@disk_cache(cache_key="sql", is_cache="use_cache")
def run_query(sql, use_cache=True):
    ...

run_query("SELECT ...", use_cache=False)  # 跳过缓存，直接执行
```

`is_cache` 参数只控制是否读写缓存，**不参与缓存键**，因此关掉再打开仍会命中同一条目。

当 `cache_key` 指定的参数值为 `None` 时同样跳过缓存（全参数模式下不适用此规则）。

### 缓存控制 API

被装饰的函数附带一组缓存管理方法：

```python
@disk_cache(cache_key="query")
def search(query):
    ...

search.cache_key("python")  # 该次调用使用的键；跳过缓存时返回 None
search.cache_invalidate("python")  # 删除单条，返回是否存在
search.cache_clear()  # 清空，返回删除条数
search.cache_prune()  # 清理过期条目，返回删除条数
search.cache_close()  # 释放底层句柄，下次调用自动重开
search.__wrapped__  # 未被装饰的原函数
```

装饰器实例本身也可以作为上下文管理器，退出时关闭它开过的所有存储：

```python
with disk_cache(cache_key="query") as cached:
    @cached
    def search(query):
        ...
```

### disk_cache

基于 [diskcache](https://github.com/grantjenks/python-diskcache) 的 SQLite 存储，支持过期、容量上限和并发访问。**新项目优先选择它。**

| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `cache_key` | `str \| list[str] \| None` | `None` | 作为缓存键的参数名；`None` 表示全部参数 |
| `cache_dir` | `str \| None` | `None` | 缓存目录，为 None 时按函数标识自动生成 |
| `is_cache` | `str` | `"cache"` | 控制是否启用缓存的参数名 |
| `expire` | `float \| None` | `86400` | 过期时间（秒），`None` 表示永不过期 |
| `size_limit` | `int \| None` | `None` | 总字节数上限，由 diskcache 自行淘汰 |
| `**settings` | | | 其余参数透传给 `diskcache.Cache`（`eviction_policy`、`cull_limit` 等） |

```python
@disk_cache(cache_key="query", expire=3600, size_limit=512 * 1024 * 1024)
def search(query):
    ...
```

### pkl_cache

每条结果一个 `.pkl` 文件，按摘要前缀分片存放，采用临时文件 + `os.replace` 原子写入。适用于结果体积大、希望直接在文件系统里查看的场景。

| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `cache_key` | `str \| list[str] \| None` | `None` | 作为缓存键的参数名；`None` 表示全部参数 |
| `cache_dir` | `str` | `".cache"` | 存储 pkl 文件的目录 |
| `is_cache` | `str` | `"cache"` | 控制是否启用缓存的参数名 |
| `expire` | `float \| None` | `None` | 过期时间（秒），`None` 表示永不过期 |
| `max_entries` | `int \| None` | `None` | 条目数软上限，按写入时间淘汰最旧的 |
| `printf` | `bool` | `False` | 兼容选项，额外把缓存事件打到 stdout |

```python
@pkl_cache(cache_key="filepath", expire=7 * 86400, max_entries=10_000)
def parse_file(filepath):
    ...
```

`cache_dir` 的相对路径在**装饰时**解析为绝对路径，不受运行期 `os.chdir` 影响。

`cache_clear()` 只会删除自己写入的分片目录与 `.pkl` 文件，不会动缓存目录下的其他内容。

反序列化本身不适合处理不可信数据，只应使用当前用户可控的缓存目录。

### 日志

缓存命中与写入以 DEBUG 级别记录到 `nltcache` logger：

```python
import logging

logging.getLogger("nltcache").setLevel(logging.DEBUG)
```

## 其他

### cached_property

重新导出自标准库 `functools.cached_property`，将方法结果缓存为实例属性。

```python
from nltcache import cached_property

class Config:
    @cached_property
    def settings(self):
        return load_settings()
```

## API 一览

| 装饰器 | 存储位置 | 淘汰策略 | 支持过期 | 支持 async |
|--------|---------|---------|---------|-----------|
| `cache` | 内存 | LRU | - | 是 |
| `lru_cache` | 内存 | LRU | - | 是 |
| `lfu_cache` | 内存 | LFU | - | 是 |
| `fifo_cache` | 内存 | FIFO | - | 是 |
| `rr_cache` | 内存 | 随机 | - | 是 |
| `ttl_cache` | 内存 | TTL | 是 | 是 |
| `vttl_cache` | 内存 | VTTL (惰性) | 是 | 是 |
| `disk_cache` | 磁盘 (SQLite) | 容量上限 | 是 | 是 |
| `pkl_cache` | 磁盘 (pkl) | 条目数上限 | 是 | 是 |
| `cached_property` | 实例属性 | - | - | - |

生成器函数（`yield`）无法被持久化装饰器缓存，装饰时会直接抛 `TypeError`；请改为返回列表。

## 从 1.x 升级

2.0 修正了若干会产出错误结果的问题，存在以下不兼容变更：

1. **缓存键算法变更。** 旧缓存不会被读取，首次运行相当于全部重算。旧的 `.cache` / `.disk_cache` 目录可以直接删除。
2. **Python 最低版本提升到 3.10。**
3. **`cache_key` 不再是必填参数**，省略时以全部参数为键。
4. **`vttl_cache` 的 `ttl` 之前从未生效**（被传给了构造函数，只对初始化数据有效），现已按每条目过期正确实现。
5. `PickleCache` / `DiskCache` 的内部结构重写，`_cache`、`_get_cache_file`、`_load_cache`、`_save_cache` 等私有成员已移除；公开的 `cache_clear()` 等方法取代了它们。
