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

الوحدة 6 — حالة الجلسة والنماذج

الوحدة السابقة عالجت ما يبقى بين المستخدمين (نموذج مُشترَك، اتّصال قاعدة). هذه الوحدة تعالج ما يبقى داخل جلسة المستخدم الواحد: قيمة عدّاد، نتيجة نموذج سابقة، خطوة معالج متعدّد الخطوات. الأداة الوحيدة لذلك هي st.session_state، وهو قاموس يبقى بين إعادات التنفيذ لنفس الجلسة. سنراه من زوايا خمس: القاعدة، الرجعات، النماذج، التنقّل، والسباقات.

القاعدة: قاموس بين إعادات التنفيذ

st.session_state قاموس (بمعنى Python الحرفيّ) خاصّ بكلّ جلسة مستخدم. يمكن الوصول إليه بمفتاح (st.session_state["nom"]) أو بسمة (st.session_state.nom).

import streamlit as st

# تهيئة أوّليّة: مرّة واحدة عند بدء الجلسة.
if "compteur" not in st.session_state:
st.session_state.compteur = 0

st.write(f"العدّاد: {st.session_state.compteur}")

if st.button("زد واحدًا"):
st.session_state.compteur += 1

المستخدم يفتح التطبيق → compteur = 0. يضغط الزرّ → إعادة تنفيذ → compteur = 1. يُغلق التطبيق ثمّ يُعيد الفتح → compteur = 0 مجدّدًا (جلسة جديدة). مستخدم آخر في المتصفّح نفسه (تبويب مختلف) → عدّاده مستقلّ.

قاعدة أوّلى ضروريّة: التهيئة الأوّليّة تكون دائمًا بـif "key" not in st.session_state:، لا بـst.session_state.compteur = 0 مباشرة (وإلّا أعاد الصفر في كلّ إعادة تنفيذ).

المكوّنات وsession_state

كلّ مكوّن له مفتاح (key) يُخزَّن قيمته في session_state تلقائيًّا:

nom = st.text_input("الاسم", key="client_nom")
# بعد كتابة "أحمد"، القيمتان متكافئتان:
assert nom == st.session_state.client_nom == "أحمد"

الفائدة: يمكن قراءة قيمة الحقل من أيّ مكان في الشيفرة بلا الحاجة إلى تمريرها كوسيطة. الفائدة الأخرى: يمكن ضبط القيمة قبل عرض المكوّن:

if "client_nom" not in st.session_state:
st.session_state.client_nom = "غير معروف"
st.text_input("الاسم", key="client_nom")

المكوّن سيعرض «غير معروف» أوّل مرّة. لا تُعطِ value= وkey= معًا لنفس المكوّن؛ ذلك سلوك غامض.

الرجعات (on_change, on_click)

كلّ مكوّن يقبل معلمة on_change (للحقول) أو on_click (للأزرار). دالّة تُنفَّذ قبل إعادة التنفيذ، مع الوصول الكامل إلى session_state.

def basculer_scoring():
st.session_state.derniere_action = "scoring"
st.session_state.compteur_scoring = st.session_state.get("compteur_scoring", 0) + 1

st.button("سجّل", on_click=basculer_scoring)
st.write("آخر فعل:", st.session_state.get("derniere_action"))
st.write("عدد التسجيلات:", st.session_state.get("compteur_scoring", 0))

فرق دقيق: الشيفرة داخل if st.button(...) تُنفَّذ بعد إعادة التنفيذ التي تلت النقر، وقد يكون الترتيب معكوسًا مع مكوّنات أخرى. الشيفرة داخل on_click تُنفَّذ قبل أيّ عرض. لضبط قيم session_state قبل عرض المكوّنات التالية، استعمل الرجعات.

مثال لـon_change:

def normaliser_id():
st.session_state.identifiant = st.session_state.identifiant.strip().upper()

st.text_input("المعرّف", key="identifiant", on_change=normaliser_id)

المستخدم يكتب « c042 » ← إعادة تنفيذ ← الرجعة تُنفَّذ ← القيمة تُصبح « C042 » ← الحقل يعرضها.

النماذج (st.form)

في الوضع العاديّ، كلّ مكوّن إدخال يُنشط إعادة تنفيذ. لصفحة بها عشرة حقول، هذا مزعج: كلّ تغيير حقل يُعيد رسم كلّ شيء. st.form يجمع مكوّنات في «نموذج» لا يُرسل إلّا عند نقر زرّ إرسال:

with st.form("nouveau_client"):
identifiant = st.text_input("المعرّف")
age = st.slider("العمر", 18, 90, 42)
anciennete = st.number_input("الأقدميّة (شهر)", 0, 240, 24)
offre = st.selectbox("العرض", ["Basic", "Standard", "Premium", "Family"])
envoye = st.form_submit_button("سجّل")

if envoye:
st.success(f"سُجِّل العميل {identifiant}")
# ... حساب الدرجة، حفظ، عرض ...

فوائد ثلاث. الأداء: لا إعادة تنفيذ عند كلّ حقل. الاتّساق: النموذج يُرسَل بقيم متسقّة كلّها. الوضوح: المستخدم يعرف متى «يُرسل».

قيود ثلاث. الرسوم داخل النموذج لا تتحدّث تفاعليًّا: لا فائدة من عرض st.metric يتغيّر مع كلّ حقل داخل نموذج. زرّ الإرسال إلزاميّ: st.form_submit_button لا يمكن استبداله بـst.button. زرّ الإرسال الوحيد: لا يمكن وضع زرّ إجراء آخر داخل النموذج (تستعمل زرَّي إرسال مختلفَين إن احتجت).

قاعدة القرار: حقول قليلة + عرض تفاعليّ → بلا نموذج. حقول كثيرة + معالجة عند الإرسال → نموذج.

التنقّل بين الخطوات

تطبيق متعدّد الخطوات (خطوة → خطوة → خطوة) يستعمل مفتاحًا في session_state لتتبّع الخطوة الحاليّة:

import streamlit as st

if "etape" not in st.session_state:
st.session_state.etape = 1

def suivant():
st.session_state.etape = min(st.session_state.etape + 1, 3)

def precedent():
st.session_state.etape = max(st.session_state.etape - 1, 1)

st.progress(st.session_state.etape / 3)

if st.session_state.etape == 1:
st.subheader("الخطوة 1 من 3 — معلومات العميل")
st.text_input("المعرّف", key="identifiant")
st.text_input("الاسم", key="nom")
elif st.session_state.etape == 2:
st.subheader("الخطوة 2 من 3 — العرض والاستهلاك")
st.selectbox("العرض", ["Basic", "Standard", "Premium", "Family"], key="offre")
st.number_input("المبلغ الشهريّ (USD)", 0.0, 500.0, 39.99, key="mensuel")
else:
st.subheader("الخطوة 3 من 3 — مراجعة وحفظ")
st.json({k: st.session_state.get(k) for k in ["identifiant", "nom", "offre", "mensuel"]})

col_p, col_n = st.columns(2)
col_p.button("السابق", on_click=precedent, disabled=st.session_state.etape == 1)
col_n.button("التالي", on_click=suivant, disabled=st.session_state.etape == 3)

خمس نقاط تصميم. مفتاح etape يُهيَّأ مرّة. الرجعات suivant وprecedent تعدّل الخطوة قبل إعادة الرسم. مؤشّر التقدّم (st.progress) يُعطي حسّ تقدّم للمستخدم. disabled يمنع الرجوع قبل الخطوة الأولى أو التقدّم بعد الأخيرة. قيم الحقول تبقى في session_state عند الانتقال بين الخطوات، فلا نفقدها.

السباقات وإعادة التنفيذ

Streamlit يُعيد التنفيذ من أعلى إلى أسفل بلا توازٍ داخل نفس الجلسة. لذلك السباقات نادرة. لكن حالة واحدة يجب فهمها: الفرق بين st.button وon_click.

# نمط قد يفشل
if st.button("سجّل"):
resultat = calculer()
st.session_state.resultat = resultat # حُفظ

st.write(st.session_state.get("resultat")) # مقروء في التنفيذ نفسه

هذا يعمل. لكن هذا لا يعمل كما يُتوقَّع:

if st.button("زد"):
st.session_state.compteur += 1 # يعمل مرّة واحدة عند النقر

st.slider("قيمة", 0, 100, key="valeur")
# بعد تحريك المنزلق، قيمة الزرّ False، فالعدّاد لا يزيد، وهذا صحيح.

الفخّ الأشيع: توقّع أنّ العدّاد يزيد كلّما تحرّك المنزلق. بالعكس، الزرّ يُعيد True فقط في التنفيذ الأوّل بعد النقر. لتنفيذ فعل مع كلّ تغيير حقل، استعمل on_change على الحقل، لا if button.

قاعدة عمليّة لاختيار الأداة
  • قيمة تُقرأ من الحقل في التنفيذ الحاليّ فقط → لا شيء، اقرأ من متغيّر عاديّ.
  • قيمة تُقرأ من عدّة أماكن → key على المكوّن، اقرأ من session_state.
  • فعل عند نقر زرّ → on_click (يعمل قبل الرسم) أو if button (يعمل بعد الرسم).
  • فعل عند تغيير حقل → on_change.
  • مجموعة حقول تُرسل معًا → st.form مع st.form_submit_button.

إعادة تعيين النموذج

بعد تسجيل عميل، نريد تنظيف الحقول:

def reinitialiser():
for cle in ["identifiant", "nom", "offre", "mensuel"]:
if cle in st.session_state:
del st.session_state[cle]

st.button("عميل جديد", on_click=reinitialiser)

del st.session_state[cle] يحذف المفتاح، فيُعيد المكوّن قيمته الافتراضيّة في التنفيذ التالي. مهمّ: لا يجوز حذف مفتاح مكوّن بعد عرضه في نفس التنفيذ؛ يرفع Streamlit استثناء StreamlitAPIException. لذلك نستعمل رجعة (تُنفَّذ قبل الرسم).

الخلاصة

  • st.session_state قاموس بين إعادات التنفيذ للجلسة الواحدة؛ يُهيَّأ بـif key not in ....
  • كلّ مكوّن بـkey= يُخزَّن قيمته في session_state تلقائيًّا؛ يقرأها ويكتبها من أيّ مكان.
  • الرجعات (on_change, on_click) تُنفَّذ قبل إعادة الرسم؛ if st.button(...) بعده.
  • st.form يجمع الحقول ويُرسلها دفعةً واحدة عند st.form_submit_button؛ لا رسوم تفاعليّة داخله.
  • الفخّ: نتيجة زرّ تُعاد True مرّة واحدة، ثمّ False؛ لحفظها استعمل session_state، ولإعادة الفعل مع تغيير حقل استعمل on_change.

الوحدة التالية: رفع الملفّات وتنزيلها لتسجيل دفعة عملاء من CSV.