برمجة بايثون 2026 — جامعة هلسنكي › الجزء 6: الملفات والأخطاء 6 / 15 السابق التالي

معالجة الأخطاء

أهداف التعلّم

بعد إتمام هذا القسم

  • ستعرف كيف تتعامل مع المُدخَلات غير الصالحة
  • ستفهم ما هي الاستثناءات في البرمجة
  • ستكون على دراية بأنواع الاستثناءات الشائعة في Python
  • ستكون قادرًا على التعامل مع الاستثناءات في برامجك الخاصة

هناك فئتان أساسيتان من الأخطاء تظهر في سياقات البرمجة:

  1. أخطاء الصياغة، التي تمنع تنفيذ البرنامج
  2. أخطاء وقت التشغيل، التي توقف التنفيذ

وأخطاء الفئة الأولى عادةً سهلة الإصلاح، لأن مفسِّر Python يحدّد موضع الخطأ عند محاولة تنفيذ البرنامج. ومن أخطاء الصياغة الشائعة نفيان نقطة رأسية في نهاية سطر عنوان، أو نفيان علامة تنصيص في نهاية سلسلة نصية.

أما أخطاء الفئة الثانية فقد يكون اكتشافها أصعب، إذ قد تظهر حصريًا في نقطة معيّنة من تنفيذ البرنامج، وفي ظروف معيّنة فقط. فقد يعمل البرنامج بشكل جيد في معظم المواقف، لكنه يتوقف بسبب خطأ في حالة حدّية بعينها. وسنركّز الآن على التعامل مع هذه الأنواع من الأخطاء.

التحقق من المُدخَل

كثير من الأخطاء التي تظهر أثناء تنفيذ البرنامج تتعلق بمُدخَل غير صالح. ومن أمثلة ذلك:

  • قيم مُدخَل ناقصة أو فارغة في حقول إلزامية، مثل السلاسل النصية الفارغة حين يكون طول السلسلة النصية حاسمًا
  • قيم سالبة حيث تُقبَل القيم الموجبة فقط، مثل -15 بوصفها كمية مكوّن في وصفة
  • ملفات مفقودة أو أخطاء إملائية في أسماء الملفات
  • قيم صغيرة جدًّا أو كبيرة جدًّا، مثلًا عند التعامل مع التواريخ والأوقات
  • فهارس غير صالحة، مثل محاولة الوصول إلى الفهرس 3 في السلسلة النصية "hey"
  • قيم من نوع خاطئ، مثل سلاسل نصية حين يُتوقَّع أعداد صحيحة

ولحسن الحظ، يمكننا نحن كمبرمجين الاستعداد لأغلب الأخطاء. فلننظر إلى برنامج يسأل المستخدم عن عمره، ويتأكّد أنه رقم مقبول (بين 0 و150 في هذه الحالة):

age = int(input("Please type in your age: "))
if age >= 0 and age <= 150:
    print("That is a fine age")
else:
    print("This is not a valid age")
Please type in your age: **25**
That is a fine age
Please type in your age: **-3**
This is not a valid age

ما دام المستخدم يكتب قيمة صحيحة، فيبدو التحقق من المُدخَل لدينا يعمل جيدًا. لكن ماذا لو كتب سلسلة نصية؟

Please type in your age: **twenty-three**
ValueError: invalid literal for int() with base 10: 'twenty-three'

الدالة int عاجزة عن تحليل السلسلة النصية twenty-three كقيمة صحيحة صالحة. فيتوقف التنفيذ وتُطبع رسالة الخطأ أعلاه.

الاستثناءات

الأخطاء التي تحدث أثناء قيام البرنامج بالتشغيل بالفعل تُسمّى استثناءات (exceptions). ويمكن الاستعداد لهذه الاستثناءات والتعامل معها بحيث يستمر التنفيذ رغم وقوعها.

ويُنجَز التعامل مع الاستثناءات في Python بجملتَي try وexcept. والفكرة هي أنه إذا تسبّب شيء ما داخل كتلة try في استثناء، فتتحقق Python من وجود كتلة except مقابلة. وإذا وُجدت كتلة كتلك، فتُنفَّذ ثم يستمر البرنامج كأن شيئًا لم يكن.

لنعدّل المثال أعلاه بحيث يستعد البرنامج لاستثناء ValueError:

try:
    age = int(input("Please type in your age: "))
except ValueError:
    age = -1

if age >= 0 and age <= 150:
    print("That is a fine age")
else:
    print("This is not a valid age")
Please type in your age: **twenty-three**
This is not a valid age

ويمكننا استخدام كتلة try للإشارة إلى أن الشيفرة داخلها قد تسبّب في خطأ. وفي جملة except مباشرة بعد الكتلة يُذكر الخطأ المعني. وفي المثال أعلاه اكتفينا بمعالجة استثناء ValueError وحده. ولو كان للاستثناء سبب آخر لتوقّف التنفيذ على أي حال، رغم كتلتَي try وexcept.

وفي المثال أعلاه، إذا التُقط الخطأ فتُضبط قيمة age إلى -1. وهذه قيمة مُدخَل غير صالحة قد برمجنا سلوكًا مسبقًا لها، لأن البرنامج يتوقّع أن يكون عمر المستخدم أكبر من 0.

وفي المثال التالي لدينا الدالة read_integer، التي تطلب من المستخدم كتابة قيمة صحيحة، لكن الدالة مستعدة أيضًا للمُدخَل غير الصالح. وتظل الدالة تطلب أعدادًا صحيحة حتى يكتب المستخدم قيمة مُدخَل صالحة.

def read_integer():
    while True:
        try:
            input_str = input("Please type in an integer: ")
            return int(input_str)
        except ValueError:
            print("This input is invalid")

number = read_integer()
print("Thank you!")
print(number, "to the power of three is", number**3)
Please type in an integer: **three**
This input is invalid
Please type in an integer: **aybabtu**
This input is invalid
Please type in an integer: **5**
Thank you!
5 to the power of three is 125

وقد يكفي أحيانًا التقاط الاستثناءات ببنية المحاولة/الالتقاط (try-except) دون فعل أي شيء حيالها. أي يكفي أن نتجاهل الموقف في كتلة except.

ولو غيّرنا المثال أعلاه بحيث لا نقبل إلا الأعداد الصحيحة الأصغر من 100، لبَدَت النتائج هكذا:

def read_small_integer():
    while True:
        try:
            input_str = input("Please type in an integer: ")
            number = int(input_str)
            if number < 100:
                return number
        except ValueError:
            pass # this command doesn't actually do anything

        print("This input is invalid")

number = read_small_integer()
print(number, "to the power of three is", number**3)
Please type in an integer: **three**
This input is invalid
Please type in an integer: **1000**
This input is invalid
Please type in an integer: **5**
Thank you!
5 to the power of three is 125

والآن لا تحوي كتلة except سوى الأمر pass، الذي لا يفعل شيئًا. ولا تسمح Python بالكتل الفارغة، لذا يكون الأمر ضروريًا.

تمرين برمجي: قراءة المُدخَل

يُرجى كتابة دالة اسمها read_input، تطلب من المستخدم المُدخَل حتى يكتب عددًا صحيحيًا واقعًا ضمن الحدود المُعطاة في وسائط الدالة. ويجب أن تُعيد الدالة القيمة الصحيحة الصالحة النهائية التي كتبها المستخدم.

ومثال على عمل الدالة:

number = read_input("Please type in a number: ", 5, 10)
print("You typed in:", number)
Please type in a number: **seven**
You must type in an integer between 5 and 10
Please type in a number: **-3**
You must type in an integer between 5 and 10
Please type in a number: **8**
You typed in: 8

الأخطاء النموذجية

فيما يلي مختارات من الأخطاء النموذجية التي ستصادفها على الأرجح، مع بعض المواقف التي قد تظهر فيها.

ValueError

وغالبًا ما يُرفع هذا الخطأ حين يكون الوسيط الممرَّر إلى الدالة غير صالح بطريقة ما. فمثلًا، استدعاء الدالة float("1,23") يسبّب خطأ، لأن الكسور العشرية تُفصل دائمًا بنقطة في Python، وهنا لدينا فاصلة.

TypeError

يحدث هذا الخطأ حين تكون القيمة من نوع خاطئ. فمثلًا، استدعاء الدالة len(10) يسبّب TypeError، لأن الدالة len تتطلب قيمة يمكن حساب طولها، مثل سلسلة نصية أو قائمة.

IndexError

يحدث هذا الخطأ الشائع عند محاولة الإشارة إلى فهرس غير موجود. فمثلًا، التعبير "abc"[5] يسبّب IndexError، لأن السلسلة النصية المعنية لا فهرس 5 لها.

ZeroDivisionError

وكما يوحي الاسم، يُرفع هذا الخطأ عند محاولة القسمة على صفر، وهو كما نعلم من الرياضيات فكرة سيئة دائمًا. فمثلًا، لو حاولنا تحديد المتوسط الحسابي لقيم في قائمة بالصيغة sum(my_list) / len(my_list)، لكن قائمتنا طولها صفر، فسيحدث هذا الخطأ.

استثناءات في التعامل مع الملفات

ومن الأخطاء الشائعة عند العمل مع الملفات FileNotFoundError (عند محاولة الوصول إلى ملف غير موجود)، وio.UnsupportedOperation (عند محاولة تنفيذ عملية على ملف لا يدعمها الوضع الذي فُتح به الملف)، أو PermissionError (ينقص البرنامج الأذونات اللازمة للوصول إلى الملف).

التعامل مع عدة استثناءات دفعة واحدة

قد تكون هناك أكثر من كتلة except مرفقة بكل كتلة try. فمثلًا، يستطيع البرنامج التالي التعامل مع كلا الخطأين FileNotFoundException وPermissionError:

try:
    with open("example.txt") as my_file:
        for line in my_file:
            print(line)
except FileNotFoundError:
    print("The file example.txt was not found")
except PermissionError:
    print("No permission to access the file example.txt")

وقد لا يلزم أحيانًا تحديد الخطأ الذي يستعد له البرنامج. ولا سيما عند التعامل مع الملفات، فغالبًا ما يكفي معرفة أن خطأً قد حدث، ثم الخروج من البرنامج بأمان. وليس من الضروري دائمًا معرفة سبب حدوث الخطأ. وإذا احتجنا تغطية جميع الاستثناءات الممكنة، فيمكننا استخدام كتلة except دون تحديد الخطأ:


try:
    with open("example.txt") as my_file:
        for line in my_file:
            print(line)
except:
    print("There was an error when reading the file.")

ملاحظة: تغطي جملة except هنا جميع الأخطاء الممكنة، حتى تلك الناتجة عن أخطاء البرمجة نفسها. أما أخطاء الصياغة وحدها فلن تُلتقط بهذه الطريقة، لأنها تمنع الشيفرة من التنفيذ أصلًا.

فمثلًا، سيرفع البرنامج التالي خطأً دائمًا، لأن اسم المتغيّر my_file كُتب في السطر الثالث بوصفه myfile:

try:
    with open("example.txt") as my_file:
        for line in myfile:
            print(line)
except:
    print("There was an error when reading the file.")

وقد تخفي كتلة except الخطأ الحقيقي: فالمشكلة هنا لم تُسبَّب بالتعامل مع الملفات بحد ذاته، بل باسم المتغيّر المكتوب خطأً. ولو لا كتلة except لعرض الخطأ المُرفَع وصار من الأسهل العثور على سببه. ولهذا يكون من الجيد عادةً استخدام كتل except المُصرَّح بها خصيصًا لأنواع أخطاء معيّنة.

تمرير الاستثناءات

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

وفي المثال التالي لدينا الدالة testing. وإذا تسبّبت في استثناء، فلم يُتعامل معه داخل الدالة ذاتها، بل في الدالة الرئيسية:

def testing(x):
    print(int(x) + 1)

try:
    number = input("Please type in a number: ")
    testing(number)
except:
    print("Something went wrong")
Please type in a number: **three**
Something went wrong

رفع الاستثناءات

يمكنك أيضًا رفع الاستثناءات بالأمر raise. وقد تبدو فكرة غريبة تعمّد إحداث أخطاء في برامجك، لكنه قد يكون في الواقع آلية مفيدة جدًّا.

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

وفي المثال التالي لدينا دالة تحسب المضروب (factorial) (مثلًا، مضروب العدد 5 هو 1 * 2 * 3 * 4 * 5). وإذا كان الوسيط الممرَّر إلى الدالة سالبًا، فترفع الدالة خطأً:

def factorial(n):
    if n < 0:
        raise ValueError("The input was negative: " + str(n))
    k = 1
    for i in range(2, n + 1):
        k *= i
    return k

print(factorial(3))
print(factorial(6))
print(factorial(-1))
6
720
Traceback (most recent call last):
  File "test.py", line 11, in <module>
    print(factorial(-1))
  File "test.py", line 3, in factorial
    raise ValueError("The input was negative: " + str(n))
ValueError: The input was negative: -1

تمرين برمجي: التحقّق من المعاملات

يُرجى كتابة دالة اسمها new_person(name: str, age: int)، تنشئ صفًّا يحوي البيانات في الوسائط وتعيده. ويجب أن يكون العنصر الأول هو الاسم والثاني هو العمر.

وإذا كانت القيم المخزّنة في متغيّرات المعاملات غير صالحة، فترفع الدالة استثناء ValueError.

ومن المعاملات غير الصالحة في هذه الحالة:

  • الاسم سلسلة نصية فارغة
  • الاسم يحوي أقل من كلمتين
  • الاسم أطول من 40 محرفًا
  • العمر رقم سالب
  • العمر أكبر من 150

تمرين برمجي: أعداد يانصيب غير صحيحة

يحتوي الملف lottery_numbers.csv على أرقام اليانصيب الفائزة بالصيغة التالية:

week 1;5,7,11,13,23,24,30
week 2;9,13,14,24,34,35,37
...etc...

ويجب أن يحوي كل سطر ترويسة week x، يتبعها سبعة أعداد صحيحة كلها بين 1 و39 شاملًا الطرفين.

لقد فسد الملف. وقد تحتوي سطور الملف على أنواع الأخطاء التالية (قد لا توجد هذه السطور بالضبط في الملف، لكن ستوجد أخطاء بصيغة مماثلة):

رقم الأسبوع غير صحيح:

week zzc;1,5,13,22,24,25,26

واحد أو أكثر من الأرقام غير صحيحة:

week 22;1,**,5,6,13,2b,34

الأرقام قليلة جدًّا:

week 13;4,6,17,19,24,33

الأرقام صغيرة جدًّا أو كبيرة جدًّا:

week 39;5,9,15,35,39,41,105

يظهر الرقم نفسه مرتين:

week 41;5,12,3,35,12,14,36

يُرجى كتابة دالة اسمها filter_incorrect()، تنشئ ملفًا اسمه correct_numbers.csv. ويجب أن يحوي الملف تلك السطور فقط من الملف الأصلي التي تكون بصيغة صحيحة.