এই পর্বে যা শিখবেন
- comprehension-কে “লুপ + ছাঁকনি + রূপান্তর” হিসেবে পড়া, আর যেকোনো comprehension-কে তার সমান for লুপে খুলে লেখা
- শেষের ছাঁকনি-
ifআর শুরুরif-else-এর পার্থক্য — কোনটি কতগুলো জিনিস রাখে - nested comprehension-এ for-এর ক্রম, 2D list চ্যাপ্টা করা আর নামতার ছক
- dict, set comprehension আর অলস generator expression — কখন কোনটি
- scope, walrus, গতির সত্যিকারের ছবি, আর কখন comprehension না লিখে লুপ লিখবেন
১ · comprehension কী — একটি বাক্যে নতুন সংগ্রহ
আগের কয়েকটি পর্বে আপনি list, tuple, dict আর set চিনেছেন। বাস্তব কাজে এদের নিয়ে সবচেয়ে বেশি যা করা হয়, তা হলো একটি সংগ্রহ থেকে আরেকটি নতুন সংগ্রহ বানানো: দামের তালিকা থেকে ভ্যাটসহ দাম, সব শিক্ষার্থী থেকে শুধু পাস করা শিক্ষার্থী, নামের তালিকা থেকে নামের প্রথম অক্ষরগুলো।
পর্ব ১২-এর লুপ দিয়ে এটা আপনি এখনই পারেন। খালি list খুলুন, লুপ চালান, শর্ত মিললে append করুন:
nums = [3, 4, 5, 6]
evens_sq = []
for n in nums:
if n % 2 == 0:
evens_sq.append(n * n)
print(evens_sq) # [16, 36]চার লাইন কোড, কিন্তু উদ্দেশ্য একটাই — “জোড় সংখ্যাগুলোর বর্গের একটি list”. পাঠককে চারটি লাইন পড়ে, খালি list, লুপ, শর্ত আর append জোড়া দিয়ে সেই উদ্দেশ্যটি আবার বানাতে হয়। Comprehension উদ্দেশ্যটিকে সরাসরি লিখতে দেয়:
evens_sq = [n * n for n in nums if n % 2 == 0]বাংলায় পড়ুন বাঁ থেকে: “n-এর বর্গ, nums-এর প্রতিটি n-এর জন্য, যদি n জোড় হয়”. এটাই একটি list comprehension. এখানে তিনটি অংশ আছে, যা প্রায় সব comprehension-এ ফিরে আসে:
🎯 রূপান্তর — কী চাই
প্রতিটি জিনিস থেকে নতুন list-এ কী যাবে। এখানে n * n. জিনিসটি যেমন আছে তেমন রাখতে চাইলে শুধু n.
🔁 উৎস — কোথা থেকে
একটি সাধারণ for, যেকোনো iterable-এর ওপর — list, string, range, dict-এর items.
for n in nums🧹 ছাঁকনি — কোনগুলো (ঐচ্ছিক)
যে জিনিস শর্ত মানে না, সে নতুন list-এ ঢোকেই না। না লিখলে সব জিনিস যায়।
if n % 2 == 0গার্মেন্টস কারখানার কোয়ালিটি-চেক লাইনের কথা ভাবুন। কনভেয়র বেল্টে একটার পর একটা শার্ট আসছে (উৎস)। একজন চেকার দেখছেন — সেলাই ঠিক আছে কি? না থাকলে শার্টটি সরিয়ে দেন (ছাঁকনি)। যেগুলো পাস করে, সেগুলোয় ট্যাগ লাগিয়ে ভাঁজ করা হয় (রূপান্তর), তারপর নতুন বাক্সে রাখা হয় (নতুন list). ম্যানেজার এই পুরো লাইনকে এক বাক্যে বর্ণনা করেন: “পাস করা প্রতিটি শার্ট, ট্যাগসহ ভাঁজ করে।” Comprehension হলো সেই এক বাক্য।
গণিতের বইয়ে এই লেখার ধরন আপনি আগেই দেখেছেন। সেট গঠন পদ্ধতিতে লেখা হয় { x² | x ∈ N, x জোড় } — “x-এর বর্গ, যেখানে x স্বাভাবিক সংখ্যা এবং জোড়”. একে বলে set-builder notation. Python-এর comprehension প্রায় হুবহু এই গণিতের বাক্য: {x * x for x in range(1, 11) if x % 2 == 0}. শুধু “|” আর “∈”-এর জায়গায় ইংরেজি শব্দ for আর in.
লুপ বলে কীভাবে বানাতে হবে (খালি list নাও, ঘোরো, যোগ করো)। Comprehension বলে কী চাই (এই শর্তের জিনিসগুলোর এই রূপ)। “কীভাবে”-র ঝামেলা Python সামলায়। আর comprehension একটি expression — অর্থাৎ এর একটি মান আছে, তাই একে একটি নামে রাখা, ফাংশনে পাঠানো বা return করা যায়, যা একটি for statement দিয়ে সরাসরি করা যায় না।
২ · list comprehension — লুপের পাশাপাশি রেখে পড়া
Comprehension নতুন কোনো জাদু নয়। প্রতিটি comprehension-কে একটি নির্দিষ্ট নিয়মে সাধারণ for লুপে খুলে লেখা যায়, আর Python ভেতরে মোটামুটি সেটাই করে। নিয়মটা এই: for আর if অংশগুলো একই ক্রমে ওপর থেকে নিচে বসান, আর রূপান্তরটি সবচেয়ে ভেতরে append-এর মধ্যে রাখুন। নিচের ধাপগুলোতে দেখুন, ছোট একটি comprehension আসলে কীভাবে চলে:
1nums = [3, 4, 5, 6]2# evens_sq = [n * n for n in nums if n % 2 == 0]3result = [] # Python-এর গোপন খালি list4for n in nums: # উৎস5 if n % 2 == 0: # ছাঁকনি6 result.append(n * n) # রূপান্তর7evens_sq = result
| n | |
| n % 2 == 0? | |
| result | |
| evens_sq |
- প্রস্তুতি। Comprehension শুরু হতেই Python একটি নতুন খালি list তৈরি করে। এর কোনো নাম নেই — আপনি একে দেখতে পান না। লুপের কোডে যে
evens_sq = []নিজে লিখতে হতো, সেটা এখানে আপনাআপনি হয়। - প্রথম জিনিস: 3. উৎস অংশ
for n in numsপ্রথম জিনিসটি দিল। ছাঁকনি জিজ্ঞেস করল “3 কি জোড়?” — না। তাই রূপান্তর অংশn * nচলেই না; 9 কখনো হিসাব হয় না। - দ্বিতীয় জিনিস: 4. ছাঁকনি পার হলো, এবার রূপান্তর চলল: 16. সেটা গোপন list-এ যোগ হলো। লক্ষ করুন — কোডে
n * nলেখা আছে সবার আগে, কিন্তু চলে সবার শেষে, শুধু ছাঁকনি পার হলে। - তৃতীয় জিনিস: 5. বিজোড়, তাই আটকে গেল। list বদলাল না।
- চতুর্থ জিনিস: 6. জোড় — 36 যোগ হলো। nums-এ যতগুলো জিনিস, উৎস অংশ ঠিক ততবার ঘুরল; কতগুলো list-এ ঢুকল, তা ঠিক করল ছাঁকনি।
- শেষ — নামের সাথে বাঁধা। জিনিস ফুরাতেই পুরো list তৈরি, আর তখনই সেটা
evens_sqনামের সাথে বাঁধা হলো। সাধারণ লুপের সাথে একটি পার্থক্য: comprehension শেষ হলেnনামটি বাইরে থাকে না (অংশ ৭)।
রূপান্তর আর ছাঁকনি — দুটিই ঐচ্ছিক। তাই comprehension চার রকম চেহারায় দেখা যায়:
| কাজ | Comprehension | নতুন list-এর দৈর্ঘ্য |
|---|---|---|
| শুধু রূপান্তর (প্রত্যেকটি বদলাও) | [p * 2 for p in prices] | সবসময় মূলটির সমান |
| শুধু ছাঁকনি (কিছু রাখো) | [m for m in marks if m >= 33] | সমান বা কম |
| দুটিই | [m + 5 for m in marks if m < 33] | সমান বা কম |
| কোনোটিই না (কপি) | [x for x in items] | সমান — তবে list(items) লেখাই ভালো |
একটি বাস্তব উদাহরণ। একটি অনলাইন দোকানের ওয়েবসাইট থেকে দামগুলো এসেছে লেখা (string) হিসেবে: ["৳120", "৳45", "৳300"]. হিসাব করতে হলে এগুলো সংখ্যা চাই। কাজটি হলো “প্রত্যেকটি থেকে টাকার চিহ্ন বাদ দিয়ে int বানাও” — শুধু রূপান্তর:
taka = [int(p.removeprefix("৳")) for p in prices] # [120, 45, 300]removeprefix (Python 3.9 থেকে) শুরুর “৳” সরায়, int() বাকি লেখাকে সংখ্যা বানায়। রূপান্তর অংশে আপনি যেকোনো expression লিখতে পারেন — ফাংশন ডাকা, method ডাকা, গণনা, এমনকি একটি tuple বা আরেকটি list.
৩ · শর্ত দুই জায়গায় — ছাঁকনি-if বনাম if-else
Comprehension নিয়ে নতুনদের সবচেয়ে বড় বিভ্রান্তি এটাই: if কখনো শেষে বসে, কখনো শুরুতে else-সহ বসে। দুটি সম্পূর্ণ আলাদা কাজ করে:
- শেষের
if— ছাঁকনি:[x for x in nums if x > 0]. প্রশ্ন: “এই জিনিসটি কি নতুন list-এ ঢুকবে?” এখানেelseলেখা যায় না, কারণ “না ঢুকলে কী ঢুকবে” প্রশ্নটার কোনো মানে নেই। - শুরুর
if-else— রূপান্তরের অংশ:[x if x > 0 else 0 for x in nums]. প্রশ্ন: “প্রতিটি জিনিসের জায়গায় কী বসবে?” এটা আসলে পর্ব ১১-এর conditional expression —ক if শর্ত else খ— যা রূপান্তর অংশে বসেছে। এখানেelseবাধ্যতামূলক, কারণ প্রতিটি জিনিসের জন্য কিছু একটা মান তো লাগবেই।
ধাপে ধাপে দুটিকে পাশাপাশি চালিয়ে দেখুন — একই সংখ্যাগুলো, কিন্তু ফল দুই রকম:
1nums = [5, -2, 7, -4]2a = [x for x in nums if x > 0] # ছাঁকনি3b = [x if x > 0 else 0 for x in nums] # রূপান্তর
| x | |
| x > 0? | |
| a (ছাঁকনি) | |
| b (if-else) |
- x = 5. শর্ত সত্য। ছাঁকনি 5-কে ঢুকতে দিল; if-else রূপান্তর বেছে নিল প্রথম মান
x, অর্থাৎ 5. দুটি list এখনো একই রকম। - x = −2 — পার্থক্য শুরু। শর্ত মিথ্যা।
a-র ছাঁকনি দরজা বন্ধ করে দিল — −2-এর কোনো চিহ্নa-তে নেই।b-তে কিন্তু দরজা নেই, সবাই ঢোকে; শুধু কী রূপে ঢুকবে তা বদলায় —else 0, তাই 0 বসল। - x = 7. আবার সত্য, দুটিতেই 7.
- x = −4.
aবাদ দিল,b0 বসাল। খেয়াল করুন,b-তে ক্রম আর অবস্থান অক্ষুণ্ণ — তিন নম্বর জায়গার জিনিস এখনো তিন নম্বরে। - ফল। ছাঁকনি দেয় কম বা সমান জিনিস (
a-তে ২টি); if-else দেয় ঠিক সমান জিনিস (b-তে ৪টি)। মনে রাখার উপায়: শেষেরifঠিক করে “কে আসবে”, শুরুরif-elseঠিক করে “কী আসবে”.
সবচেয়ে প্রচলিত SyntaxError: [x for x in nums if x > 0 else 0]. শেষে else লিখলে Python বিভ্রান্ত হয়, কারণ ছাঁকনির জায়গায় শুধু একটি শর্ত বসার কথা। কোনো জিনিসকে বাদ না দিয়ে বদলাতে চাইলে if-else নিয়ে যান সামনে। আর দুটো একসাথেও চলে: [m if m <= 100 else 100 for m in marks if m >= 0] — ঋণাত্মক ভুল নম্বর বাদ, ১০০-র বেশি হলে ১০০. তবে এমন লাইন পড়তে কষ্ট হয়; অংশ ৮-এর নিয়ম দেখুন।
৪ · nested comprehension — একাধিক for-এর ক্রম
একটি comprehension-এ একাধিক for থাকতে পারে। তখন সেগুলো কাজ করে পর্ব ১২-এর nested loop-এর মতো। নিয়মটি একটিই, আর এটা জানলেই কোনো বিভ্রান্তি থাকে না: for-গুলো যে ক্রমে বাঁ থেকে ডানে লেখা, nested লুপে সেই ক্রমেই ওপর থেকে নিচে বসে। প্রথম for সবচেয়ে বাইরের লুপ, শেষ for সবচেয়ে ভেতরের। নিচে দেখুন:
1pairs = [(r, c)2 for r in range(2) # বাইরের3 for c in range(3)] # ভেতরের4# সমান লুপ:5for r in range(2):6 for c in range(3):7 pairs.append((r, c))
| r (বাইরের) | |
| c (ভেতরের) | |
| এ পর্যন্ত জোড়া |
- বাইরের লুপ শুরু: r = 0. প্রথম for (
for r) বাইরের লুপ, তাই সে প্রথমে একটি মান দেয়: 0. তারপর ভেতরের for শুরু হয়, c = 0. প্রথম জোড়া(0, 0). - ভেতরের লুপ ঘুরছে: c = 1. r এখনো 0 — বাইরের লুপ নড়েনি। ভেতরের লুপ দ্রুত ঘোরে, ঠিক ঘড়ির মিনিটের কাঁটার মতো। জোড়া
(0, 1). - c = 2 — ভেতরের লুপ শেষ।
(0, 2). range(3) ফুরাল। এখন বাইরের লুপ এক ধাপ এগোবে। - r = 1, আর c আবার 0 থেকে। বাইরের লুপ পরের মান দিল, আর ভেতরের লুপ নতুন করে পুরোটা শুরু হলো। জোড়া
(1, 0). ঘণ্টার কাঁটা এক ঘর এগোলে মিনিটের কাঁটা আবার শূন্য থেকে ঘোরে। - c = 1.
(1, 1). - c = 2.
(1, 2). ভেতরের লুপ আবার ফুরাল, আর বাইরের range(2)-ও ফুরাল। - ফল: 2 × 3 = ৬টি জোড়া। ক্রম খেয়াল করুন — প্রথম উপাদান (r) ধীরে বদলায়, দ্বিতীয় (c) দ্রুত। এটা হুবহু নিচের nested লুপের ক্রম। for দুটির জায়গা বদলে লিখলে ক্রম হতো
(0, 0), (1, 0), (0, 1), …
২D list চ্যাপ্টা করা
একটি স্কুলের তিনটি সেকশনের শিক্ষার্থীর নাম আলাদা আলাদা list-এ আছে: sections = [["রহিম", "সুমি"], ["করিম"], ["নাদিয়া", "তানভীর"]]. সবাইকে একটি সারিতে চাই। লুপে লিখলে: “প্রতিটি সেকশনের জন্য, সেকশনের প্রতিটি শিক্ষার্থীর জন্য, শিক্ষার্থীকে যোগ করো”. সেই ক্রমেই comprehension:
everyone = [s for sec in sections for s in sec]
# ['রহিম', 'সুমি', 'করিম', 'নাদিয়া', 'তানভীর']একটি list-এর ভেতরের list খুলে এক সারি করাকে বলে flatten করা। প্রথম for sec in sections বাইরের, তাই সেটা আগে; দ্বিতীয় for s in sec প্রথমটির sec ব্যবহার করছে, তাই সেটাকে পরে আসতেই হবে। উল্টো লিখলে (for s in sec for sec in sections) Python বলবে sec এখনো সংজ্ঞায়িত নয় — বাস্তবে ঠিক এই ভুলটি অনেক হয়।
comprehension-এর ভেতরে comprehension — নামতার ছক
আরেক ধরনের nesting আছে, যা দেখতে একই রকম কিন্তু ফল আলাদা: রূপান্তর অংশেই আরেকটি comprehension. এতে একটি list-এর list তৈরি হয়, চ্যাপ্টা list নয়:
table = [[i * j for j in range(1, 4)] for i in range(1, 4)]
# [[1, 2, 3], [2, 4, 6], [3, 6, 9]] — প্রতিটি সারি একটি listএটা পড়ুন বাইরে থেকে: “1 থেকে 3-এর প্রতিটি i-এর জন্য একটি সারি; সেই সারি হলো প্রতিটি j-এর জন্য i × j”. বাইরের for i সারি ঠিক করে, ভেতরের comprehension প্রতি সারির ঘরগুলো। এখানে বাইরের for লেখা আছে শেষে, কারণ ভেতরের comprehension পুরোটা হলো রূপান্তর অংশ। দুই ধরনের nesting গুলিয়ে ফেলা খুব সহজ — তাই এখানে থেমে মিলিয়ে নিন:
| লেখা | ফল | বাইরের লুপ কোনটি |
|---|---|---|
[x for row in m for x in row] | একটি চ্যাপ্টা list | প্রথম for (row) |
[[x * 2 for x in row] for row in m] | list-এর list, আকার একই | শেষের for (row), ভেতরেরটি রূপান্তর |
৫ · dict আর set comprehension
বন্ধনী বদলালেই অন্য ধরনের সংগ্রহ। দ্বিতীয় বন্ধনী { }-এর ভেতরে চাবি: মান লিখলে তৈরি হয় dict — এর নাম dict comprehension. কোলন না থাকলে, শুধু একটি মান থাকলে তৈরি হয় set — set comprehension.
dict comprehension — তিনটি পরিচিত কাজ
- দুটি list থেকে dict: নামের list আর গ্রেডের list থেকে
{নাম: গ্রেড}:{n: g for n, g in zip(names, grades, strict=True)}. (শুধু জোড়া লাগাতে চাইলেdict(zip(names, grades))-ও চলে; comprehension দরকার হয় যখন পথে কিছু বদলাতে বা ছাঁকতে হয়।) - প্রতিটি মান বদলানো: চায়ের দোকানের মূল্যতালিকায় ১৫% ভ্যাট যোগ:
{item: round(p * 1.15, 2) for item, p in price.items()}. চাবি একই, মান নতুন। - dict উল্টে দেওয়া:
{p: item for item, p in price.items()}— দাম থেকে জিনিস খোঁজার dict. সতর্কতা: দুটি জিনিসের দাম সমান হলে নতুন dict-এ একটিই থাকবে, কারণ চাবি অনন্য; শেষে যেটি আসে, সেটি আগেরটিকে মুছে দেয়।
ভেতরে .items() খেয়াল করুন। dict-এর ওপর সরাসরি লুপ চালালে শুধু চাবি আসে (পর্ব ১৬); চাবি আর মান দুটোই চাইলে .items(), আর for item, p in ...-এ unpacking দিয়ে জোড়াটি দুই নামে ভাগ হয়।
set comprehension — শুধু অনন্যগুলো
পাঁচজন শিক্ষার্থীর নামের প্রথম অক্ষরগুলো কী কী? {s[0] for s in students}. set নিজেই ডুপ্লিকেট বাদ দেয় (পর্ব ১৭), তাই “Rahim” আর “Rana”-র দুটি R মিলে একটি R. মনে রাখবেন, set-এর ক্রম নিশ্চিত নয় — ছাপানোর সময় নির্দিষ্ট ক্রম চাইলে sorted() দিন।
{} একা মানে খালি dict, set নয়। খালি set চাইলে set(). আর tuple comprehension বলে কিছু নেই — (x for x in items) লিখলে tuple নয়, একটি generator expression তৈরি হয় (পরের অংশ)। tuple চাইলে tuple(x for x in items).
এবার এ পর্যন্ত যা দেখলেন সব একসাথে চালিয়ে দেখুন — দামের রূপান্তর, পাস করা শিক্ষার্থী, if-else, দুটি list থেকে dict, প্রথম অক্ষরের set আর সেকশন চ্যাপ্টা করা:
prices = ["৳120", "৳45", "৳300", "৳80"]
taka = [int(p.removeprefix("৳")) for p in prices]
print(taka, sum(taka))
marks = {"রহিম": 78, "সুমি": 91, "করিম": 29, "নাদিয়া": 65}
passed = [name for name, m in marks.items() if m >= 33]
print(passed)
print([m if m >= 33 else 0 for m in marks.values()])
names = ["রহিম", "সুমি", "করিম"]
grades = ["A", "A+", "F"]
print({n: g for n, g in zip(names, grades, strict=True)})
students = ["Rahim", "Sumi", "Rana", "Karim", "Sadia"]
print(sorted({s[0] for s in students}))
sections = [["রহিম", "সুমি"], ["করিম"], ["নাদিয়া", "তানভীর"]]
everyone = [s for sec in sections for s in sec]
print(everyone, len(everyone))প্রথম লাইন: [120, 45, 300, 80] 545 — চারটি লেখা থেকে চারটি সংখ্যা (শুধু রূপান্তর, দৈর্ঘ্য সমান)। এরপর ['রহিম', 'সুমি', 'নাদিয়া'] — করিম ২৯ পেয়ে ছাঁকনিতে আটকেছে (দৈর্ঘ্য কম)। তার পরের লাইন [78, 91, 0, 65] — if-else বলে করিম বাদ পড়েনি, তার জায়গায় 0 (দৈর্ঘ্য সমান)। তারপর dict {'রহিম': 'A', 'সুমি': 'A+', 'করিম': 'F'}; strict=True থাকায় দুই list-এর দৈর্ঘ্য না মিললে চুপচাপ ভুল না হয়ে ValueError আসত। set থেকে ['K', 'R', 'S'] — পাঁচটি নাম, কিন্তু অনন্য অক্ষর তিনটি; sorted দেওয়ায় ক্রম প্রতিবার একই। শেষে পাঁচজনের চ্যাপ্টা list আর দৈর্ঘ্য 5.
৬ · generator expression — দরকারমতো একটি একটি মান
ধরুন আপনার কাছে চারটি নম্বর আছে, আর শুধু মোট চাই। list comprehension দিয়ে লিখলে: sum([m for m in marks]). এখানে Python আগে পুরো একটি নতুন list বানায়, তারপর sum সেটা যোগ করে, তারপর list-টি ফেলে দেওয়া হয়। চারটি নম্বরে কিছু যায়-আসে না। কিন্তু এক কোটি লাইনের লগ ফাইলে? শুধু যোগ করার জন্য এক কোটি জিনিসের list মেমরিতে রাখা অপচয়।
সমাধান: তৃতীয় বন্ধনী বাদ দিন। sum(m for m in marks). প্রথম বন্ধনীর ভেতরের এই অংশটি একটি generator expression — সংক্ষেপে genexp. এটা কোনো সংগ্রহ বানায় না। এটা একটি generator object দেয়, যার কাছে “পরের মান দাও” বললে সে ঠিক একটি মান হিসাব করে দেয়, তারপর থেমে অপেক্ষা করে। পর্ব ১২-এর range-এর মতোই এটা lazy. ধাপে ধাপে দেখুন:
1marks = [70, 85, 60]2gen = (m for m in marks) # কিছুই হিসাব হয়নি3total = sum(gen)
| gen-এর অবস্থা | |
| sum-এর ভেতরের যোগফল | |
| একসাথে মেমরিতে নতুন মান | |
| total |
- generator তৈরি, কাজ নয়। লাইন ২ চললে কোনো মান হিসাব হয় না, কোনো list তৈরি হয় না। শুধু একটি ছোট object তৈরি হয়, যে মনে রাখে “marks-এর ওপর হাঁটব, প্রতিটি m দেব”. এক কোটি জিনিস হলেও এই object একই রকম ছোট।
- sum প্রথম মান চাইল।
sumভেতরে একটি লুপ চালায়; প্রতি ঘোরায় সে generator-কে বলে “পরের মান”. generator তখন প্রথমবার কাজ করল — 70 দিল, আর সেখানেই থেমে গেল। - দ্বিতীয় মান। generator ঠিক যেখানে থেমেছিল সেখান থেকে চলল, 85 দিল। sum যোগ করে 155 রাখল। 70-কে আর কোথাও জমিয়ে রাখার দরকার নেই — একসাথে মেমরিতে মাত্র একটি নতুন মান।
- তৃতীয় মান। 60, যোগফল 215.
- ফুরাল। generator জানাল আর মান নেই (ভেতরে এর নাম
StopIteration, পর্ব ১২-এর for লুপের মতো)। sum লুপ থামিয়ে 215 ফেরত দিল। সাবধান:genএখন খালি। আবারsum(gen)লিখলে পাবেন 0 — generator একবারই হাঁটা যায়।
ফাংশনের একমাত্র argument হলে genexp-এর নিজের বন্ধনী বাদ দেওয়া যায় — তাই sum((m for m in marks)) না লিখে sum(m for m in marks). দুটি argument থাকলে বন্ধনী লাগবে: sorted((m for m in marks), reverse=True).
যেখানে genexp সবচেয়ে মানায়
- সংক্ষেপকারী ফাংশনে:
sum,max,min,any,all,"".join— যারা পুরো ধারা একবার হেঁটে একটি উত্তর দেয়। যেমন বর্গের যোগফল:sum(x * x for x in nums); ফেল গোনা:sum(1 for m in marks if m < 33). - আগেভাগে থামায় যেখানে:
any(m < 33 for m in marks)প্রথম ফেল পেলেইTrueদিয়ে থামে (short-circuit). list comprehension দিলে আগে সবগুলো হিসাব হতো, তারপর any দেখত — অকারণ কাজ। - বিশাল বা অসীম ধারায়: ফাইলের লাইন (পর্ব ২৫), নেটওয়ার্ক থেকে আসা ডেটা — যা পুরোটা মেমরিতে আঁটে না।
আর কখন list comprehension? যখন ফলটি একাধিকবার ব্যবহার করবেন, index দিয়ে ধরবেন, len() জানবেন বা ছাপাবেন। genexp একবার হাঁটার পরই শেষ। generator নিজে লেখা, yield আর ভেতরের যন্ত্রপাতি বিস্তারিত দেখবেন পর্ব ৩৪-এ (iterator, generator আর yield).
[ ... ]একবার হেঁটে একটি উত্তর →
( ... )৭ · scope — comprehension-এর নিজের ঘর, আর walrus
পর্ব ১২-এ দেখেছেন, সাধারণ for লুপের loop variable লুপ শেষেও টিকে থাকে, শেষ মান নিয়ে। Comprehension-এ তা হয় না। Python 3-এ প্রতিটি comprehension-এর নিজস্ব scope আছে — একটি ছোট আলাদা ঘর। ভেতরের x বাইরের x-কে ছোঁয় না, আর comprehension শেষ হলে ভেতরের নামটি হারিয়ে যায়।
নিচের কোডে এটা আর generator expression, dict comprehension একসাথে দেখুন:
marks = [78, 91, 29, 65]
total = sum(m for m in marks) # কোনো list তৈরি হয় না
print("মোট:", total)
print("বর্গের যোগফল:", sum(x * x for x in range(1, 5)))
print("কেউ ফেল?", any(m < 33 for m in marks))
price = {"চা": 10, "সিঙ্গারা": 15, "কেক": 40}
with_vat = {item: round(p * 1.15, 2) for item, p in price.items()}
print(with_vat)
by_price = {p: item for item, p in price.items()}
print(by_price[15])
x = "আগের মান"
squares = [x * x for x in range(4)]
print(squares, x) # comprehension-এর x বাইরে আসেনি
for x in range(4):
pass
print(x) # for লুপের x বাইরে রয়ে গেছেপ্রথম তিন লাইনের আউটপুট: “মোট: 263”, “বর্গের যোগফল: 30” (1 + 4 + 9 + 16) আর “কেউ ফেল? True” — ২৯ পাওয়া প্রথম ফেলেই any থেমে গেছে। কোনোটিতেই মাঝখানে list তৈরি হয়নি। এরপর ভ্যাটসহ দাম {'চা': 11.5, 'সিঙ্গারা': 17.25, 'কেক': 46.0} — round(..., 2) না দিলে float-এর হিসাবে কখনো লম্বা দশমিক লেজ দেখা যায় (পর্ব ০৭)। উল্টানো dict-এ 15 চাবি দিয়ে খুঁজলে “সিঙ্গারা”. তারপর আসল পরীক্ষা: comprehension-এর ভেতরে x 0 থেকে 3 পর্যন্ত ঘুরেছে, অথচ বাইরের x এখনো “আগের মান” — [0, 1, 4, 9] আগের মান. শেষে একই কাজ সাধারণ for দিয়ে করলে x বাইরে এসে 3 হয়ে গেছে।
কেন এই পার্থক্য? কারণ comprehension একটি expression — একটি মান তৈরির যন্ত্র। যন্ত্রের ভেতরের কাজের নাম বাইরে ছড়িয়ে পড়লে বিপদ: Python 2-এ list comprehension-এর variable সত্যিই বাইরে বেরিয়ে আসত, আর বাইরের একই নামের ভ্যারিয়েবলকে নিঃশব্দে বদলে দিত। Python 3 এটা বন্ধ করেছে (ভাবনার প্রশ্ন ৩-এ ইতিহাস)।
walrus দিয়ে একবার হিসাব, দুবার ব্যবহার
কখনো রূপান্তরের ফল দিয়েই ছাঁকতে হয়। ধরুন প্রতিটি দামের লেখা থেকে সংখ্যা বের করে শুধু ১০০-র বেশিগুলো রাখবেন। সাধারণভাবে লিখলে int(...) দুবার চলে — একবার ছাঁকনিতে, একবার রূপান্তরে। পর্ব ০৮-এর walrus operator := দিয়ে একবারেই:
big = [t for p in prices if (t := int(p.removeprefix("৳"))) > 100]
# [120, 300]ছাঁকনিতে t হিসাব হয়ে নাম পায়, রূপান্তর অংশ সেটাই ব্যবহার করে। একটি অদ্ভুত নিয়ম জানা থাকা ভালো: comprehension-এর ভেতরে walrus দিয়ে বানানো নাম (t) loop variable-এর মতো ভেতরে আটকে থাকে না — বাইরের scope-এ চলে আসে (PEP 572 ইচ্ছা করেই এটা রেখেছে, যাতে “শেষ যেটা মিলেছিল” বাইরে থেকে দেখা যায়)। বেশি চতুরতা না করে walrus শুধু তখনই ব্যবহার করুন, যখন একই দামি হিসাব দুবার হচ্ছে। নইলে দুই ধাপে লিখুন: আগে taka = [...], তারপর [t for t in taka if t > 100].
৮ · কখন comprehension নয় — পড়ার যোগ্যতা আর গতি
Comprehension শেখার পর অনেকে সবকিছু এক লাইনে লিখতে চান। কিন্তু comprehension-এর একমাত্র উদ্দেশ্য কোডকে পড়তে সহজ করা। যেখানে সেটা হয় না, সেখানে সাধারণ লুপই ভালো। চারটি নিয়ম:
১. side effect হলে লুপ
[print(n) for n in names] কাজ করে, কিন্তু অকারণে [None, None, …]-এর একটি list বানিয়ে ফেলে দেয়। Comprehension-এর কাজ নতুন সংগ্রহ বানানো; ছাপানো, ফাইলে লেখা, অন্য list বদলানোর মতো side effect-এর জন্য for লুপ।
২. দুটির বেশি for/if হলে ভাঙুন
একটি for আর একটি if — চমৎকার। দুটি for — ঠিক আছে। তিনটি for বা for-if-for-if — পাঠক হারিয়ে যাবেন। Google-এর Python style guide-ও একাধিক for বা ছাঁকনিকে নিরুৎসাহিত করে।
ভাগ করুন, নাম দিন৩. জটিল যুক্তি ফাংশনে
রূপান্তরে try/except, একাধিক শর্ত বা কয়েক ধাপের হিসাব লাগলে সেটা একটি ছোট ফাংশনে রাখুন (পর্ব ১৩), তারপর [grade(m) for m in marks]. ফাংশনের নাম নিজেই ব্যাখ্যা।
৪. এক লাইনে না আঁটলে ভাবুন
Comprehension একাধিক লাইনে ভাঙা যায় (অংশ ৪-এর মতো), আর সেটা ঠিক আছে। কিন্তু ভাঙার পরও যদি বোঝা কঠিন হয়, তাহলে এটা লুপ হওয়ার যোগ্য।
স্পষ্টতা > সংক্ষেপগতি — সত্যি, তবে মূল কারণ নয়
একটি append-লুপের প্রতি ঘোরায় Python-কে result.append নামটি খুঁজে বের করতে হয়, তারপর একটি method ডাকতে হয়। list comprehension-এর ভেতরে এর জন্য একটি বিশেষ bytecode নির্দেশ (LIST_APPEND) আছে, যা এই খোঁজা আর ডাকার খরচ বাঁচায়। তাই একই কাজে comprehension সাধারণত একটু দ্রুত — কত দ্রুত, তা Python-এর version, ডেটা আর কাজের ওপর নির্ভর করে। Python 3.12 থেকে list, dict আর set comprehension-এর আলাদা ফাংশন-কাঠামো সরিয়ে ফেলায় (ভাবনার প্রশ্ন ৪) পার্থক্যটা আরেকটু বেড়েছে।
কিন্তু রূপান্তর অংশে যদি একটি ভারী কাজ থাকে (ফাইল পড়া, জটিল হিসাব), তাহলে লুপের খরচ তার তুলনায় নগণ্য। গতির জন্য comprehension বেছে নেবেন না — পরিষ্কারের জন্য বেছে নিন, গতি বোনাস। সত্যিকারের গতির প্রশ্নে মেপে দেখুন (পর্ব ৪৩), আর লাখ লাখ সংখ্যার হিসাবে NumPy (পর্ব ৪৬)।
Python সব সময় map আর filter-এর মতো ফাংশনাল টুল রেখেছে: list(map(str.upper, names)) আর [n.upper() for n in names] একই কাজ করে। map বা filter-এর সাথে lambda লাগলে comprehension প্রায় সবসময় পড়তে সহজ — list(filter(lambda m: m >= 33, marks))-এর চেয়ে [m for m in marks if m >= 33]. Python-এর স্রষ্টা Guido van Rossum ২০০৫ সালে এই যুক্তিতেই Python 3 থেকে এগুলো সরানোর প্রস্তাব দিয়েছিলেন; শেষে map আর filter রয়ে গেছে, আর reduce সরে গেছে functools module-এ (ভাবনার প্রশ্ন ৫)। এদের বিস্তারিত পর্ব ৩৮-এ।
C-তে comprehension-এর কোনো প্রতিরূপ নেই — নতুন array বানাতে হাতে লুপ আর আকারের হিসাব লাগে। C++20-এর ranges-এ views::filter আর views::transform জুড়ে যা পাওয়া যায়, তা অনেকটা Python-এর generator expression-এর মতো: অলস, আর কোনো নতুন container বানায় না। নতুন std::vector চাইলে C++-এ আলাদা করে তৈরি করতে হয়, Python-এ শুধু বন্ধনী বদলালেই চলে। C++-এর অলস view-গুলো দেখতে C++ কোর্সের ranges ও views পর্ব দেখুন।
[x for x in range(5) if x % 2]কী দেয়? —[1, 3];x % 2বিজোড়ে 1 (truthy), জোড়ে 0 (falsy).- list comprehension আর generator expression-এর পার্থক্য? — প্রথমটি সঙ্গে সঙ্গে পুরো list বানায়; দ্বিতীয়টি অলস, চাওয়া মাত্র একটি একটি মান দেয় আর একবারই হাঁটা যায়।
- Python-এ tuple comprehension আছে কি? — না;
( ... for ... )একটি generator expression. tuple চাইলেtuple(...). - if-else comprehension-এর কোথায় বসে? — ছাঁকনি হলে শেষে শুধু
if; প্রতিটি জিনিসের মান বেছে নিতে হলে শুরুতেa if শর্ত else b. - Python 3-এ comprehension-এর loop variable বাইরে পাওয়া যায়? — না, comprehension-এর নিজস্ব scope আছে; Python 2-এর list comprehension-এ পাওয়া যেত।
৯ · AI-এর যুগে — AI-এর লেখা comprehension যাচাই
AI comprehension খুব পছন্দ করে, কারণ এর প্রশিক্ষণের কোডে এগুলো প্রচুর আছে, আর এক লাইনের উত্তর “চতুর” দেখায়। সমস্যা হলো, AI প্রায়ই সংক্ষেপকে পরিষ্কারের ওপরে রাখে। তিনটি পরিচিত ধরন:
| AI প্রায়ই যা লেখে | সমস্যা | পরিষ্কার রূপ |
|---|---|---|
| তিন স্তরের for আর দুটি if এক লাইনে | পড়া যায় না, ভুল ধরা যায় না | লুপ, বা ছোট ধাপে ভাগ করে নাম দেওয়া |
[print(x) for x in xs] | side effect-এর জন্য বাড়তি None-এর list | for x in xs: print(x) |
sum([p for p in prices]) | শুধু যোগের জন্য পুরো list তৈরি | sum(prices) বা sum(p for p in ...) |
[x for x in xs if c else d] | SyntaxError — else ভুল জায়গায় | [x if c else d for x in xs] |
{v: k for k, v in d.items()} বিনা মন্তব্যে | সমান মান থাকলে চাবি নিঃশব্দে হারায় | আগে মানগুলো অনন্য কি না যাচাই |
AI-কে comprehension লিখতে বা রিভিউ করতে বলার সময় সীমাগুলো আগেই বলে দিন। নিচের কাঠামোটি কপি করে ব্যবহার করুন:
AI-এর লেখা comprehension পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১list comprehension Python-এর নিজের আবিষ্কার নয়। এই ধারণা কোথা থেকে এল, আর Python কেন অন্য ভাষার ধাঁচ না নিয়ে ইংরেজি শব্দ for আর if বেছে নিল?
ধারণাটির শিকড় গণিতে — সেট গঠন পদ্ধতি, { x² | x ∈ S, x > 0 }. প্রোগ্রামিং ভাষায় একে প্রথম গুরুত্ব দিয়ে আনে SETL, ১৯৬০-এর দশকের শেষে নিউ ইয়র্ক বিশ্ববিদ্যালয়ে Jacob T. Schwartz-এর তৈরি একটি ভাষা, যার মূল ভাবনাই ছিল গণিতের সেট দিয়ে প্রোগ্রাম লেখা। SETL-এ “set former” নামে প্রায় হুবহু গণিতের মতো লেখা যেত। পরে ফাংশনাল প্রোগ্রামিংয়ের ভাষাগুলো — Miranda, তারপর ১৯৯০ সালে প্রকাশিত Haskell — একই ধারণাকে list-এর ওপর আনে: [x*x | x <- xs, even x]. “list comprehension” নামটিও এসেছে ফাংশনাল প্রোগ্রামিংয়ের এই ধারা থেকে।
Python-এ এটা যোগ হয় Python 2.0-এ (২০০০ সালের অক্টোবর), PEP 202 অনুযায়ী। তার আগে Python-এ এই কাজের জন্য ছিল লুপ, অথবা map আর filter-এর সাথে lambda — যা অনেকের কাছে কঠিন লাগত। Python-এর অফিশিয়াল ডকুমেন্টেশন নিজেই বলে যে list comprehension-এর ধারণা নেওয়া হয়েছে Haskell থেকে।
কিন্তু Python Haskell-এর চিহ্নগুলো (|, <-) নেয়নি। এর বদলে নিয়েছে নিজের পরিচিত শব্দ — for আর if, যা পাঠক আগেই for লুপ আর if statement-এ চেনেন। এর একটি সুন্দর ফল: comprehension-কে খুলে লুপে লেখার নিয়ম প্রায় যান্ত্রিক — for আর if একই ক্রমে নিচে নিচে বসান। নতুন কিছু মুখস্থ করতে হয় না, শুধু পুরোনো দুটি গঠন নতুনভাবে সাজাতে হয়। এটা Python-এর একটি পুরোনো দর্শনের সাথে মেলে — চিহ্নের চেয়ে শব্দ, কারণ শব্দ পড়া যায়।
এরপর ধারণাটি ছড়িয়ে পড়ে: Python 2.4-এ generator expression, Python 3.0-তে (আর পেছনে 2.7-এ) dict আর set comprehension. অন্য ভাষাও একই পথে হেঁটেছে — C#-এর LINQ, Scala-র for-comprehension, JavaScript-এ প্রস্তাবিত হয়েও শেষে বাদ পড়া array comprehension.
মূল উপলব্ধি: ভালো ভাষা-নকশা প্রায়ই নতুন আবিষ্কার নয়, বরং অন্য জায়গার প্রমাণিত ধারণাকে নিজের পাঠকের পরিচিত ভাষায় অনুবাদ করা। Python গণিত আর Haskell থেকে ধারণা নিয়েছে, কিন্তু চেহারা দিয়েছে নিজের।
প্র ০২list comprehension আসার চার বছর পর Python-এ generator expression এল। কেন শুধু list comprehension যথেষ্ট ছিল না, আর বন্ধনী বদলানোর মতো ছোট পরিবর্তন কেন এত বড় ব্যাপার?
list comprehension জনপ্রিয় হওয়ার পর একটি সমস্যা চোখে পড়তে লাগল। লোকে লিখত sum([x*x for x in data]), max([len(line) for line in f]) — যেখানে পুরো list-এর কোনো দরকারই নেই, শুধু একবার হেঁটে একটি উত্তর চাই। বড় ডেটায় এই মাঝখানের list মেমরি খেত, আর সময়ও নষ্ট করত। অথচ Python 2.2-তে (PEP 255) yield দিয়ে generator ফাংশন লেখা যেত, যা অলসভাবে একটি একটি মান দেয়। কিন্তু প্রতিবার একটি ছোট generator ফাংশন লেখা ঝামেলা।
সমাধান এল PEP 289-এ, যা লিখেছিলেন Raymond Hettinger, আর যুক্ত হলো Python 2.4-এ (২০০৪)। ধারণা: comprehension-এর একই গঠন, কিন্তু প্রথম বন্ধনীতে — আর ফলাফল একটি generator. ফাংশনের একমাত্র argument হলে বাড়তি বন্ধনীও বাদ, তাই sum(x*x for x in data) পড়তে একদম স্বাভাবিক বাক্যের মতো।
নকশার সময় একটি সূক্ষ্ম প্রশ্ন উঠেছিল: generator অলস, অর্থাৎ ভেতরের কোড পরে চলে। তাহলে (x + y for x in xs)-এ y-এর মান কোন মুহূর্তের — তৈরির সময়ের, না চলার সময়ের? PEP 289 একটি মাঝামাঝি নিয়ম নিল: প্রথম for-এর উৎস (xs) তৈরির মুহূর্তেই হিসাব হয়ে যায়, যাতে উৎসে ভুল থাকলে error তখনই আসে; বাকি সব — রূপান্তর, ছাঁকনি, পরের for-গুলো — চলে মান চাওয়ার সময়। এই নিয়মের প্রভাব আজও আছে: generator তৈরি করে পরে বাইরের একটি ভ্যারিয়েবল বদলালে ফল বদলে যেতে পারে।
বড় ছবিতে generator expression Python-কে “ধারা” (stream) ভাবনার দিকে ঠেলে দিয়েছে: ফাইলের লাইন → ছাঁকনি → রূপান্তর → সংক্ষেপ, প্রতিটি ধাপ অলস, কোথাও পুরো ডেটা মেমরিতে নেই। এক GB-র লগ ফাইল থেকে error-এর সংখ্যা গোনা যায় কয়েক KB মেমরিতে: sum(1 for line in f if "ERROR" in line). Python 3-এ map, filter, zip অলস হওয়ার পেছনেও একই দর্শন।
মূল উপলব্ধি: “আগে সব বানাও” আর “চাইলে বানাও” — এই দুটির মধ্যে বেছে নেওয়াই মেমরি-সচেতন প্রোগ্রামিংয়ের মূল। Python দুটিকে একই গঠনে রেখেছে, পার্থক্য শুধু বন্ধনীতে — তাই সঠিকটি বেছে নেওয়ার খরচ প্রায় শূন্য।
প্র ০৩Python 2-এ [x for x in data] চলার পর x বাইরে পাওয়া যেত। Python 3 কেন এটা বন্ধ করল, আর তার জন্য ভেতরে কী বদলাতে হলো?
Python 2-এ list comprehension আসলে একটি for লুপের সংক্ষিপ্ত রূপ হিসেবে বানানো হয়েছিল, একই scope-এ। তাই সাধারণ লুপের মতো এর variable-ও বাইরে থেকে যেত। এতে একটি বিপজ্জনক বাগ সম্ভব ছিল: ফাংশনের শুরুতে x = 10, মাঝখানে কোথাও [x * 2 for x in items], শেষে x ব্যবহার — আর x আর 10 নেই, items-এর শেষ উপাদান! কোনো error নেই, শুধু ভুল ফল।
মজার ব্যাপার হলো, Python 2.4-এর generator expression-এ এই সমস্যা কখনো ছিল না। generator অলস, তাই তার ভেতরের কোড চলে পরে, অন্য সময়ে — সেজন্য তাকে আলাদা একটি লুকানো ফাংশনের মতো বানাতে হয়েছিল, আর ফাংশনের ভেতরের নাম বাইরে আসে না। ফলে একই ভাষায় দুই নিয়ম: [...]-এর variable বাইরে আসে, (...)-এর আসে না। Guido van Rossum পরে তাঁর “History of Python” ব্লগে লিখেছেন, list comprehension-এর এই আচরণ ছিল একটি পুরোনো বাস্তবায়নের “dirty little secret”, যা তিনি Python 3-এ ঠিক করতে চেয়েছিলেন।
Python 3.0 (২০০৮) ছিল ইচ্ছাকৃতভাবে পেছনের সাথে না-মেলা version, তাই এমন পরিবর্তনের সুযোগ ছিল। সেখানে list comprehension-কেও generator expression-এর মতো একটি লুকানো nested ফাংশন হিসেবে বানানো হলো। নতুন dict আর set comprehension-ও একই নিয়মে এল। ফলাফল: চার ধরনের comprehension-এর এক নিয়ম, আর কোনো নাম ভেতর থেকে ফাঁস হয় না।
এর একটি দামও ছিল। প্রতিবার comprehension চালানো মানে একটি ছোট ফাংশন object তৈরি করে ডাকা — একটি নতুন stack frame-সহ। ছোট comprehension-এ এই খরচ চোখে পড়ার মতো, যা প্রশ্ন ৪-এর বিষয়। আরেকটি পার্শ্বফল: class-এর body-তে comprehension লিখলে সে class-এর অন্য নাম সরাসরি দেখতে পায় না (প্রথম for-এর উৎস ছাড়া), কারণ class-এর scope ভেতরের ফাংশনে দেখা যায় না — একটি পরিচিত ধাঁধা, যা পর্ব ২০-এ scope শেখার পর আরও পরিষ্কার হবে।
মূল উপলব্ধি: একটি expression-এর ভেতরের কাজ বাইরের পৃথিবীকে নিঃশব্দে বদলালে কোড নিয়ে যুক্তি করা কঠিন হয়। Python 3 কিছুটা গতি ছেড়ে দিয়ে সামঞ্জস্য আর নিরাপত্তা বেছে নিয়েছিল — আর এখন সেই গতিও ফিরে আসছে।
প্র ০৪Python 3.12-এ PEP 709 comprehension-কে “inline” করেছে। এর মানে কী, কেন করা হলো, আর এতে আপনার কোডের আচরণ কি কিছু বদলায়?
প্রশ্ন ৩-এ দেখেছেন, Python 3.0 থেকে প্রতিটি list, dict বা set comprehension ভেতরে একটি লুকানো ফাংশন ছিল। আপনি যখন [x * 2 for x in items] চালাতেন, CPython একটি ফাংশন object তৈরি করত, সেটা ডাকত (নতুন frame সহ), ভেতরে লুপ চালাত, তারপর ফেরত আসত। বড় ডেটায় এই খরচ লুপের তুলনায় নগণ্য, কিন্তু ছোট comprehension — যেমন তিন-চারটি উপাদান — বারবার চালালে ফাংশন ডাকার খরচই বড় হয়ে দাঁড়াত।
PEP 709 (লেখক Carl Meyer), যা যুক্ত হয়েছে Python 3.12-এ (২০২৩), এই লুকানো ফাংশন সরিয়ে দিয়েছে। এখন comprehension-এর bytecode সরাসরি চারপাশের কোডের ভেতরেই বসে — একে বলে inlining. আলাদা ফাংশন object নেই, আলাদা frame নেই। PEP-এর মাপজোখে শুধু comprehension নিয়ে বানানো মাইক্রোবেঞ্চমার্কে লক্ষণীয় গতিবৃদ্ধি দেখা গেছে; বাস্তব প্রোগ্রামে লাভ নির্ভর করে comprehension কত ঘন ঘন আর কত ছোট, তার ওপর।
তাহলে scope-এর নিয়ম কি ভেঙে গেল? না — এটাই এই PEP-এর সবচেয়ে যত্নের অংশ। ফাংশন না থাকলেও CPython loop variable-কে আলাদা করে রাখে: comprehension শুরুর আগে বাইরের একই নামের মান সরিয়ে রাখে, শেষে ফিরিয়ে দেয়। তাই x এখনো বাইরে ফাঁস হয় না — এই পর্বের দ্বিতীয় চালানো কোডটি 3.12-এর আগে-পরে একই ফল দেয়।
কিছু খুঁটিনাটি অবশ্য বদলেছে, যা সাধারণ কোডে চোখে পড়ে না: traceback-এ comprehension-এর জন্য আলাদা <listcomp> frame আর দেখা যায় না; comprehension-এর ভেতরে locals() ডাকলে এখন বাইরের ফাংশনের নামগুলোও আসে। আর generator expression inline হয়নি — কারণ সে অলস, পরে চলে, তাই তার নিজের frame লাগবেই।
এটা CPython-এর গত কয়েক বছরের “Faster CPython” প্রচেষ্টার অংশ, যেখানে 3.11 থেকে interpreter অনেক জায়গায় দ্রুত হয়েছে (পর্ব ০৫)। লক্ষ করুন, ভাষার নিয়ম (comprehension-এর আলাদা scope) একই রইল, শুধু বাস্তবায়ন বদলাল।
মূল উপলব্ধি: ভাষার অর্থ আর তার বাস্তবায়ন আলাদা জিনিস। একটি ভালো ভাষা প্রথমে অর্থ পরিষ্কার করে, তারপর সেই অর্থ না ভেঙে বাস্তবায়নকে দ্রুত করে। আপনার কাজ পরিষ্কার কোড লেখা — গতির অনেকটা Python নিজেই এনে দেয়।
প্র ০৫map আর filter থাকতে comprehension কেন? Guido van Rossum ২০০৫ সালে কেন lambda, map, filter আর reduce সরিয়ে দিতে চেয়েছিলেন, আর শেষে কী হলো?
Python-এ ১৯৯৪ সালের দিকে (Python 1.0) lambda, map, filter আর reduce যোগ হয়েছিল — Lisp-এর মতো functional programming ভাষা থেকে আসা একজন ব্যবহারকারীর পাঠানো কোড থেকে। Guido নিজে কখনো এগুলোর বড় ভক্ত ছিলেন না। list comprehension (২০০০) আর generator expression (২০০৪) আসার পর তাঁর মনে হলো, এগুলোর প্রায় সব কাজ এখন আরও পরিষ্কারভাবে করা যায়।
২০০৫ সালের মার্চে তিনি Artima-তে “The fate of reduce() in Python 3000” নামে একটি লেখা প্রকাশ করেন (Python 3000 ছিল Python 3-এর তখনকার নাম)। তাঁর যুক্তি: filter(lambda x: x > 0, nums)-এর চেয়ে [x for x in nums if x > 0] পড়া সহজ; map(f, xs)-এর চেয়ে [f(x) for x in xs]. আর reduce নিয়ে তিনি সবচেয়ে কড়া ছিলেন — তাঁর মতে প্রায় সব reduce-ভিত্তিক কোড বুঝতে কাগজ-কলম নিয়ে বসতে হয়, আর এর বেশিরভাগ ব্যবহার একটি সাধারণ লুপ বা sum-এর মতো ফাংশনে লিখলে পরিষ্কার হয়। any() আর all() যোগ করার প্রস্তাবও সেই সময়েই আসে (Python 2.5-এ যুক্ত হয়)।
সম্প্রদায়ের প্রতিক্রিয়া ছিল প্রবল, বিশেষ করে lambda রাখার পক্ষে। শেষ ফল এক ধরনের সমঝোতা: lambda, map আর filter Python 3-এ রয়ে গেল (map আর filter অলস হয়ে গেল, ঠিক generator expression-এর মতো); শুধু reduce built-in থেকে সরে গেল functools module-এ, যেখান থেকে এখনো import করে ব্যবহার করা যায়।
আজকের বাস্তব নির্দেশনা: যদি আগে থেকেই একটি নামওয়ালা ফাংশন থাকে, map(str.strip, lines) সংক্ষিপ্ত আর পরিষ্কার — অনেকে এটাই পছন্দ করেন। কিন্তু lambda লিখতে হলে, বা রূপান্তর আর ছাঁকনি দুটোই লাগলে, comprehension প্রায় সবসময় পড়তে সহজ। Python-এর আদর্শ “একটি স্পষ্ট উপায় থাকা উচিত” (Zen of Python) — আর সাধারণ কাজের জন্য সেই উপায় হয়ে উঠেছে comprehension.
মূল উপলব্ধি: একই কাজের একাধিক উপায় থাকলে পাঠকের বোঝা বাড়ে। Python-এর ইতিহাস দেখায়, ভাষা টুল সরাতে চাইলেও ব্যবহারকারীর অভ্যাস আর পুরোনো কোডের ভার মেনে চলতে হয় — তাই নিয়ম বদলায় ধীরে, কিন্তু পছন্দের পথ বদলায় সংস্কৃতিতে।
অনুশীলন
এগুলোর বেশিরভাগে নতুন কোড লিখতে হবে না — অনুমান করা, খুলে লেখা আর মিলিয়ে নেওয়াই আসল কাজ।
-
হাতে চালান: চালানোর আগে খাতায় লিখুন কী পাবেন — (ক)
[c * 2 for c in "abc"]; (খ)[n for n in range(10) if n % 3 == 0]; (গ){w[-1] for w in ["tea", "bee", "pie"]}; (ঘ)[n if n % 2 else -n for n in range(4)]. তারপর REPL-এ মিলিয়ে নিন।(ক)
['aa', 'bb', 'cc']— string-এর ওপর লুপ একটি একটি অক্ষর দেয়,* 2দুবার লেখে। (খ)[0, 3, 6, 9]— 0-ও 3 দিয়ে বিভাজ্য। (গ){'e'}— তিনটি শব্দের শেষ অক্ষরই e, set-এ একটিই থাকে। (ঘ)[0, 1, -2, 3]— এটা if-else রূপান্তর, তাই চারটি জিনিসই আছে;n % 2জোড়ে 0 (falsy), তাই জোড় সংখ্যার জায়গায়-n. 0-এর ক্ষেত্রে-0হলো 0. -
খুলে লিখুন:
result = [name.title() for sec in sections for name in sec if len(name) > 3]— এটাকে সমান for লুপে খাতায় লিখুন। কোন for বাইরের?ifকোন লুপের ভেতরে বসবে?বাঁ থেকে ডানের ক্রম মেনে ওপর থেকে নিচে:
result = [] for sec in sections: # প্রথম for — বাইরের for name in sec: # দ্বিতীয় for — ভেতরের if len(name) > 3: # if শেষে, তাই সবচেয়ে ভেতরে result.append(name.title())রূপান্তর (
name.title()) সবসময় সবচেয়ে ভেতরেরappend-এ যায়, যদিও comprehension-এ সেটা সবার আগে লেখা। দুটি for আর একটি if — এটাই প্রায় সর্বোচ্চ সীমা, যার বেশি হলে লুপই ভালো। -
দুই ধরনের nesting:
m = [[1, 2, 3], [4, 5, 6]]. (ক)[x for row in m for x in row]আর (খ)[[row[i] for row in m] for i in range(3)]— কোনটি কী দেয়? দ্বিতীয়টিকে গণিতে কী বলে?(ক)
[1, 2, 3, 4, 5, 6]— চ্যাপ্টা list; প্রথম for (row) বাইরের। (খ)[[1, 4], [2, 5], [3, 6]]— এখানে বাইরের for হলো শেষেরfor i in range(3); প্রতিটি i-এর জন্য একটি নতুন সারি, যাতে প্রতিটি পুরোনো সারির i-তম উপাদান। সারি আর কলাম জায়গা বদলেছে — গণিতে একে বলে ম্যাট্রিক্সের transpose. Python-এ এর আরও ছোট রূপ[list(col) for col in zip(*m)];*mlist-টিকে ছড়িয়ে আলাদা argument বানায়, যা পর্ব ১৫-এ দেখেছেন। -
AI অডিট: একটি AI এই তিন লাইন দিয়েছে:
[print(n) for n in names];total = sum([p for p in prices if p > 100]);res = [x*y for x in a if x for y in b if y if x != y]. প্রতিটির সমস্যা কী, আর কীভাবে লিখবেন?প্রথমটি side effect-এর জন্য comprehension —
None-এর একটি অপ্রয়োজনীয় list বানিয়ে ফেলে দেয়। লিখুনfor n in names: print(n)(বা ছোট কাজেprint(*names, sep="\n")). দ্বিতীয়টিতে শুধু যোগের জন্য পুরো list — তৃতীয় বন্ধনী সরিয়েsum(p for p in prices if p > 100).তৃতীয়টি চলে, কিন্তু দুটি for আর তিনটি if এক লাইনে — পড়ে বোঝা কঠিন। লুপে ভাঙলে উদ্দেশ্য স্পষ্ট হয়: “a-র প্রতিটি অশূন্য x, b-র প্রতিটি অশূন্য y, যদি x আর y আলাদা হয়, তাহলে গুণফল”. হয় তিন স্তরের for লুপ লিখুন, নয়তো শর্তগুলো একটি ছোট ফাংশনে রেখে comprehension-কে এক for-এ নামিয়ে আনুন। শিক্ষা: যে comprehension বাংলায় এক বাক্যে পড়া যায় না, সেটা লুপ হওয়ার যোগ্য।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমPython-এ result = [f(x) for x in items if ok(x)] চলার সময় ভেতরে যে ধাপগুলো ঘটে, তা সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📦 একটি নতুন খালি list তৈরি হয়
- 👉
itemsথেকে পরের জিনিসটিxনামে আসে - 🧹 ছাঁকনি
ok(x)যাচাই হয় - 🎯 সত্য হলে
f(x)হিসাব হয়ে list-এ যোগ হয়, তারপর আবার পরের জিনিস - 🏷️ জিনিস ফুরালে পুরো list-টি
resultনামের সাথে বাঁধা হয়
Python-এ [x * 2 for x in range(5) if x % 2 == 1]-এর মান কী?
- [2, 6]
- [0, 4, 8]
- [1, 3]
- [2, 4, 6, 8]
Python-এ [(r, c) for r in range(2) for c in range(2)]-এর মান কী?
- [(0, 0), (1, 0), (0, 1), (1, 1)]
- [(0, 0), (1, 1)]
- [(0, 0), (0, 1), (1, 0), (1, 1)]
- SyntaxError
Python-এ নিচের কোনটি লিখলে SyntaxError হয়?
- [x if x > 0 else 0 for x in nums]
- [x for x in nums if x > 0 else 0]
- [x for x in nums if x > 0]
- [x if x < 100 else 100 for x in nums if x > 0]
Python-এ {v: k for k, v in {"a": 1, "b": 2, "c": 1}.items()}-এর মান কী?
- {1: 'a', 2: 'b', 1: 'c'}
- {1: 'a', 2: 'b'}
- ValueError
- {1: 'c', 2: 'b'}
Python 3-এ x = 10, তারপর ys = [x for x in range(3)], তারপর print(x) চালালে কী ছাপা হবে?
- 10
- 2
- NameError
- [0, 1, 2]
১ থেকে এক কোটি পর্যন্ত সংখ্যার বর্গের যোগফল বের করার জন্য Python-এ মেমরির দিক থেকে সবচেয়ে ভালো লেখা কোনটি?
- sum([x * x for x in range(1, 10**7 + 1)])
- sum({x * x for x in range(1, 10**7 + 1)})
- sum(x * x for x in range(1, 10**7 + 1))
- sum([x * x for x in list(range(1, 10**7 + 1))])
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- comprehension = উৎস + ঐচ্ছিক ছাঁকনি + রূপান্তর, একটি expression-এ। বাংলায় পড়ুন: “… , প্রতিটি … -এর জন্য, যদি …”. খুলে লিখলে for আর if একই ক্রমে নিচে নিচে, রূপান্তর সবচেয়ে ভেতরে।
- শেষের
ifঠিক করে কে আসবে (দৈর্ঘ্য কমতে পারে); শুরুরa if শর্ত else bঠিক করে কী আসবে (দৈর্ঘ্য সমান)। - একাধিক for: প্রথমটি বাইরের, শেষটি ভেতরের — nested লুপের হুবহু ক্রম। রূপান্তরে আরেকটি comprehension থাকলে ফল list-এর list.
[...]list,{k: v ...}dict,{...}set,(...)অলস generator expression —sum,any,max-এ list নয়, genexp দিন। Python 3-এ comprehension-এর variable বাইরে আসে না।- side effect, দুটির বেশি for/if বা জটিল যুক্তি হলে লুপ বা ফাংশন। গতি বোনাস, মূল কারণ পড়ার সুবিধা।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৯ · mutability, alias আর copy/deepcopyপরবর্তী পর্বএকই object-এর একাধিক নাম, আর কখন সত্যিকারের কপি লাগে।
- Python প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — ৮টি মডিউল, কোন পর্ব কখন।
- C++ প্রোগ্রামিং · ranges আর viewsতুলনা করুনC++20-এ অলস filter আর transform — Python-এর generator expression-এর আত্মীয়।