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

আউটপুট কী হবে? — Python কোড পড়ার অনুশীলন

What Will This Print? — Python Output-Prediction Practice

Taught in Bangla, this practice page has 50 "what will this print?" Python questions grouped by course module, from numbers, strings and f-strings through defaults, scope, aliasing, exceptions, MRO, dunder methods, generators, closures and itertools. Every option is explained, every answer is checked on CPython 3.14, and a trace-table method helps you run code in your head.

এই পর্বে যা পাবেন

  • কাগজে-কলমে Python কোড চালানোর (dry run) পদ্ধতি — নাম আর object আলাদা করে দেখা, call stack আর “কখন মূল্যায়ন হয়” একসাথে
  • মডিউল ১ থেকে ৭ পর্যন্ত ৫০টি প্রশ্ন — সহজ থেকে কঠিন, প্রতিটির পূর্ণ ব্যাখ্যা আর কোন পর্ব ঝালাবেন তার লিংক
  • exception-এর ঠিক নাম, SyntaxError আর “ক্রম নিশ্চিত নয়” — “অদ্ভুত” উত্তরগুলো আলাদা করে চেনা ও কারণসহ লেখা
  • ৮টি “নিজে চালিয়ে দেখুন” প্রোগ্রাম — একটি লাইন বদলে আগে অনুমান, তারপর চালিয়ে যাচাই
  • AI-কে দিয়ে নতুন প্রশ্ন বানানো, আর তার উত্তর interpreter দিয়ে যাচাইয়ের নিয়ম

১ · মডিউল ৮ শুরু — পরীক্ষা ও ইন্টারভিউর প্রস্তুতি

অভিনন্দন! ৫১টি পর্বে আপনি Python-এর ভ্যারিয়েবল আর টাইপ থেকে শুরু করে list, dict, exception, class, generator, asyncio আর একটি পূর্ণ ক্যাপস্টোন প্রজেক্ট পর্যন্ত এসেছেন। এবার শেখা জিনিস এমনভাবে ঝালিয়ে নেওয়ার পালা, যাতে পরীক্ষার হলে বা ইন্টারভিউর টেবিলে চাপের মধ্যেও ঠিক উত্তর মাথায় আসে।

এই শেষ মডিউলে নতুন কোনো বিষয় নেই। আছে চারটি অনুশীলনের পর্ব, প্রতিটি আলাদা ধরনের প্রশ্নের জন্য:

পর্ব ৫২ · আউটপুট কী হবে?

এই পর্ব। ছোট কোড দেখে বলতে হবে কী ছাপা হবে — বা কোন exception উঠবে। লিখিত পরীক্ষা, MCQ আর টেকনিক্যাল ইন্টারভিউ, তিন জায়গাতেই আসে।

কৌশল: trace table

পর্ব ৫৩ · MCQ প্রশ্নব্যাংক

ধারণা, সংজ্ঞা আর তথ্যের দ্রুত প্রশ্ন — keyword, built-in, ডেটা স্ট্রাকচারের জটিলতা, OOP. চাকরির প্রাথমিক বাছাই পরীক্ষায় এ ধরনের প্রশ্ন প্রায়ই আসে।

কৌশল: সময় ধরে চর্চা

পর্ব ৫৪ · লিখিত ও ভাইভা

“পার্থক্য লিখুন”, “উদাহরণসহ ব্যাখ্যা করুন”, “ফাংশন লিখুন” আর মুখোমুখি প্রশ্নের উত্তর কীভাবে সাজাবেন।

কৌশল: উত্তরের কাঠামো

পর্ব ৫৫ · কোডিং ইন্টারভিউ

সমস্যা শুনে প্রশ্ন করা, পরিকল্পনা বলা, Python-এ কোড লেখা, টেস্ট করা — সফটওয়্যার কোম্পানির ইন্টারভিউর পুরো ধারা।

কৌশল: জোরে ভাবা

“আউটপুট কী হবে?” প্রশ্ন এত বেশি আসে কেন

প্রশ্নকর্তা আসলে জানতে চান: আপনি কি interpreter-এর মতো করে কোড পড়তে পারেন? Python দেখতে সহজ, তাই অনেকে ধরে নেন এখানে চমক নেই। অথচ সবচেয়ে বেশি বাগ আসে ঠিক সহজ-দেখতে জায়গা থেকে: দুটি নাম একই list দেখাচ্ছে, default মান একবারই তৈরি হয়েছে, লুপের lambda শেষ মানটাই মনে রেখেছে, generator একবার ব্যবহারেই ফুরিয়ে গেছে। যিনি কোড পড়েই এগুলো দেখতে পান, তিনিই বাগ খুঁজে পান, AI-এর লেখা কোড যাচাই করতে পারেন, আর code review-তে কাজের মন্তব্য দিতে পারেন।

আপনার লক্ষ্যশুরু করুনতারপর
বিশ্ববিদ্যালয়ের লিখিত পরীক্ষাপর্ব ৫২ (আউটপুট) ও ৫৪ (লিখিত)পর্ব ৫৩ দিয়ে দ্রুত ঝালাই
সরকারি বা ব্যাংকের IT/প্রোগ্রামার পদের পরীক্ষাপর্ব ৫৩ (MCQ) ও ৫২পর্ব ৫৪-এর লিখিত ও ভাইভা অংশ
সফটওয়্যার কোম্পানির ইন্টারভিউপর্ব ৫৫ (কোডিং)পর্ব ৫২ — অনেক টেকনিক্যাল রাউন্ড “এই কোড কী ছাপবে?” দিয়েই শুরু হয়
শুধু নিজের শেখা যাচাইএই পর্ব, প্রথম থেকে শেষযে ভাগে বেশি ভুল, সেই পর্বে ফিরে যান

এই পাতা কীভাবে ব্যবহার করবেন

  1. প্রথম দফা — অপশন ঢেকে রাখুন। প্রতিটি কোড পড়ে খাতায় নিজের উত্তর লিখুন, তারপর অপশনে চাপুন। প্রথম চেষ্টায় কতগুলো ঠিক হলো, অনুশীলনের শেষে স্কোর দেখাবে।
  2. দ্বিতীয় দফা — ভুলগুলোর কারণ খুঁজুন। প্রতিটি ভুল অপশনের ব্যাখ্যা পড়ুন। প্রতিটি ভাগের শেষে “রিভিশনের জন্য” অংশে লেখা আছে কোন পর্বগুলো ঝালাতে হবে।
  3. তৃতীয় দফা — নিজে চালান। প্রতিটি ভাগের “নিজে চালিয়ে দেখুন” কোড চালান। একটি লাইন বদলে আগে ফল অনুমান করুন, তারপর চালিয়ে মেলান।
এই পাতার ধরে নেওয়া পরিবেশ

সব উত্তর CPython 3.14-এর জন্য, কোড একটি .py ফাইল হিসেবে চালানো (REPL-এ লাইন ধরে লেখা নয়)। match লাগে Python 3.10 বা পরের সংস্করণ। কোডের টুকরোয় import না থাকলে ধরে নিন সেটি লেখা আছে। exception-এর উত্তরে শুধু traceback-এর শেষ লাইন দেখানো হয়েছে; পুরো traceback stderr-এ যায়। যে উত্তর সংস্করণ বা implementation-এর ওপর নির্ভর করে, প্রশ্নেই তা বলা আছে।

C/C++ জানলে

C/C++-এর আউটপুট প্রশ্নে “undefined behavior” একটি বড় উত্তর; Python-এ তা প্রায় নেই — প্রায় প্রতিটি প্রোগ্রামের হয় নির্দিষ্ট ফল, নয় নির্দিষ্ট নামের exception. তবে Python-এর নিজস্ব চমক আছে: প্রতিটি ভ্যারিয়েবল আসলে একটি object-এর reference, // ভাগ শূন্যের দিকে নয়, নিচের দিকে গোল করে (C-তে -7 / 2 হয় -3, Python-এ -7 // 2 হয় -4), আর default argument একবারই তৈরি হয়। তুলনার জন্য দেখুন C-এর আউটপুট প্রশ্ন আর C++-এর আউটপুট প্রশ্ন.

পরীক্ষা ও ইন্টারভিউতে এ ধরনের প্রশ্ন সাধারণত এই চেহারায় আসে:

  • “নিচের প্রোগ্রামের আউটপুট লিখুন” — খাতায় trace table এঁকে দেখান; শুধু উত্তর নয়, ধাপগুলোও নম্বর পায়।
  • “এই কোড চালালে কী হবে?” — উত্তর exception হলে তার ঠিক নাম আর কোন লাইনে উঠল তা লিখুন; তার আগে যা ছাপা হয়েছে, সেটাও।
  • MCQ-তে চারটি আউটপুট থেকে বেছে নেওয়া — আগে নিজে হিসাব করুন, তারপর অপশন দেখুন।
  • ইন্টারভিউতে “এই কোডে কী সমস্যা?” — mutable default, alias বা late binding আগে বলুন, সঙ্গে সমাধান; এটাই অভিজ্ঞতার লক্ষণ।

২ · Python কোড মাথায় চালানোর পদ্ধতি

আউটপুট বলার একটাই নির্ভরযোগ্য উপায়: interpreter যেভাবে চালায়, ঠিক সেভাবে এক লাইন এক লাইন করে চালানো, আর প্রতিটি পরিবর্তন লিখে রাখা। একে বলে dry run. Python-এ এর জন্য একটি সাধারণ ভ্যারিয়েবল-টেবিল যথেষ্ট নয়, কারণ Python-এ ভ্যারিয়েবল একটি বাক্স নয় — এটি একটি object-এর গায়ে লাগানো নামের স্টিকার।

একটি গার্মেন্টস কারখানার গুদামের কথা ভাবুন। প্রতিটি কাপড়ের বান্ডিল (object) একবারই আছে, কিন্তু তার গায়ে একাধিক ট্যাগ (নাম) ঝোলানো যায় — “অর্ডার-৭”, “লাল-স্টক”। একটি ট্যাগ ধরে বান্ডিলে কাপড় যোগ করলে, অন্য ট্যাগ ধরে দেখলেও সেই বাড়তি কাপড় দেখা যাবে, কারণ বান্ডিল একটিই। আবার একটি ট্যাগ খুলে অন্য বান্ডিলে লাগালে প্রথম বান্ডিল বদলায় না। Python-এ b = a মানে নতুন ট্যাগ লাগানো (alias), b.append(x) মানে বান্ডিলটাই বদলানো (mutation), আর b = [...] মানে ট্যাগ খুলে অন্য জায়গায় লাগানো (rebinding).

Python-এর trace table-এর ছয়টি নিয়ম

  1. নাম আর object আলাদা কলামে। বাঁ পাশে নাম, ডান পাশে object (L1, L2… নাম দিয়ে), মাঝে তীর। দুটি নাম একই object-এ তীর দিলে, একটির মাধ্যমে বদল অন্যটিতেও দেখা যাবে।
  2. প্রতিটি লাইনে জিজ্ঞেস করুন: নতুন বাঁধন, নাকি object বদল? = সবসময় নাম বাঁধে, কপি করে না। append, += (list-এ), x[i] = … object বদলায়। int, str, tuple immutable — এদের “বদল” আসলে নতুন object.
  3. কখন মূল্যায়ন হয়, লিখে রাখুন। def লাইন চলার সময় default মান তৈরি হয় আর decorator চলে; ফাংশনের শরীর চলে ডাকার সময়। print(a(), b())-এ সব argument আগে মূল্যায়ন হয়, তারপর ছাপা।
  4. ফাংশন ডাকলে call stack-এ নতুন ঘর। নিজস্ব local নাম নিয়ে। নাম খোঁজা চলে LEGB ক্রমে; আর ফাংশনের কোথাও কোনো নামে assign থাকলে সেই নাম পুরো ফাংশনে local.
  5. অলস জিনিস আলাদা চিহ্ন দিন। generator, map, zip, range — এরা মান তৈরি করে চাওয়ার সময়। generator, map, zip-এর মতো iterator একবার শেষ হলে আর কিছু দেয় না; range অবশ্য বারবার চালানো যায়।
  6. আউটপুট কলামে হুবহু লিখুন। print মাঝে একটি space আর শেষে newline দেয়; list-এর ভেতরের string repr দিয়ে ছাপা হয় (['a'], উদ্ধৃতিসহ); float ছাপা হয় সবচেয়ে ছোট সঠিক রূপে।

নিচের ডায়াগ্রামে একটি পূর্ণ প্রোগ্রাম এই পদ্ধতিতে চালানো হয়েছে। এখানে দুটি ফাঁদ একসাথে: mutable default argument আর alias. “পরে” চাপার আগে নিজে পুরো আউটপুট লিখে নিন।

1def register(name, roster=[]):2    roster.append(name)3    return roster45a = register("Rina")6b = register("Karim")7c = register("Joy", [])8b.append("Mitu")9print(a)10print(a is b, c)
Trace table — নাম → object
default L1
a
b
c
call stack
  1. লাইন ১ — def চলল, default তৈরি হলো একবারই। def নিজেই একটি statement. এটি চলার মুহূর্তে [] মূল্যায়ন হয়ে একটি খালি list (L1) তৈরি হয়, আর ফাংশনের সাথে জমা থাকে (register.__defaults__-এ)। পরে যতবার ডাকুন, নতুন খালি list তৈরি হবে না।
  2. লাইন ৫ — প্রথম ডাক। roster দেওয়া হয়নি, তাই সেটি L1-কে দেখায়। append L1-কেই বদলায়: ['Rina']. ফেরত আসে L1 নিজেই — কপি নয় — আর a এখন L1-এর আরেক নাম।
  3. লাইন ৬ — দ্বিতীয় ডাক। আবার default, অর্থাৎ আবার সেই L1. এবার L1 হলো ['Rina', 'Karim'], আর b-ও L1. লক্ষ করুন, a-কে আমরা ছুঁইনি, তবু a দিয়ে দেখলে এখন দুটি নাম।
  4. লাইন ৭ — নিজের list দিয়ে ডাক। [] এখানে ডাকার সময় নতুন তৈরি হলো (L2). default ব্যবহারই হলো না, তাই L1 অপরিবর্তিত। c → L2 = ['Joy'].
  5. লাইন ৮ — alias দিয়ে বদল। b.append("Mitu") L1-কে বদলায়। a, b আর ফাংশনের default — তিনটিই একই L1, তাই তিন জায়গাতেই Mitu চলে এল। পরের register() ডাক শুরু হবে তিনটি নাম নিয়ে!
  6. লাইন ৯ — ছাপা। print(a) L1 ছাপে: ['Rina', 'Karim', 'Mitu']. list-এর ভেতরের string উদ্ধৃতিসহ, কারণ list তার element-কে repr দিয়ে দেখায়।
  7. লাইন ১০ — is আর c. a is b জিজ্ঞেস করে: দুটি নাম কি একই object? হ্যাঁ — True. আর c আলাদা L2: ['Joy']. পুরো আউটপুট দুই লাইন। সমাধান: default হিসেবে None দিন, আর ভেতরে if roster is None: roster = [].

নিচের কোডটি হুবহু ডায়াগ্রামের প্রোগ্রাম, সঙ্গে ফাংশনের জমা থাকা default আর None দিয়ে সমাধান। চালানোর আগে শেষ দুই লাইনের আউটপুট অনুমান করুন।

def register(name, roster=[]):
    roster.append(name)
    return roster

a = register("Rina")
b = register("Karim")
c = register("Joy", [])
b.append("Mitu")
print(a)
print(a is b, c)
print(register.__defaults__)

def register_fixed(name, roster=None):
    if roster is None:
        roster = []
    roster.append(name)
    return roster

print(register_fixed("Rina"), register_fixed("Karim"))

আউটপুট চার লাইন: ['Rina', 'Karim', 'Mitu'], True ['Joy'], (['Rina', 'Karim', 'Mitu'],), আর ['Rina'] ['Karim']. তৃতীয় লাইন প্রমাণ করে default list ফাংশনের গায়েই জমা আছে, আর তাতে Mitu-ও ঢুকে গেছে। register_fixed-এ None একটি immutable sentinel; প্রতিটি ডাকে শরীরের ভেতরে নতুন list তৈরি হয়, তাই দুটি ডাক আলাদা। পুরো ব্যাখ্যা: পর্ব ২০ · scope, closure আর default-এর ফাঁদ, পর্ব ১৯ · mutability, alias আর copy.

পাঁচ রকম উত্তর হতে পারে

পরীক্ষার্থীরা ধরে নেন প্রতিটি কোড একটি নির্দিষ্ট লেখা ছাপবে। বাস্তবে উত্তর পাঁচ রকম, আর ভালো প্রশ্নকর্তা ঠিক এটাই যাচাই করেন:

✅ নির্দিষ্ট আউটপুট

ভাষার নিয়ম ফল পুরোপুরি ঠিক করে দেয়। বেশিরভাগ প্রশ্ন এই ধরনের — dict-এর ক্রমও এখানে পড়ে, কারণ Python 3.7 থেকে dict ঢোকানোর ক্রম রাখার নিশ্চয়তা দেয়।

যেমন: -7 // 2 হলো -4

⛔ নির্দিষ্ট নামের exception

প্রোগ্রাম চলতে চলতে একটি লাইনে থামে। ঠিক নাম লিখুন — TypeError, NameError, IndexError, KeyError, UnboundLocalError — আর মনে রাখুন, তার আগের লাইনগুলোর আউটপুট ছাপা হয়ে গেছে।

যেমন: "5" + 5

🧱 SyntaxError

Python পুরো ফাইল আগে bytecode-এ compile করে, তারপর চালায়। তাই ফাইলের শেষ লাইনে SyntaxError থাকলেও প্রথম লাইনের print চলে না — কিছুই ছাপা হয় না।

যেমন: match-এ আগে capture

🔀 ক্রম নিশ্চিত নয় / সংস্করণ-নির্ভর

string-এর set-এর ক্রম প্রতিবার চালালে বদলাতে পারে (hash randomization). আবার কিছু ফল ভাষার নিয়ম নয়, CPython-এর implementation detail — যেমন বড় int-এ is — বা সংস্করণভেদে বদলায় — যেমন error বার্তার ভাষা বা 3.14-এর নতুন warning.

যেমন: {"a", "b", "c"}

(মাঝারি) একটি set-এ তিনটি string — CPython 3.14-এ একটি .py ফাইল হিসেবে চালালে কী ছাপা হবে?

print({"a", "b", "c"})
  • {'a', 'b', 'c'}
  • {'a', 'b', 'c'} — কারণ set নিজে থেকে সাজায়
  • {'c', 'b', 'a'}
  • তিনটি অক্ষরই থাকবে, কিন্তু ক্রম নিশ্চিত নয় — চালানো থেকে চালানোয় বদলাতে পারে

৩ · ভিত্তি — সংখ্যা, অপারেটর, string আর f-string

এই ভাগের কোড ছোট, কিন্তু ফাঁদ গভীর। প্রতিটি এক্সপ্রেশনে তিনটি প্রশ্ন করুন: ফলের টাইপ কী (int, float, নাকি str)? and/or কি আসলে True/False ফেরত দিচ্ছে, নাকি একটি operand? আর তুলনাটি কি chained comparison? মনে রাখুন, Python 3-এ / সবসময় float দেয়, আর // ও % নিচের দিকে (ঋণাত্মক অসীমের দিকে) গোল করে।

(সহজ) চার রকম ভাগ — কী ছাপা হবে?

print(7 / 2, 7 // 2, -7 // 2, -7 % 3)
  • 3.5 3 -3 -1
  • 3.5 3 -4 2
  • 3 3 -4 2
  • 3.5 3.5 -3.5 2

(মাঝারি) round() কোন দিকে গোল করে?

print(round(2.5), round(3.5), round(2.675, 2))
  • 3 4 2.68
  • 2 4 2.67
  • 2 3 2.67
  • 3 4 2.67

(সহজ) 0.1 + 0.2 কি 0.3?

print(0.1 + 0.2 == 0.3, 0.1 + 0.2)
  • True 0.3
  • False 0.3
  • False 0.30000000000000004
  • True 0.30000000000000004

(মাঝারি) f-string-এর format spec — ঠিক কী দেখা যাবে?

x = 3.14159
n = 42
print(f"{x:.2f}|{n:>5}|{n:05}|{'ab':*^6}|")
  • 3.14|42 |00042|**ab**|
  • 3.14| 42|00042|**ab**|
  • 3.14159| 42|42000|ab****|
  • 3.14| 42| 42|**ab**|

(মাঝারি) Chained comparison — কী ছাপা হবে?

print(5 > 3 > 1, 3 > 2 == True, 1 < 3 > 2)
  • True True True
  • True False True
  • False False True
  • SyntaxError

(মাঝারি) and/or কী ফেরত দেয়?

print(0 or "x", repr("" and 5), [] or [0], 3 and 4)
  • True False True True
  • x 5 [0] 3
  • x '' [0] 4
  • x False [0] True

(কঠিন) is বনাম == — একটি .py ফাইল হিসেবে CPython 3.14-এ কী ছাপা হবে?

a = 1000
b = 1000
x = int("1000")
y = int("1000")
print(a == x, a is b, x is y)
  • True False False
  • True True False
  • True True True
  • False True False

নিজে চালিয়ে দেখুন — সংখ্যা, string আর অপারেটর

ওপরের কয়েকটি প্রশ্ন আর দুটি নতুন, একটি প্রোগ্রামে। চালানোর আগে প্রতিটি লাইনের ফল খাতায় লিখুন, তারপর মিলিয়ে দেখুন।

import math

print("1)", 7 / 2, 7 // 2, -7 // 2, -7 % 3)
print("2)", round(2.5), round(3.5), round(2.675, 2))
print("3)", 0.1 + 0.2, 0.1 + 0.2 == 0.3, math.isclose(0.1 + 0.2, 0.3))

s = "Bangladesh"
print("4)", s[-4:], s[::-1][:3], s[2:100])
print("5)", 0 or "x", [] or [0], 3 and 4, repr("" and 5))

x, y = int("300"), int("300")
print("6)", x == y, x is y, 3 > 2 == True, 5 > 3 > 1)
print(f"7) [{3.14159:.2f}] [{42:>5}] [{42:05}] [{'ab':*^6}]")
print("8)", "2" * 3, int("2") * 3, True + True)

১) 3.5 3 -4 2. ২) 2 4 2.67 — banker's rounding আর float-এর আসল মান। ৩) 0.30000000000000004 False True — math.isclose ছোট পার্থক্য মেনে নেয়। ৪) desh hse ngladesh — slicing সীমার বাইরে গেলেও error দেয় না, যতটুকু আছে ততটুকু দেয়; [::-1] উল্টো করে। ৫) x [0] 4 ''. ৬) True False False True — রানটাইমে তৈরি দুটি 300 আলাদা object. ৭) width আর alignment-এর হুবহু রূপ। ৮) 222 6 2 — str × int মানে পুনরাবৃত্তি, আর bool আসলে int-এর subclass (True = 1). একটি পরীক্ষা: print("5" + 5) যোগ করে চালান — TypeError: can only concatenate str (not "int") to str.

৪ · নিয়ন্ত্রণ আর ফাংশন — লুপ, default, scope আর recursion

এই ভাগে দুটি প্রশ্ন সবসময় মাথায় রাখুন। এক: জিনিসটি কখন মূল্যায়ন হয় — def লাইনে (default মান), নাকি ডাকার সময় (শরীর)? দুই: নামটি কোন scope-এর? ফাংশনের ভেতরে কোনো নামে assign থাকলেই নামটি পুরো ফাংশনে local — assign-এর লাইনের আগেও। আর লুপের else চলে তখনই, যখন লুপ break ছাড়া শেষ হয়।

(সহজ) for-এর পরে else — কী ছাপা হবে?

for n in [3, 5, 8, 9]:
    if n % 2 == 0:
        print("even", n)
        break
else:
    print("none")

for n in [3, 5]:
    if n % 2 == 0:
        break
else:
    print("none")
  • শুধু even 8
  • even 8, তারপর none (দুই লাইনে)
  • none, তারপর none
  • even 8, none, none

(মাঝারি) Mutable default, এক print-এ তিন ডাক — কী ছাপা হবে?

def add(x, box=[]):
    box.append(x)
    return box

print(add(1), add(2), add(3, []))
  • [1] [2] [3]
  • [1] [1, 2] [3]
  • [1, 2] [1, 2] [3]
  • [1] [1, 2] [1, 2, 3]

(মাঝারি) Default মান কখন ঠিক হয়?

n = 10
def f(x=n):
    return x
n = 20
print(f())
  • 10
  • 20
  • NameError
  • None

(মাঝারি) Keyword-only parameter — কী ঘটবে?

def f(a, b=2, *, c=3):
    return a + b * c

print(f(1), f(1, c=10))
print(f(1, 2, 3))
  • 7 21, তারপর 9
  • 7 21, তারপর TypeError
  • শুধু TypeError — কিছুই ছাপা হয় না
  • 7 33, তারপর TypeError

(মাঝারি) Recursion-এ ডাকের আগে আর পরে print

def show(n):
    if n == 0:
        return
    print(n, end=" ")
    show(n - 1)
    print(n, end=" ")

show(3)
  • 3 2 1
  • 3 2 1 1 2 3
  • 3 2 1 0 1 2 3
  • 1 2 3 3 2 1

(কঠিন) Global ভ্যারিয়েবল ফাংশনের ভেতরে বাড়ানো

count = 0
def inc():
    count += 1
inc()
print(count)
  • 1
  • 0
  • UnboundLocalError
  • NameError

(কঠিন) match-এ একটি constant-এর নাম — কী ছাপা হবে?

RED = "red"
color = "blue"
match color:
    case RED:
        print("matched", RED)
  • কিছুই ছাপা হয় না
  • matched red
  • matched blue
  • SyntaxError

নিজে চালিয়ে দেখুন — default, recursion আর closure

def add(x, box=[]):
    box.append(x)
    return box

print("1)", add(1), add(2), add(3, []))

def show(n):
    if n == 0:
        return
    print(n, end=" ")
    show(n - 1)
    print(n, end=" ")

print("2)", end=" ")
show(3)
print()

def counter():
    c = 0
    def inc():
        nonlocal c
        c += 1
        return c
    return inc

a, b = counter(), counter()
print("3)", a(), a(), b())
print("4)", list(range(5, 0, -2)), list(range(3, 3)), len(range(0, 10, 3)))
print("5)", print("inner"))

১) [1, 2] [1, 2] [3]. ২) 3 2 1 1 2 3. ৩) 1 2 1 — প্রতিটি counter() ডাক নিজের আলাদা c তৈরি করে, আর closure সেটি ধরে রাখে; nonlocal ছাড়া c += 1 আবার UnboundLocalError দিত। ৪) [5, 3, 1] [] 4 — range-এর শেষ মান বাদ যায়; শুরু আর শেষ সমান হলে খালি; 0, 3, 6, 9 মিলে চারটি। ৫) আগে inner, তারপর 5) None — ভেতরের print আগে চলে (argument মূল্যায়ন), আর print নিজে None ফেরত দেয়। return ছাড়া প্রতিটি ফাংশনই None ফেরত দেয়।

৫ · ডেটা স্ট্রাকচার — list, tuple, dict, set আর alias

মডিউল ৩-এর প্রায় সব আউটপুট প্রশ্নের চাবি একটি: কতগুলো object আছে, আর কোন নাম কোনটিকে দেখায়? b = a কপি করে না। a[:], list(a), a.copy() একটি shallow copy বানায় — বাইরের list নতুন, ভেতরের object পুরোনো। sort(), append(), remove()-এর মতো যে method object বদলায়, তারা None ফেরত দেয়।

(সহজ) Alias আর slice-কপি

a = [1, 2, 3]
b = a
b.append(4)
c = a[:]
c.append(5)
print(a, len(c))
  • [1, 2, 3] 4
  • [1, 2, 3, 4] 5
  • [1, 2, 3, 4, 5] 5
  • [1, 2, 3] 5

(মাঝারি) * 3 দিয়ে বানানো grid

grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)
  • [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
  • [[1, 0, 0], [0, 0, 0], [0, 0, 0]]
  • [[1, 1, 1], [0, 0, 0], [0, 0, 0]]
  • TypeError

(সহজ) sort() আর sorted()

nums = [3, 1, 2]
r = nums.sort()
print(r, nums, sorted("bca"))
  • None [1, 2, 3] ['a', 'b', 'c']
  • [1, 2, 3] [1, 2, 3] ['a', 'b', 'c']
  • None [3, 1, 2] abc
  • [1, 2, 3] [3, 1, 2] ['a', 'b', 'c']

(মাঝারি) একই key দুইবার

d = {"b": 1, "a": 2, "b": 3}
print(d, list(d))
  • {'b': 3, 'a': 2} ['b', 'a']
  • {'a': 2, 'b': 3} ['a', 'b']
  • {'b': 1, 'a': 2, 'b': 3} ['b', 'a', 'b']
  • SyntaxError

(মাঝারি) dict.get আর [] একই print-এ

d = {"x": 1}
print(d.get("y"), d.get("y", 0), d["y"])
  • None 0, তারপর KeyError
  • None 0 None
  • KeyError: 'y' — কিছুই ছাপা হয় না
  • None 0 KeyError

(কঠিন) set-এ 1, True আর 1.0

print({1, True, 1.0}, len({3, 1, 3, 2, 1}))
  • {1, True, 1.0} 5
  • {1} 3
  • {1, 1.0} 3
  • {True} 3

(কঠিন) tuple-এর ভেতরের list-এ +=

t = (1, [2])
try:
    t[1] += [3]
except TypeError:
    print("TypeError")
print(t)
  • শুধু (1, [2, 3])
  • TypeError, তারপর (1, [2, 3])
  • TypeError, তারপর (1, [2])
  • (1, [2], [3])

(মাঝারি) লুপ চলার সময় list থেকে মোছা

nums = [2, 2, 3]
for n in nums:
    if n == 2:
        nums.remove(n)
print(nums)
  • [2, 3]
  • [3]
  • [2, 2, 3]
  • RuntimeError

নিজে চালিয়ে দেখুন — alias, copy, comprehension আর unpacking

import copy

a = [1, 2, 3]
b = a
b.append(4)
c = a[:]
c.append(5)
print("1)", a, c)

grid = [[0] * 3] * 3
grid[0][0] = 1
safe = [[0] * 3 for _ in range(3)]
safe[0][0] = 1
print("2)", grid[2], safe[2])

nested = [[1], [2]]
shallow = list(nested)
deep = copy.deepcopy(nested)
nested[0].append(9)
print("3)", shallow, deep)

x = 10
sq = [x * x for x in range(3)]
print("4)", x, sq)

first, *mid, last = [1, 2, 3, 4, 5]
print("5)", first, mid, last)
nums = [2, 2, 3]
print("6)", [n for n in nums if n != 2], {1, True, 1.0})

১) [1, 2, 3, 4] [1, 2, 3, 4, 5]. ২) [1, 0, 0] [0, 0, 0] — comprehension প্রতি সারিতে নতুন list বানায়। ৩) [[1, 9], [2]] [[1], [2]] — shallow copy-র ভেতরের list পুরোনোটাই, তাই 9 দেখা যায়; deep copy-তে ভেতরের list-ও নতুন। ৪) 10 [0, 1, 4] — Python 3-এ comprehension-এর লুপ ভ্যারিয়েবল নিজের scope-এ থাকে, বাইরের x ছোঁয় না (Python 2-তে list comprehension বাইরের নাম বদলে দিত)। ৫) 1 [2, 3, 4] 5 — starred assignment-এ *mid সবসময় একটি list পায়, কিছু না থাকলে খালি list. ৬) [3] {1}.

৬ · বড় প্রোগ্রাম গোছানো — import, exception আর ফাইল

এই ভাগের প্রশ্নে সময়ের ক্রমই আসল। import একটি module-এর কোড প্রথমবার পুরোটা চালায়, তারপর sys.modules-এ রেখে দেয় — পরের import শুধু সেই object ফেরত দেয়। try-এর পরে except চলে exception হলে, else চলে না হলে, আর finally চলে সবসময় — এমনকি return-এর পথেও।

(মাঝারি) একই module তিনবার import — python main.py চালালে কী ছাপা হবে?

# helper.py
print("loading", __name__)
VALUE = 42

# main.py
import helper
import helper
from helper import VALUE
print(__name__, VALUE)
  • loading helper, তারপর __main__ 42
  • loading helper তিনবার, তারপর __main__ 42
  • loading __main__, তারপর main 42
  • loading helper দুইবার, তারপর __main__ 42

(সহজ) ওপরের helper.py ফাইলটি সরাসরি python helper.py দিয়ে চালালে কী ছাপা হবে?

# helper.py
print("loading", __name__)
VALUE = 42
  • loading helper
  • loading __main__
  • loading main
  • loading helper.py

(মাঝারি) try, except, else, finally আর return

def f(x):
    try:
        r = 10 // x
    except ZeroDivisionError:
        print("E", end=" ")
        return -1
    else:
        print("L", end=" ")
        return r
    finally:
        print("F", end=" ")

print(f(2))
print(f(0))
  • L F 5, তারপর E F -1
  • L 5 F, তারপর E -1 F
  • L F 5, তারপর E L F -1
  • L F 5, তারপর E -1

(কঠিন) try-তে return, finally-তেও return — CPython 3.14-এ কী ঘটবে?

def g():
    try:
        return "try"
    finally:
        return "finally"

print(g())
  • try
  • finally (সঙ্গে stderr-এ একটি SyntaxWarning)
  • try, তারপর finally
  • SyntaxError

(কঠিন) except-এর নামটি ব্লকের পরে

try:
    1 / 0
except ZeroDivisionError as e:
    pass
print(e)
  • division by zero
  • None
  • ZeroDivisionError('division by zero')
  • NameError

(মাঝারি) ফাইলের মোড "w" আর "a"

from pathlib import Path
p = Path("note.txt")
p.write_text("one\n")
with open(p, "w") as f:
    f.write("two\n")
with open(p, "a") as f:
    f.write("three\n")
print(p.read_text().splitlines())
  • ['one', 'two', 'three']
  • ['three']
  • ['two', 'three']
  • ['two\n', 'three\n']

ক্রমএকটি try-এর প্রথম লাইন চলে, দ্বিতীয় লাইনে ValueError ওঠে; সঙ্গে আছে except ValueError, else আর finally, তারপর ব্লকের বাইরে একটি print. ঘটনাগুলো কোন ক্রমে ঘটবে? (▲▼ চেপে সাজান)

  1. try-এর প্রথম লাইন চলে
  2. দ্বিতীয় লাইনে ValueError — try-এর বাকি লাইন বাদ পড়ে
  3. মিলে যাওয়া except ValueError ব্লক চলে
  4. finally ব্লক চলে (else বাদ, কারণ exception হয়েছে)
  5. পুরো try statement-এর পরের print চলে

নিজে চালিয়ে দেখুন — exception-এর পথ

def f(x):
    try:
        r = 10 // x
    except ZeroDivisionError:
        print("E", end=" ")
        return -1
    else:
        print("L", end=" ")
        return r
    finally:
        print("F", end=" ")

print(f(2))
print(f(0))

def h():
    for i in range(3):
        try:
            return i
        finally:
            print("cleanup", i)

print(h())

try:
    1 / 0
except ZeroDivisionError as e:
    saved = e
print(type(saved).__name__, "e" in dir())

আউটপুট: L F 5, E F -1, cleanup 0, 0, আর ZeroDivisionError False. h()-এ লুপের প্রথম ঘোরাতেই return 0, কিন্তু ফেরার আগে finally চলে — তাই আগে cleanup 0. শেষ লাইনে "e" in dir() মিথ্যা: except-এর নাম মুছে গেছে, কিন্তু saved দিয়ে exception object-টি রয়ে গেছে।

finally-তে return দেখতে কেমন, আর 3.14 কী বলে — নিচের কোডটি এই পাতায় চালানো যাবে না (stderr-এ warning আসে), তাই আউটপুট পাশে দেওয়া হলো:

def g():
    try:
        return "try"
    finally:
        return "finally"

print(g())
$ python3.14 finally_return.py
finally_return.py:5: SyntaxWarning: 'return' in a 'finally' block
  return "finally"
finally

সংস্করণ-নির্ভর: warning-টি আসে ফাইল compile হওয়ার সময়, তাই চালানোর আগেই। Python 3.13 বা তার আগে একই কোড কোনো warning ছাড়াই finally ছাপত। PEP 765 finally ব্লক থেকে বের হয়ে যাওয়া break ও continue-এর জন্যও একই warning যোগ করেছে। আর import করা module-এ warning দেখা যায় শুধু প্রথমবার compile-এর সময় — পরে cache করা .pyc থেকে চললে আর দেখায় না।

৭ · অবজেক্ট-ওরিয়েন্টেড Python — attribute, MRO, dunder আর generator

OOP-এর আউটপুট প্রশ্নে attribute খোঁজার নিয়মটি মুখস্থ রাখুন: obj.x পড়লে Python আগে object-এর নিজের __dict__-এ খোঁজে, না পেলে class-এ, তারপর MRO ধরে base class-গুলোতে। কিন্তু obj.x = … লিখলে সবসময় object-এর নিজের ঘরে লেখে। আর print(obj) ডাকে __str__, কিন্তু list-এর ভেতরে থাকলে __repr__.

(সহজ) Class attribute বনাম instance attribute

class Dog:
    sound = "woof"

a, b = Dog(), Dog()
a.sound = "bark"
Dog.sound = "grr"
print(a.sound, b.sound)
  • bark grr
  • bark woof
  • grr grr
  • bark bark

(মাঝারি) Class-এ রাখা list

class Team:
    members = []
    def add(self, m):
        self.members.append(m)

x, y = Team(), Team()
x.add("Sakib")
y.add("Tamim")
print(x.members)
  • ['Sakib', 'Tamim']
  • ['Sakib']
  • []
  • AttributeError

(কঠিন) Diamond inheritance আর super()

class A:
    def hi(self): print("A", end=" ")
class B(A):
    def hi(self): print("B", end=" "); super().hi()
class C(A):
    def hi(self): print("C", end=" "); super().hi()
class D(B, C):
    def hi(self): print("D", end=" "); super().hi()

D().hi()
  • D B C A
  • D B A C A
  • D B A
  • D C B A

(কঠিন) __eq__ আছে, __hash__ নেই — CPython 3.14-এ কী ঘটবে?

class P:
    def __init__(self, x): self.x = x
    def __eq__(self, o): return self.x == o.x

print(P(1) == P(1))
print({P(1)})
  • True, তারপর {<__main__.P object at …>}
  • True, তারপর TypeError
  • False, তারপর TypeError
  • শুধু TypeError

(মাঝারি) __str__ আর __repr__ — কোনটি কখন?

class Taka:
    def __init__(self, v): self.v = v
    def __str__(self): return f"৳{self.v}"
    def __repr__(self): return f"Taka({self.v})"

t = Taka(50)
print(t, [t], f"{t!r}")
  • ৳50 [Taka(50)] Taka(50)
  • ৳50 [৳50] Taka(50)
  • Taka(50) [Taka(50)] Taka(50)
  • ৳50 [৳50] ৳50

(মাঝারি) Setter ছাড়া property

class Temp:
    def __init__(self): self._c = 0
    @property
    def f(self): return self._c * 9 / 5 + 32

t = Temp()
t._c = 100
print(t.f)
t.f = 50
  • 212.0, তারপর AttributeError
  • শুধু 212.0 — শেষ লাইন নিঃশব্দে f বদলায়
  • 212, তারপর AttributeError
  • শুধু AttributeError

(সহজ) dataclass-এর repr আর ==

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int = 0

p, q = Point(1), Point(1, 0)
print(p, p == q, p is q)
  • Point(x=1, y=0) True False
  • <__main__.Point object at …> False False
  • Point(x=1, y=0) True True
  • Point(1, 0) True False

(মাঝারি) একই generator দুবার ব্যবহার

g = (n * n for n in range(4))
print(sum(g), sum(g), list(g))
  • 14 0 []
  • 14 14 [0, 1, 4, 9]
  • 14 0 [0, 1, 4, 9]
  • 14, তারপর StopIteration

(মাঝারি) দুটি decorator — কোনটি বাইরে?

def bold(f): return lambda: "<b>" + f() + "</b>"
def ital(f): return lambda: "<i>" + f() + "</i>"

@bold
@ital
def hi(): return "hi"

print(hi())
  • <i><b>hi</b></i>
  • <b>hi</b>
  • <b><i>hi</i></b>
  • <b>hi</b><i>hi</i>

নিজে চালিয়ে দেখুন — class attribute, MRO, repr আর generator

class Team:
    members = []
    def __init__(self, name):
        self.name = name
    def add(self, m):
        self.members.append(m)
x, y = Team("A"), Team("B")
x.add("Sakib")
y.add("Tamim")
print("1)", x.members, y.members is x.members)

class A:
    def hi(self): return "A"
class B(A):
    def hi(self): return "B" + super().hi()
class C(A):
    def hi(self): return "C" + super().hi()
class D(B, C):
    def hi(self): return "D" + super().hi()
print("2)", D().hi(), [k.__name__ for k in D.__mro__])

class Taka:
    def __init__(self, v): self.v = v
    def __str__(self): return f"৳{self.v}"
    def __repr__(self): return f"Taka({self.v})"
t = Taka(50)
print("3)", t, [t])

g = (n * n for n in range(4))
print("4)", sum(g), sum(g))

১) ['Sakib', 'Tamim'] True — দুই দলের একটিই list. ২) DBCA ['D', 'B', 'C', 'A', 'object'] — __mro__ দিয়ে MRO নিজেই দেখে নিন; প্রশ্নে সন্দেহ হলে এটাই সবচেয়ে দ্রুত যাচাই। ৩) ৳50 [Taka(50)]. ৪) 14 0 — generator-এর exhaustion. একটি পরীক্ষা: Team-এর __init__-এ self.members = [] যোগ করে চালান — ১ নম্বর লাইন কী হবে, আগে অনুমান করুন।

৮ · আধুনিক Python — with, closure, itertools, type hint আর concurrency

এই ভাগের ফাঁদগুলো সময় নিয়ে: with শেষে __exit__ চলে খোলার উল্টো ক্রমে; closure ভ্যারিয়েবলের মান নয়, ভ্যারিয়েবলটিকেই ধরে রাখে (late binding); groupby শুধু পাশাপাশি থাকা সমান মান জোড়ে। আর concurrency-র প্রশ্নে আগে জিজ্ঞেস করুন: কোন অংশের ক্রম আসলে নিশ্চিত?

(মাঝারি) এক with-এ দুটি context manager

class CM:
    def __init__(self, n): self.n = n
    def __enter__(self):
        print("enter", self.n)
        return self
    def __exit__(self, *exc):
        print("exit", self.n)
        return False

with CM(1), CM(2):
    print("body")
  • enter 1 → enter 2 → body → exit 2 → exit 1
  • enter 1 → enter 2 → body → exit 1 → exit 2
  • enter 1 → exit 1 → enter 2 → body → exit 2
  • enter 2 → enter 1 → body → exit 1 → exit 2

(কঠিন) __exit__ True ফেরত দিলে

class Quiet:
    def __enter__(self): return self
    def __exit__(self, et, ev, tb):
        print("exit:", et.__name__)
        return True

with Quiet():
    1 / 0
    print("after")
print("next")
  • exit: ZeroDivisionError, after, next
  • exit: ZeroDivisionError, তারপর next
  • exit: ZeroDivisionError, তারপর traceback
  • শুধু next

(মাঝারি) লুপে বানানো lambda

fs = [lambda: i for i in range(3)]
print([f() for f in fs])
  • [2, 2, 2]
  • [0, 1, 2]
  • [3, 3, 3]
  • NameError

(মাঝারি) না সাজিয়ে groupby

from itertools import groupby
print([(k, len(list(g))) for k, g in groupby("aabba")])
  • [('a', 2), ('b', 2), ('a', 1)]
  • [('a', 3), ('b', 2)]
  • [('a', 2), ('b', 2)]
  • TypeError

(সহজ) Type hint কি চলার সময় যাচাই হয়?

def double(x: int) -> int:
    return x * 2

print(double("ha"), double([0]))
  • haha [0, 0]
  • TypeError
  • None None
  • mypy-র error বার্তা ছাপা হয়

(কঠিন) asyncio.gather — কোন ক্রমে?

import asyncio

async def job(name, delay):
    await asyncio.sleep(delay)
    print("done", name)
    return name

async def main():
    r = await asyncio.gather(job("A", 0.2), job("B", 0.1))
    print(r)

asyncio.run(main())
  • done B → done A → ['A', 'B']
  • done A → done B → ['A', 'B']
  • done B → done A → ['B', 'A']
  • done B → done A → None

(মাঝারি) Thread আর join() — কোন আউটপুট নিশ্চিত?

import threading

results = []
def work(n):
    results.append(n * n)

ts = [threading.Thread(target=work, args=(i,)) for i in range(4)]
for t in ts: t.start()
for t in ts: t.join()
print(sorted(results), len(results))
  • [0, 1, 4, 9] 4
  • ক্রম অনিশ্চিত, তাই আউটপুট বলা যায় না
  • [] 0
  • কখনো কখনো তিনটি মান — race condition

নিজে চালিয়ে দেখুন — with, closure, groupby আর thread

import threading
from itertools import groupby

class CM:
    def __init__(self, n): self.n = n
    def __enter__(self):
        print("enter", self.n)
        return self
    def __exit__(self, *exc):
        print("exit", self.n)
        return False

with CM(1), CM(2):
    print("body")

late = [lambda: i for i in range(3)]
fixed = [lambda i=i: i for i in range(3)]
print([f() for f in late], [f() for f in fixed])
print([(k, len(list(g))) for k, g in groupby("aabba")])
print([(k, len(list(g))) for k, g in groupby(sorted("aabba"))])

results = []
def work(n):
    results.append(n * n)

ts = [threading.Thread(target=work, args=(i,)) for i in range(4)]
for t in ts: t.start()
for t in ts: t.join()
print(sorted(results))

আউটপুট: enter 1, enter 2, body, exit 2, exit 1 (পাঁচ লাইন); তারপর [2, 2, 2] [0, 1, 2] — default argument দিয়ে late binding-এর সমাধান; [('a', 2), ('b', 2), ('a', 1)] আর সাজানোর পরে [('a', 3), ('b', 2)]; শেষে [0, 1, 4, 9]. thread থেকে সরাসরি print করা হয়নি — করলে লাইনগুলোর ক্রম আর নিশ্চিত থাকত না।

৯ · বাস্তব প্রয়োগ — Pythonic idiom, regex আর বাংলা লেখা

মডিউল ৭-এর কোডে ফাঁদ কম, কিন্তু খালি ইনপুট আর Unicode-এ চমক থাকে। মনে রাখুন: খালি iterable-এ any মিথ্যা, all সত্য; dict-এর ওপর max key দেখে, মান নয়; আর Python 3-এর str হলো Unicode code point-এর ক্রম — একটি বাংলা “অক্ষর” চোখে একটি হলেও একাধিক code point হতে পারে।

(সহজ) খালি list-এ any, all আর sum

print(any([]), all([]), sum([], 10))
  • False True 10
  • False False 0
  • True True 10
  • None None 10

(মাঝারি) dict-এর ওপর max

d = {"x": 9, "y": 2}
print(max(d), max(d, key=d.get), sorted(d, key=d.get))
  • y x ['y', 'x']
  • x x ['y', 'x']
  • 9 9 [2, 9]
  • y y ['x', 'y']

(মাঝারি) \d কি বাংলা অঙ্ক চেনে?

import re
t = "মূল্য ১২০ টাকা, 30 tk"
print(re.findall(r"\d+", t), re.findall(r"\d+", t, re.ASCII))
  • ['১২০', '30'] ['30']
  • ['30'] ['30']
  • ['১', '২', '০', '30'] ['30']
  • ['30'] []

(মাঝারি) বাংলা string-এর দৈর্ঘ্য

s = "বাংলা"
print(len(s), len("ক্ষ"), s[0])
  • 5 3 ব
  • 3 1 ব
  • 5 1 বা
  • 15 9 ব

(কঠিন) বাংলা অঙ্কের string থেকে int

print(int("১২৩") + 1, "১২৩".isdigit())
  • 124 True
  • ValueError
  • ১২৩1 True
  • TypeError

নিজে চালিয়ে দেখুন — বাংলা লেখা, regex আর idiom

import re
from collections import Counter

text = "মূল্য ১২০ টাকা, 30 tk"
print("1)", re.findall(r"\d+", text))
print("2)", re.findall(r"\d+", text, re.ASCII))
print("3)", int("১২০") + 1)

word = "বাংলা"
print("4)", len(word), [hex(ord(ch)) for ch in word])
print("5)", len("ক্ষ"))
print("6)", Counter("banana").most_common(2))

d = {"x": 9, "y": 2}
print("7)", max(d), max(d, key=d.get))
print("8)", any([]), all([]), sum([], 10))

১) ['১২০', '30']. ২) ['30']. ৩) 121. ৪) 5 ['0x9ac', '0x9be', '0x982', '0x9b2', '0x9be'] — পাঁচটি code point আলাদা করে দেখা যাচ্ছে: ব, া, ং, ল, া. ৫) 3. ৬) [('a', 3), ('n', 2)] — Counter গুনে সবচেয়ে বেশি থেকে সাজায়। ৭) y x. ৮) False True 10.

১০ · AI-এর যুগে — interpreter-ই শেষ বিচারক

আজ অনেকে AI-কে কোড দেখিয়ে জিজ্ঞেস করেন “এটা কী ছাপবে?” সাধারণ কোডে উত্তর প্রায়ই ঠিক আসে। কিন্তু এই পাতায় যে জায়গাগুলো অনুশীলন করলেন — mutable default, closure-এর late binding, is আর int cache, generator-এর exhaustion, MRO — সেখানে ভাষা-মডেল প্রায়ই আত্মবিশ্বাসী সুরে ভুল বলে। কারণ সে কোড চালায় না; অনুরূপ লেখা দেখে সম্ভাব্য উত্তর অনুমান করে। আর ইন্টারনেটের পুরোনো লেখায় Python 2-এর নিয়ম (7 / 2 হলো 3, print একটি statement, comprehension-এর নাম বাইরে ফাঁস হওয়া) এখনো প্রচুর, তাই AI মাঝে মাঝে সেগুলো মিশিয়ে ফেলে। নতুন সংস্করণের বদলও — যেমন 3.14-এর finally-return warning বা নতুন error বার্তা — সে প্রায়ই জানে না।

AI-এর উত্তর একটি অনুমান, interpreter-এর আউটপুট একটি পর্যবেক্ষণ।
আর “ক্রম নিশ্চিত নয়” ধরনের কোডে একবার চালানোও প্রমাণ নয় — প্রমাণ হলো ভাষার নিয়ম।

AI সাধারণত যেখানে Python-এর আউটপুট ভুল বলে

যে ধরনের কোডAI-এর সাধারণ ভুলকীভাবে ধরবেন
mutable default, এক print-এ কয়েকটি ডাকপ্রতিটি ডাকের “তখনকার” মান আলাদা করে লেখেনিজে চালান; f.__defaults__ ছাপিয়ে দেখুন
লুপে lambda বা nested functionপ্রতিটি closure-কে তৈরির সময়ের মান দেয়চালান; সমাধান হিসেবে i=i বা functools.partial চেয়ে নিন
is দিয়ে int বা string তুলনা“-5 থেকে 256” নিয়ম ভুল জায়গায় খাটায়, অথবা একটি নির্দিষ্ট ফলকে নিশ্চিত বলেপ্রশ্ন করুন: এটা কি ভাষার নিয়ম, নাকি CPython-এর implementation detail?
generator, map, zip দুবার ব্যবহারদ্বিতীয়বারও একই মান দেয়চালান; iterator একবারের জিনিস — দরকারে list বানান
set-এর string, thread-এর printএকটি নির্দিষ্ট ক্রম লিখে দেয়কয়েকবার চালান; নিয়মে নিশ্চয়তা আছে কি না দেখুন
exception-এর উত্তরভুল exception নাম, বা আগের লাইনের আউটপুট বাদ দেয়traceback-এর শেষ লাইন আর stdout আলাদা করে মেলান

AI দিয়ে নতুন অনুশীলনের প্রশ্ন বানাতে পারেন — শর্ত একটাই, উত্তর যাচাইয়ের দায়িত্ব আপনার। নিচের prompt-টি AI-কে এমনভাবে বাঁধে, যাতে সে আগে যুক্তি দেখায় আর “ক্রম নিশ্চিত নয়” ধরনের উত্তর লুকাতে না পারে:

আমি Python 3.14-এর “আউটপুট কী হবে?” ধরনের প্রশ্নের অনুশীলন করছি। বিষয়: [যেমন: mutable default, closure আর generator]. ধরে নাও CPython 3.14, কোড একটি .py ফাইল হিসেবে চালানো হবে। ১. ৮টি ছোট সম্পূর্ণ Python প্রোগ্রাম দাও (প্রতিটি ১৫ লাইনের মধ্যে, শুধু standard library). কঠিনতা মেশাও: ৩টি সহজ, ৩টি মাঝারি, ২টি কঠিন। ২. অন্তত ২টির উত্তর একটি exception রাখো (কোন exception আর কোন লাইনে, আর তার আগে কী ছাপা হয় — তা-ও বলবে), আর ১টির উত্তর "ক্রম নিশ্চিত নয়"। ৩. প্রতিটির জন্য ৪টি অপশন দাও, কিন্তু উত্তর এখন বলো না। ৪. শেষে আলাদা অংশে প্রতিটির উত্তর দাও: কোন নাম কোন object-কে দেখায়, কোন জিনিস কখন মূল্যায়ন হয়, আর Python-এর কোন নিয়ম কাজ করছে। ৫. কোনো আচরণ Python সংস্করণভেদে বা CPython-এর implementation detail হলে তা স্পষ্ট লেখো। নিশ্চিত না হলে "নিশ্চিত নই" বলো। আমি প্রতিটি প্রোগ্রাম python3.14 দিয়ে চালিয়ে তোমার উত্তর মিলিয়ে দেখব।

AI-এর দেওয়া প্রশ্ন বা উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট

যে ফাঁদে সবাই পড়ে

Python-এর “আউটপুট কী হবে?” প্রশ্নে বারবার ফিরে আসা পাঁচটি গভীর ফাঁদ। প্রতিটি নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Default argument একবারই তৈরি হয় কেন — Python কি এটা ভুল করে বানিয়েছে?

না, এটি ইচ্ছাকৃত নকশার একটি ফল। Python-এ def একটি সাধারণ statement, যা চলার সময় একটি function object তৈরি করে। default মানগুলো সেই মুহূর্তে একবার মূল্যায়ন হয়ে function object-এর __defaults__ tuple-এ জমা থাকে। প্রতিটি ডাকে নতুন করে মূল্যায়ন হয় না। int বা str-এর মতো immutable default-এ এতে কোনো সমস্যা নেই, কারণ তাদের বদলানো যায় না। কিন্তু list, dict বা set default হলে, একটি ডাক সেটি বদলালে পরের সব ডাক সেই বদলানো object পায়।

  • অফিসিয়াল স্বীকৃতি: Python-এর নিজের FAQ-তে প্রশ্নটি আছে — “Why are default values shared between objects?” — আর টিউটোরিয়ালে আলাদা করে সতর্কবাণী দেওয়া আছে। অর্থাৎ ভাষার নকশাকারীরা জানেন এটি নতুনদের অবাক করে।
  • বিকল্প প্রস্তাব: ২০২১ সালে PEP 671 (Chris Angelico) def f(x=>[]) ধরনের “late-bound default” প্রস্তাব করে, যা প্রতিটি ডাকে মূল্যায়ন হতো। প্রস্তাবটি এখনো গৃহীত হয়নি; তাই আজও প্রচলিত সমাধান None sentinel.
  • টুলের সাহায্য: Pylint-এর W0102 (dangerous-default-value) আর flake8-bugbear-এর B006 (ruff-এও আছে) mutable default দেখলেই সতর্ক করে।

মজার ব্যাপার, এই আচরণকে কাজেও লাগানো হয়: def fib(n, _memo={}) লিখে একটি সহজ cache বানানো যায়, যা ডাকের পর ডাকে টিকে থাকে। তবে আজ এ কাজে functools.cache অনেক পরিষ্কার। আউটপুট প্রশ্নে পড়ার নিয়ম: ফাংশনের প্রথম লাইনে mutable default দেখলেই খাতায় একটি আলাদা object (L1) আঁকুন, যার জীবন ফাংশনের সমান লম্বা — আর মনে রাখুন print(f(1), f(2))-এ দুটি argument একই object হলে দুটিই শেষ অবস্থা দেখাবে।

মূল উপলব্ধি: default মান “ফাংশন তৈরির সময়ের” জিনিস, “ডাকের সময়ের” নয়। immutable হলে নিরাপদ; mutable হলে None দিয়ে শুরু করে ভেতরে নতুন object বানান।

প্র ০২লুপের closure সবাই শেষ মানটাই কেন মনে রাখে — আর এর তিনটি সমাধানের পার্থক্য কী?

closure কোনো মান কপি করে না; সে বাইরের scope-এর ভ্যারিয়েবলটিকে ধরে রাখে। CPython-এ এটি হয় একটি “cell” object দিয়ে, যা ফাংশনের __closure__-এ দেখা যায়। লুপ যতবার ঘোরে, একই ভ্যারিয়েবল i নতুন মান পায়; সব lambda একই cell দেখে। ডাকার সময় তারা cell-এর তখনকার মান পড়ে — লুপ শেষ, তাই শেষ মান। একে বলে late binding, আর Python-এর FAQ-তে এর জন্যও আলাদা প্রশ্ন আছে: “Why do lambdas defined in a loop with different values all return the same result?”

  • lambda i=i: i: default মান তৈরি হয় lambda তৈরির মুহূর্তে (প্র ০১-এর নিয়ম), তাই প্রতিটি lambda নিজের মান পায়। অসুবিধা: কেউ ভুল করে argument দিলে মান বদলে যায়।
  • functools.partial(f, i): একটি নতুন callable, যার argument তৈরির সময়েই বাঁধা। পরিষ্কার, আর argument লুকানো থাকে না।
  • factory ফাংশন: def make(i): return lambda: i — প্রতিটি ডাক নতুন scope আর নতুন cell তৈরি করে।

এই সমস্যা শুধু Python-এর নয়। JavaScript-এ var দিয়ে লুপে closure বানালে হুবহু একই বাগ হতো; ২০১৫ সালের ES2015 let এনে প্রতিটি ঘোরায় নতুন binding দিয়ে সমস্যাটি মিটিয়েছে। Python সেই পথে যায়নি — লুপ এখানে নতুন scope তৈরি করে না। তবে comprehension নিজের scope পায় (Python 3 থেকে); Python 3.12-এর PEP 709 comprehension-কে ভেতরে inline করলেও এই আচরণ বদলায়নি, তাই [lambda: i for i in range(3)]-এর ফল এখনো [2, 2, 2].

মূল উপলব্ধি: closure “নাম” ধরে, “মান” নয়। মান আটকাতে চাইলে তৈরির মুহূর্তে মূল্যায়ন হয় এমন কিছু ব্যবহার করুন — default argument, partial বা নতুন ফাংশন-ডাক।

প্র ০৩is আর ==-এর পার্থক্য কী, আর ছোট সংখ্যায় is কেন “কাজ করে”?

== জিজ্ঞেস করে “মান কি সমান?” — এটি __eq__ ডাকে, যা প্রতিটি class নিজের মতো লিখতে পারে। is জিজ্ঞেস করে “এরা কি একই object?” — এটি কোনো method ডাকে না, শুধু পরিচয় (identity) মেলায়; CPython-এ কার্যত মেমরি ঠিকানা তুলনা। দুটি আলাদা list-এ একই সংখ্যা থাকলে == সত্য, is মিথ্যা।

  • Small int cache: CPython চালু হওয়ার সময়ই -5 থেকে 256 পর্যন্ত int object আগে থেকে বানিয়ে রাখে, আর এই মানগুলো যেখানেই লাগুক, সেই একই object দেয়। তাই int("100") is int("100") সত্য, কিন্তু 300-এ মিথ্যা।
  • Constant sharing: একই ফাইলের একই code object-এ একই literal দুবার থাকলে compiler একটিই constant রাখে — তাই স্ক্রিপ্টে a = 1000; b = 1000-এ a is b সত্য হতে পারে, অথচ REPL-এ আলাদা লাইনে লিখলে মিথ্যা।
  • String interning: identifier-এর মতো দেখতে ছোট string অনেক সময় একই object হয়ে যায়; sys.intern() দিয়ে জোর করা যায়।

এগুলোর কোনোটিই ভাষার নিয়ম নয় — সবই CPython-এর implementation detail, যা PyPy বা ভবিষ্যৎ সংস্করণে বদলাতে পারে। এই বিপদের কারণে Python 3.8 থেকে x is 5-এর মতো literal-এর সাথে is লিখলে compiler SyntaxWarning দেয়: "is" with 'int' literal. Did you mean "=="? তাহলে is কোথায় ঠিক? যেখানে Python একটিমাত্র object-এর নিশ্চয়তা দেয় — None, True, False, Ellipsis আর নিজের বানানো sentinel. PEP 8 এই কারণে বলে: if x is None, কখনো if x == None নয় — কারণ একটি class চাইলে __eq__ দিয়ে == None-কে সত্য বানাতে পারে।

মূল উপলব্ধি: মান তুলনায় সবসময় ==; পরিচয় তুলনায় is, শুধু singleton-এ। আউটপুট প্রশ্নে বড় int বা string-এ is দেখলে উত্তরে লিখুন: “CPython-এ এই ফল, কিন্তু নিশ্চয়তা নেই।”

প্র ০৪float-এ 0.1 + 0.2 ≠ 0.3 কেন, আর Python এত লম্বা সংখ্যা ছাপে কেন?

Python-এর float হলো IEEE 754 standard-এর ৬৪-বিট double — প্রায় সব আধুনিক CPU এই ফরম্যাটে হিসাব করে। এতে সংখ্যা রাখা হয় binary ভগ্নাংশে। যেমন দশমিকে ১/৩ শেষ হয় না (0.333…), তেমনি binary-তে ১/১০ শেষ হয় না। তাই 0.1 আসলে রাখা হয় তার সবচেয়ে কাছের binary মান হিসেবে, যা 0.1-এর চেয়ে সামান্য বড়। 0.2-ও তাই। যোগ করলে ছোট ছোট ভুল জমে, আর ফল এমন একটি float হয় যা “0.3 লিখলে যে float পাওয়া যায়” তার পরেরটি।

  • লম্বা repr: Python 3.1 থেকে (আর 2.7-এ) float-এর repr হলো এমন সবচেয়ে ছোট দশমিক লেখা, যা পড়ে আবার হুবহু একই float পাওয়া যায় (David Gay-এর অ্যালগরিদমের ওপর ভিত্তি করে)। 0.1-এর জন্য সেটি “0.1”, কিন্তু যোগফলের জন্য “0.30000000000000004” — কারণ এর চেয়ে ছোট লেখা অন্য float-কে বোঝাত।
  • round-এর চমক: round(2.675, 2) হলো 2.67, কারণ 2.675-এর আসল মান 2.67499999…; Python-এর ডকুমেন্টেশন নিজেই এই উদাহরণ দিয়ে বলে এটি বাগ নয়। আর ঠিক মাঝখানের মানে Python 3 banker's rounding করে (round(2.5) → 2), যাতে অনেক সংখ্যা যোগ করলে ওপরের দিকে পক্ষপাত না জমে; Python 2 মাঝখানের মান শূন্য থেকে দূরে নিত।
  • কী করবেন: তুলনায় math.isclose() (Python 3.5, PEP 485); টাকা-পয়সায় decimal.Decimal("0.1") বা পয়সায় int; ভগ্নাংশে fractions.Fraction.

পরীক্ষার উত্তরে শুধু “False” লিখলে আংশিক নম্বর; সাথে লিখুন “0.1 binary-তে ঠিকঠাক রাখা যায় না, IEEE 754 double-এ যোগফল 0.30000000000000004”. এটি C, Java, JavaScript — সব ভাষায় একই, কারণ সমস্যাটি ভাষার নয়, ফরম্যাটের।

মূল উপলব্ধি: float একটি কাছাকাছি মান, সঠিক মান নয়। float-এ == প্রায় সবসময় ভুল প্রশ্ন; “কতটা কাছে?” জিজ্ঞেস করুন।

প্র ০৫Generator একবার ব্যবহারেই ফুরিয়ে যায় কেন, আর কোন কোন জিনিস এভাবে আচরণ করে?

generator একটি iterator — এক দিকে চলা একটি কার্সর, যে প্রতিবার next()-এ একটি মান তৈরি করে দেয় আর নিজের অবস্থান মনে রাখে। শেষে পৌঁছালে StopIteration তোলে, আর তারপর চিরকাল তাই করবে; পেছনে ফেরার বা শুরু থেকে আবার চালানোর কোনো উপায় নেই। sum, list, for — সবাই ভেতরে next() ডাকে আর StopIteration চুপচাপ সামলায়, তাই দ্বিতীয়বার কোনো error আসে না, শুধু খালি ফল।

  • ইতিহাস: generator এসেছে Python 2.2-এ (PEP 255, ২০০১), generator expression 2.4-এ (PEP 289). Python 3.0-এ map, filter, zip list-এর বদলে iterator ফেরত দিতে শুরু করে — মেমরি বাঁচাতে। তাই Python 2-এর অনেক পুরোনো কোড, যা m = map(...)-কে দুবার ব্যবহার করত, Python 3-এ চুপচাপ ভুল ফল দেয়।
  • একবারের জিনিস: generator, map, filter, zip, enumerate, reversed, খোলা ফাইল, iter(x). বারবার চলে: list, tuple, str, dict, set, range — এরা iterable; প্রতিটি for লুপে নতুন iterator বানায়।
  • লুকানো খরচ: 3 in g লিখলে generator 3 পর্যন্ত মান খেয়ে ফেলে; পরের লুপ শুরু হয় তার পর থেকে।

নিজে পরীক্ষা করার সহজ উপায়: iter(x) is x সত্য হলে x নিজেই একটি iterator — অর্থাৎ একবারের। একাধিকবার দরকার হলে list() বানিয়ে রাখুন, বা ফাংশনে মোড়ান যাতে প্রতিবার নতুন generator তৈরি হয়; itertools.tee একটি বিকল্প, তবে সেটিও ভেতরে মান জমিয়ে রাখে।

মূল উপলব্ধি: iterable হলো বই, iterator হলো বুকমার্ক। বুকমার্ক শেষ পাতায় পৌঁছালে আবার পড়তে নতুন বুকমার্ক লাগে — আর generator নিজেই একটি বুকমার্ক, বই নয়।

শব্দকোষ

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

মনে রাখুন

  1. আউটপুট অনুমান নয় — trace table এঁকে লাইন ধরে চালান: নাম আর object আলাদা কলামে, তীর দিয়ে যুক্ত; call stack; আর কোন জিনিস কখন মূল্যায়ন হয়।
  2. = নাম বাঁধে, কপি করে না। alias দিয়ে বদল সব নামে দেখা যায়; [:] shallow copy; [[0]*3]*3 একই সারি তিনবার; বদলানো method None দেয়।
  3. default মান আর decorator চলে def-এর সময়, একবার; closure নাম ধরে (late binding); generator একবারের; finally সব পথে চলে।
  4. উত্তর পাঁচ রকম: নির্দিষ্ট আউটপুট, নির্দিষ্ট exception (আগের আউটপুটসহ), SyntaxError (কিছুই চলে না), ক্রম নিশ্চিত নয়, সংস্করণ বা implementation-নির্ভর।
  5. AI-এর আউটপুট-অনুমান যাচাই করুন: নিজে trace করুন, python3.14 দিয়ে চালান, stdout আর stderr আলাদা দেখুন, implementation detail-কে নিশ্চিত বলবেন না।

পরের পর্বে: কোড পড়ার পরে এবার দ্রুত ধারণার যাচাই — পুরো Python কোর্স থেকে বিষয়ভিত্তিক MCQ প্রশ্নব্যাংক, সময় ধরে অনুশীলনের কৌশলসহ।

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

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