الفصل الخامس عشر: بناء مشروع متكامل – تطبيق قائمة المهام
مقدمة الفصل
لقد وصلت إلى المحطة النهائية والأكثر أهمية في رحلتك التعليمية. هذا الفصل هو تتويج لكل ما تعلمته. سنقوم هنا بجمع كل المهارات التي اكتسبتها—من المتغيرات والدوال، إلى القوائم والقواميس، وصولًا إلى البرمجة كائنية التوجه والتعامل مع الملفات—لبناء مشروع حقيقي ومتكامل من الألف إلى الياء.
مشروعنا سيكون تطبيقًا لإدارة المهام (To-Do List) يعمل من سطر الأوامر. من خلال بنائه، ستتعلم كيف يفكر المبرمجون المحترفون عند تحويل فكرة إلى منتج عملي.
1. التخطيط للمشروع
قبل كتابة أي سطر برمجي، يجب أن نخطط. التخطيط الجيد هو نصف العمل.
أ. ميزات التطبيق (ماذا سيفعل؟)
تطبيقنا يجب أن يسمح للمستخدم بالقيام بالوظائف الأساسية التالية:
- عرض جميع المهام الحالية.
- إضافة مهمة جديدة.
- تحديث مهمة معينة واعتبارها “مُنجزة”.
- حذف مهمة من القائمة.
- حفظ المهام بشكل دائم بحيث لا تُفقد عند إغلاق التطبيق.
ب. هيكل المشروع (كيف سننظمه؟)
لتطبيق مبادئ التنظيم التي تعلمناها، لن نضع كل الكود في ملف واحد. بدلاً من ذلك، سنقسم المشروع إلى ثلاثة ملفات (وحدات)، كل ملف له مسؤولية محددة وواضحة:
todo_project/
│
├── main.py # (الملف الرئيسي): مسؤول عن الواجهة والتفاعل مع المستخدم.
├── task.py # (وحدة المهمة): يحتوي على "مخطط" أو كلاس المهمة.
└── storage.py # (وحدة التخزين): مسؤولة عن حفظ واسترجاع المهام من ملف.
هذا التقسيم يجعل مشروعنا نظيفًا، سهل الفهم، وقابلاً للتطوير في المستقبل.
2. بناء المشروع خطوة بخطوة
الجزء الأول: بناء “مخطط” المهمة (ملف task.py
)
سنبدأ بتصميم “قلب” تطبيقنا، وهو كلاس Task
. هذا الكلاس سيمثل كل مهمة على حدة، وسيحتوي على كل البيانات (الخصائص) والأفعال (الدوال) المتعلقة بها.
الآن، في مجلد مشروعك، أنشئ ملفًا جديدًا باسم task.py
واتبع الخطوات التالية:
1. استيراد وحدة datetime
نحتاج إلى تسجيل وقت وتاريخ إنشاء كل مهمة. أفضل أداة لهذا هي وحدة datetime
من مكتبة بايثون القياسية.
اكتب السطر التالي في ملف task.py
:
import datetime
2. تعريف كلاس Task
سنقوم الآن بإنشاء المخطط (الكلاس) الذي سيتم استخدامه لإنشاء كل مهمة في تطبيقنا.
أضف السطر التالي:
class Task:
3. بناء المُنشئ (دالة __init__
)
المُنشئ هو دالة خاصة تعمل تلقائيًا في كل مرة ننشئ فيها مهمة جديدة. وظيفتها هي تهيئة الخصائص الأولية للمهمة، مثل عنوانها وحالتها.
أضف الأسطر التالية داخل كلاس Task
(تذكر المسافة البادئة):
def __init__(self, title, is_done=False):
self.title = title
self.is_done = is_done
self.created_at = datetime.datetime.now()
شرح هذه الجزئية بالتفصيل:
def __init__(self, title, is_done=False):
: هذا هو تعريف المُنشئ.self
تشير إلى المهمة (الكائن) نفسها التي يتم إنشاؤها.title
هو عنوان المهمة الذي يجب أن نزوده به عند إنشائها.is_done=False
يعني أن أي مهمة جديدة ستكون حالتها “غير منجزة” بشكل افتراضي.
self.title = title
: هذا السطر يقوم بإنشاء خاصية اسمهاtitle
داخل الكائن ويخزن فيها قيمة العنوان الذي تم تمريره.self.is_done = is_done
: يقوم بإنشاء خاصيةis_done
ويخزن فيها الحالة.self.created_at = datetime.datetime.now()
: يقوم بإنشاء خاصيةcreated_at
ويسجل فيها الوقت والتاريخ الحاليين بالضبط عند إنشاء المهمة.
4. إضافة دالة لتحديث حالة المهمة نحتاج إلى طريقة لتغيير حالة المهمة من “غير منجزة” إلى “منجزة”. سنفعل ذلك عبر دالة (method) داخل الكلاس.
أضف الأسطر التالية داخل كلاس Task
:
def mark_as_done(self):
self.is_done = True
شرح هذه الجزئية:
mark_as_done
هي دالة بسيطة جدًا، كل ما تفعله هو تغيير قيمة الخاصيةis_done
إلىTrue
.
5. تحديد كيفية طباعة المهمة (دالة __str__
)
إذا حاولنا طباعة كائن المهمة مباشرة (print(my_task)
), سيعرض بايثون شيئًا غير مفهوم. دالة __str__
الخاصة تسمح لنا بتحديد الشكل النصي الذي نريده للمهمة عند طباعتها.
أضف الأسطر التالية داخل كلاس Task
:
def __str__(self):
status_icon = "✅" if self.is_done else "🔘"
formatted_date = self.created_at.strftime("%Y-%m-%d")
return f"[{status_icon}] {self.title} (Added: {formatted_date})"
شرح هذه الجزئية:
status_icon = "✅" if self.is_done else "🔘"
: هذا تعبير ثلاثي. إذا كانت المهمة منجزة (is_done
هوTrue
)، ستكون الأيقونة ✅. وإلا، ستكون 🔘.formatted_date = self.created_at.strftime("%Y-%m-%d")
: نستخدم دالةstrftime
لتنسيق التاريخ وعرضه بشكل مقروء.return f"..."
: نعيد نصًا منسقًا يجمع كل المعلومات معًا بشكل أنيق.
الكود الكامل لملف task.py
بعد اتباع كل الخطوات السابقة، يجب أن يبدو ملف task.py
الخاص بك هكذا:
# This is the complete code for: task.py
import datetime
class Task:
"""Represents a single task in our to-do list."""
def __init__(self, title, is_done=False):
"""Initializes a new Task."""
self.title = title
self.is_done = is_done
self.created_at = datetime.datetime.now()
def mark_as_done(self):
"""Marks the task as done."""
self.is_done = True
def __str__(self):
"""Returns a user-friendly string representation of the task."""
status_icon = "✅" if self.is_done else "🔘"
formatted_date = self.created_at.strftime("%Y-%m-%d")
return f"[{status_icon}] {self.title} (Added: {formatted_date})"
الجزء الثاني: بناء وحدة التخزين (ملف storage.py
)
هذه الوحدة ستكون مسؤولة عن حفظ واسترجاع قائمة مهامنا من وإلى ملف على القرص الصلب. سنستخدم تنسيق JSON لأنه سهل القراءة والكتابة في بايثون.
الآن، أنشئ ملفًا جديدًا باسم storage.py
واتبع الخطوات التالية:
1. استيراد الوحدات اللازمة
سنحتاج إلى وحدة json
للتعامل مع البيانات بصيغة JSON، وسنحتاج إلى استيراد كلاس Task
من ملف task.py
الذي أنشأناه للتو.
اكتب الأسطر التالية في ملف storage.py
:
import json
import datetime
from task import Task
شرح هذه الجزئية:
نحتاج إلى datetime
أيضًا هنا لأننا سنتعامل مع تواريخ عند قراءة البيانات من الملف.
2. تحديد اسم ملف التخزين من الجيد دائمًا وضع اسم الملف في متغير ثابت في أعلى الملف لتسهيل تغييره لاحقًا إذا أردنا.
أضف السطر التالي:
STORAGE_FILE = "tasks.json"
3. بناء دالة save_tasks
هذه الدالة ستحول قائمة كائنات Task
إلى صيغة يمكن حفظها في ملف JSON.
أضف الأسطر التالية:
def save_tasks(tasks):
list_to_save = []
for task in tasks:
task_dict = {
"title": task.title,
"is_done": task.is_done,
"created_at": task.created_at.isoformat()
}
list_to_save.append(task_dict)
with open(STORAGE_FILE, "w", encoding="utf-8") as f:
json.dump(list_to_save, f, indent=4, ensure_ascii=False)
شرح هذه الجزئية:
- لا يمكن لـ JSON حفظ كائنات بايثون مباشرة. لذلك، نحن بحاجة لتحويل كل كائن
task
إلى قاموسdict
بسيط. - داخل الحلقة، ننشئ قاموسًا لكل مهمة. لاحظ أننا نحول التاريخ إلى نص باستخدام
isoformat
، لأن كائناتdatetime
لا يمكن حفظها مباشرة. - في النهاية، نستخدم
json.dump
لحفظ قائمة القواميس في الملف.indent=4
تجعل الملف منظمًا وسهل القراءة للإنسان.
4. بناء دالة load_tasks
هذه الدالة ستقوم بالعكس: ستقرأ البيانات من ملف JSON وتحولها مرة أخرى إلى قائمة من كائنات Task
.
أضف الأسطر التالية:
def load_tasks():
try:
with open(STORAGE_FILE, "r", encoding="utf-8") as f:
list_of_dicts = json.load(f)
loaded_tasks = []
for task_dict in list_of_dicts:
task_obj = Task(task_dict['title'], task_dict['is_done'])
task_obj.created_at = datetime.datetime.fromisoformat(task_dict['created_at'])
loaded_tasks.append(task_obj)
return loaded_tasks
except (FileNotFoundError, json.JSONDecodeError):
return []
شرح هذه الجزئية:
- نضع الكود داخل
try...except
للتعامل مع الحالة التي لا يكون فيها الملف موجودًا (عند أول تشغيل)، أو إذا كان الملف فارغًا أو تالفًا. في هذه الحالات، سنعيد قائمة فارغة. json.load
يقرأ الملف ويعيد لنا قائمة من القواميس.- داخل الحلقة، نمر على كل قاموس ونستخدم بياناته لإنشاء كائن
Task
جديد. - نحول نص التاريخ من
isoformat
مرة أخرى إلى كائنdatetime
حقيقي باستخدامfromisoformat
. - نعيد القائمة النهائية المليئة بكائنات
Task
.
الكود الكامل لملف storage.py
يجب أن يبدو ملف storage.py
الخاص بك هكذا:
# This is the complete code for: storage.py
import json
import datetime
from task import Task
STORAGE_FILE = "tasks.json"
def save_tasks(tasks):
"""Saves a list of task objects to the JSON file."""
list_to_save = []
for task in tasks:
task_dict = {
"title": task.title,
"is_done": task.is_done,
"created_at": task.created_at.isoformat()
}
list_to_save.append(task_dict)
with open(STORAGE_FILE, "w", encoding="utf-8") as f:
json.dump(list_to_save, f, indent=4, ensure_ascii=False)
def load_tasks():
"""Loads tasks from the JSON file and returns a list of task objects."""
try:
with open(STORAGE_FILE, "r", encoding="utf-8") as f:
list_of_dicts = json.load(f)
loaded_tasks = []
for task_dict in list_of_dicts:
task_obj = Task(task_dict['title'], task_dict['is_done'])
task_obj.created_at = datetime.datetime.fromisoformat(task_dict['created_at'])
loaded_tasks.append(task_obj)
return loaded_tasks
except (FileNotFoundError, json.JSONDecodeError):
# If the file doesn't exist or is empty/corrupt, return an empty list
return []
الجزء الثالث: بناء الواجهة الرئيسية (ملف main.py
)
هذا هو الملف الذي سيقوم المستخدم بتشغيله. إنه يجمع كل الأجزاء معًا.
الآن، أنشئ ملفًا جديدًا باسم main.py
واتبع الخطوات التالية:
1. استيراد الأدوات اللازمة
سنحتاج إلى استيراد الدوال من storage.py
وكلاس Task
من task.py
.
اكتب الأسطر التالية في ملف main.py
:
from storage import save_tasks, load_tasks
from task import Task
2. بناء دالة لعرض القائمة الرئيسية من الجيد دائمًا وضع الكود الذي يتكرر، مثل عرض القائمة، في دالة خاصة به.
أضف الأسطر التالية:
def show_menu():
print("\n--- To-Do List Application ---")
print("1. View all tasks")
print("2. Add a new task")
print("3. Mark a task as done")
print("4. Delete a task")
print("5. Exit")
3. بناء الدالة الرئيسية main
هذه هي الدالة التي ستحتوي على المنطق الرئيسي للتطبيق.
أضف الأسطر التالية:
def main():
tasks_list = load_tasks()
while True:
show_menu()
choice = input("Enter your choice: ")
# ... a-logic-for-choices-will-go-here ...
شرح هذه الجزئية:
tasks_list = load_tasks()
: أول شيء نفعله هو تحميل أي مهام محفوظة من الجلسات السابقة.while True:
: نستخدم حلقة لا نهائية لإبقاء البرنامج يعمل ويعرض القائمة بشكل مستمر حتى يختار المستخدم الخروج.
4. برمجة خيارات المستخدم داخل حلقة while
سنقوم الآن بملء الفراغ داخل الحلقة بمنطق كل خيار.
أضف كتلة if/elif/else
التالية داخل حلقة while
:
if choice == '1':
print("\n--- Your Tasks ---")
if not tasks_list:
print("Your to-do list is empty.")
else:
for i, task in enumerate(tasks_list, start=1):
print(f"{i}. {task}")
elif choice == '2':
title = input("Enter the title of the new task: ")
tasks_list.append(Task(title))
save_tasks(tasks_list)
print("Task added successfully.")
elif choice == '3':
try:
task_num = int(input("Enter the task number to mark as done: "))
if 1 <= task_num <= len(tasks_list):
tasks_list[task_num - 1].mark_as_done()
save_tasks(tasks_list)
print("Task marked as done.")
else:
print("Invalid task number.")
except ValueError:
print("Please enter a valid number.")
elif choice == '4':
try:
task_num = int(input("Enter the task number to delete: "))
if 1 <= task_num <= len(tasks_list):
del tasks_list[task_num - 1]
save_tasks(tasks_list)
print("Task deleted successfully.")
else:
print("Invalid task number.")
except ValueError:
print("Please enter a valid number.")
elif choice == '5':
print("Goodbye!")
break
else:
print("Invalid choice. Please choose a number from 1 to 5.")
شرح هذه الجزئية:
- عرض المهام (
choice == '1'
): يتحقق أولاً مما إذا كانت القائمة فارغة. إذا لم تكن كذلك، فإنه يستخدمenumerate
لعرض المهام مرقمة. - إضافة مهمة (
choice == '2'
): يطلب عنوانًا، ينشئ كائنTask
جديدًا، يضيفه إلى القائمة، ثم يستدعي فورًاsave_tasks
لحفظ التغيير. - تحديث وحذف (
choice == '3'
,'4'
): يطلبان رقم المهمة. لاحظ أننا نستخدمtask_num - 1
للوصول إلى الفهرس الصحيح في القائمة (لأن المستخدم يرى الأرقام من 1، بينما الفهرسة تبدأ من 0). ثم يقومان بالتعديل أو الحذف، ويحفظان التغييرات. - الخروج (
choice == '5'
): يستخدمbreak
لكسر حلقةwhile
وإنهاء البرنامج.
5. تشغيل الدالة الرئيسية
أخيرًا، نحتاج إلى سطر يخبر بايثون ببدء تشغيل دالة main
عند تنفيذ الملف.
أضف هذه الكتلة في نهاية ملف main.py
:
if __name__ == "__main__":
main()
شرح هذه الجزئية:
هذه عبارة بايثونية قياسية ومهمة. إنها تضمن أن دالة main
لن يتم استدعاؤها إلا إذا تم تشغيل هذا الملف (main.py
) مباشرة، وليس إذا تم استيراده كوحدة في ملف آخر.
الكود الكامل لملف main.py
يجب أن يبدو ملف main.py
الخاص بك هكذا:
# This is the complete code for: main.py
from storage import save_tasks, load_tasks
from task import Task
def show_menu():
"""Displays the main menu to the user."""
print("\n--- To-Do List Application ---")
print("1. View all tasks")
print("2. Add a new task")
print("3. Mark a task as done")
print("4. Delete a task")
print("5. Exit")
def main():
"""The main function that runs the application."""
tasks_list = load_tasks()
while True:
show_menu()
choice = input("Enter your choice: ")
if choice == '1':
print("\n--- Your Tasks ---")
if not tasks_list:
print("Your to-do list is empty.")
else:
for i, task in enumerate(tasks_list, start=1):
print(f"{i}. {task}")
elif choice == '2':
title = input("Enter the title of the new task: ")
tasks_list.append(Task(title))
save_tasks(tasks_list)
print("Task added successfully.")
elif choice == '3':
try:
task_num = int(input("Enter the task number to mark as done: "))
if 1 <= task_num <= len(tasks_list):
tasks_list[task_num - 1].mark_as_done()
save_tasks(tasks_list)
print("Task marked as done.")
else:
print("Invalid task number.")
except ValueError:
print("Please enter a valid number.")
elif choice == '4':
try:
task_num = int(input("Enter the task number to delete: "))
if 1 <= task_num <= len(tasks_list):
del tasks_list[task_num - 1]
save_tasks(tasks_list)
print("Task deleted successfully.")
else:
print("Invalid task number.")
except ValueError:
print("Please enter a valid number.")
elif choice == '5':
print("Goodbye!")
break
else:
print("Invalid choice. Please choose a number from 1 to 5.")
# This line ensures that main() is called only when the script is executed directly
if __name__ == "__main__":
main()
3. تشغيل التطبيق
الآن بعد أن أصبحت كل الملفات (task.py
, storage.py
, main.py
) جاهزة في نفس المجلد، كل ما عليك فعله هو فتح سطر الأوامر في هذا المجلد وتشغيل الملف الرئيسي:
python main.py
جرب إضافة مهام، عرضها، حذفها، ثم أغلق التطبيق وأعد تشغيله. ستجد أن مهامك لا تزال محفوظة!
4. خاتمة الكتاب
تهانينا! لقد وصلت إلى نهاية رحلتك التعليمية. لم تعد مجرد شخص يقرأ عن البرمجة، بل أصبحت شخصًا يبني تطبيقات حقيقية. هذا المشروع هو دليل على أنك الآن تفهم كيفية دمج كل المفاهيم التي درسناها معًا في منتج عملي ومتكامل.
هذه ليست النهاية، بل هي البداية الحقيقية. عالم البرمجة واسع وممتع، والآن لديك الأساس القوي الذي يمكنك البناء عليه.
الخطوات التالية المقترحة:
- توسيع هذا المشروع: حاول إضافة ميزات جديدة، مثل تعديل المهام، أو تحديد أولويات لها.
- تعلم إطار عمل ويب: استخدم معرفتك في بايثون لتعلم Django أو Flask لبناء تطبيقات ويب.
- استكشاف علم البيانات: ابدأ بتعلم مكتبات مثل Pandas و Matplotlib لتحليل البيانات.
- المساهمة في مشاريع مفتوحة المصدر: ابحث عن مشاريع على GitHub وشارك في تطويرها.
نصيحتي الأخيرة لك: لا تتوقف عن التعلم والممارسة. البرمجة مهارة تنمو بالتجربة. ابحث، جرب، اخطئ، وتعلم من أخطائك. أتمنى لك كل التوفيق في رحلتك كمبرمج.