انتقل إلى المحتوى الرئيسي

الوحدة 5 — التخزين المؤقّت للبيانات والموارد

بعد الوحدات الأربع الأولى، أصبح لدينا تطبيق يعرض جداول ورسوم. المشكلة: كلّ نقرة تُعيد قراءة ملفّ CSV، وتُعيد تحميل النموذج، وتُعيد كلّ حساب سابق. تطبيق يستغرق أربع ثوانٍ لكلّ تفاعل غير قابل للاستعمال. حلّ Streamlit مزخرف بديكوريتَين: @st.cache_data و@st.cache_resource. الفرق بينهما ليس تفصيلًا: إساءة استعمال أحدهما بمكان الآخر مصدر أخطاء صامتة تُلوّث حالة التطبيق. هذه الوحدة تُوضّح متى وأيّهما.

القاعدة الحاسمة

  • @st.cache_data: لكلّ ما يُعتبَر بيانات (إطارات، قواميس، قوائم، نتائج حسابيّة). Streamlit يحفظ نسخة، ويُعيدها في كلّ استدعاء بالوسائط نفسها. الكائن المُعاد جديد في كلّ مرّة (يُنسَخ فعلًا).
  • @st.cache_resource: لكلّ ما هو مورد مشترك لا يجب نسخه (نموذج مُحمَّل، اتّصال قاعدة بيانات، عميل API). Streamlit يحفظ إشارة إلى الكائن نفسه، ويُعيدها لجميع الجلسات.

الاختلاف الجوهريّ: cache_data ينسخ، cache_resource لا ينسخ. الآثار العمليّة كثيرة.

متى تختار كلًّا منهما

اختر cache_data إن كنت تُريد: قراءة ملفّ CSV، استفسار قاعدة بيانات، حساب تجميع بـpandas، نتيجة استدعاء API يمكن تسلسلها. كلّ هذا بيانات يمكن حفظها ونسخها.

اختر cache_resource إن كنت تُريد: تحميل نموذج scikit-learn أو PyTorch أو TensorFlow، فتح اتّصال بـPostgres أو Redis، إنشاء عميل OpenAI. كلّ هذا موارد تُشارَك بلا نسخ.

قاعدة الشكّ: إذا كان الشيء ثقيلًا في الذاكرة (نموذج ضخم) أو له حالة داخليّة (اتّصال)، فهو cache_resource. إذا كان قيمة يمكن كتابتها في ملفّ (jsonable, picklable) وتُقرأ منه، فهو cache_data.

تخزين قراءة البيانات

مثال قياسيّ:

import streamlit as st
import pandas as pd

@st.cache_data
def charger_clients(chemin: str) -> pd.DataFrame:
df = pd.read_csv(chemin, parse_dates=["date_derniere_interaction"])
df["a_risque"] = df["score_risque"] > 0.5
return df

clients = charger_clients("donnees/clients.csv")
st.dataframe(clients)

أوّل استدعاء يقرأ الملفّ ويحفظ النتيجة. كلّ استدعاء لاحق بنفس الوسيطة ("donnees/clients.csv") يُعيد النسخة المُخزَّنة، بلا قراءة جديدة. تغيير الوسيطة ("donnees/autres.csv") يقرأ ملفًّا جديدًا ويحفظه بمفتاح جديد.

تنبيه: مفتاح التخزين يُحسَب من قيم الوسائط، لا من محتوى الملفّ. إذا تغيّر محتوى clients.csv بلا تغيّر مساره، لن يلاحظ التخزين المؤقّت شيئًا. الحلول: أضف وسيطة mtime بقيمة os.path.getmtime(chemin)، أو استعمل معلمة ttl=3600 لإعادة الحساب كلّ ساعة، أو ضع زرّ «تحديث» يستدعي charger_clients.clear().

تخزين النموذج

النموذج المُدرَّب مورد ثقيل يجب أن يُحمَّل مرّة واحدة لكلّ الجلسات:

import streamlit as st
import joblib

@st.cache_resource
def charger_modele():
modele = joblib.load("modele_churn.joblib")
return modele

modele = charger_modele()

هذا السطر البسيط يُغيّر كلّ شيء. بلا @st.cache_resource، كلّ نقرة تحمّل النموذج من القرص (200 ميغابايت مثلًا)، فيتحوّل التطبيق إلى تدمير للذاكرة. مع الديكوريتور، النموذج يُحمَّل مرّة واحدة عند أوّل طلب، ويبقى في الذاكرة إلى إعادة تشغيل الخادم.

الفارق الجوهريّ عن cache_data: النموذج المُعاد هو الكائن نفسه (بنفس عنوان الذاكرة)، لا نسخة. إذا عدّلته من مكانٍ (modele.n_estimators = 200)، سيرى بقيّة الجلسات التعديل. هذا لا يحدث مع cache_data.

تخزين اتّصال بقاعدة بيانات

نمط شائع لتطبيقات Streamlit المُنتِجة:

import streamlit as st
import psycopg2

@st.cache_resource
def obtenir_connexion():
return psycopg2.connect(
host=st.secrets["db_host"],
dbname=st.secrets["db_name"],
user=st.secrets["db_user"],
password=st.secrets["db_password"],
)

@st.cache_data(ttl=60)
def requete(sql: str, _conn) -> pd.DataFrame:
return pd.read_sql(sql, _conn)

conn = obtenir_connexion()
df = requete("SELECT * FROM clients WHERE a_risque = true", conn)
st.dataframe(df)

ثلاث نقاط تصميم مهمّة. الاتّصال مورد (cache_resource) لأنّه له حالة داخليّة (مقبس مفتوح). الاستفسار بيانات (cache_data) مع ttl=60 لإعادة القراءة كلّ دقيقة. الوسيطة _conn تبدأ بشرطة سفليّة: Streamlit يتجاهلها في حساب مفتاح التخزين. لولا الشرطة، سيحاول Streamlit تسلسل الاتّصال لبناء المفتاح، ويرفع خطأ لأنّ الاتّصال غير قابل للتسلسل.

إبطال التخزين

ثلاث آليّات، من الأنعم للأخشن:

ttl: أعمار افتراضيّة. @st.cache_data(ttl=3600) يعيد الحساب بعد ساعة. مناسب لبيانات تتغيّر بوتيرة معروفة (يوميّة، ساعيّة).

clear(): charger_clients.clear() يمسح تخزين هذه الدالّة تحديدًا. مفيد في زرّ «تحديث الآن». st.cache_data.clear() يمسح كلّ ذاكرات cache_data مرّة واحدة (نادرًا ما تحتاجه).

تغيير الوسائط: أضف معلمة تشير إلى «إصدار» البيانات، مثل mtime أو رقم إصدار من قاعدة البيانات. تغيّرها يخلق مفتاحًا جديدًا.

مثال زرّ التحديث:

if st.sidebar.button("تحديث البيانات"):
charger_clients.clear()
st.rerun()

st.rerun() يعيد تنفيذ السكربت فورًا؛ فيُعاد استدعاء charger_clients بلا تخزين، فتُقرأ البيانات من جديد.

أفخاخ الكائنات القابلة للتعديل

المسألة الأدقّ في هذه الوحدة. Streamlit يُصدر تحذيرًا عمليًّا لكلّ من يعدّل نتيجة cache_data بعد استلامها. مثال خاطئ:

@st.cache_data
def charger_donnees():
return pd.read_csv("clients.csv")

df = charger_donnees()
df["nouveau_champ"] = 42 # تعديل مباشر لإطار مُخزَّن مؤقّتًا

نظريًّا، cache_data ينسخ عند القراءة، فالتعديل يقع على نسخة والأصل سليم. عمليًّا، Streamlit ينسخ باستعمال pickle وقد لا يلتقط كلّ التعديلات. القاعدة الآمنة: لا تعدّل مباشرة نتيجة cache_data؛ أنشئ نسخة صريحة (df = charger_donnees().copy()) قبل التعديل.

مع cache_resource المسألة أخطر: لا نسخ إطلاقًا. تعديل النموذج بعد استلامه يُلوّث كلّ الجلسات الأخرى. القاعدة: لا تعدّل نتيجة cache_resource أبدًا؛ إن احتجت نموذجًا مختلفًا، اكتب دالّة تُنتج نموذجًا جديدًا بمعلمات مختلفة.

قياس الأثر

الأثر العمليّ للتخزين مذهل. تطبيق يقرأ 50 ألف صفٍّ من CSV، يستنتج بنموذج غابة عشوائيّة، ويرسم:

  • بلا تخزين: 2.8 ثانية لكلّ نقرة.
  • cache_data على القراءة: 0.9 ثانية (توفير 1.9 ث).
  • إضافة cache_resource على النموذج: 0.14 ثانية (توفير 0.76 ث).
  • إضافة cache_data على الاستنتاج الدفعيّ: 0.02 ثانية.

من ثلاث ثوانٍ إلى عشرين مللي‌ثانية بأربعة أسطر ديكور. هذا مكسب لا يُقارَن.

الفخّ الأخطر: تحميل النموذج بـcache_data

كلّ يوم يُخطئ مطوّر في وضع @st.cache_data فوق دالّة تحميل نموذج. Streamlit يُحاول تسلسل النموذج (300 ميغابايت مثلًا) عند كلّ استدعاء لبناء المفتاح، فيتباطأ التطبيق أكثر من عدم استعمال تخزين إطلاقًا. الأعراض: نقرة تستغرق 5 ثوانٍ بدل 3. القاعدة: النماذج والاتّصالات ← cache_resource دائمًا.

الخلاصة

  • @st.cache_data للبيانات القابلة للنسخ (إطارات، قواميس، نتائج). @st.cache_resource للموارد المشتركة (نماذج، اتّصالات، عملاء).
  • مفتاح التخزين يُحسَب من قيم الوسائط؛ الوسائط بشرطة سفليّة (_conn) تُتجاهَل.
  • ثلاث آليّات إبطال: ttl، .clear()، تغيير الوسائط.
  • لا تعدّل نتيجة تخزين مؤقّت مباشرة؛ انسخ (copy()) أو أعد إنشاء الكائن بمعلمات جديدة.

الوحدة التالية: حالة الجلسة والنماذج لإدارة ما يجب أن يبقى بين التفاعلات.