Python-এ জেনারেটর: yield, iterator এবং list ও dict জেনারেটর
Python-এর generator হলো এমন একটি অবজেক্ট, যা মান একটি একটি করে দেয় এবং চাওয়ার সময়ই হিসাব করে, সব মান মেমোরিতে জমিয়ে রাখে না; এটি তৈরি হয় yield কীওয়ার্ডসহ একটি ফাংশন দিয়ে, অথবা বন্ধনীর ভেতরে একটি generator expression দিয়ে। List, dict ও set comprehension হলো একটি লাইনে তৈরি কালেকশন বানানোর সংক্ষিপ্ত উপায়: [x * 2 for x in xs], {k: v for k, v in pairs}, {x for x in xs}।
Python-এর generator হলো এমন একটি অবজেক্ট, যা মান একটি একটি করে দেয় — যখন চাওয়া হয় তখনই — এবং পুরো ক্রমটি মেমোরিতে জমিয়ে রাখে না। এটি তৈরি হয় দুটি উপায়ে: yield ব্যবহার করা ফাংশন দিয়ে, অথবা বন্ধনীর ভেতরে generator expression দিয়ে। পাশাপাশি আছে list, dict ও set-এর «জেনারেটর» — comprehension, যা এক লাইনে তৈরি কালেকশন বানায়। দেখা যাক python yield কীভাবে কাজ করে, generator ও iterator-এর পার্থক্য কী, এবং comprehension-কে কীভাবে অপঠনযোগ্য না করা যায়।
Iterator: for লুপের ভিত্তি
Generator বুঝতে আগে iterator বুঝতে হয়। Iterable হলো এমন কিছু, যার উপর লুপ চালানো যায়: তালিকা, স্ট্রিং, dictionary, ফাইল। Iterator হলো এমন অবজেক্ট, যা বর্তমান অবস্থান মনে রাখে এবং next() ডাকলে পরের উপাদান দেয়; উপাদান ফুরোলে ছোঁড়ে StopIteration।
nums = [10, 20, 30]
it = iter(nums) # তালিকা থেকে iterator নেওয়া
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
next(it) # StopIteration
ঠিক এটিই করে for লুপ: iter() ডাকে, তারপর StopIteration না আসা পর্যন্ত next()। নিজের iterator লিখতে যায় ক্লাসে __iter__ ও __next__ মেথড:
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
print(list(Countdown(3))) # [3, 2, 1]
কাজ করে, কিন্তু অনেক বাড়তি কোড লাগে। Generator একই কাজ তিন লাইনে করে।
Python-এর generator কী এবং yield কীভাবে কাজ করে
Generator ফাংশন একটি সাধারণ ফাংশন, যার ভেতরে yield আছে। তাকে ডাকলে কোড চলে না, বরং একটি generator অবজেক্ট ফেরত আসে। প্রতিটি next() শরীর চালায় কাছের yield পর্যন্ত, মান দেয় এবং ফাংশনকে স্থানীয় ভেরিয়েবলসহ থামিয়ে রাখে। পরের next() ঠিক সেই জায়গা থেকেই চলতে থাকে।
def countdown(start):
while start > 0:
yield start
start -= 1
gen = countdown(3)
print(gen) # <generator object countdown at 0x...>
print(next(gen)) # 3
print(list(gen)) # [2, 1] — থামার জায়গা থেকে চলল
ফাংশন শেষ হলে (বা return-এ পৌঁছালে) generator নিজেই StopIteration ছোঁড়ে, আর for লুপ গুছিয়ে শেষ হয়। এখানেই yield ও return-এর মূল পার্থক্য: return ফাংশন শেষ করে এবং একটি মান দেয়, yield ফাংশনকে থামায় এবং যতগুলো খুশি মান দিতে পারে।
Generator ও iterator: পার্থক্য
প্রতিটি generator একটি iterator: তার __iter__ ও __next__ আছে। কিন্তু প্রতিটি iterator generator নয়। পার্থক্য তৈরির উপায়ে এবং সুযোগ-সুবিধায়।
| Iterator (ক্লাস) | Generator | |
|---|---|---|
| কীভাবে তৈরি | ক্লাস, যার __iter__ ও __next__ আছে | yield-সহ ফাংশন বা (x for x in ...) |
| অবস্থা | নিজে attribute-এ রাখতে হয় | স্থানীয় ভেরিয়েবলে নিজে থেকেই সংরক্ষিত হয় |
| StopIteration | নিজে ছুড়তে হয় | ফাংশন শেষে নিজে আসে |
| অতিরিক্ত | যা লিখবেন | send(), throw(), close() |
দুটোর একটি মৌলিক মিল: একবারই পার হওয়া যায়। ফুরিয়ে যাওয়া generator-এর উপর আবার for চালালে কিছু আসে না — নতুন বানাতে হয়।
অসীম generator এবং বড় ফাইল
Generator শেষ হতে বাধ্য নয়। অসীম ক্রম নিরাপদ, যতক্ষণ সীমিত সংখ্যক উপাদান নেওয়া হয়:
from itertools import islice
def ids():
n = 1
while True:
yield f"ID-{n:04d}"
n += 1
print(list(islice(ids(), 3))) # ['ID-0001', 'ID-0002', 'ID-0003']
সবচেয়ে কাজের ব্যবহার বড় ফাইল লাইন ধরে ধরে প্রক্রিয়া করা। Generator-কে পাইপলাইনে জোড়া যায়, তাই মেমোরিতে থাকে শুধু একটি লাইন, পুরো ফাইল নয়:
def read_lines(path):
with open(path, encoding="utf-8") as f:
for line in f:
yield line.rstrip("\n")
def only_errors(lines):
for line in lines:
if "ERROR" in line:
yield line
for line in only_errors(read_lines("app.log")):
print(line)
yield from এবং send
yield from অন্য একটি iterable-এর সব মান পাঠিয়ে দেয়, আলাদা নেস্টেড লুপ লাগে না:
def flatten(groups):
for g in groups:
yield from g
print(list(flatten([[1, 2], [3], [4, 5]]))) # [1, 2, 3, 4, 5]
send() মেথড generator-এর ভেতরে একটি মান পাঠায়: সেটি হয়ে যায় yield এক্সপ্রেশনের ফল। প্রথম send-এর আগে next() দিয়ে generator চালু করতে হয়। যেমন একটি গড় হিসাবকারী:
def averager():
total, count = 0, 0
avg = None
while True:
value = yield avg
total += value
count += 1
avg = total / count
a = averager()
next(a) # প্রথম yield পর্যন্ত যাওয়া
print(a.send(10)) # 10.0
print(a.send(20)) # 15.0
বাস্তব কোডে send কমই লাগে, কিন্তু ইন্টারভিউতে এ নিয়ে প্রায়ই প্রশ্ন হয়।
range কি generator?
না। range একটি lazy ক্রম: সেও সব সংখ্যা জমিয়ে রাখে না, কিন্তু সূচক, len(), in-পরীক্ষা এবং যতবার খুশি পার হওয়া — এসব সমর্থন করে। Generator এর কিছুই পারে না।
r = range(5)
print(r[2], len(r), 3 in r) # 2 5 True
print(list(r), list(r)) # দুবারই [0, 1, 2, 3, 4]
List comprehension: তালিকা জেনারেটর
List comprehension হলো [এক্সপ্রেশন for x in কালেকশন] ধরনের লেখা। এটি নতুন একটি তালিকা বানায়, মূলটিতে হাত দেয় না। পড়া হয় বাঁ থেকে ডানে: আগে «কী রাখব», পরে «কোথা থেকে নেব»।
amounts = [100, 250, 400]
# সাধারণ লুপ
with_markup = []
for a in amounts:
with_markup.append(a * 1.2)
# একই কাজ comprehension দিয়ে
with_markup = [a * 1.2 for a in amounts]
print(with_markup) # [120.0, 300.0, 480.0]
«তালিকা জেনারেটর» নামটি থাকলেও ফল একটি সাধারণ তৈরি তালিকা, generator নয়: সব উপাদান একবারে হিসাব হয়ে যায়।
if ও if else শর্তসহ list comprehension
এখানে সবচেয়ে বেশি গোলমাল হয়। ফিল্টার-শর্ত যায় শেষে এবং উপাদান বাদ দেয়। বাছাই-শর্ত (ternary) যায় শুরুতে এবং প্রতিটি উপাদানকে দুটির একটি মানে বদলায়, কিছু বাদ দেয় না।
amounts = [100, 250, 400]
big = [a for a in amounts if a > 200] # [250, 400]
labels = ["বেশি" if a > 200 else "কম" for a in amounts]
# ['কম', 'বেশি', 'বেশি']
# ফিল্টার ও বাছাই একসঙ্গে
odd_marks = ["জোড়" if n % 2 == 0 else "বিজোড়" for n in range(6) if n > 2]
# ['বিজোড়', 'জোড়', 'বিজোড়']
[a for a in amounts if a > 200 else 0] লেখা সিনট্যাক্স ত্রুটি: else শুধু শুরুতে থাকা শর্তের সঙ্গে চলে।
নেস্টেড list comprehension ও দ্বিমাত্রিক তালিকা
দ্বিতীয় for লেখা হয় নেস্টেড লুপের একই ক্রমে, উপর থেকে নিচে। তালিকার তালিকা জোড়া লাগানোর সময় ক্রম গুলিয়ে ফেলার ঝুঁকি কমায়:
groups = [[1, 2], [3], [4, 5]]
flat = [x for g in groups for x in g]
# সমতুল্য:
# for g in groups:
# for x in g:
# flat.append(x)
print(flat) # [1, 2, 3, 4, 5]
দ্বিমাত্রিক তালিকা (ম্যাট্রিক্স) বানানো হয় comprehension-এর ভেতরে comprehension। প্রধান ফাঁদ তালিকার গুণ: সারিগুলো একই অবজেক্ট হয়ে যায়।
bad = [[0] * 3] * 2
bad[0][0] = 1
print(bad) # [[1, 0, 0], [1, 0, 0]] — দুটি সারিই বদলে গেছে
grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 1
print(grid) # [[1, 0, 0], [0, 0, 0]]
# ট্রান্সপোজ
m = [[1, 2, 3], [4, 5, 6]]
print([[row[i] for row in m] for i in range(3)]) # [[1, 4], [2, 5], [3, 6]]
Dictionary ও set জেনারেটর
একই কৌশল অন্য কালেকশনেও চলে। কোঁকড়া বন্ধনীতে কোলনসহ হলে dictionary (dict comprehension), কোলন ছাড়া হলে set (set comprehension)।
spent = [("food", 500), ("taxi", 300), ("food", 200)]
categories = {cat for cat, _ in spent}
print(categories) # {'food', 'taxi'} — set-এর ক্রম নিশ্চিত নয়
last = {cat: amount for cat, amount in spent}
print(last) # {'food': 200, 'taxi': 300}
squares = {n: n ** 2 for n in range(4)} # {0: 0, 1: 1, 2: 4, 3: 9}
swapped = {v: k for k, v in squares.items()}
last-এর দিকে লক্ষ করুন: একই key আবার এলে শেষ মানটি জেতে, তাই 500-এর যোগফল হারিয়ে গেছে। যোগ করতে চাইলে comprehension যথেষ্ট নয় — লুপ লিখুন, অথবা collections.Counter / defaultdict ব্যবহার করুন।
tuple জেনারেটর?
Python-এ আলাদা «tuple comprehension» নেই। গোল বন্ধনী দিলে হয় generator expression, আর tuple পেতে সেটি tuple()-এ দিতে হয়:
t = tuple(n * 2 for n in range(3)) # (0, 2, 4)
Generator expression: জমিয়ে না রেখে দেওয়া
তৃতীয় বন্ধনী বদলে বর্গাকার বন্ধনীর জায়গায় গোল বন্ধনী দিলে হয় generator expression। এটি আগেই কিছু হিসাব করে না, একটি একটি করে দেয়। এক মিলিয়ন উপাদানের ক্ষেত্রে মেমোরির পার্থক্য বিশাল:
import sys
as_list = [n for n in range(1_000_000)]
as_gen = (n for n in range(1_000_000))
print(sys.getsizeof(as_list)) # প্রায় ৮ MB
print(sys.getsizeof(as_gen)) # কয়েকশো বাইট
Generator expression বেশির ভাগ সময় aggregate ফাংশনের ভেতরে থাকে। যদি generator একমাত্র আর্গুমেন্ট হয়, অতিরিক্ত বন্ধনী লিখতে হয় না। any() ও all() প্রথম উত্তরেই থেমে যায় এবং বাকিটা দেখে না।
spent = [("food", 500), ("taxi", 300), ("food", 200)]
total = sum(a for _, a in spent) # 1000
has_big = any(a > 400 for _, a in spent) # True, এরপর 500 দেখে না
biggest = max(spent, key=lambda p: p[1]) # ('food', 500)
বাছাইয়ের নিয়ম সহজ: ফলটি একাধিকবার লাগলে, সূচক বা len() লাগলে — তালিকা নিন। একবার পার হয়ে কিছু হিসাব করলে — generator।
Scope: ভেরিয়েবল বাইরে ছড়ায় না
এখানে অভিজ্ঞরাও হোঁচট খান:
x = 10
squares = [x for x in range(3)]
print(x) # 10
Comprehension-এর ভেতরের লুপ-ভেরিয়েবল নিজের scope-এ থাকে এবং বাইরের ভেরিয়েবল বদলায় না (Python 3-এ; Python 2-এ আলাদা ছিল)। সাধারণ for লুপের পরে x হতো 2। Python 3.12 থেকে (PEP 709) comprehension আশেপাশের ফাংশনের ভেতরে সরাসরি জুড়ে যায় এবং দ্রুত চলে, তবে দৃশ্যমানতার নিয়ম একই থেকেছে।
Comprehension-এ * ও ** আনপ্যাক: Python 3.15 (PEP 798)
ফাংশন কলে এবং তালিকার লিটারালে আনপ্যাকের তারকাচিহ্ন অনেক দিন ধরেই লেখা যেত, কিন্তু comprehension-এর ভেতরে তা ছিল SyntaxError। Python 3.15-এ নিষেধ উঠেছে: *উপাদান মানে «এখানে তার সব উপাদান বসাও»। কাজ করে list, set ও generator-এ, অ্যাসিঙ্ক্রোনাস-সহও।
# Python 3.15+
groups = [[1, 2], [3], [4, 5]]
flat = [*g for g in groups] # [1, 2, 3, 4, 5]
uniq = {*g for g in groups} # set
lazy = (*g for g in groups) # অলস generator
দ্বিতীয় তারকাচিহ্ন একই কাজ করে dictionary-র জন্য। এটি স্তরভিত্তিক সেটিংসে কাজের: «ডিফল্ট → ফাইল → ফ্ল্যাগ», প্রতিটি পরের স্তর আগের key-গুলো ঢেকে দেয়।
# Python 3.15+
layers = [{"debug": False, "port": 80}, {"port": 8080}, {"debug": True}]
config = {**layer for layer in layers}
# {'debug': True, 'port': 8080}
কোর্সের দুটি সতর্কতা। তারকাচিহ্ন বন্ধনী ছাড়া শর্তসূচক এক্সপ্রেশনের সঙ্গে যায় না: [*w if w else 0 for w in weeks] সিনট্যাক্স ত্রুটি, শর্তটি বন্ধনীতে নিতে হবে। আর এই কোড Python 3.14 বা তার নিচে চলবে না: লাইব্রেরি পুরনো সংস্করণ সমর্থন করলে দ্বিতীয় for অথবা itertools.chain.from_iterable ব্যবহার করুন।
Asynchronous generator
async def ফাংশনে yield থাকলে তা asynchronous generator। এর উপর চলে async for, আর ভেতরে await দিয়ে নেটওয়ার্ক বা ডিস্কের জন্য অপেক্ষা করা যায়। Asynchronous comprehension-ও আছে।
import asyncio
async def ticks(n):
for i in range(n):
await asyncio.sleep(0.1)
yield i
async def main():
result = [i async for i in ticks(3)]
print(result) # [0, 1, 2]
asyncio.run(main())
সাধারণ ভুল
- তিন তলার comprehension। দুটি
for, দুটিifও একটি ternary এক লাইনে — এক সপ্তাহ পরে কেউ পড়তে পারবে না। এক লাইনে না ধরলে সাধারণ লুপ লিখুন: PEP 8 পাঠযোগ্যতাকে সংক্ষিপ্ততার উপরে রাখে। - generator-এর দ্বিতীয়বার পার। দ্বিতীয়
sum(gen)দেবে 0, কারণ generator ইতিমধ্যে ফুরিয়ে গেছে। - শুধু পার্শ্বপ্রতিক্রিয়ার জন্য comprehension।
[print(x) for x in xs]অপ্রয়োজনীয়None-এর তালিকা বানায়। কাজের জন্যforলুপ নিন। - generator expression থেকে তালিকার আশা।
(x for x in xs)[0]—TypeError: generator-এ সূচক নেই।
অনুশীলন
fib()নামে একটি generator লিখুন, যা অসীমভাবে Fibonacci সংখ্যা দেয়, এবংisliceদিয়ে প্রথম 10টি নিন।- এক লাইনে «শব্দ → দৈর্ঘ্য» dictionary বানান, তিন অক্ষরের বেশি শব্দগুলোর জন্য।
- তালিকা না বানিয়ে দশ লাখ পর্যন্ত জোড় সংখ্যার বর্গের যোগফল বের করুন।
chunks(seq, n)generator লিখুন, যা তালিকাকেnউপাদানের টুকরোয় ভাগ করে দেয়।
সারসংক্ষেপ
Iterator একটি একটি করে উপাদান দেয় next()-এর মাধ্যমে; generator সেটি লেখার সবচেয়ে সহজ উপায়: python yield-সহ ফাংশন বা গোল বন্ধনীর এক্সপ্রেশন। List, dict ও set comprehension এক লাইনে তৈরি কালেকশন দেয়; ফিল্টার if যায় শেষে, বাছাই if else যায় শুরুতে। মেমোরি বাঁচাতে বা ডেটার স্রোত একবার প্রক্রিয়া করতে generator নিন; ফলাফল বারবার লাগলে তালিকা। Comprehension, generator এবং Python 3.15-এর নতুন আনপ্যাক কোর্সে খরচের ট্র্যাকারের উদাহরণে বাস্তবে শেখানো হয়েছে।
সাধারণ প্রশ্ন
Generator-এর ভেতরে return ব্যবহার করা যায় কি?
হ্যাঁ। মান ছাড়া return শুধু generator শেষ করে; মানসহ return মানটি StopIteration অপবাদের value অ্যাট্রিবিউটে রাখে, যা yield from এক্সপ্রেশন পায়।
Generator-কে তালিকায় কীভাবে বদলাব?
তাকে list()-এ দিন: list(gen)। এরপর generator ফুরিয়ে যায় এবং সব মান মেমোরিতে চলে আসে।
List comprehension নাকি append-সহ লুপ, কোনটি দ্রুত?
সাধারণত comprehension একটু দ্রুত, কারণ প্রতিটি ধাপে append মেথড ডাকে না। তবে পাঠযোগ্যতা দিয়ে বাছাই করুন; পার্থক্য খুব কমই সিদ্ধান্তমূলক।
একটি অবজেক্ট generator কিনা কীভাবে জানব?
অবজেক্টের জন্য inspect.isgenerator(obj), ফাংশনের জন্য inspect.isgeneratorfunction(f)। কোনো অবজেক্ট iterator কি না, যাচাই করতে isinstance(obj, collections.abc.Iterator) ব্যবহার করুন।
Generator expression আর list comprehension-এর পার্থক্য কী?
বর্গাকার বন্ধনী পুরো তালিকা এখনই মেমোরিতে বানায়; গোল বন্ধনী একটি generator দেয়, যা চাওয়া মতো উপাদান হিসাব করে এবং একবারই পার হওয়া যায়।
“Python 3.15: от новичка до профи” কোর্সে বিষয়টি অনুশীলনসহ বিস্তারিত শেখানো হয়েছে।
কোর্সে যান