অধ্যায় 24

এক্সেপশন — ভাঙা অবস্থা সামলানো

try/except দিয়ে নির্দিষ্ট ত্রুটি ধরা, কেন except Exception: pass প্রায় সবসময়ই ভুল, else আর finally, নিজে raise করা, আর ত্রুটির পরিবার থেকে সঠিক মাপেরটা বেছে নেওয়া।

36 মিনিটPython 3.12
  1. 1সমস্যা
  2. 2বোঝা
  3. 3উদাহরণ
  4. 4অনুমান
  5. 5নিজে করা
  6. 6কঠিন করা

যে সমস্যাটা আমরা সমাধান করছি

গত অধ্যায়ের শেষ প্রশ্নটায় একটা প্রোগ্রাম তিনটে লাইন ঠিকঠাক প্রক্রিয়া করে চতুর্থটায় থেমে গিয়েছিল। ছোট করে দেখলে এরকম:

python
numbers = ["10", "twenty", "30"]
total = 0

for text in numbers:
    total += int(text)

print(total)
text
ValueError: invalid literal for int() with base 10: 'twenty'

10 ঠিকঠাক যোগ হয়েছিল। 30 কখনো যোগ হলো না। আর print(total) কখনো চললই না।

একটা খারাপ লাইনের জন্য গোটা কাজটা বন্ধ। এখন পর্যন্ত আমাদের কাছে দুটোই বিকল্প ছিল — হয় সব তথ্য নিখুঁত হবে, নয়তো প্রোগ্রাম থেমে যাবে।

python
numbers = ["10", "twenty", "30"]
total = 0

for text in numbers:
    try:
        total += int(text)
    except ValueError:
        print("skipping", text)

print(total)
text
skipping twenty
40

তৃতীয় একটা বিকল্প আছে: সমস্যাটা ঘটতে দিন, তারপর তার উত্তর দিন।

এই অধ্যায় শেষে আপনি পারবেন

  • try / except দিয়ে একটা নির্দিষ্ট ত্রুটি ধরতে
  • বলতে কেন খালি except: বা except Exception: pass প্রায় সবসময়ই ভুল
  • as err দিয়ে ত্রুটিটা হাতে নিয়ে তার বার্তা পড়তে
  • else আর finally কী করে তা বলতে
  • নিজে raise করতে, আর কখন করা উচিত তা বুঝতে
  • ত্রুটির পরিবার চিনতে, আর সঠিক মাপের ত্রুটিটা ধরতে

পূর্বশর্ত: ফাইল পড়া আর লেখা।


একটা ত্রুটিও একটা বস্তু

as লিখলে ত্রুটিটাকে হাতে পাওয়া যায়:

python
try:
    int("twenty")
except ValueError as err:
    print(type(err))
    print(err)
text
<class 'ValueError'>
invalid literal for int() with base 10: 'twenty'

এতদিন যে লাল লেখাগুলো টার্মিনালে দেখে এসেছেন, সেগুলো আসলে এই বস্তুগুলোই — আর ছাপলে যা আসে সেটাই তার বার্তা।

এর একটা বাস্তব ফল আছে: ত্রুটিটা নিজেই সবচেয়ে ভালো ব্যাখ্যাটা বহন করে। নিজের ভাষায় «কিছু একটা ভুল হয়েছে» লেখার চেয়ে err-টা ছেপে দেওয়া প্রায় সবসময়ই বেশি কাজের।

সঠিক ত্রুটিটা ধরুন

python
try:
    int("twenty")
except TypeError:
    print("caught")
text
ValueError: invalid literal for int() with base 10: 'twenty'

except কেবল যে ধরনটা লেখা আছে সেটাই ধরে। এখানে TypeError লেখা, আর যা ঘটেছে সেটা ValueError — তাই except ব্লকটা যেন নেই-ই।

এটা বিরক্তিকর মনে হতে পারে, কিন্তু এটাই কাজের জিনিসটা। আপনি কোন ভুলটার জন্য প্রস্তুত, সেটা except লাইনটাই বলে দেয় — আর যেটার জন্য প্রস্তুত নন, সেটা আগের মতোই থামিয়ে দেয়, যা ঠিক কাজ।

সেজন্যই এটা লিখবেন না:

python
numbers = ["10", "twenty", "30"]
total = 0

for text in numbers:
    try:
        total += int(text)
    except Exception:
        pass

print(total)
text
40

উত্তরটা ঠিক, আর কোডটা তবুও খারাপ। except Exception: pass সব ত্রুটি গিলে ফেলে — যেগুলোর কথা আপনি ভেবেছেন আর যেগুলোর কথা ভাবেননি, দুটোই। কালকে numbers-এর জায়গায় ভুল করে একটা ডিকশনারি এলে, বা ভিতরের কোডে একটা বানান ভুল হলে, এই প্রোগ্রামটা নীরবে ভুল উত্তর দেবে।

নিয়ম: যে ত্রুটিটার জন্য আপনার পরিকল্পনা আছে, ঠিক সেটাই ধরুন। আর ধরলে কিছু একটা করুন — অন্তত বলুন কী হলো।

একাধিক ধরন

python
def get(data, key):
    try:
        return data[key]
    except KeyError:
        return "no such key"
    except IndexError:
        return "no such position"


print(get({"pen": 15}, "pen"))
print(get({"pen": 15}, "bag"))
print(get([1, 2, 3], 9))
text
15
no such key
no such position

কয়েকটা except পরপর লেখা যায়, আর প্রথম যেটা মেলে সেটাই চলে — বাকিগুলো দেখাও হয় না।

একই উত্তর হলে একসাথে লেখা যায়:

python
for value in ["10", None, "x"]:
    try:
        print(int(value))
    except (ValueError, TypeError) as err:
        print(type(err).__name__, "-", err)
text
10
TypeError - int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
ValueError - invalid literal for int() with base 10: 'x'

type(err).__name__ ত্রুটির ধরনটার নাম দেয় — লগ লেখার সময় কাজে আসে।

ত্রুটিদের একটা পরিবার আছে

python
print(issubclass(FileNotFoundError, OSError))
print(issubclass(ValueError, Exception))
print(issubclass(KeyError, LookupError))
print(issubclass(IndexError, LookupError))
text
True
True
True
True

ত্রুটিগুলো একটা গাছের মতো সাজানো, আর পিতাকে ধরলে সন্তানও ধরা পড়ে:

python
try:
    raise FileNotFoundError("gone")
except OSError as err:
    print("caught as OSError:", err)
text
caught as OSError: gone

কাজে লাগার মতো কয়েকটা সম্পর্ক: FileNotFoundError আর PermissionError দুটোই OSError; KeyError আর IndexError দুটোই LookupError; আর প্রায় সবকিছুই Exception।

এখান থেকে নিয়মটা আসে: যত ছোট মাপের ত্রুটি ধরা যায়, ততই ভালো। except Exception লেখা সবচেয়ে সহজ আর সবচেয়ে কম কাজের।

else আর finally

python
def read(text):
    try:
        value = int(text)
    except ValueError:
        print("bad:", text)
        return None
    else:
        print("good:", value)
        return value
    finally:
        print("done with", text)


print(read("10"))
print(read("ten"))
text
good: 10
done with 10
10
bad: ten
done with ten
None

else চলে যদি try ব্লকে কিছুই না ভাঙে। এতে try-এর ভিতরটা ছোট রাখা যায় — কেবল যে লাইনটা ভাঙতে পারে সেটুকু — আর সফল হলে যা করার তা else-এ যায়। try-এর ভিতরে বেশি কোড রাখলে এমন কিছুও ধরা পড়ে যাকে ধরার কথা ছিল না।

finally সবসময় চলে, যাই ঘটুক। এমনকি return-এর পরেও:

python
def risky():
    try:
        return "from try"
    finally:
        print("finally still runs")


print(risky())
text
finally still runs
from try

return মানটা ঠিক করে ফেলেছে, কিন্তু ফাংশন থেকে বেরোনোর আগে finally চলে। পরিষ্কার করার কাজের জন্য এটাই জায়গা — যদিও ফাইলের ক্ষেত্রে with এই কাজটা আগেই করে দেয়।

নিজে raise করা

ধরা যতটা দরকার, ছোঁড়াও ততটাই।

python
def line_total(price, quantity):
    if quantity < 0:
        raise ValueError(f"quantity cannot be negative: {quantity}")
    return price * quantity


print(line_total(15.0, 3))
print(line_total(15.0, -1))
text
45.0
ValueError: quantity cannot be negative: -1

raise ছাড়া বিকল্প কী ছিল? None ফেরত দেওয়া, বা 0 — আর তখন ভুলটা ভ্রমণ করত। কোনো একটা মোট হিসাবের ভিতরে ঢুকে যেত, আর অনেক পরে একটা অদ্ভুত সংখ্যা হয়ে দেখা দিত, যার উৎস খুঁজে পাওয়া যেত না।

একটা ত্রুটি ভুলটাকে তার জন্মের জায়গাতেই থামায়, আর বার্তায় কারণটাও বলে দেয়।

বার্তাটা লেখার সময় যে মানটা সমস্যা করেছে সেটা ভিতরে রাখুন। "invalid quantity" আর "quantity cannot be negative: -1" — দ্বিতীয়টা পড়ে কাজ শুরু করা যায়।

ধরে আবার ছোঁড়াও যায়, আরও ভালো বার্তা দিয়ে:

python
def parse(text):
    try:
        return int(text)
    except ValueError:
        raise ValueError(f"not a number: {text!r}")


print(parse("10"))
print(parse("ten"))
text
10
ValueError: not a number: 'ten'

{text!r} মানটাকে repr করে বসায়, তাই উদ্ধৃতি চিহ্নগুলো দেখা যায় — not a number: 'ten' আর not a number: ten-এর মধ্যে পার্থক্যটা বোঝা যায় যখন মানটা আসলে একটা খালি লেখা বা ফাঁকা স্পেস।

আগে জিজ্ঞেস করা, নাকি পরে ক্ষমা চাওয়া

python
prices = {"pen": 15}

if "bag" in prices:
    print(prices["bag"])
else:
    print("not stocked")

try:
    print(prices["bag"])
except KeyError:
    print("not stocked")
text
not stocked
not stocked

দুটোই ঠিক। পাইথনে দ্বিতীয়টাই বেশি প্রচলিত, আর তার একটা কারণ আছে: প্রথমটা দুবার খোঁজে — একবার in-এ, একবার [...]-এ — আর মাঝখানে কিছু বদলে গেলে তবু ভাঙতে পারে।

তবে যা নিয়মিত ঘটে তার জন্য if লেখাই পরিষ্কার। ব্যতিক্রমের জন্য ব্যতিক্রম — নামটা এমনিতেই মনে করিয়ে দেয়।


একটা সম্পূর্ণ উদাহরণ

orders.txt, যেখানে ইচ্ছে করে দুটো খারাপ লাইন আছে:

text
pen,15.0,3
bag,eight,1

ink,120.0,2
clip,5.0

main.py:

python
"""Read an order file, and keep going when a line is wrong."""

from pathlib import Path

TAX_RATE = 0.15


def parse_line(raw, number):
    """Returns one order line, or raises ValueError saying which line was wrong."""
    parts = raw.split(",")
    if len(parts) != 3:
        raise ValueError(f"line {number}: expected 3 fields, got {len(parts)}")

    name, price, quantity = parts
    try:
        return name, float(price), int(quantity)
    except ValueError as err:
        raise ValueError(f"line {number}: {err}")


def read_order(path):
    """Returns the good lines and the complaints, never raising for bad data."""
    try:
        text = path.read_text(encoding="utf-8")
    except FileNotFoundError:
        return [], [f"no such file: {path}"]

    lines = []
    problems = []

    for number, raw in enumerate(text.splitlines(), start=1):
        if not raw.strip():
            continue
        try:
            lines.append(parse_line(raw, number))
        except ValueError as err:
            problems.append(str(err))

    return lines, problems


def main():
    lines, problems = read_order(Path("orders.txt"))

    total = 0.0
    for name, price, quantity in lines:
        amount = round(price * quantity * (1 + TAX_RATE), 2)
        total += amount
        print(f"{name:<6} {amount:>9.2f}")

    print(f"{'total':<6} {total:>9.2f}")

    print()
    print(f"{len(lines)} lines used, {len(problems)} skipped")
    for problem in problems:
        print(" -", problem)


if __name__ == "__main__":
    main()
text
pen        51.75
ink       276.00
total     327.75

2 lines used, 2 skipped
 - line 2: could not convert string to float: 'eight'
 - line 5: expected 3 fields, got 2

চারটে জিনিস দেখার মতো।

parse_line ছোঁড়ে, read_order ধরে। নিচের ফাংশনটা জানে না গোটা প্রোগ্রামের কী করা উচিত — সে কেবল জানে এই লাইনটা ব্যবহারযোগ্য নয়। কী করা হবে সেই সিদ্ধান্তটা উপরের ফাংশনের। ত্রুটি ছোঁড়া হয় নিচে, ধরা হয় উপরে — এই বিভাজনটাই ব্যতিক্রম ব্যবস্থাটার মূল কাজ।

লাইন নম্বরটা বার্তার ভিতরে আছে। could not convert string to float: 'eight' নিজে থেকে বলে না কোথায় দেখতে হবে; line 2: ... বলে। যে তথ্যটা ধরার সময় আপনার কাছে আছে আর পরে থাকবে না, সেটা বার্তায় ঢোকানোই নিয়ম।

read_order নিজে কখনো ভাঙে না। সে দুটো জিনিস ফেরত দেয় — যা কাজে লাগল আর যা লাগল না। ফাইলটা না থাকলেও একই আকারের উত্তর আসে, তাই main-কে দুরকম পথ লিখতে হয় না।

ভিতরের try ব্লকটা ঠিক এক লাইনের। float(price) আর int(quantity) — কেবল যে অংশটা ভাঙতে পারে সেটুকুই তার ভিতরে। parts = raw.split(",") বাইরে, কারণ সেটা ভাঙার কথা নয়, আর ভাঙলে আমি জানতে চাই।


কিছু ভাঙা অবস্থা ও তার সমাধান

ত্রুটিটা ধরা পড়ছে না, যদিও except লেখা আছে ধরনটা মেলেনি। বার্তার প্রথম শব্দটাই ত্রুটির নাম — সেটাই লিখুন, নয়তো তার পিতাকে ধরুন।

except আছে কিন্তু প্রোগ্রামটা তবু থেমে যাচ্ছে ভাঙা লাইনটা try ব্লকের ভিতরে নেই। ইনডেন্টেশন দেখুন।

SyntaxError: expected 'except' or 'finally' block একটা try লেখা হয়েছে যার কোনো except নেই। try একা দাঁড়াতে পারে না।

সবকিছু «কাজ করছে» কিন্তু উত্তরগুলো ভুল কোথাও except Exception: pass আছে। সেটা খুঁজুন — সমস্যা সেখানেই।

raise করলাম কিন্তু কোনো বার্তা দেখা যাচ্ছে না raise ValueError লেখা হয়েছে, raise ValueError("...") নয়। বন্ধনী আর বার্তাটা দিন।

finally-তে return লিখলে ত্রুটিটা হারিয়ে যায় সেটাই হয়, আর সেজন্যই finally-তে return লিখবেন না। সেখানে কেবল পরিষ্কার করার কাজ।