缓存进阶
在缓存中, 你已经了解了如何使用 @st.cache
装饰器来进行缓存。 在本文中,您将了解Streamlit的缓存功能是如何实现的,以便您可以使用它来改善Streamlit应用程序的性能。
缓存使用键值对来存储,其中key是由以下部分的hash组成:
- 调用函数的输入参数
- 函数中使用的任何外部变量的值
- 函数主体
- 缓存函数中使用到的任何函数的主体
而值是元组:
- 缓存的输出
- 缓存的输出的hash(你将马上看到)
对于键和输出哈希,Streamlit使用专门的哈希函数,该函数知道如何遍历代码, 散列特殊对象,并可以由用户自定义其行为。
举个例子,当函数 expensive_computation(a, b)
, 被 @st.cache
装饰时,并以 a=2
和 b=21
执行, Streamlit会进行以下操作:
- 计算缓存的key
- 若key可以在缓存中找到,则:
- 提取出以前的缓存元组(缓存输出和缓存输出的hash).
- 执行 输出突变检查, 计算输出的新哈希并将其与存储的
output_hash
进行比较.- 若两hash不同,显示告警Cached Object Mutated . (Note: 设置
allow_output_mutation=True
可以禁用这步).
- 若两hash不同,显示告警Cached Object Mutated . (Note: 设置
- 若输入的hash在缓存中找不到:
- 执行缓存函数 (i.e. output =
expensive_computation(2, 21)
). - 根据函数的
output
计算output_hash
. - 将
key → (output, output_hash)
存入缓存.
- 执行缓存函数 (i.e. output =
- 返回输出.
如果遇到错误,则会引发异常。. 如果在对键或输出进行哈希处理时发生错误,则会引发UnhashableTypeError
错误。如果您遇到任何问题,请参阅 fixing caching issues.
hash_funcs
参数
如上所述,Streamlit的缓存功能依赖于散列来计算缓存对象的键,并检测缓存结果中的意外变化。
为了增强表达能力,Streamlit允许您使用hash_funcs
参数覆盖此哈希过程。假设您定义了一个称为FileReference
的类型,该类型指向文件系统中的一个文件:
class FileReference:
def __init__(self, filename):
self.filename = filename
@st.cache
def func(file_reference):
...
默认情况下,Streamlit 散列通过递归导航自定义类(如FileReference
)的结构来对它们进行散列。在上例中,它的hash是属性filename
的hash。只要文件名不变,哈希值将保持不变。
但是,如果您想让哈希器检查文件的修改时间而不只是文件名,该怎么办?? 可以使用@st.cache
装饰器的 hash_funcs
参数:
class FileReference:
def __init__(self, filename):
self.filename = filename
def hash_file_reference(file_reference):
filename = file_reference.filename
return (filename, os.path.getmtime(filename))
@st.cache(hash_funcs={FileReference: hash_file_reference})
def func(file_reference):
...
另外,你可以通过文件的内容来散列FileReference
对象:
class FileReference:
def __init__(self, filename):
self.filename = filename
def hash_file_reference(file_reference):
with open(file_reference.filename) as f:
return f.read()
@st.cache(hash_funcs={FileReference: hash_file_reference})
def func(file_reference):
...
** 注意**:
由于Streamlit的哈希函数是递归工作的,因此您不必对hash_file_reference中的内容进行哈希处理, 您可以返回一个原始类型,在这种情况下,返回文件的内容,Streamlit的内部哈希器将从中计算出实际的哈希值。
典型的哈希函数
尽管可以编写自定义哈希函数,但让我们来看看Python提供的一些现成的工具。 以下是一些哈希函数的列表以及使用它们的合理时间。
Python’s id
function | Example
- 速度: 快
- 用例:如果您要对单个对象进行哈希处理,例如打开数据库连接或TensorFlow会话。 无论脚本重新运行多少次,这些对象都只实例化一次。
lambda _: None
| Example
- 速度: 快
- 用例:如果你想关闭这种类型的散列。如果你知道对象不会改变,这是有用的。
Python’s hash()
function | Example
- 速度:根据要缓存的对象的大小,可能会变慢
- 用例:如果Python已经知道如何正确哈希此类型。
Custom hash function | Example
- 速度: N/a
- 用例:如果您想覆盖Streamlit对特定类型进行哈希处理的方式。
示例1:传递数据库连接
假设我们想打开一个数据库连接,这个连接可以跨 Streamlit 应用程序的多个运行重用 。因此,可以利用缓存对象是用引用来存放的这一特点来自动初始化和重用连接:
@st.cache(allow_output_mutation=True)
def get_database_connection():
return db.get_connection()
仅3行代码,数据库只会创建一次且被存入缓存.然后,以后的每一次 get_database_conection
调用时, 已经被创建的链接将被自动重用。 即,它变成了一个单例.
小贴士:
使用allow_output_mutation = True
可以禁用不变性检查。这样可以防止Streamlit尝试对输出连接进行哈希处理,并在此过程中关闭Streamlit的变异警告。
如果要编写一个接收数据库连接作为输入的函数,该怎么办? 你可以使用 hash_funcs
:
@st.cache(hash_funcs={DBConnection: id})
def get_users(connection):
# Note: We assume that connection is of type DBConnection.
return connection.execute_sql('SELECT * from Users')
这里我们使用python内建的 id
函数, 因为链接对象通过 get_database_conection
函数从streamlit缓存获得的。这意味着每次都会传递相同的连接实例,因此,它始终具有相同的ID。但是,如果您碰巧有第二个连接对象指向另一个完全不同的数据库,将它传递给get_users
仍然是安全的,因为它可以保证ID和第一个ID不同 。
这些设计模式适用于任何指向外部资源的对象,例如数据库连接或Tensorflow会话。
示例2:关闭特定类型的哈希
您可以通过为其提供返回常量的自定义哈希函数来完全关闭特定类型的哈希。可能这样做的原因之一是避免对已知不会改变的大型,散列缓慢的对象进行散列。 例如:
@st.cache(hash_funcs={FooType: lambda _: None})
def func(huge_constant_dataframe):
...
当Streamlit遇到此类型的对象时,它将始终将该对象转换为 None
, 不论FooType
是什么实例。这意味着所有实例的哈希值都是相同的,这有效地抵消了哈希机制。
示例3:使用python hash()
有时,您可能想使用Python的默认哈希来代替Streamlit。例如,可能您遇到了Streamlit无法哈希的类型,但它可以用Python的内置 hash()
函数哈希。在这种情况下,解决方案非常简单:
@st.cache(hash_funcs={FooType: hash})
def func(...):
...