সরাসরি পর্বে যান
পর্ব ২১ · ৫৫-এর মধ্যে · মডিউল ৩

মাঝপথের প্রজেক্ট — পরীক্ষার ফলের একটি CLI টুল

Halfway Project — An Exam Result Manager on the Command Line

Taught in Bangla, this halfway project builds a command-line results and grading tool in Python: planning single-purpose functions, choosing a list-of-dicts data model, validating input without exceptions, reports with sorted and comprehensions, avoiding alias and mutable-default bugs, and testing with assert.

এই পর্বে যা শিখবেন

  • একটি অস্পষ্ট ইচ্ছা (“ফলাফল বানাতে খুব ঝামেলা”) থেকে কমান্ডের টেবিল আর MVP-র সীমারেখা লেখা
  • প্রোগ্রামকে এক-কাজের ফাংশনে ভাগ করা, আর হিসাব ও ছাপানোর কাজ আলাদা রাখা
  • list of dicts বনাম dict of dicts — কোনটি কেন, আর তার দাম কী
  • মাইলস্টোনে বানানো, exception ছাড়া ইনপুট যাচাই, sorted-এর key আর comprehension দিয়ে রিপোর্ট
  • alias আর mutable default-এর ফাঁদ এড়ানো, assert দিয়ে টেস্ট, আর AI-কে দায়িত্বের সাথে pair-programmer হিসেবে ব্যবহার

১ · একটি আসল প্রোগ্রাম — অনেক ছোট ফাংশন আর একটি লুপ

বিশটি পর্বে আপনি Python-এর মূল সব হাতিয়ার হাতে নিয়েছেন: ভ্যারিয়েবল, string, শর্ত, লুপ, ফাংশন, list, tuple, dict, set, comprehension, আর মেমরিতে object কীভাবে ভাগাভাগি হয় তার ছবি। কিন্তু হাতিয়ার চেনা আর হাতিয়ার দিয়ে কিছু বানানো আলাদা দক্ষতা — রান্নার সব মসলা চিনলেই বিয়েবাড়ির বিরিয়ানি রাঁধা যায় না। মডিউল ৩-এর এই শেষ পর্বে আমরা সব মিলিয়ে একটি সত্যিকারের ছোট টুল বানাব।

রাজশাহীর একটি স্কুলের শিক্ষক নাসরিন আপা প্রতি পরীক্ষার পর খাতায় নম্বর তোলেন, তারপর ক্যালকুলেটরে প্রত্যেকের মোট নম্বর, GPA, কে প্রথম, কে ফেল — সব হাতে বের করেন। একটি ভুল যোগ মানে পুরো মেধাতালিকা নতুন করে লেখা। তিনি চান একটি ছোট প্রোগ্রাম, যেখানে নম্বর লিখলেই বাকিটা নিজে থেকে হয়ে যায়।

আমাদের প্রোগ্রামটি হবে একটি CLI টুল — terminal-এ চলে, প্রতিটি লাইনে একটি কমান্ড পড়ে, উত্তর লেখে, আবার পরের কমান্ডের জন্য অপেক্ষা করে। শেষে এর সাথে কথোপকথন হবে এমন:

> add Rahim 78 85 90
  ok: Rahim added, GPA 4.67
> add Sumi 85 88 80
  ok: Sumi added, GPA 5.00
> add Tania 95 abc 70
  usage: add NAME bangla english math (each 0-100)
> show
  1. Rahim  253  GPA 4.67
  2. Sumi   253  GPA 5.00
> quit
মূল ধারণা

যেকোনো আসল প্রোগ্রাম আসলে অনেকগুলো ছোট ফাংশন, যার প্রত্যেকটি একটি কাজ ভালোভাবে করে, আর সেগুলোকে জুড়ে রাখে একটি লুপ, যা কমান্ড পড়ে ঠিক ফাংশনটিকে ডাকে। ফাংশনগুলো হলো রান্নাঘরের রাঁধুনি; লুপ হলো ওয়েটার, যে অর্ডার নেয় আর খাবার পৌঁছে দেয়। ওয়েটার রান্না করে না, রাঁধুনি টেবিলে যায় না।

এই পর্বের আসল বিষয় ফলাফলের প্রোগ্রাম নয়, পদ্ধতি: চাহিদা লেখা → কাজকে ফাংশনে ভাগ করা → ডেটার আকার ঠিক করা → ছোট ছোট ধাপে (milestone) বানানো → প্রতিটি ধাপ পরীক্ষা করা। একই পদ্ধতি চলে দোকানের হিসাব, লাইব্রেরির বইয়ের তালিকা বা হাসপাতালের রোগীর খাতায়। C++ কোর্সের মাঝপথের প্রজেক্টে একই পদ্ধতিতে একটি দোকানের হিসাবের টুল বানানো হয়েছে; এখানে বিষয় আলাদা, আর Python-এর নিজস্ব হাতিয়ারগুলো (dict, comprehension, sorted) কাজে লাগবে।

২ · চাহিদা — কমান্ডের টেবিল আর গ্রেডিং স্কেল

নতুনদের সবচেয়ে সাধারণ ভুল হলো সমস্যা না বুঝেই কোড লিখতে বসা। দর্জি মাপ না নিয়ে জামা সেলাই করলে যত সুন্দর সেলাই-ই হোক, গায়ে লাগবে না। সফটওয়্যারে মাপ নেওয়ার নাম requirement বা চাহিদা লেখা। নাসরিন আপার সাথে কথা বলে আমরা ঠিক করলাম টুলটি কী কী কমান্ড বুঝবে, আর প্রতিটির ফল কী হবে:

কমান্ডকী করেকখন “ঠিক আছে” বলব
add Rahim 78 85 90একজন ছাত্র যোগ — নাম, তারপর বাংলা, ইংরেজি, গণিতের নম্বরযোগ হলে GPA-সহ নিশ্চিতকরণ; নম্বর কম/বেশি, অক্ষর, ১০০-র বেশি বা ঋণাত্মক হলে কিছুই বদলায় না, usage বার্তা; একই নাম আবার দিলে error
showফলাফলের তালিকা, মোট নম্বরের ক্রমেমোট নম্বর বেশি থেকে কম; সমান হলে নামের বর্ণক্রমে; সাথে ক্রমিক নম্বর আর GPA
topসর্বোচ্চ মোট নম্বর পাওয়া ছাত্র(রা)সমান নম্বর হলে সবার নাম
failযারা অন্তত এক বিষয়ে ফেলকেউ না থাকলে nobody failed
avgবিষয়ভিত্তিক গড় নম্বরএক ঘর দশমিকে
find riনামের অংশ দিয়ে খোঁজাছোট-বড় হাতের অক্ষর না মেনে; না পেলে no match
quitবের হওয়াকতজন ছাত্র ছিল জানিয়ে শেষ
অন্য যেকোনো লেখাভুল টাইপপ্রোগ্রাম বন্ধ হয় না; কোন কমান্ড আছে জানিয়ে পরের লাইন পড়ে

লক্ষ করুন, এই টেবিলটিই একটি ছোট specification. এতে কীভাবে বানানো হবে তার একটি শব্দও নেই — list না dict, কোন ফাংশন — শুধু কী করবে। তাই পরে ভেতরের কোড পুরো বদলে গেলেও টেবিলটি একই থাকে, আর এই টেবিল থেকেই টেস্ট লেখা যায়। প্রতিটি সারির শেষ কলামটি আসলে একটি টেস্টের বর্ণনা।

গ্রেডিং স্কেল — উদাহরণ হিসেবে

GPA বের করতে একটি নিয়ম লাগবে। আমরা বাংলাদেশের মাধ্যমিক পর্যায়ে প্রচলিত স্কেলটি উদাহরণ হিসেবে নিলাম। প্রতিটি বিষয়ের নম্বর থেকে একটি grade point, আর সব বিষয়ের grade point-এর গড় হলো GPA. কোনো এক বিষয়ে F পেলে GPA শূন্য।

নম্বরগ্রেডGrade point
৮০–১০০A+৫.০০
৭০–৭৯A৪.০০
৬০–৬৯A-৩.৫০
৫০–৫৯B৩.০০
৪০–৪৯C২.০০
৩৩–৩৯D১.০০
০–৩২F০.০০

এটি শেখার জন্য সরল করা নিয়ম। বাস্তবের বোর্ড পরীক্ষায় ঐচ্ছিক (চতুর্থ) বিষয়, ব্যবহারিক অংশ আর অন্য কিছু নিয়মও আছে, আর প্রতিষ্ঠানভেদে স্কেল আলাদা হতে পারে। ভালো খবর হলো, আমাদের নকশায় স্কেলটি থাকবে একটি মাত্র জায়গায় — একটি tuple-এ। নিয়ম বদলালে শুধু সেই এক লাইন বদলাতে হবে।

যা এখন বানাব না — MVP-র সীমারেখা

নাসরিন আপা আরও চাইলেন: ফলাফল ফাইলে রেখে দেওয়া, Excel-এ পাঠানো, অভিভাবককে SMS, প্রতিটি ছাত্রের রিপোর্ট কার্ড ছাপা। সবই যৌক্তিক, কিন্তু সব একসাথে বানাতে গেলে কয়েক সপ্তাহেও শেষ হবে না। তাই প্রথমে একটি MVP — সবচেয়ে ছোট সংস্করণ, যা তাঁর সবচেয়ে বড় কষ্ট দূর করে: হাতে যোগ আর মেধাতালিকা সাজানো।

✅ প্রথম সংস্করণে আছে

উপরের টেবিলের সাতটি কমান্ড; প্রতিটি ভুল ইনপুটে পরিষ্কার বার্তা; তিনটি নির্দিষ্ট বিষয়।

MVP

⏳ পরে (লিখে রাখা আছে)

ফাইলে সংরক্ষণ (পর্ব ২৫), JSON/CSV-তে আদান-প্রদান (পর্ব ২৬), নামে space (“Abdur Rahim”), বিষয়ের সংখ্যা বদলানো, নম্বর সংশোধন।

পরের সংস্করণ

প্রথম সংস্করণে ফাইলে সংরক্ষণ নেই — প্রোগ্রাম বন্ধ করলেই সব নম্বর মুছে যাবে। এটি জেনেশুনে নেওয়া সিদ্ধান্ত, আর লিখে রাখা আছে। কাজের মাঝখানে নতুন নতুন চাহিদা ঢুকে প্রজেক্ট শেষই না হওয়ার রোগের নাম scope creep. লিখিত MVP-ই এর ওষুধ: নতুন ইচ্ছা এলে “না” নয়, বলুন “হ্যাঁ, পরের তালিকায়।”

৩ · পরিকল্পনা — কাজকে এক-কাজের ফাংশনে ভাগ করা

পর্ব ০২-এ শিখেছিলেন decomposition — বড় সমস্যাকে ছোট, আলাদাভাবে সমাধানযোগ্য টুকরোয় ভাগ করা। আর পর্ব ১৩-এ দেখেছেন, Python-এ প্রতিটি টুকরো সাধারণত একটি function. কমান্ডের টেবিলের দিকে তাকিয়ে প্রশ্ন করুন: “এখানে কোন কোন আলাদা কাজ আছে, যার প্রত্যেকটিকে এক বাক্যে বলা যায়?” উত্তরগুলোই আমাদের ফাংশনের তালিকা:

ফলাফল ব্যবস্থাপক — তিন স্তরের নকশা

কথা বলা (লুপ) → নিয়ম ও হিসাব (ফাংশন) → ডেটা (dict)

কমান্ড-লুপinput() পড়ে, শব্দে ভাগ করে
handle()কমান্ড দেখে ঠিক ফাংশন ডাকে
printফেরত আসা লাইনগুলো ছাপে
parse_marks()লেখা → নম্বরের list, নয়তো None
grade_point() · gpa()নম্বর → grade point → GPA
add_student()যাচাই, তারপর যোগ
sheet_key()সাজানোর নিয়ম
dbসব ছাত্রের dict
SUBJECTS · SCALEবিষয় আর গ্রেডিং স্কেল
পরে: ফাইলশুধু এই স্তর বাড়বে
ছবি ২১.১ — বাঁয়ের স্তর মানুষের সাথে কথা বলে, মাঝের স্তর নিয়ম জানে, ডানে ডেটা। মাঝের কোনো ফাংশন input() পড়ে না, print()-ও করে না।

সবচেয়ে গুরুত্বপূর্ণ নিয়মটি ছবির নিচে লেখা: হিসাবের ফাংশন কিছু পড়ে না, কিছু ছাপে না — শুধু মান নেয় আর মান ফেরত দেয়। পর্ব ১৩-এর print বনাম return-এর শিক্ষা এখানে নকশার নীতি হয়ে দাঁড়াল। এই নীতির নাম separation of concerns — আলাদা দায়িত্ব আলাদা জায়গায়। লাভ তিনটি:

  • পরীক্ষা সহজ: gpa([78, 85, 90]) কীবোর্ড ছাড়াই ডেকে ফল মেলানো যায় — অংশ ৯-এ ঠিক তাই করব।
  • বদল সহজ: কাল যদি নাসরিন আপা চান বার্তাগুলো বাংলায় হোক, বা টুলটি একটি ওয়েবপেজে চলুক, শুধু বাঁয়ের স্তর বদলাবে; gpa-র একটি লাইনও না।
  • বোঝা সহজ: প্রতিটি ফাংশন এক স্ক্রিনে আঁটে, নাম পড়েই কাজ বোঝা যায়।

লক্ষ করুন, ফাংশনগুলোর বেশিরভাগই pure function — একই ইনপুটে সবসময় একই ফল, বাইরের কিছু বদলায় না। শুধু add_student জেনেশুনে একটি side effect ঘটায়: db-তে নতুন ছাত্র যোগ করে। আর db সে পায় parameter হিসেবে, কোনো global ভ্যারিয়েবল থেকে নয় — তাই টেস্টে একটি খালি dict দিয়েই তাকে পরীক্ষা করা যায়।

৪ · ডেটা মডেল — list of dicts, নাকি dict of dicts?

ফাংশন লেখার আগে ঠিক করতে হবে ডেটা কোন আকারে থাকবে। একে বলে data model. একজন ছাত্রের তথ্য হলো একটি নাম আর তিনটি নম্বর। পর্ব ১৬-এর dict এখানে স্বাভাবিক পছন্দ: {"name": "Rahim", "marks": [78, 85, 90]}. নম্বরগুলো ক্রম মেনে চলে (বাংলা, ইংরেজি, গণিত), তাই সেগুলো একটি list-এ (পর্ব ১৪)। প্রশ্ন হলো, অনেক ছাত্রকে একসাথে কীভাবে রাখব? দুটি প্রার্থী:

# মডেল ক — list of dicts: যোগ করার ক্রমে সারি
students = [
    {"name": "Rahim", "marks": [78, 85, 90]},
    {"name": "Karim", "marks": [92, 66, 71]},
]

# মডেল খ — dict of dicts: ছোট হাতের নাম দিয়ে চাবি
db = {
    "rahim": {"name": "Rahim", "marks": [78, 85, 90]},
    "karim": {"name": "Karim", "marks": [92, 66, 71]},
}
প্রশ্নক · list of dictsখ · dict of dicts
নাম দিয়ে একজনকে খোঁজাপুরো list হাঁটতে হয় (পর্ব ০২-এর linear search)db["rahim"] — সরাসরি, তালিকা যত বড়ই হোক
একই নাম দুবার ঢোকানো আটকানোপ্রতিবার লুপ চালিয়ে মিলিয়ে দেখতে হয়if key in db — এক লাইনে
যোগ করার ক্রম মনে রাখাহ্যাঁহ্যাঁ — Python 3.7 থেকে dict-এর ক্রম ঢোকানোর ক্রমেই থাকে, এটা ভাষার নিশ্চয়তা
একই নামের দুজন আলাদা ছাত্ররাখা যায়রাখা যায় না — চাবি অনন্য হতে হয়
সব ছাত্রের ওপর লুপfor s in studentsfor s in db.values()

আমরা বেছে নিলাম মডেল খ, কারণ আমাদের দুটি কমান্ড — add-এ ডুপ্লিকেট পরীক্ষা আর ভবিষ্যতের নম্বর সংশোধন — নাম দিয়ে খোঁজার ওপর দাঁড়িয়ে। চাবিটি নামের casefold() করা রূপ (পর্ব ০৯), যাতে “Rahim” আর “rahim” একই ছাত্র ধরা হয়, অথচ দেখানোর জন্য আসল বানান "name"-এ থেকে যায়। চাবি হিসেবে string চলে, কারণ string hashable; list চাবি হতে পারত না।

দামটাও লিখে রাখুন: একই নামের দুজন ছাত্র রাখা যাবে না। আসল স্কুলে তাই নাম নয়, রোল নম্বর হয় চাবি। MVP-তে আমরা এই সীমা জেনেশুনে মেনে নিলাম; পরের সংস্করণে চাবি বদলে রোল করলে শুধু add_student বদলাবে। এভাবে প্রতিটি সিদ্ধান্তের কারণ আর দাম লিখে রাখলে ছয় মাস পরে “এটা এমন কেন?” প্রশ্নের উত্তর খুঁজতে হয় না।

এবার দেখুন, কমান্ড আসার সাথে সাথে db কীভাবে বড় হয় — আর বাতিল হওয়া কমান্ডে কেন একটুও বদলায় না:

db = {}শুরু — খালি dict
{}
len(db) == 0 · show লিখলে: (no students yet)
> add Rahim 78 85 90প্রথম ছাত্র
{
  "rahim": {"name": "Rahim", "marks": [78, 85, 90]}
}
> add Karim 92 66 71নতুন চাবি, শেষে যোগ
{
  "rahim": {"name": "Rahim", "marks": [78, 85, 90]},
  "karim": {"name": "Karim", "marks": [92, 66, 71]}
}
> add Sumi 85 88 80ক্রম = ঢোকানোর ক্রম
{
  "rahim": {"name": "Rahim", "marks": [78, 85, 90]},
  "karim": {"name": "Karim", "marks": [92, 66, 71]},
  "sumi":  {"name": "Sumi",  "marks": [85, 88, 80]}
}
> add rahim 50 50 50বাতিল — চাবি আগেই আছে
"rahim".casefold() == "rahim"  → "rahim" in db → True
error: rahim already exists
db অপরিবর্তিত — এখনো ৩ জন
> add Tania 95 abc 70বাতিল — db পর্যন্ত পৌঁছায়ইনি
"abc".isdecimal() → False  → parse_marks() ফেরত দিল None
usage: add NAME bangla english math (each 0-100)
add_student() ডাকাই হয়নি — db এখনো ৩ জন
  1. শুরু। db = {} — একটি খালি dict. রিপোর্টের কমান্ডগুলো এই অবস্থাতেও crash না করে “(no students yet)” বলবে; খালি অবস্থা একটি আসল edge case.
  2. প্রথম ছাত্র। চাবি "rahim" (casefold করা), মান একটি ভেতরের dict. বাইরের dict = সব ছাত্র, ভেতরের dict = একজন ছাত্র।
  3. দ্বিতীয় ছাত্র। নতুন চাবি শেষে যোগ হলো। db["karim"]["marks"][0] লিখলে পাবেন করিমের বাংলার নম্বর ৯২ — বাইরে থেকে ভেতরে, এক এক ধাপ।
  4. তৃতীয় ছাত্র। dict-এর ক্রম ঢোকানোর ক্রমে থাকে, তাই আউটপুট প্রতিবার একই — কিন্তু এটি মেধার ক্রম নয়। মেধাতালিকা আলাদা করে সাজাতে হবে (অংশ ৬)।
  5. ডুপ্লিকেট। ছোট হাতে লেখা “rahim” casefold করলে আগের চাবির সাথে মেলে। add_student কিছু না বদলে error ফেরত দিল। চাবি দিয়ে খোঁজা বলে এই পরীক্ষা এক লাইনের।
  6. ভুল নম্বর। এখানে সমস্যা আরও আগে ধরা পড়ল — parse_marks “abc” দেখে None দিল, তাই add_student ডাকাই হলো না। “আগে সব যাচাই, তারপর বদল” — ডেটা কখনো অর্ধেক-বদলানো অবস্থায় থাকে না।

৫ · একটি কমান্ডের যাত্রা — আর exception ছাড়া ইনপুট যাচাই

প্রতিটি CLI টুলের হৃদয় একটি লুপ: একটি লাইন পড়া → বুঝে নেওয়া → কাজ করা → ফল জানানো → আবার পড়া। আমাদের লুপে input() পুরো একটি লাইন একটি string হিসেবে দেয় (পর্ব ১০), আর str.split() সেটিকে space ধরে শব্দের list বানায়। তারপর cmd, *args = ... — পর্ব ১৫-এর star unpacking — প্রথম শব্দটি cmd-এ, বাকি সব args list-এ রাখে। কোন কমান্ড, তা বাছাই করে পর্ব ১১-এর match statement. “পরে” চেপে add Rahim 78 85 90 লাইনটির পুরো যাত্রা দেখুন:

1line = input().strip()2cmd, *args = line.split()3match cmd:  case "add":4    marks = parse_marks(args[1:])5    if name.casefold() in db: ...6    db[key] = {"name": name, "marks": list(marks)}7    return f"ok: {name} added, GPA ..."8print("  " + out)
এই মুহূর্তের অবস্থা
line
cmd
args
marks
db
  1. পড়া। input() Enter পর্যন্ত পুরো লাইন string হিসেবে দিল; strip() দুই পাশের বাড়তি space ফেলে দিল। input() সবসময় string দেয় — “78” এখনো সংখ্যা নয়।
  2. শব্দে ভাগ। split() যত space-ই থাকুক, শব্দগুলো আলাদা করে। star unpacking প্রথম শব্দ cmd-এ আর বাকি চারটি args-এ রাখল। (খালি লাইনে split() খালি list দেয় — সেজন্য আসল কোডে line.split() or [""].)
  3. শাখা বাছাই। match “add” দেখে ঠিক শাখায় গেল। কমান্ডের লেখা তুলনা পুরো প্রোগ্রামে শুধু এখানে হয় — একে বলে dispatch.
  4. যাচাই (validate). parse_marks দেখল: ঠিক ৩টি শব্দ, প্রত্যেকটি অঙ্ক দিয়ে গঠিত, আর ১০০-র বেশি নয়। সব ঠিক, তাই [78, 85, 90] ফেরত দিল। কোনো একটি ভুল হলে দিত None.
  5. ব্যবসার নিয়ম। "rahim" in db মিথ্যা — নতুন ছাত্র। ডুপ্লিকেট হলে এখানেই error বার্তা ফেরত যেত, db না ছুঁয়ে।
  6. জমা (store). সব যাচাই পার, তাই এবার — এবং শুধু এবারই — db বদলাল। লক্ষ করুন list(marks): নম্বরের একটি নতুন কপি জমা হলো। কেন, তা অংশ ৭-এ।
  7. বার্তা তৈরি। add_student কিছু ছাপে না; একটি string ফেরত দেয়। GPA: ৭৮→৪.০, ৮৫→৫.০, ৯০→৫.০; গড় ১৪ ÷ ৩ = ৪.৬৬৬…, দুই ঘরে ৪.৬৭.
  8. ছাপা। একেবারে বাইরের লুপ ফেরত আসা লাইনটি ছাপল। তারপর আবার input()-এ ফিরে পরের কমান্ডের অপেক্ষা।
মূল ধারণা

আগে সব যাচাই, তারপর একবারে বদল। প্রতিটি কমান্ড হয় পুরোপুরি সফল, নয়তো কিছুই বদলায় না। bKash-এ টাকা পাঠানোর মাঝপথে নেটওয়ার্ক গেলে আপনার টাকা কাটা গেল অথচ প্রাপক পেলেন না — এমন অবস্থা যেন কখনো না হয়। আমাদের ছোট টুলেও একই নীতি।

exception ছাড়া যাচাই — শর্ত আর string method দিয়ে

“78” থেকে সংখ্যা পেতে int("78") লিখতে হয়। কিন্তু int("abc") চালালে Python থেমে যায় একটি ValueError দিয়ে। এই ধরনের ভুল ধরে সামলানোর যন্ত্র try/except আসবে পর্ব ২৪-এ। এখন আমরা অন্য পথ নেব: int() ডাকার আগেই পরীক্ষা করব লেখাটি সত্যিই একটি অ-ঋণাত্মক পূর্ণসংখ্যা কি না। এই পুরো কাজকে বলে input validation — বাইরের ডেটা বিশ্বাস করার আগে পরীক্ষা।

ইনপুটisdigit()isdecimal()int() পারবে?
"78"TrueTrueহ্যাঁ, 78
"৭৮" (বাংলা অঙ্ক)TrueTrueহ্যাঁ, 78
"-5", "8.5", "abc", ""FalseFalseনা (বা ঋণাত্মক)
"²" (superscript দুই)TrueFalseনা — ValueError

শেষ সারিটিই কারণ যে আমরা isdigit() নয়, isdecimal() ব্যবহার করেছি। isdigit() কিছু Unicode চিহ্নকেও “অঙ্ক” মানে, যেগুলো int() পড়তে পারে না; isdecimal() শুধু দশমিক পদ্ধতির অঙ্ক মানে — যেগুলো int() নিশ্চিতভাবে পড়তে পারে। আর সুখবর: বাংলা অঙ্ক দশমিক অঙ্ক, তাই নাসরিন আপা বাংলা কীবোর্ডে “৭৮” লিখলেও টুলটি ঠিকঠাক 78 পড়বে। এরপর সীমা পরীক্ষা — int(r) <= 100. ঋণাত্মক সংখ্যা আগেই বাদ, কারণ “-” চিহ্ন অঙ্ক নয়।

def parse_marks(raw):
    """['78', '85', '90'] → [78, 85, 90]; কোনো সমস্যা থাকলে None"""
    if len(raw) != len(SUBJECTS):                      # শব্দের সংখ্যা
        return None
    if not all(r.isdecimal() and int(r) <= 100 for r in raw):   # ধরন + সীমা
        return None
    return [int(r) for r in raw]                       # সব ঠিক — এবার রূপান্তর

দুটি সূক্ষ্ম বিষয় লক্ষ করুন। r.isdecimal() and int(r) <= 100-এ পর্ব ০৮-এর short-circuit কাজ করছে: isdecimal() মিথ্যা হলে int(r) ডাকাই হয় না, তাই “abc”-তে কোনো error ওঠে না। আর প্রতিটি সমস্যায় ফাংশন শুরুতেই None ফেরত দেয় — পর্ব ১১-এর guard clause. ফেরত None মানে “বৈধ নয়”; কী বার্তা দেখাবে, তা ঠিক করে ডাকা জায়গা। None পরীক্ষা হয় is None দিয়ে।

কখনো eval(input()) নয়। পর্ব ১০-এ দেখেছেন, eval লেখাটিকে Python কোড হিসেবে চালায়। নম্বরের জায়গায় কেউ কোড লিখে দিলে সেটাই চলবে — এটি injection-এর সবচেয়ে সরল রূপ। বাইরের লেখাকে সবসময় ডেটা হিসেবে পরীক্ষা করুন, কোড হিসেবে নয়।

৬ · রিপোর্ট — sorted, key আর comprehension

ডেটা জমা হলো; এবার তা থেকে উত্তর বের করার পালা। show, top, fail, avg, find — পাঁচটি কমান্ডই আসলে একই কাজের রূপভেদ: ছাত্রদের তালিকা হেঁটে কিছু বেছে নেওয়া বা কিছু হিসাব করা। পর্ব ১৮-এর list comprehension ঠিক এই কাজের জন্য:

rows = sorted(db.values(), key=sheet_key)              # মেধাক্রমে সাজানো list

failed  = [s["name"] for s in rows if gpa(s["marks"]) == 0.0]
matches = [s["name"] for s in rows if text.casefold() in s["name"].casefold()]
bangla_avg = sum(s["marks"][0] for s in rows) / len(rows)   # len(rows) > 0 আগেই নিশ্চিত

প্রতিটি comprehension এক বাক্যে পড়া যায়: “প্রতিটি ছাত্রের নাম নাও, যদি তার GPA শূন্য হয়।” কিন্তু সবচেয়ে মজার অংশ প্রথম লাইনে — মেধাতালিকা সাজানো। পর্ব ১৩-এ দেখেছিলেন, sorted-কে একটি ফাংশন দেওয়া যায় (first-class function), যা প্রতিটি উপাদান থেকে একটি “তুলনার চাবি” বানায়। একে বলে key function. আমাদের চাবি একটি tuple:

def sheet_key(s):
    return (-sum(s["marks"]), s["name"])     # মোট বেশি থেকে কম, তারপর নাম A→Z

tuple তুলনা হয় অভিধানের মতো (lexicographic): প্রথম উপাদান আগে, সমান হলে দ্বিতীয়টি। আমরা চাই মোট নম্বর বেশি থেকে কম, কিন্তু নাম A থেকে Z — দুটি উল্টো দিক। reverse=True দিলে দুটোই উল্টে যেত। কৌশল: সংখ্যাটির আগে মাইনাস। −২৫৩ < −২২৯, তাই বড় মোট আগে আসে, আর নাম স্বাভাবিক ক্রমেই থাকে। “পরে” চেপে দেখুন:

db.values()ঢোকানোর ক্রমে
Rahim  [78, 85, 90]  মোট 253
Karim  [92, 66, 71]  মোট 229
Sumi   [85, 88, 80]  মোট 253
Joy    [45, 30, 60]  মোট 135
Rina   [92, 66, 71]  মোট 229
sheet_key(s)প্রত্যেকের জন্য একবার ডাকা হলো
Rahim  → (-253, 'Rahim')
Karim  → (-229, 'Karim')
Sumi   → (-253, 'Sumi')
Joy    → (-135, 'Joy')
Rina   → (-229, 'Rina')
প্রথম উপাদান তুলনাছোট সংখ্যা আগে
-253  Rahim, Sumi      ← এখনো সমান
-229  Karim, Rina      ← এখনো সমান
-135  Joy
দ্বিতীয় উপাদান তুলনাসমান হলে নাম
(-253, 'Rahim') < (-253, 'Sumi')    'R' < 'S'
(-229, 'Karim') < (-229, 'Rina')    'K' < 'R'
ফল: Rahim, Sumi, Karim, Rina, Joy
enumerate(rows, 1)ক্রমিক নম্বরসহ ছাপা
1. Rahim  253  GPA 4.67
2. Sumi   253  GPA 5.00
3. Karim  229  GPA 4.17
4. Rina   229  GPA 4.17
5. Joy    135  GPA 0.00
  1. কাঁচা ডেটা। db.values() ছাত্রদের ভেতরের dict-গুলো দেয়, ঢোকানোর ক্রমে। এটি মেধার ক্রম নয়।
  2. চাবি বানানো। sorted প্রতিটি উপাদানের জন্য একবার sheet_key ডাকে, আর ফলগুলো দিয়ে তুলনা করে। আমরা key=sheet_key লিখেছি বন্ধনী ছাড়া — ফাংশনটি নিজেই পাঠিয়েছি, ডেকে ফল নয়।
  3. প্রথম মানদণ্ড। −২৫৩ সবচেয়ে ছোট, তাই প্রথমে। কিন্তু Rahim আর Sumi দুজনেরই ২৫৩ — tuple-এর প্রথম উপাদানে সিদ্ধান্ত হলো না।
  4. টাই ভাঙা। সমান হলে Python দ্বিতীয় উপাদান তুলনা করে: string তুলনা অক্ষর ধরে, “R” আগে “S”. স্পেকের দ্বিতীয় নিয়ম (“সমান হলে নামের বর্ণক্রমে”) এক শব্দেই কোডে বসে গেল।
  5. ছাপা। পর্ব ১২-এর enumerate 1 থেকে গুনে ক্রমিক নম্বর দেয়, আর পর্ব ১০-এর format spec {:<6}, {:>4} কলাম সোজা রাখে। খেয়াল করুন: মোট সমান হলেও Sumi-র GPA বেশি! মেধা মোট দিয়ে মাপবেন না GPA দিয়ে — এটি কোডের নয়, নীতির প্রশ্ন; স্পেকে লিখে রাখতে হয়।

অতিরিক্ত নিশ্চয়তা: Python-এর sorted stable — দুটি উপাদানের চাবি পুরো সমান হলে তারা আগের ক্রমেই থাকে। আমাদের চাবিতে নাম থাকায় এটি লাগেনি, কিন্তু অনেক সময় “আগে নাম দিয়ে সাজাও, তারপর নম্বর দিয়ে” — এমন দুই ধাপের সাজানো এই গুণের ওপর নির্ভর করে।

৭ · দুটি লুকানো ফাঁদ — alias আর mutable default

পর্ব ১৯ আর ২০-এ যে দুটি ফাঁদ দেখেছেন, বাস্তব প্রজেক্টে ঠিক সেগুলোই সবচেয়ে বেশি ধরা পড়ে — কারণ ছোট উদাহরণে এরা চুপচাপ থাকে, বড় প্রোগ্রামে হঠাৎ জেগে ওঠে।

ফাঁদ ১ — নম্বরের list-এর alias

ধরুন add_student-এ লেখা হলো {"name": name, "marks": marks}, কপি ছাড়া। তখন db-র ভেতরের list আর ডাকা জায়গার marks — দুটি নাম, একটিই list object. এটাই alias. পরে কেউ সেই marks list আবার ব্যবহার করলে — যেমন পরের ছাত্রের জন্য একই list বদলে নিলে — আগের ছাত্রের নম্বরও নিঃশব্দে বদলে যাবে। কোনো error নেই, শুধু ভুল মেধাতালিকা।

marks = [78, 85, 90]
db["rahim"] = {"name": "Rahim", "marks": marks}         # ✗ alias
marks[0] = 40                                           # অন্য কাজে list-টি বদলালাম…
print(db["rahim"]["marks"])                             # [40, 85, 90] — রহিমের নম্বর বদলে গেল!

db["rahim"] = {"name": "Rahim", "marks": list(marks)}   # ✓ নতুন list, নিজস্ব কপি

নিয়ম: বাইরে থেকে আসা mutable object নিজের দীর্ঘস্থায়ী ডেটায় রাখার আগে কপি করুন। এখানে list-এর ভেতরে শুধু int (যা immutable), তাই list(marks)-এর মতো একটি shallow copy-ই যথেষ্ট। ভেতরে আরও list থাকলে লাগত copy.deepcopy (পর্ব ১৯)। parse_marks অবশ্য প্রতিবার comprehension দিয়ে নতুন list বানায়, তাই আজকের প্রোগ্রামে বিপদ ঘটত না — কিন্তু add_student জানে না কে তাকে ডাকবে। টেস্ট থেকে, ভবিষ্যতের ফাইল-পড়ার কোড থেকে, বা অন্য কারও লেখা ফাংশন থেকে — তাই সে নিজেই নিজেকে রক্ষা করে।

ফাঁদ ২ — default-এ list

প্রজেক্টে একটি “ইতিহাস” রাখতে চাইলেন — কোন কমান্ড কখন এল। প্রথম চিন্তায় আসে:

def log_command(line, history=[]):        # ✗ default list একবারই তৈরি হয়
    history.append(line)
    return history

def log_command(line, history=None):      # ✓ প্রতিবার নতুন, যদি না দেওয়া হয়
    if history is None:
        history = []
    history.append(line)
    return history

পর্ব ২০-এ দেখেছেন কেন প্রথমটি ভুল: default argument-এর মান তৈরি হয় def চলার সময় একবার, প্রতি ডাকে নয়। ফলে সব ডাক একই list ভাগ করে, আর দুটি আলাদা “সেশন”-এর ইতিহাস মিশে যায়। এই mutable default-এর ফাঁদ এড়ানোর প্রচলিত পথ None default আর ভেতরে নতুন list. আমাদের মূল নকশা আরও সরল পথ নিয়েছে: db কোনো default বা global নয়, প্রতিটি ফাংশন তাকে স্পষ্টভাবে parameter হিসেবে পায়। যে ডেটা বদলায়, তা কোথা থেকে আসছে — কোড পড়েই দেখা যায়।

মূল ধারণা

দুটি ফাঁদের মূল একই — একটি mutable object-এর দুটি নাম, অথচ আপনি ভাবছেন দুটি আলাদা জিনিস. প্রজেক্টে নিজেকে বারবার জিজ্ঞেস করুন: “এই list বা dict কি অন্য কেউও ধরে আছে?” উত্তর অনিশ্চিত হলে কপি করুন।

৮ · মাইলস্টোনে বানানো — আর পুরো টুল চালিয়ে দেখা

নতুনরা প্রায়ই ২০০ লাইন একবারে লিখে প্রথমবার চালান, আর পান একের পর এক error — কোনটি কোথা থেকে, বোঝার উপায় নেই। অভিজ্ঞরা আগে ছোট কিছু বানান যা চলে, তারপর একটু একটু যোগ করেন, আর প্রতিটি যোগের পর প্রোগ্রাম আবার চলে। কিছু ভাঙলে দোষী শেষ যোগ করা অংশটুকুই। প্রতিটি ধাপকে বলে একটি milestone:

  1. M1loopকঙ্কাল
  2. M2add/showপ্রথম আসল কাজ
  3. M3validateভুল ইনপুট আটকানো
  4. M4reportsMVP পূর্ণ
  5. M5testsপ্রমাণ ও গোছানো
M1 · ~১০ লাইনলুপ ঘোরে, কোনো কাজ করে না
> add Rahim 78 85 90
  [add: coming soon]
> dance
  error: unknown command 'dance'
> quit
টেস্ট: quit-এ বের হয় · অজানা কমান্ডে বার্তা · খালি লাইনে crash নয়
M2 · ~২৫ লাইনdb, add_student, gpa, show
> add Rahim 78 85 90
  ok: Rahim added, GPA 4.67
> show
  1. Rahim  253  GPA 4.67
এখনো যাচাই নেই — "add Tania abc" দিলে ValueError-এ প্রোগ্রাম থামে
টেস্ট: হাতে হিসাব করা GPA মেলানো · খালি show
M3 · ~৩৫ লাইনparse_marks, ডুপ্লিকেট পরীক্ষা
> add Tania 95 abc 70
  usage: add NAME bangla english math (each 0-100)
> add rahim 50 50 50
  error: rahim already exists
টেস্ট: কম/বেশি নম্বর · অক্ষর · -5 · 8.5 · 101 · ডুপ্লিকেট নাম
M4 · ~৫৫ লাইনএখানে MVP সম্পূর্ণ — নাসরিন আপাকে দেখানো যায়
> top
  top 253: Rahim, Sumi
> fail
  Joy
> avg
  bangla: 78.4
টেস্ট: সমান নম্বরে দুজন top · কেউ ফেল না হলে বার্তা · গড় হাতে মেলানো
M5 · test_core.pyআচরণ একই, এবার প্রমাণসহ
PASS grade_point: every boundary of the scale
PASS parse_marks: 5 kinds of bad input rejected
PASS stored marks are a copy, not an alias
প্রতিটি নতুন বাগের জন্য একটি নতুন assert — চিরকাল থাকে
  1. M1 — কঙ্কাল। শুধু while লুপ, split আর match; প্রতিটি কমান্ড “coming soon” বলে। তবু নিশ্চিত হলো মৌলিক জিনিস: প্রোগ্রাম চলে, লুপ ঘোরে, বের হওয়া যায়।
  2. M2 — add আর show. db, grade_point, gpa, add_student এল। একটি সচেতন শর্টকাট: এখনো যাচাই নেই, ভুল ইনপুটে প্রোগ্রাম থামবে। আগে মূল হিসাব ঠিক, তারপর পাহারা।
  3. M3 — যাচাই। parse_marks আর ডুপ্লিকেট পরীক্ষা। এই মাইলস্টোনের টেস্টগুলো সব ভুল ইনপুট — কারণ সঠিক ইনপুট M2-তেই পরীক্ষা হয়ে গেছে।
  4. M4 — রিপোর্ট। sheet_key, comprehension আর গড় — স্পেকের সব কমান্ড তৈরি, এটাই MVP. প্রতিটি মাইলস্টোনে ভাবুন: “আজই থামতে হলে হাতে কী থাকবে?” M4-এর পরে থামলেও নাসরিন আপার হাতে কাজের টুল।
  5. M5 — টেস্ট আর গোছানো। মূল ফাংশনগুলোর জন্য assert. এরপর নির্ভয়ে কোড গোছানো যায় (একে বলে refactoring) — আচরণ বদলালে টেস্টই জানিয়ে দেবে।

প্রতিটি মাইলস্টোন শেষে একটি git commit করুন (যেমন git commit -m "M3: validate marks"). পরে কিছু ভাঙলে আগের চলা অবস্থায় ফিরে যাওয়া যায়, আর কোন পরিবর্তনে ভেঙেছে তাও দেখা যায়। Git শিখতে ABCL TECH-এর Software Engineering ও Git কোর্স দেখুন।

পুরো টুল — একটি স্ক্রিপ্ট করা সেশনসহ

এবার M4 পর্যন্ত পুরো প্রোগ্রাম, এক ফাইলে। প্রজেক্ট বলে এটি অন্য উদাহরণের চেয়ে লম্বা — প্রায় ৬০ লাইন — কিন্তু প্রতিটি ফাংশন এই পর্বে আলাদা করে দেখেছেন। ইনপুট আগে থেকেই দেওয়া: একটি সেশন, যাতে ইচ্ছা করে কয়েকটি ভুল কমান্ডও রাখা আছে। চালানোর আগে অনুমান করুন show কী ছাপবে, তারপর “Run” চাপুন:

SUBJECTS = ("bangla", "english", "math")
SCALE = ((80, 5.0), (70, 4.0), (60, 3.5), (50, 3.0), (40, 2.0), (33, 1.0))

def grade_point(mark):
    for low, point in SCALE:
        if mark >= low:
            return point
    return 0.0                                   # 0-32 = F

def gpa(marks):
    points = [grade_point(m) for m in marks]
    return 0.0 if 0.0 in points else round(sum(points) / len(points), 2)

def parse_marks(raw):
    if len(raw) != len(SUBJECTS) or not all(r.isdecimal() and int(r) <= 100 for r in raw):
        return None                              # invalid: caller shows usage
    return [int(r) for r in raw]

def add_student(db, name, marks):
    if name.casefold() in db:
        return f"error: {name} already exists"
    db[name.casefold()] = {"name": name, "marks": list(marks)}   # a copy
    return f"ok: {name} added, GPA {gpa(marks):.2f}"

def sheet_key(s):
    return (-sum(s["marks"]), s["name"])         # total high->low, then name A->Z

def handle(db, cmd, args):
    if cmd in ("show", "top", "fail", "avg", "find") and not db:
        return ["(no students yet)"]
    rows = sorted(db.values(), key=sheet_key)
    match cmd:
        case "add":
            marks = parse_marks(args[1:]) if args else None
            if marks is None:
                return [f"usage: add NAME {' '.join(SUBJECTS)} (each 0-100)"]
            return [add_student(db, args[0], marks)]
        case "show":
            return [f"{i}. {s['name']:<6}{sum(s['marks']):>4}  GPA {gpa(s['marks']):.2f}"
                    for i, s in enumerate(rows, 1)]
        case "top":
            best = sum(rows[0]["marks"])
            return [f"top {best}: " + ", ".join(s["name"] for s in rows if sum(s["marks"]) == best)]
        case "fail":
            return [s["name"] for s in rows if gpa(s["marks"]) == 0.0] or ["nobody failed"]
        case "avg":
            return [f"{sub}: {sum(s['marks'][i] for s in rows) / len(rows):.1f}"
                    for i, sub in enumerate(SUBJECTS)]
        case "find" if args:
            return [s["name"] for s in rows if args[0].casefold() in s["name"].casefold()] or ["no match"]
        case _:
            return [f"error: unknown command '{cmd}' (try: add show top fail avg find quit)"]

db = {}
while (line := input().strip()) != "quit":
    print(">", line)
    cmd, *args = line.split() or [""]
    for out in handle(db, cmd, args):
        print("  " + out)
print(f"> quit\n  bye: {len(db)} students (kept in memory only)")

ইনপুট: data-stdin-এ ১৬টি লাইন — শুরুতে খালি অবস্থায় show, পাঁচজন বৈধ ছাত্র (Rahim, Karim, Sumi, Joy, Rina), তিনটি বাতিলযোগ্য add (ছোট হাতে লেখা ডুপ্লিকেট “rahim”, অক্ষরসহ নম্বর, ১০১), তারপর পাঁচটি রিপোর্ট, একটি অজানা কমান্ড আর quit. আউটপুট হাতে মিলিয়ে নিন: খালি show-এ (no students yet); পাঁচটি ok (GPA 4.67, 4.17, 5.00, 0.00, 4.17 — জয় ইংরেজিতে ৩০ পেয়ে F, তাই 0.00); তারপর error: rahim already exists আর দুবার usage বার্তা। মেধাতালিকা: Rahim 253, Sumi 253, Karim 229, Rina 229, Joy 135; top 253: Rahim, Sumi; fail-এ Joy; গড় bangla 78.4, english 67.0, math 74.4; find ri-তে Karim আর Rina (“kaRIm”-এর ভেতরেও “ri” আছে); dance-এ কমান্ডের তালিকাসহ error; শেষে bye: 5 students. কয়েকটি খুঁটিনাটি: (১) লুপের print(">", line) কমান্ডটি আবার ছাপে, যাতে স্ক্রিপ্ট করা সেশনে প্রশ্ন-উত্তর পাশাপাশি দেখা যায়; নিজের terminal-এ চালালে এর বদলে input("> ") লিখুন। (২) handle কিছু ছাপে না, লাইনের একটি list ফেরত দেয় — তাই তাকেও টেস্ট করা যায়। (৩) [...] or ["nobody failed"]: খালি list falsy, তাই or বিকল্প বার্তাটি দেয়। (৪) case "find" if args একটি guard: শুধু find লিখলে শব্দ নেই, তাই শেষ শাখার বার্তা আসে। (৫) quit ছাড়া ইনপুট শেষ হয়ে গেলে input() একটি EOFError তোলে — সেটি সামলানো পর্ব ২৪-এর কাজ, তাই আমাদের সেশন quit দিয়ে শেষ।

C/C++ জানলে

C++ কোর্সের মাঝপথের প্রজেক্টেও একই পদ্ধতি — চাহিদা, ডেটা মডেল, “আগে যাচাই, পরে বদল”, মাইলস্টোন, স্ক্রিপ্ট করা ইনপুটে টেস্ট। পার্থক্য যন্ত্রে: সেখানে std::getline + std::istringstream দিয়ে লাইন ভাঙা, struct আর enum class দিয়ে টাইপ, আর stream-এর ব্যর্থতা দিয়ে ভুল সংখ্যা ধরা; Python-এ split(), dict, আর isdecimal() দিয়ে আগেভাগে পরীক্ষা — টাইপ ঘোষণার একটি লাইনও লাগেনি, কিন্তু ভুল টাইপ ধরার দায়ও পুরোটা আপনার। পাশাপাশি দেখতে C++ কোর্সের মাঝপথের প্রজেক্ট বা C কোর্সের মাঝপথের প্রজেক্ট পর্বটি দেখুন।

৯ · টেস্ট — assert দিয়ে প্রমাণ, আর পরের ধাপ

“চালিয়ে দেখেছি, চলছে” — এটি টেস্ট নয়; আপনি চালিয়েছেন শুধু নিজের মাথার ইনপুট দিয়ে। নাসরিন আপা টাইপ করবেন এমন জিনিস, যা আপনি কল্পনাও করেননি। সবচেয়ে বেশি বাগ লুকিয়ে থাকে সীমানায়: ৭৯ আর ৮০, ৩২ আর ৩৩, ০ আর ১০০. তাই প্রতিটি মূল ফাংশনের জন্য ছোট ছোট পরীক্ষা লিখুন — একেকটি unit test. পর্ব ১৩-এর assert এর সবচেয়ে সরল যন্ত্র: শর্ত সত্য হলে নিঃশব্দে এগোয়, মিথ্যা হলে প্রোগ্রাম থামিয়ে জানায়।

SCALE = ((80, 5.0), (70, 4.0), (60, 3.5), (50, 3.0), (40, 2.0), (33, 1.0))

def grade_point(mark):
    for low, point in SCALE:
        if mark >= low:
            return point
    return 0.0

def parse_marks(raw, n=3):
    if len(raw) != n or not all(r.isdecimal() and int(r) <= 100 for r in raw):
        return None
    return [int(r) for r in raw]

assert [grade_point(m) for m in (100, 80, 79, 33, 32, 0)] == [5.0, 5.0, 4.0, 1.0, 0.0, 0.0]
print("PASS grade_point: every boundary of the scale")
assert parse_marks(["78", "85", "90"]) == [78, 85, 90]
assert parse_marks(["৭৮", "৮৫", "৯০"]) == [78, 85, 90]      # Bangla digits work too
for bad in (["78", "85"], ["78", "-5", "90"], ["8.5", "1", "1"], ["101", "0", "0"], ["78", "²", "90"]):
    assert parse_marks(bad) is None, bad
print("PASS parse_marks: 5 kinds of bad input rejected")
marks = [78, 85, 90]
db = {"rahim": {"name": "Rahim", "marks": list(marks)}}
marks[0] = 0                                       # caller reuses its list
assert db["rahim"]["marks"] == [78, 85, 90]
print("PASS stored marks are a copy, not an alias")

আউটপুট তিনটি লাইন: PASS grade_point: every boundary of the scale, PASS parse_marks: 5 kinds of bad input rejected, PASS stored marks are a copy, not an alias. কোনো assert ব্যর্থ হলে সেখানেই AssertionError আসত, পরের PASS আর ছাপা হতো না — তাই তিনটি PASS মানে সবগুলো শর্ত সত্য। প্রথম টেস্ট স্কেলের প্রতিটি গুরুত্বপূর্ণ সীমা (৮০ আর ৭৯, ৩৩ আর ৩২) একসাথে দেখে — >=-এর জায়গায় ভুল করে > লিখলে ৮০-তে ৪.০ আসত আর টেস্ট ধরে ফেলত। দ্বিতীয় অংশ পাঁচ রকম ভুল ইনপুট — কম শব্দ, ঋণাত্মক, দশমিক, ১০০-র বেশি, আর superscript “²” — বাতিল হওয়া নিশ্চিত করে; assert ..., bad-এর দ্বিতীয় অংশ ব্যর্থ হলে কোন ইনপুটে ব্যর্থ, তা দেখাত। বাংলা অঙ্কের টেস্টটি একটি সচেতন সিদ্ধান্তকে চিরস্থায়ী করে রাখে। শেষ টেস্ট অংশ ৭-এর alias-এর ফাঁদ পাহারা দেয়: কেউ পরে list(marks) “অপ্রয়োজনীয়” ভেবে মুছে দিলে এটি ব্যর্থ হবে। (টেস্টের ফাংশনগুলো এখানে মূল প্রোগ্রাম থেকে কপি করা, কারণ এখনো আমরা এক ফাইল থেকে আরেক ফাইলে import শিখিনি — পর্ব ২২-এ from result_tool import parse_marks লিখলেই হবে।)

একটি নিয়ম মনে রাখুন: প্রতিটি ধরা পড়া বাগের জন্য একটি নতুন টেস্ট, আর সেটি চিরকাল রেখে দিন। একবার ঠিক করা বাগ পরে কোনো পরিবর্তনে ফিরে আসাকে বলে regression; এই টেস্টগুলো তার পাহারাদার। assert শেখার জন্য চমৎকার, কিন্তু Python -O অপশনে চালালে assert বাদ পড়ে যায় — তাই আসল প্রজেক্টে টেস্ট থাকে আলাদা ফাইলে, pytest দিয়ে চালানো (পর্ব ৩৯)।

পরের সংস্করণ — কোন পর্ব কোন দরজা খুলবে

ইচ্ছাকী লাগবেকোথায় শিখবেন
ফাংশনগুলো আলাদা ফাইলে, টেস্ট থেকে importmodule আর importপর্ব ২২
int()-এর ভুল আর EOF শান্তভাবে সামলানোtry/exceptপর্ব ২৪
বন্ধ করলেও নম্বর থাকুকফাইলে লেখা-পড়া, with, pathlibপর্ব ২৫
Excel-এ খোলা যায় এমন ফলাফল, বা JSON-এ রাখাcsv আর json moduleপর্ব ২৬
ছাত্রকে নিজের নিয়মসহ একটি টাইপ বানানোclass Student, পরে dataclassপর্ব ২৯, ৩২
শত শত টেস্ট এক কমান্ডেpytestপর্ব ৩৯
python result_tool.py --sort gpa-এর মতো আসল CLI অপশনargparseপর্ব ৪৯

লক্ষ করুন, তিন স্তরে ভাগ করা নকশার লাভ এখানে চোখে পড়ে: ফাইলে সংরক্ষণ যোগ হলে নতুন দুটি ফাংশন আসবে (save, load), কিন্তু gpa, parse_marks বা sheet_key-এর একটি লাইনও বদলাবে না। আর এখনকার টেস্টগুলো তখনও হুবহু পাস করতে হবে — সেটাই প্রমাণ যে নতুন কাজ পুরোনোটা ভাঙেনি।

পরীক্ষা ও ইন্টারভিউতে
  • isdigit() আর isdecimal()-এর পার্থক্য? — isdigit() superscript-এর মতো কিছু চিহ্নকেও অঙ্ক মানে, যা int() পড়তে পারে না; isdecimal() শুধু দশমিক অঙ্ক মানে, তাই int()-এর আগে যাচাইয়ে এটিই নিরাপদ।
  • একাধিক মানদণ্ডে (মোট কমে, নাম বাড়ে) কীভাবে সাজাবেন? — key ফাংশন থেকে tuple ফেরত দিন, যেমন (-total, name); tuple প্রথম উপাদান আগে তুলনা করে।
  • নাম দিয়ে খোঁজা বেশি হলে list of dicts না dict of dicts? — dict of dicts; চাবি দিয়ে সরাসরি খোঁজা আর ডুপ্লিকেট ধরা যায়, তবে চাবি অনন্য হতে হয়।
  • ফাংশনে আসা list সরাসরি জমা রাখলে কী সমস্যা? — alias তৈরি হয়; বাইরে list বদলালে জমা রাখা ডেটাও বদলায়। list(x) বা x.copy() দিয়ে কপি রাখুন।
  • হিসাবের ফাংশনে print না করে return কেন? — return করা মান পরীক্ষা, পুনর্ব্যবহার আর অন্য ইন্টারফেসে (ওয়েব, ফাইল) দেখানো যায়; ছাপা থাকে বাইরের স্তরে।

১০ · AI-এর যুগে — AI-কে দায়িত্বশীল pair-programmer বানানো

এই পুরো প্রজেক্ট AI-কে এক লাইনে বললে কয়েক সেকেন্ডে একটি চলা কোড পাওয়া যায়। তাহলে এত পরিকল্পনা কেন? কারণ AI সাধারণত দেয় একটি ৮০–১৫০ লাইনের বিশাল স্ক্রিপ্ট: সব কাজ এক লুপে, যাচাই আর হিসাব আর ছাপা মেশানো, global ভ্যারিয়েবল, আর এমন কিছু যা আপনি এখনো শেখেননি (class, try/except, lambda). কোডটি চলে — যতক্ষণ না নাসরিন আপা এমন কিছু টাইপ করেন যা AI ভাবেনি। তখন আপনি এমন কোড ঠিক করতে বসেছেন, যা আপনি বোঝেন না।

ভালো ব্যবহার হলো AI-কে pair programming-এর সঙ্গীর মতো দেখা: সে প্রস্তাব দেয়, আপনি সিদ্ধান্ত নেন। কোড চাওয়ার আগে চান পরিকল্পনা আর টেস্ট; তারপর একবারে একটি ফাংশন; প্রতিটি পড়ে বুঝে, টেস্ট চালিয়ে, তবেই পরেরটি।

AI প্রায়ই যা করেসমস্যাকী চাইবেন
সব কাজ while True-এর ভেতরে এক বিশাল ব্লকেকোনো অংশ আলাদা করে পরীক্ষা করা যায় নাএক-কাজের ফাংশন; লুপে শুধু dispatch
সরাসরি int(input())“abc” লিখলেই প্রোগ্রাম থামেআগে isdecimal() আর সীমা পরীক্ষা
বাইরের list সরাসরি জমা রাখেalias — নিঃশব্দে ডেটা বদলায়জমার আগে কপি
ছাপার কাজ হিসাবের ফাংশনের ভেতরেফল পুনর্ব্যবহার বা টেস্ট করা যায় নাহিসাব return করবে, ছাপা বাইরে
টাই হলে কী হবে, বলা নেইমেধাতালিকা প্রতিবার আলাদা হতে পারে, বা অন্যায্যস্পষ্ট দ্বিতীয় মানদণ্ড, স্পেকে লেখা

পুরো কোড নয় — আগে পরিকল্পনা আর টেস্ট চান। নিচের কাঠামোটি কপি করে নিজের প্রজেক্ট বসিয়ে ব্যবহার করুন:

আমি Python শিখছি; এখন পর্যন্ত জানি ফাংশন, list, tuple, dict, set, comprehension, match আর f-string. class, try/except, lambda, import এখনো শিখিনি — এগুলো ব্যবহার করবে না। আমি একটি command-line টুল বানাচ্ছি: শিক্ষক ছাত্রের নাম আর তিন বিষয়ের নম্বর যোগ করবেন, আর show / top / fail / avg / find / quit কমান্ড চলবে। গ্রেডিং স্কেল: ৮০+ = 5.0, ৭০+ = 4.0, ৬০+ = 3.5, ৫০+ = 3.0, ৪০+ = 2.0, ৩৩+ = 1.0, বাকি F. এখনই পুরো কোড লিখবে না। প্রথমে দাও: ১. ফাংশনের তালিকা — প্রত্যেকটির নাম, কী নেয়, কী ফেরত দেয়, এক বাক্যে কাজ। হিসাবের কোনো ফাংশন print বা input করবে না। ২. ডেটা মডেলের দুটি বিকল্প আর প্রত্যেকটির সুবিধা-অসুবিধা। ৩. প্রতিটি ফাংশনের জন্য অন্তত ৩টি assert টেস্ট, যার মধ্যে সীমার মান (৭৯/৮০, ৩২/৩৩, ০, ১০০) আর ভুল ইনপুট ("abc", "-5", "8.5", "101") থাকবে। ৪. তুমি কোথায় কোনো অনুমান করছ (যেমন সমান নম্বরে কে আগে), তার তালিকা। আমি অনুমোদন দিলে তারপর একবারে একটি ফাংশন লিখবে।

AI-এর দেওয়া প্রতিটি ফাংশন গ্রহণের আগে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১পুরো টুল একবারে না লিখে M1 থেকে M5 — ছোট ছোট ধাপে কেন? “Gall’s law” কী বলে, আর বড় সফটওয়্যার প্রজেক্টে এর প্রমাণ কোথায়?

১৯৭৫ সালে মার্কিন শিশুরোগ বিশেষজ্ঞ John Gall প্রকাশ করেন একটি রসিক অথচ গভীর বই, “General Systemantics” (পরের সংস্করণগুলোর নাম “Systemantics” আর “The Systems Bible”). বইটি ছিল বড় বড় ব্যবস্থা — সরকারি দপ্তর থেকে কারখানা — কেন প্রায়ই প্রত্যাশামতো কাজ করে না, তার পর্যবেক্ষণ। তার একটি বাক্য সফটওয়্যার জগতে এত বিখ্যাত হয় যে এখন একে বলে Gall’s law: কাজ করে এমন প্রতিটি জটিল ব্যবস্থা দেখা যায় কাজ করা একটি সরল ব্যবস্থা থেকে ধীরে ধীরে বড় হয়েছে; শুরু থেকেই জটিল করে নকশা করা ব্যবস্থা কখনো কাজ করে না, আর জোড়াতালি দিয়েও তাকে চালানো যায় না — তখন একটি কাজ করা সরল ব্যবস্থা থেকে আবার শুরু করতে হয়।

সফটওয়্যারে কেন এটি এত সত্য? কারণ প্রতিটি নতুন অংশ আগের অংশগুলোর সাথে নতুন নতুন উপায়ে মেশে। দশটি অংশ একসাথে প্রথমবার চালালে কোনো ভুল দেখা দিলে দোষী যেকোনো একটি, বা যেকোনো দুটির মিলন — সম্ভাবনার সংখ্যা বিশাল। কিন্তু নয়টি অংশ আগেই কাজ করছে জানা থাকলে আর দশম অংশ যোগ করে ভুল এলে, সন্দেহের তালিকায় শুধু একটি অংশ। আমাদের M2-তে GPA ভুল এলে আমরা জানি সমস্যা gpa বা grade_point-এ, লুপে নয় — কারণ লুপ M1-এ প্রমাণিত।

ইতিহাসেও এর বহু প্রমাণ। একই বছর, ১৯৭৫ সালে, Fred Brooks “The Mythical Man-Month”-এ IBM-এর OS/360 প্রকল্পের অভিজ্ঞতা থেকে লেখেন, প্রথম সংস্করণটি প্রায় অবশ্যই ফেলে দিতে হবে — “plan to throw one away”. বিপরীত দিকে, Linux শুরু হয়েছিল ১৯৯১ সালে Linus Torvalds-এর একটি ছোট শখের kernel হিসেবে, যা তিনি নিজেই বলেছিলেন “বড় আর পেশাদার কিছু হবে না”। আজ সেটি সার্ভার থেকে Android ফোন পর্যন্ত চলে — কারণ প্রতিটি ধাপে এটি একটি চলা ব্যবস্থা ছিল। Python নিজেও তাই: Guido van Rossum ১৯৮৯ সালের বড়দিনের ছুটিতে একটি শখের প্রজেক্ট হিসেবে শুরু করেন, প্রথম প্রকাশ ১৯৯১-এ, আর তারপর তিন দশক ধরে একটু একটু বেড়েছে।

আধুনিক “agile” পদ্ধতি — ছোট ছোট ধাপে, প্রতিবার ব্যবহারযোগ্য কিছু দেওয়া — এই একই শিক্ষার আনুষ্ঠানিক রূপ। আর MVP ধারণাটি বলে, প্রথম চলা সংস্করণ ব্যবহারকারীর হাতে দিয়ে তাঁর প্রতিক্রিয়া শুনুন; নাসরিন আপা হয়তো M4 দেখেই বলবেন “আসলে আমার রোল নম্বর দিয়ে খোঁজা লাগবে” — যা আপনি কখনো আন্দাজ করতেন না।

মূল উপলব্ধি: জটিলতা নকশা করে আনা যায় না, তাকে বেড়ে উঠতে দিতে হয়। প্রতিটি মাইলস্টোনের শেষে একটি চলা প্রোগ্রাম মানে প্রতিটি ধাপে একটি নিরাপদ জায়গা, যেখানে ফিরে আসা যায়।

প্র ০২২০২৬ সালে এসে কালো পর্দার command-line টুল কেন বানাব? মানুষ তো বোতাম আর অ্যাপ চায়। Unix-এর দর্শন আর Doug McIlroy-র pipe এখানে কী শেখায়?

নাসরিন আপার মতো সাধারণ ব্যবহারকারীর জন্য শেষ পর্যন্ত হয়তো একটি ওয়েবপেজ বা মোবাইল অ্যাপই ঠিক। কিন্তু প্রোগ্রামার, ডেটা বিশ্লেষক আর সিস্টেম অ্যাডমিনের কাছে CLI আজও সবচেয়ে শক্তিশালী যন্ত্রগুলোর একটি — আর এই কোর্সে আপনি যে টুলগুলো ব্যবহার করছেন, তার বেশিরভাগই CLI: python, pip, git, পরে pytest আর ruff.

প্রথম কারণ স্বয়ংক্রিয়তা. এই পর্বেই দেখলেন, পুরো সেশন একটি টেক্সটে লিখে এক ক্লিকে চালানো যায়। একশোটি সেকশনের ফলাফল একসাথে বানানো, রাত ১২টায় নিজে থেকে রিপোর্ট — CLI-তে এগুলো ছোট একটি script. বোতামের অ্যাপে একই কাজ মানে কাউকে বসে একশোবার ক্লিক করা। আর যা স্বয়ংক্রিয়ভাবে চালানো যায়, তা স্বয়ংক্রিয়ভাবে টেস্টও করা যায়।

দ্বিতীয় কারণ জোড়া লাগানো. ১৯৭৩ সালে Bell Labs-এ Doug McIlroy-র দীর্ঘদিনের প্রস্তাবে Ken Thompson Unix-এ pipe (|) যোগ করেন — এক প্রোগ্রামের আউটপুট সরাসরি আরেকটির ইনপুট। এর থেকে জন্ম নেয় Unix দর্শন, যা McIlroy পরে এভাবে সারসংক্ষেপ করেন (Peter Salus-এর ১৯৯৪ সালের বই “A Quarter Century of Unix”-এ উদ্ধৃত): এমন প্রোগ্রাম লিখুন যা একটি কাজ করে এবং ভালোভাবে করে; এমন প্রোগ্রাম লিখুন যা একসাথে কাজ করে; এমন প্রোগ্রাম লিখুন যা লেখার স্রোত (text stream) সামলায়, কারণ সেটাই সার্বজনীন ইন্টারফেস। আমাদের টুল লেখা পড়ে, লেখা দেয় — তাই অন্য কোনো প্রোগ্রাম এর আউটপুট থেকে, ধরুন, শুধু “error” লাইনগুলো গুনে নিতে পারে, কেউ এর জন্য আলাদা কোড না লিখেই।

তৃতীয় কারণ দূর থেকে আর কম সম্পদে চালানো. ক্লাউডের সার্ভারে সাধারণত কোনো পর্দা-জানালা থাকে না; ssh দিয়ে ঢুকে CLI-ই একমাত্র পথ। ধীর ইন্টারনেটেও কয়েক লাইনের লেখা আসা-যাওয়া করে। আর শেষ কারণটি এই কোর্সের জন্য সবচেয়ে গুরুত্বপূর্ণ: শেখার জন্য CLI সবচেয়ে স্বচ্ছ. বোতাম, রং, বিন্যাস নেই — শুধু যুক্তি। একটি ভালো CLI-এর ভেতরের স্তরগুলো (হিসাবের ফাংশন) পরে হুবহু একটি ওয়েব অ্যাপের পেছনে বসানো যায় (পর্ব ৪৮-এ FastAPI) — কারণ আমরা হিসাব আর কথা বলা আলাদা রেখেছি।

AI-এর যুগে CLI-এর গুরুত্ব বরং বেড়েছে: AI coding agent-রা মূলত terminal-এ কমান্ড চালিয়েই কাজ করে, আর যে টুল লেখা দিয়ে চালানো যায়, তাকে মানুষ আর AI দুজনেই সহজে ব্যবহার করতে পারে।

মূল উপলব্ধি: CLI কোনো পুরোনো অবশেষ নয়; এটি এমন ইন্টারফেস, যা স্বয়ংক্রিয় করা, জোড়া লাগানো আর টেস্ট করা যায়। “একটি কাজ ভালোভাবে” — Unix-এর এই নীতিই আমাদের ছোট ফাংশনগুলোর নীতি।

প্র ০৩আমরা ফাংশন লেখার আগে ডেটা মডেল ঠিক করলাম। Fred Brooks-এর বিখ্যাত কথা “Show me your tables”-এর অর্থ কী? ডেটা স্ট্রাকচার আগে বেছে নেওয়া এত গুরুত্বপূর্ণ কেন?

১৯৭৫ সালের “The Mythical Man-Month” বইয়ে Fred Brooks — যিনি IBM-এর System/360 আর OS/360 প্রকল্পের নেতৃত্ব দিয়েছিলেন — একটি পর্যবেক্ষণ লেখেন, যা আজও উদ্ধৃত হয়: কেউ যদি শুধু flowchart দেখান আর table লুকিয়ে রাখেন, তিনি বিভ্রান্তই থাকবেন; কিন্তু table দেখালে সাধারণত flowchart লাগবেই না — সেটা নিজে থেকেই স্পষ্ট হয়ে যাবে (“Show me your tables, and I won’t usually need your flowcharts; they’ll be obvious.”)। এখানে “table” মানে ডেটার কাঠামো, আর “flowchart” মানে কোডের যুক্তি।

কথাটি পরে বহু রূপে ফিরে এসেছে। Rob Pike ১৯৮৯ সালের “Notes on Programming in C”-এ তাঁর পঞ্চম নিয়মে লেখেন: ডেটাই প্রধান; ঠিক ডেটা স্ট্রাকচার বেছে নিয়ে ভালোভাবে সাজালে অ্যালগরিদম প্রায় সবসময় নিজে থেকেই স্পষ্ট হয়। Linus Torvalds ২০০৬ সালে git-এর মেইলিং লিস্টে লেখেন, খারাপ প্রোগ্রামাররা কোড নিয়ে চিন্তা করেন, ভালো প্রোগ্রামাররা চিন্তা করেন ডেটা স্ট্রাকচার আর তাদের সম্পর্ক নিয়ে। git নিজেই এর উদাহরণ — এর পুরো শক্তি আসে একটি সরল ডেটা মডেল থেকে (object, যার নাম তার বিষয়বস্তুর hash).

আমাদের প্রজেক্টে এর প্রভাব সরাসরি দেখুন। মডেল খ (dict of dicts) বেছে নেওয়ার সাথে সাথে ডুপ্লিকেট পরীক্ষা হয়ে গেল এক লাইনের in; নাম দিয়ে খোঁজায় লুপই লাগল না। মডেল ক নিলে এই দুটি কাজের জন্য আলাদা লুপ-ফাংশন লিখতে হতো, আর সেগুলোতে বাগ ঢোকার সুযোগ থাকত। আবার নম্বরগুলো dict-এ ({"bangla": 78, ...}) না রেখে list-এ রাখায় গড় বের করা হলো সূচক ধরে একটি সরল comprehension; বিনিময়ে বিষয়ের ক্রম মনে রাখতে SUBJECTS tuple লাগল। প্রতিটি পছন্দ পরের কোডের আকার ঠিক করে দেয়।

ডেটা মডেল বদলানোও কোড বদলানোর চেয়ে অনেক দামি। একটি ফাংশনের ভেতরের যুক্তি বদলালে সাধারণত শুধু সেটিই বদলায়; কিন্তু ডেটার আকার বদলালে তাকে যে-ই ছোঁয়, সবাইকে বদলাতে হয় — আর ফাইলে বা ডেটাবেসে জমা পুরোনো ডেটাকেও নতুন আকারে আনতে হয়। এজন্যই বড় সংস্থায় ডেটাবেসের নকশা (schema) বদলানো একটি আলাদা, সাবধানী প্রক্রিয়া।

ব্যবহারিক কৌশল: কোড লেখার আগে কাগজে আপনার ডেটার একটি নমুনা লিখুন — ঠিক যেভাবে অংশ ৪-এর stepper-এ db দেখানো হয়েছে। তারপর স্পেকের প্রতিটি কমান্ডের জন্য নিজেকে জিজ্ঞেস করুন: “এই আকারে উত্তরটি বের করতে কত লাইন লাগবে?” কোনো কমান্ডের উত্তর জটিল মনে হলে, সমস্যা প্রায়ই কোডে নয়, ডেটার আকারে।

মূল উপলব্ধি: ডেটা স্ট্রাকচার হলো বাড়ির ভিত, কোড হলো দেয়াল আর আসবাব। ভিত ঠিক থাকলে দেয়াল সোজা ওঠে; ভিত বাঁকা হলে প্রতিটি দেয়ালে জোড়াতালি লাগে।

প্র ০৪একটি ৬০ লাইনের স্ক্রিপ্ট একটানা লিখলেও তো চলে। ছোট ছোট ফাংশন আর তার জন্য আলাদা টেস্ট লেখার বাড়তি পরিশ্রম কেন সার্থক?

প্রথম দিন একটানা স্ক্রিপ্ট সত্যিই দ্রুত। সমস্যা দেখা দেয় দ্বিতীয় দিন থেকে — যখন কিছু বদলাতে হয়। গ্রেডিং স্কেল বদলাল, নতুন একটি বিষয় এল, বা একটি বাগ ধরা পড়ল। একটানা স্ক্রিপ্টে যুক্তি ছড়িয়ে আছে লুপের নানা জায়গায়; একটি বদল অন্য কোথায় প্রভাব ফেলবে, তা জানার একমাত্র উপায় পুরো প্রোগ্রাম হাতে চালিয়ে সব কমান্ড আবার দেখা। মানুষ সেটা প্রতিবার করে না — তাই পুরোনো কাজ নিঃশব্দে ভাঙে।

ছোট ফাংশনের প্রথম লাভ হলো প্রতিটি অংশ আলাদা করে পরীক্ষা করা যায়. grade_point(79) কত, তা জানতে পুরো টুল চালিয়ে একজন ছাত্র যোগ করতে হয় না। দ্বিতীয় লাভ ভুলের জায়গা ছোট হয়: টেস্ট ব্যর্থ হলে সে নিজেই বলে দেয় কোন ফাংশন, কোন ইনপুটে। তৃতীয় লাভ নির্ভয়ে বদলানো: টেস্টগুলো একটি নিরাপত্তা-জাল; কোড গোছান, দ্রুত করুন, নতুন কাজ যোগ করুন — কিছু ভাঙলে কয়েক সেকেন্ডে জানবেন।

এই ধারণার ইতিহাস দীর্ঘ। Kent Beck ১৯৯০-এর দশকে Smalltalk-এর জন্য SUnit নামে একটি ছোট টেস্ট-কাঠামো লেখেন; পরে Erich Gamma-র সাথে Java-র জন্য JUnit. এর ছাঁচে Python-এ unittest module যোগ হয় Python 2.1-এ (২০০১). Beck-এর ২০০২ সালের বই “Test-Driven Development: By Example” এক ধাপ এগিয়ে বলে — কোড লেখার আগে টেস্ট লিখুন; ব্যর্থ টেস্ট দেখুন, তারপর এমন কোড লিখুন যাতে সেটি পাস করে। আমাদের অংশ ২-এর কমান্ড-টেবিলের শেষ কলাম আসলে সেই আগে-লেখা টেস্টেরই বর্ণনা।

তবে একটি সতর্কতা মনে রাখুন। Edsger Dijkstra ১৯৬৯ সালে NATO-র সফটওয়্যার ইঞ্জিনিয়ারিং সম্মেলনে বলেন, টেস্ট বাগের উপস্থিতি দেখাতে পারে, অনুপস্থিতি কখনো নয়। আমাদের তিনটি PASS প্রমাণ করে না যে টুলে কোনো বাগ নেই; শুধু বলে যে পরীক্ষিত ক্ষেত্রগুলোতে নেই। তাই কোন ক্ষেত্র পরীক্ষা করবেন, সেটাই আসল দক্ষতা — সীমানা (৭৯/৮০), খালি অবস্থা, ভুল ধরন, অদ্ভুত Unicode.

AI-এর যুগে এই যুক্তি আরও জোরালো। AI এক মিনিটে ৬০ লাইন লিখে দেয়, কিন্তু সেই ৬০ লাইন সঠিক কি না, তা কে বলবে? ছোট ফাংশন হলে প্রতিটি আপনি পড়ে বুঝতে পারেন; টেস্ট থাকলে AI-এর পরের সংশোধন পুরোনো কিছু ভাঙল কি না, তা যন্ত্রই বলে দেয়।

মূল উপলব্ধি: কোড একবার লেখা হয়, কিন্তু বদলানো হয় বহুবার। ছোট ফাংশন আর টেস্ট লেখার খরচ প্রথম দিনে, লাভ প্রতিটি পরের দিনে।

প্র ০৫কেন বলা হয় ইনপুট যাচাই নিরাপত্তার প্রথম প্রতিরক্ষা? একটি ফলাফলের টুলে “abc” টাইপ করা আর একটি ব্যাংকের সিস্টেমে হ্যাকারের আক্রমণ — এ দুইয়ের মিল কোথায়?

মিলটি মূলে: দুই ক্ষেত্রেই প্রোগ্রাম এমন ডেটা পাচ্ছে, যা তার নির্মাতা আশা করেননি। নাসরিন আপা ভুল করে “abc” লিখলেন — এটি দুর্ঘটনা। একজন আক্রমণকারী ইচ্ছা করে এমন কিছু লেখেন যাতে প্রোগ্রাম অপ্রত্যাশিত কিছু করে — এটি আক্রমণ। কিন্তু প্রোগ্রামের চোখে দুটি একই: বাইরের ডেটা, যা বিশ্বাস করা হয়েছিল অথচ পরীক্ষা করা হয়নি।

নিরাপত্তার জগতে সবচেয়ে পুরোনো আর এখনো সবচেয়ে প্রচলিত দুর্বলতার একটি হলো injection — যেখানে ব্যবহারকারীর লেখা ডেটা হিসেবে না থেকে কোড বা কমান্ড হিসেবে চলে যায়। ওয়েব-নিরাপত্তার সংস্থা OWASP-এর “Top 10” তালিকায় injection বহু বছর ধরে আছে। ২০০৭ সালের বিখ্যাত xkcd কার্টুন “Exploits of a Mom”-এ এক মা ছেলের নাম রাখেন এমনভাবে যে স্কুলের ডেটাবেসে নাম ঢোকাতেই ছাত্রদের পুরো টেবিল মুছে যায় — “little Bobby Tables” এখন প্রোগ্রামারদের মধ্যে প্রবাদ। পর্ব ১০-এর eval(input()) একই বিপদের Python রূপ।

যাচাই না করার দাম শুধু নিরাপত্তায় নয়। ১৯৯৬ সালের ৪ জুন ইউরোপের Ariane 5 রকেট উৎক্ষেপণের প্রায় ৪০ সেকেন্ড পরে ধ্বংস হয়। তদন্তে দেখা যায়, একটি বেগের মান 64-bit দশমিক সংখ্যা থেকে 16-bit পূর্ণসংখ্যায় রূপান্তরের সময় সীমা ছাড়িয়ে যায়, আর সেই রূপান্তরে সীমার পরীক্ষা রাখা হয়নি — কারণ পুরোনো Ariane 4-এ মানটি কখনো অত বড় হতো না। আমাদের int(r) <= 100 ঠিক সেই ধরনের সীমা-পরীক্ষা।

একসময় নেটওয়ার্ক প্রোটোকলে জনপ্রিয় নীতি ছিল Jon Postel-এর “robustness principle” (১৯৮০ সালের দিকের RFC-তে): নিজে যা পাঠাও তাতে কঠোর হও, অন্যের কাছ থেকে যা নাও তাতে উদার হও। কিন্তু অভিজ্ঞতায় দেখা গেল, অতি-উদারভাবে আধা-ভুল ইনপুট মেনে নেওয়া অনেক নিরাপত্তা-ছিদ্রের জন্ম দেয়; পরে IETF-এর আলোচনায় এই নীতির সমালোচনা জোরালো হয়েছে। আধুনিক পরামর্শ হলো সীমানায় কঠোর যাচাই: কী গ্রহণযোগ্য তা স্পষ্ট করে বলুন (allow-list) — “শুধু ০–১০০-র দশমিক অঙ্ক” — আর বাকি সব বাতিল, কী কী খারাপ তার তালিকা বানানোর বদলে।

আরেকটি নীতি আমাদের নকশায় আছে: যাচাই হয় সীমানায়, একবার. লেখা থেকে সংখ্যা হওয়ার মুহূর্তে parse_marks সব পরীক্ষা করে; তারপর ভেতরের ফাংশনগুলো (gpa, sheet_key) নিশ্চিন্তে ধরে নেয় যে নম্বর বৈধ int. বাইরের অবিশ্বস্ত জগৎ আর ভেতরের বিশ্বস্ত জগতের মাঝে একটি স্পষ্ট দরজা।

মূল উপলব্ধি: বাইরের প্রতিটি ডেটা — কীবোর্ড, ফাইল, নেটওয়ার্ক — যতক্ষণ না প্রমাণিত হয়, ততক্ষণ সন্দেহজনক। ভুল ইনপুট আটকানো শেখাই নিরাপদ প্রোগ্রামিংয়ের প্রথম ধাপ; পর্ব ৫০-এ এর পুরো রূপ দেখবেন।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — অনুমান করা, পড়া আর মিলিয়ে নেওয়াই আসল কাজ।

  1. আউটপুট অনুমান: খালি টুলে পরপর এই কমান্ডগুলো দিলে কী কী ছাপা হবে? add Mim 80 80 80, add Anik 100 70 70, add Bina 90 90 32, show, top, fail.

    তিনটি ok: Mim GPA 5.00 (তিনটিই ৮০, A+), Anik GPA 4.33 (৫ + ৪ + ৪ = ১৩, ÷ ৩ = ৪.৩৩), Bina GPA 0.00 (৩২ = F). মোট নম্বর: Bina ২১২, Anik ২৪০, Mim ২৪০. show: 1. Anik 240 GPA 4.33, 2. Mim 240 GPA 5.00, 3. Bina 212 GPA 0.00 — Anik আর Mim সমান, তাই নামের ক্রমে Anik আগে। top 240: Anik, Mim. fail: Bina. লক্ষ করুন, Bina-র মোট নম্বর অনেক হলেও এক বিষয়ে F মানে GPA শূন্য — আর মেধাতালিকা মোট দিয়ে হওয়ায় একজন ফেল করা ছাত্রও তালিকায় আছে। এটি কি চাওয়া আচরণ? নাসরিন আপাকে জিজ্ঞেস করার মতো প্রশ্ন।

  2. Edge case শিকার: add কমান্ডের জন্য এমন ছয়টি ইনপুট লিখুন, যেগুলো এখনো পরীক্ষা হয়নি অথবা অদ্ভুত আচরণ দেখাতে পারে। প্রতিটির জন্য অনুমান করুন টুল কী করবে।

    কয়েকটি ভালো উদাহরণ: (১) শুধু add — args খালি, তাই marks = None, usage বার্তা। (২) add 123 50 50 50 — নাম “123” গৃহীত হয়! নাম যাচাই নেই; চাইলে name.isalpha() শর্ত যোগ করা যায়। (৩) add Rahim 078 85 90 — “078” দশমিক অঙ্ক, int দেয় 78; গৃহীত। (৪) ADD Rahim 78 85 90 — কমান্ড ছোট-বড় হাতের অক্ষর মানে, তাই unknown command; চাইলে cmd.casefold(). (৫) add Rahim 78 85 90 (বাড়তি space) — split() সব space সামলায়, ঠিকঠাক চলে। (৬) add Rahim 0 0 0 — বৈধ, GPA 0.00, fail-এ আসবে। লক্ষ করুন, (২) আর (৪) বাগ নয়, স্পেকের ফাঁক — কী হওয়া উচিত তা স্পেকে লেখা নেই। Edge case খোঁজা প্রায়ই চাহিদার প্রশ্ন খুঁজে বের করে।

  3. একটি কমান্ড যোগ করুন: remove NAME কমান্ড লিখুন, যা ছাত্রকে মুছে দেয় (ছোট-বড় হাতের অক্ষর না মেনে), না পেলে error দেয়। আগে হিসাবের ফাংশন remove_student(db, name), তারপর handle-এ শাখা, শেষে অন্তত দুটি assert.

    একটি সম্ভাব্য সমাধান:

    def remove_student(db, name):
        key = name.casefold()
        if key not in db:
            return f"error: no student named {name}"
        removed = db.pop(key)              # যাচাইয়ের পরে বদল; মুছে যাওয়া dict হাতে থাকে
        return f"ok: {removed['name']} removed"
    
    # handle()-এর match-এ নতুন শাখা:
    #     case "remove" if args:
    #         return [remove_student(db, args[0])]
    
    test_db = {"rahim": {"name": "Rahim", "marks": [78, 85, 90]}}
    assert remove_student(test_db, "RAHIM") == "ok: Rahim removed"
    assert test_db == {}
    assert remove_student(test_db, "Rahim") == "error: no student named Rahim"

    del db[key]-এর বদলে db.pop(key) কেন? pop মুছে ফেলার সাথে সাথে মুছে যাওয়া মানটি ফেরত দেয়, তাই বার্তায় আসল বানান “Rahim” দেখানো যায় — ব্যবহারকারী “RAHIM” লিখলেও। লক্ষ করুন, তিনটি টেস্টই সুনির্দিষ্ট: পুরো বার্তা হুবহু মেলানো, আর মোছার পরে db সত্যিই খালি কি না। “error দিয়ে শুরু হয় কি না”-র মতো ঢিলেঢালা টেস্ট অনেক ভুল ধরতে পারে না। case "remove" if args-এর if args শুধু remove লেখার ক্ষেত্রে args[0]-এর IndexError আটকায়।

  4. AI অডিট: যেকোনো AI-কে শুধু এটুকু বলুন: “Python-এ ছাত্রদের ফলাফল ব্যবস্থাপনার একটি command-line প্রোগ্রাম লেখো।” কোনো শর্ত দেবেন না। ফলাফলটি এই পর্বের চেকলিস্ট ধরে যাচাই করুন, তারপর অংশ ১০-এর prompt দিয়ে আবার চেষ্টা করুন।

    সাধারণত যা পাবেন: একটি while True মেনু (“1. Add 2. Show …”) যার ভেতরে সব কাজ; নম্বর পড়া হয় সরাসরি int(input()) দিয়ে, তাই “abc”-তে প্রোগ্রাম থামে, অথবা AI একটি try/except বসিয়ে দেয় যা আপনি এখনো শেখেননি; ১০০-র বেশি বা ঋণাত্মক নম্বর প্রায়ই গৃহীত হয়; ছাত্রদের একটি global list; সমান নম্বরে ক্রম অনির্দিষ্ট; কোনো টেস্ট নেই। অনেক সময় একটি class-ও থাকে। দ্বিতীয় prompt-এ পাবেন ফাংশনের তালিকা আর টেস্ট আগে, কোড পরে — আর আপনি প্রতিটি ধাপে বলতে পারবেন “এটা বুঝলাম” বা “এটা বদলাও”। শিক্ষা: AI-এর উত্তরের মান অনেকটাই নির্ভর করে আপনি কী শর্ত চাইছেন তার ওপর — আর সেই শর্তগুলো জানতে হলে আপনাকেই নকশার মূলনীতি জানতে হবে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি CLI টুলে add Rahim 78 85 90 লাইন প্রক্রিয়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ⌨️ input() পুরো লাইনটি string হিসেবে পড়ে
  2. ✂️ split() লাইনটিকে কমান্ড আর বাকি শব্দে ভাগ করে
  3. 🔀 match কমান্ড দেখে ঠিক শাখা বাছাই করে
  4. 🛡️ নম্বরগুলো যাচাই হয় — অঙ্ক কি না, ০–১০০-র মধ্যে কি না
  5. 💾 সব ঠিক থাকলে ডেটায় নতুন ছাত্র জমা হয়
  6. 🖨️ ফেরত আসা নিশ্চিতকরণ বার্তা ছাপা হয়

Python-এ ব্যবহারকারীর লেখা একটি string-কে int()-এ পাঠানোর আগে, শুধু অ-ঋণাত্মক পূর্ণসংখ্যা মেনে নিতে কোন পরীক্ষাটি সবচেয়ে নিরাপদ?

  • s.isdigit()
  • s.isdecimal()
  • s.isalnum()
  • len(s) > 0

sorted(rows, key=f), যেখানে f(s) ফেরত দেয় (-s["total"], s["name"]). ফলাফলের ক্রম কেমন হবে?

  • মোট কম থেকে বেশি, সমান হলে নাম Z থেকে A
  • মোট বেশি থেকে কম, সমান হলে নাম Z থেকে A
  • শুধু নামের বর্ণক্রমে, মোট উপেক্ষিত
  • মোট বেশি থেকে কম, সমান হলে নাম A থেকে Z

ছাত্রদের তথ্য রাখতে list of dicts-এর বদলে নাম-চাবির dict of dicts বেছে নেওয়ার প্রধান সুবিধা কোনটি?

  • নাম দিয়ে সরাসরি খোঁজা আর এক লাইনে ডুপ্লিকেট নাম ধরা
  • শুধু dict-ই যোগ করার ক্রম মনে রাখে
  • একই নামের দুজন ছাত্র রাখা যায়
  • ছাত্ররা স্বয়ংক্রিয়ভাবে নম্বরের ক্রমে সাজানো থাকে

marks = [78, 85, 90], তারপর db["r"] = {"marks": marks}, তারপর marks[0] = 40. এখন db["r"]["marks"] কী?

  • [78, 85, 90], কারণ dict নিজের কপি রাখে
  • TypeError, কারণ dict-এর ভেতরের list বদলানো যায় না
  • [40, 85, 90]
  • [] — list খালি হয়ে যায়

def log(line, history=[]): history.append(line); return history. পরপর log("a") আর log("b") ডাকলে দ্বিতীয় ডাক কী ফেরত দেয়?

  • ['b']
  • ['a', 'b']
  • TypeError
  • None

Python-এ result = [n for n in names if n.startswith("Z")] or ["no match"] — কোনো নাম “Z” দিয়ে শুরু না হলে result কী?

  • []
  • False
  • SyntaxError
  • ['no match']

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. আসল প্রোগ্রাম = অনেক এক-কাজের ফাংশন + একটি কমান্ড-লুপ. হিসাবের ফাংশন মান return করে, কিছু পড়ে না বা ছাপে না; লুপ পড়ে, dispatch করে, ছাপে।
  2. কোডের আগে লিখুন কমান্ডের টেবিল (চাহিদা), ফাংশনের তালিকা আর ডেটা মডেল; MVP-র বাইরের ইচ্ছা “পরের তালিকায়”।
  3. নাম দিয়ে খোঁজা আর ডুপ্লিকেট ধরা দরকার হলে dict of dicts; চাবি casefold() করা নাম। দাম: একই নামের দুজন নয়।
  4. যাচাই সীমানায়, একবার: শব্দের সংখ্যা, isdecimal(), সীমা — আগে সব যাচাই, তারপর বদল. সাজাতে key ফাংশন থেকে (-total, name) tuple; বাছাইয়ে comprehension.
  5. বাইরের list জমার আগে কপি, default-এ list নয়; প্রতিটি মূল ফাংশনের সীমার মানে assert, আর প্রতিটি নতুন বাগে একটি নতুন টেস্ট।

পরের পর্বে: মডিউল ৩ শেষ — আপনি এখন একটি সম্পূর্ণ প্রোগ্রাম বানাতে পারেন! মডিউল ৪ শুরু হচ্ছে বড় প্রোগ্রাম গোছানো দিয়ে। প্রথমেই module, package আর import — আমাদের টুলের ফাংশনগুলোকে আলাদা ফাইলে সরিয়ে, টেস্ট থেকে সরাসরি import করার পথ।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান