跳到主要内容

使用缓存

所有缓存实例均取自CacheAbstract的派生类。通过以下方法,可以判断某未知量是否是缓存的实例。

is_a_cache: bool = isinstance(value, dash_file_cache.caches.abstract.CacheAbstract)

这里的caches模块主要是用来帮助初始化服务(service)的。绝大多数情况下,缓存只会按照以下方式使用:

import dash_file_cache as dfc

service = dfc.ServiceData(dfc.CachePlain(1))

其中,dfc.CachePlain可以被dfc.CacheQueue或是dfc.CacheFile取代。不同类型的缓存对应着不同的使用情境:

缓存
使用情境
CachePlain服务service总是由单进程(但允许由多线程)访问。若数据需要被置于内存中、且不需要使用背景 callback。
CacheQueue服务service需要被多进程访问。使用背景 callback 时,CachePlain无法正常工作,但该缓存仍可工作,且数据亦置于内存中。
CacheFile服务service需要被多进程访问,且数据需要被置于硬盘、而非内存中。
提示

背景callback是dash>=2.6提供的新功能。要了解详情,请查看

https://dash.plotly.com/background-callbacks

将缓存应用于服务中

CachePlain

缓存的用途如下:

service = dfc.ServiceData(dfc.CachePlain(cache_size: int))

其中,容量指的是缓存中最多能同时保有的实例数目。缓存填满时,向缓存添加新值、会导致最久未使用(least-recently-used,LRU)的值删去。

CacheQueue

service = dfc.ServiceData(dfc.CacheQueue(cache_size: int, qobj: queue.Queue))

不同于CachePlain,初始化基于队列的缓存、需要提供一个队列实例、作为第二个初始化参数。该队列实例应当取自于进程数据管理器。在绝大多数情形中,该管理器只能在if __name__ == "__main__"保护的区域内实例化。例如,

use_queue_with_manager.py
import multiprocessing as mproc
import dash
import dash_file_cache as dfc


def create_app():
app = dash.Dash("demo")

ctx = mproc.get_context("spawn")
man = ctx.Manager()
service = dfc.ServiceData(dfc.CacheQueue(1, man.Queue()))
service.serve(app)
return app


if __name__ == "__main__":
app = create_app()
app.run()

其中,需要确保create_app()if __name__ == "__main__"的范围内调用。这是因为create_app初始化了一个新的Manager()实例。

CacheFile

service = dfc.ServiceData(dfc.CacheFile(cache_dir: str | None = None, chunk_size: int = 1))

初始化CacheFile需要提供两个参数。

第一个参数,cache_dir,决定了缓存数据保存的位置。未指定该值的情况下,则使用系统临时文件夹作为该位置。直到不再需要用到缓存的时候(通常在程序退出时触发),会删除该位置对应的文件夹。

不同于CacheQueue,对CacheFile的初始化可以置于任意位置。

独立使用缓存

注意

或许,读者不应使用以下部分介绍的内部功能呢。最合适地使用CacheAbstract的方式仍然是和ServiceData共用。

抽象类CacheAbstract提供了以下功能:

方法
说明
dump向缓存添加一个值。由于某些缓存支持 LRU 调度,添加该值可能会导致某旧值移出缓存。
load从缓存中获取一个值。运行该方法不会导致所得值移出缓存。
remove从缓存中显式地移出一个值。移出的值以后将无法被访问到。
__contains__检查缓存中是否存在某一关键字。

对某一缓存而言,其用法如下:

test_cache.py
import dash_file_cache as dfc

# 以下代码是按照Python>=3.12的风格撰写的
def test[S, T](cache: dfc.caches.abstract.CacheAbstract[S, T], info: S, value: T):
cache.dump("newval", info=info, value=value)
assert "newval" in cache
_info, _value_loader = cache.load_info("newval")
assert info == _info

# _value_loader提供了对值的延迟加载。
_value = _value_loader()
assert value == _value

以上泛型函数接受三个输入值。第一个值是缓存。第二和第三个值是infovalue。使用缓存时,infovalue均会储存在缓存中。然而,读取缓存值的时候,以下返回值的行为将有所不同:

_info, _value_loder = cache.load("newval")
_info: S
_value_loader: Callable[[], T]

其中,_info会立刻返回。然而,_value_loader不是返回值、而是一个将要提供返回值的闭包(closure)。这种机制允许一些轻量的、用于检查的信息存放在info中,而大体量的数据则储存于value中,从而实现以下的条件读取机制:

  1. 检查_info是否满足某些条件。
  2. 若条件不满足,直接舍弃_value_loader,不实际载入这一部分的值。

独立使用CachePlain的范例

以如下代码为例:

CachePlain_and_conditonal_loading.py
import io
import dash_file_cache as dfc

data = io.BytesIO(" " * 1024)
len_data = len(data)

cache = dfc.CachePlain()
cache.dump("newval", info=len_data, value=data)

info, value_loader = cache.load("newval")
if isinstance(info, int) and info > 0:
print(value_loader())

所载入的info是数据的长度。当该长度为0时,跳过载入数据的步骤。

独立使用CacheQueue的范例

使用CacheQueue略显麻烦,因为CacheQueue本身包含了一些无法传递到子进程的数据。因此,CacheQueue专门提供了一个名为mirror的属性,以允许在子进程中访问缓存。

CacheQueue_and_pool.py
import multiprocessing as mproc
from concurrent.futures import ProcessPoolExecutor
import dash_file_cache as dfc

if __name__ == "__main__":
ctx = mproc.get_context("spawn")
man = ctx.Manager()

cache = dfc.CacheQueue(3, man.Queue())
cache_m = cache.mirror # `cache`无法直接传递到子进程中。

with ProcessPoolExecutor(mp_context=ctx) as exe:
exe.submit(cache_m.dump, "a", 1, 1).result()
exe.submit(cache_m.dump, "b", 2, 2).result()
exe.submit(cache_m.dump, "c", 3, 3).result()
exe.submit(cache_m.dump, "d", 4, 4).result()

print(dict(cache.cache.items()))
# {'d': (4, 4), 'c': (3, 3), 'b': (2, 2)}

该例子中,cache.mirror传递给了进程池,且值都是在子进程中添加的。此后,可以在主进程中访问缓存值。

不宜独立使用CacheFile

不同于可以接受任意类型数据的CachePlainCacheQueueCacheFile只能用于在硬盘上缓存可以被保存成文件的数据。换言之,CacheFile本身不提供文件序列化或反序列化的功能。与ServiceData同时使用时,由于用户只能向缓存注册一条路径、或一个类文件对象,CacheFile的用途将与CachePlainCacheQueue并无分别。然而,不使用ServiceData的话,绝大多数情况下,CacheFile无法直接用来取代CachePlainCacheQueue

CacheFile具备以下行为:

  1. CacheFile注册一条路径时,缓存中只注册了路径本身,因为这种情况下,假定文件是一直存在于硬盘上的。
  2. CacheFile注册一个类文件对象(StringIOBytesIO)时,类文件对象的拷贝则会写入到缓存空间中。