এক্সেপশন — ভাঙা অবস্থা সামলানো
try/except দিয়ে নির্দিষ্ট এরর ধরা, কেন except Exception: pass প্রায় সবসময়ই ভুল, else আর finally, নিজে raise করা, আর এররের পরিবার থেকে সঠিক মাপেরটা বেছে নেওয়া।
কীভাবে শিখতে চান?
যে সমস্যাটা আমরা সমাধান করছি
গত অধ্যায়ের শেষ প্রশ্নটায় একটা প্রোগ্রাম তিনটা লাইন ঠিকঠাক প্রসেস করে চার নম্বরটায় থেমে গিয়েছিল। ছোট করে দেখলে এরকম:
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
total += int(text)
print(total)ValueError: invalid literal for int() with base 10: 'twenty'(এররের আগে পাইথন traceback-এর কয়েকটি লাইনও ছাপে; এই অধ্যায়ে সাধারণত শুধু শেষ লাইনটি দেখানো হয়েছে।)
10 ঠিকঠাক যোগ হয়েছিল। 30 কখনো যোগ হলো না। আর print(total) কখনো চললই না।
একটা খারাপ লাইনের জন্য গোটা কাজটা বন্ধ। এখন পর্যন্ত আমাদের কাছে দুটোই বিকল্প ছিল — হয় সব ডেটা নিখুঁত হবে, নয়তো প্রোগ্রাম থেমে যাবে।
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except ValueError:
print("skipping", text)
print(total)skipping twenty
40তৃতীয় একটা বিকল্প আছে: সমস্যাটা ঘটতে দিন, তারপর তার উত্তর দিন।
এই অধ্যায় শেষে আপনি পারবেন
try/exceptদিয়ে একটা নির্দিষ্ট এরর ধরতে- বলতে কেন খালি
except:বাexcept Exception: passপ্রায় সবসময়ই ভুল as errদিয়ে এররটা হাতে নিয়ে তার বার্তা পড়তেelseআরfinallyকী করে তা বলতে- নিজে
raiseকরতে, আর কখন করা উচিত তা বুঝতে - এররের পরিবার চিনতে, আর সঠিক মাপের এররটা ধরতে
আগে যা জানা লাগবে: ফাইল পড়া আর লেখা।
একটা এররও একটা অবজেক্ট
as লিখলে এররটাকে হাতে পাওয়া যায়:
try:
int("twenty")
except ValueError as err:
print(type(err))
print(err)<class 'ValueError'>
invalid literal for int() with base 10: 'twenty'এতদিন যে লাল লেখাগুলো টার্মিনালে দেখে এসেছেন, সেগুলো আসলে এই অবজেক্টগুলোই — আর ছাপলে যা আসে সেটাই তার বার্তা।
এর একটা বাস্তব ফল আছে: এররটা নিজেই সবচেয়ে ভালো ব্যাখ্যাটা বহন করে। নিজের ভাষায় “কিছু একটা ভুল হয়েছে” লেখার চেয়ে err-টা ছেপে দেওয়া প্রায় সবসময়ই বেশি কাজের।
সঠিক এররটা ধরুন
try:
int("twenty")
except TypeError:
print("caught")ValueError: invalid literal for int() with base 10: 'twenty'except কেবল যে টাইপটা লেখা আছে সেটাই ধরে। এখানে TypeError লেখা, আর যা ঘটেছে সেটা ValueError — তাই except ব্লকটা যেন নেই-ই।
এটা বিরক্তিকর মনে হতে পারে, কিন্তু এটাই কাজের জিনিসটা। আপনি কোন ভুলটার জন্য প্রস্তুত, সেটা except লাইনটাই বলে দেয় — আর যেটার জন্য প্রস্তুত নন, সেটা আগের মতোই থামিয়ে দেয়, যা ঠিক কাজ।
সেজন্যই এটা লিখবেন না:
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except Exception:
pass
print(total)40উত্তরটা ঠিক, আর কোডটা তবুও খারাপ। except Exception: pass সব এরর গিলে ফেলে — যেগুলোর কথা আপনি ভেবেছেন আর যেগুলোর কথা ভাবেননি, দুটোই। কালকে numbers-এর জায়গায় ভুল করে একটা ডিকশনারি এলে, বা ভিতরের কোডে একটা বানান ভুল হলে, এই প্রোগ্রামটা নীরবে ভুল উত্তর দেবে।
নিয়ম: যে এররটার জন্য আপনার পরিকল্পনা আছে, ঠিক সেটাই ধরুন। আর ধরলে কিছু একটা করুন — অন্তত বলুন কী হলো।
একাধিক টাইপ
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))15
no such key
no such positionকয়েকটা except পরপর লেখা যায়, আর প্রথম যেটা মেলে সেটাই চলে — বাকিগুলো দেখাও হয় না।
একই উত্তর হলে একসাথে লেখা যায়:
for value in ["10", None, "x"]:
try:
print(int(value))
except (ValueError, TypeError) as err:
print(type(err).__name__, "-", err)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__ এররের টাইপটার নাম দেয় — লগ লেখার সময় কাজে আসে।
এররদের একটা পরিবার আছে
print(issubclass(FileNotFoundError, OSError))
print(issubclass(ValueError, Exception))
print(issubclass(KeyError, LookupError))
print(issubclass(IndexError, LookupError))True
True
True
Trueএররগুলো একটা গাছের মতো সাজানো, আর পিতাকে ধরলে সন্তানও ধরা পড়ে:
try:
raise FileNotFoundError("gone")
except OSError as err:
print("caught as OSError:", err)caught as OSError: goneকাজে লাগার মতো কয়েকটা সম্পর্ক: FileNotFoundError আর PermissionError দুটোই OSError; KeyError আর IndexError দুটোই LookupError; আর প্রায় সবকিছুই Exception।
এখান থেকে নিয়মটা আসে: যত ছোট মাপের এরর ধরা যায়, ততই ভালো। except Exception লেখা সবচেয়ে সহজ আর সবচেয়ে কম কাজের।
else আর finally
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"))good: 10
done with 10
10
bad: ten
done with ten
Noneelse চলে যদি try ব্লকে কিছুই না ভাঙে। এতে try-এর ভিতরটা ছোট রাখা যায় — কেবল যে লাইনটা ভাঙতে পারে সেটুকু — আর সফল হলে যা করার তা else-এ যায়। try-এর ভিতরে বেশি কোড রাখলে এমন কিছুও ধরা পড়ে যাকে ধরার কথা ছিল না।
finally সবসময় চলে, যাই ঘটুক। এমনকি return-এর পরেও:
def risky():
try:
return "from try"
finally:
print("finally still runs")
print(risky())finally still runs
from tryreturn মানটা ঠিক করে ফেলেছে, কিন্তু ফাংশন থেকে বেরোনোর আগে finally চলে। পরিষ্কার করার কাজের জন্য এটাই জায়গা — যদিও ফাইলের বেলায় with এই কাজটা আগেই করে দেয়।
নিজে raise করা
ধরা যতটা দরকার, ছোঁড়াও ততটাই।
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))45.0
ValueError: quantity cannot be negative: -1raise ছাড়া বিকল্প কী ছিল? None ফেরত দেওয়া, বা 0 — আর তখন ভুলটা ভ্রমণ করত। কোনো একটা মোট হিসাবের ভিতরে ঢুকে যেত, আর অনেক পরে একটা অদ্ভুত সংখ্যা হয়ে দেখা দিত, যার উৎস খুঁজে পাওয়া যেত না।
একটা এরর ভুলটাকে তার জন্মের জায়গাতেই থামায়, আর বার্তায় কারণটাও বলে দেয়।
বার্তাটা লেখার সময় যে মানটা সমস্যা করেছে সেটা ভিতরে রাখুন। "invalid quantity" আর "quantity cannot be negative: -1" — দ্বিতীয়টা পড়ে কাজ শুরু করা যায়।
ধরে আবার ছোঁড়াও যায়, আরও ভালো বার্তা দিয়ে। parse.py:
def parse(text):
try:
return int(text)
except ValueError:
raise ValueError(f"not a number: {text!r}")
print(parse("10"))
print(parse("ten"))10
Traceback (most recent call last):
File ".../parse.py", line 3, in parse
return int(text)
^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'ten'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File ".../parse.py", line 9, in <module>
print(parse("ten"))
^^^^^^^^^^^^
File ".../parse.py", line 5, in parse
raise ValueError(f"not a number: {text!r}")
ValueError: not a number: 'ten'এবার পুরো আউটপুটটাই দেখানো হলো, কারণ এখানে শেষ লাইনটা পুরো গল্প বলে না। (File লাইনে আপনার মেশিনে ফাইলের পুরো পথ থাকবে; এখানে সেটা ... দিয়ে ছোট করা হয়েছে।) পাইথন দুটো traceback ছেপেছে: প্রথমটা আসল int()-এর এরর, দ্বিতীয়টা আমাদের নতুন ValueError। মাঝের লাইন During handling of the above exception, another exception occurred: বলছে যে দ্বিতীয়টা প্রথমটা সামলানোর সময় ঘটেছে। কোনো তথ্য হারায়নি — কিন্তু পড়ে মনে হয় যেন except ব্লকের ভিতরেই কোনো বাগ ছিল।
ইচ্ছে করে নতুন এরর ছুঁড়লে সেটা স্পষ্ট করে বলুন: except ValueError as err: লিখে তারপর raise ValueError(f"not a number: {text!r}") from err। তখন মাঝের লাইনটা হয়ে যায় The above exception was the direct cause of the following exception: — অর্থাৎ “প্রথমটাই দ্বিতীয়টার কারণ”, যা ঠিক আমাদের উদ্দেশ্য।
{text!r} মানটাকে repr করে বসায়, তাই কোটেশন চিহ্নগুলো দেখা যায় — not a number: 'ten' আর not a number: ten-এর মধ্যে পার্থক্যটা বোঝা যায় যখন মানটা আসলে একটা খালি স্ট্রিং বা ফাঁকা স্পেস।
আগে জিজ্ঞেস করা, নাকি পরে ক্ষমা চাওয়া
prices = {"pen": 15}
if "bag" in prices:
print(prices["bag"])
else:
print("not stocked")
try:
print(prices["bag"])
except KeyError:
print("not stocked")not stocked
not stockedদুটোই ঠিক। পাইথনে দ্বিতীয়টাই বেশি প্রচলিত, আর তার একটা কারণ আছে: প্রথমটা দুবার খোঁজে — একবার in-এ, একবার [...]-এ — আর মাঝখানে কিছু বদলে গেলে তবু ভাঙতে পারে।
তবে যা নিয়মিত ঘটে তার জন্য if লেখাই পরিষ্কার। ব্যতিক্রমের জন্য ব্যতিক্রম — নামটা এমনিতেই মনে করিয়ে দেয়।
একটা সম্পূর্ণ উদাহরণ
orders.txt, যেখানে ইচ্ছে করে দুটো খারাপ লাইন আছে:
pen,15.0,3
bag,eight,1
ink,120.0,2
clip,5.0main.py:
"""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()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 লিখবেন না। সেখানে কেবল পরিষ্কার করার কাজ।
ধাপ ৪ / ৬ — অনুমান
যাচাই করুন
একটা except লেখা আছে। প্রোগ্রামটার কী হবে?
try:
int("twenty")
except TypeError:
print("caught")- A`caught` ছাপা হয়
- Bকিছুই ছাপা হয় না, প্রোগ্রামটা নীরবে শেষ হয়
- C`TypeError` নিয়ে থেমে যায়
- Dপ্রোগ্রামটা `ValueError` নিয়ে থেমে যায় — `except`-টা যেন নেই-ই
গোটা লুপটা try-এর ভিতরে। কী ছাপা হবে?
values = ["10", "x", "30"]
total = 0
try:
for text in values:
total += int(text)
except ValueError:
print("gave up")
print(total)- Agave up 40
- Bgave up 10
- C40
- Dgave up 0
try আর except — দুটোতেই return আছে। কী ছাপা হবে?
def read(text):
try:
return int(text)
except ValueError:
return 0
finally:
print("checked", text)
print(read("7"))
print(read("seven"))- Achecked 7 7 checked seven 0
- B7 0
- Cchecked 7 7 0
- D7 checked 7 0 checked seven
উত্তর দিতে অ্যাকাউন্ট লাগবে
উত্তর মিলিয়ে দেখতে সাইন ইন করুন
প্রশ্নগুলো উপরে আছে, আর মাথায় মাথায় উত্তর ভেবে নেওয়াই আসল কাজ। সঠিক উত্তর, ব্যাখ্যা আর তিন ধাপের ইঙ্গিত দেখতে সাইন ইন করুন।
নিজে করুন
গত অধ্যায়ের summary.py-তে ফিরে যান, যেটা people.txt পড়ত।
এখন সেটাকে এমন করুন যাতে সে কখনো ক্র্যাশ না করে, আর শেষে বলে দেয় কোন লাইনগুলো বাদ গেল ও কেন। একটা parse_line(raw, number) ফাংশন বানান যে খারাপ লাইনের জন্য লাইন নম্বরসহ ValueError ছোঁড়ে।
তারপর ফাইলটাকে ইচ্ছে করে নষ্ট করুন — একটা লাইন থেকে কমা সরিয়ে দিন, একটাতে সংখ্যার জায়গায় অক্ষর বসান, আর একটা লাইন একেবারে খালি রাখুন। যেমন:
rafi,12
nadia 30
sami,ten
lina,25খালি লাইনটা কোনো অভিযোগ নয় — গত অধ্যায়ের মতোই সেটা চুপচাপ বাদ যায়। তাই প্রোগ্রামটা শেষ পর্যন্ত চলবে, আর দুটো অভিযোগ দেবে। বার্তাগুলো যদি সম্পূর্ণ উদাহরণের মতো করে লেখেন (expected 2 fields, got ... আর line {number}: {err}), তাহলে আউটপুটের শেষটা ঠিক এরকম হবে:
2 lines used, 2 skipped
- line 2: expected 2 fields, got 1
- line 3: invalid literal for int() with base 10: 'ten'তারপর চারটা পরীক্ষা:
except ValueError-এর জায়গায়except Exception: passলিখুন। আউটপুট কী হয়, আর সেটা কেন বেশি বিপজ্জনক?parse_line-এর বার্তা থেকে লাইন নম্বরটা সরান। এবার একটা খারাপ লাইন খুঁজে বের করতে কত সময় লাগে?tryব্লকটা বড় করুন — গোটা লুপের শরীরটা তার ভিতরে ঢোকান। একটা লাইন খারাপ হলে এখন কী হয়?finallyযোগ করুন যে প্রতিটা লাইনের পর কিছু ছাপে, আর ভালো-খারাপ দুই রকম লাইনেই সে চলে কিনা দেখুন।
তৃতীয় পরীক্ষাটা এই অধ্যায়ের সবচেয়ে কাজের অভ্যাসটা শেখায়: try ব্লক যত ছোট, ধরাটা তত সৎ।
ধাপ ৬ / ৬
কঠিন করা — অধ্যায়ের কুইজ
সহজ থেকে কঠিন — দশটি প্রশ্ন, শেষেরগুলো ইচ্ছে করেই কঠিন।
সাইন ইন করে কুইজ দিন