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

iterator, generator আর yield — একটি একটি করে মান

Iterators, Generators & yield — producing values one at a time

Taught in Bangla, this lesson opens up the for loop: iterables versus iterators, iter(), next() and StopIteration, the one-pass iterator bug, writing your own iterator class, generators with yield, list versus generator memory use, pipelines with yield from, infinite generators, and send, close and PEP 479.

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

  • যেকোনো for লুপকে iter(), next() আর StopIteration-এ খুলে লেখা
  • “দ্বিতীয় লুপে কিছুই ছাপে না” বাগটি চেনা ও এড়ানো, আর কোন built-in iterator ফেরত দেয় তা জানা
  • __iter__ আর __next__ দিয়ে নিজের iterator class, তারপর একই কাজ চার লাইনের generator-এ
  • generator pipeline, yield from, অসীম generator আর islice দিয়ে কয়েকটি নেওয়া
  • কখন list ফেরত দেবেন আর কখন generator, আর AI-এর লেখা iterator-কোড যাচাইয়ের চেকলিস্ট

১ · for লুপের ভেতরের কথোপকথন — iterable আর iterator

পর্ব ১২ থেকে আপনি for লুপ লিখছেন — list-এর ওপর, string-এর ওপর, range-এর ওপর, ফাইলের লাইনের ওপর। কখনো ভেবেছেন, একই for এত ভিন্ন জিনিসের সাথে কীভাবে কাজ করে? list মেমরিতে সাজানো থাকে, কিন্তু ফাইলের লাইন থাকে ডিস্কে, আর range-এর সংখ্যাগুলো কোথাও জমানোই থাকে না। তবু লুপ একই রকম দেখতে।

রহস্যটা হলো, for লুপ নিজে কিছুই জানে না সে কিসের ওপর চলছে। সে শুধু একটি ছোট চুক্তি মানে, যার নাম iterator protocol. চুক্তিতে মাত্র দুটি প্রশ্ন:

  1. শুরুতে একবার: “আমাকে একটি iterator দিন।” — এটাই iter(obj).
  2. তারপর বারবার: “পরের জিনিসটা দিন।” — এটাই next(it). জিনিস ফুরালে উত্তর আসে একটি বিশেষ exception দিয়ে: StopIteration. for লুপ সেটা ধরে চুপচাপ থেমে যায়।

একটি গল্পের বইয়ের কথা ভাবুন। বইটি হলো iterable — এর ভেতরে সব পাতা আছে, আর যে কেউ যতবার খুশি শুরু থেকে পড়তে পারেন। পড়তে বসলে আপনি একটি বুকমার্ক রাখেন — সেটাই iterator. বুকমার্ক জানে কোন পাতা পর্যন্ত পড়া হলো, আর শুধু সামনে এগোয়। আপনার ছোট ভাইও একই বই পড়তে চাইলে তার জন্য আলাদা বুকমার্ক লাগবে — একটি বই, দুটি বুকমার্ক, দুজন দুই জায়গায়। আর বুকমার্ক শেষ পাতায় পৌঁছালে সেটা আর পেছনে যায় না; আবার পড়তে হলে নতুন বুকমার্ক নিতে হয়।

এই ছবিটা মাথায় রাখলে এই পর্বের প্রায় সব কিছু সহজ হয়ে যায়। দুটির পার্থক্য এক টেবিলে:

বিষয়iterable (বই)iterator (বুকমার্ক)
সংজ্ঞাiter() দিলে একটি iterator দেয়next() দিলে পরের জিনিস দেয়
dunder method__iter____next__, আর __iter__ যা নিজেকেই ফেরত দেয়
কতবার হাঁটা যায়যতবার খুশি — প্রতিবার নতুন iteratorএকবার — শেষে ফুরিয়ে যায়
len(), indexlist, str, range-এ হয়হয় না
উদাহরণlist, tuple, str, dict, set, rangegenerator, zip(...), map(...), enumerate(...), ফাইল object

একটি সূক্ষ্ম কিন্তু জরুরি কথা: প্রতিটি iterator নিজেও একটি iterable, কারণ তার __iter__ নিজেকেই ফেরত দেয়। এজন্যই for x in some_iterator লেখা যায়। কিন্তু উল্টোটা সত্যি নয় — list একটি iterable, iterator নয়। list-এর ওপর সরাসরি next() ডাকলে TypeError: 'list' object is not an iterator.

মূল ধারণা

iterable হলো যার কাছে “হাঁটার একটি নতুন উপায় দাও” বলা যায়। iterator হলো সেই হাঁটার চলমান অবস্থা — সে মনে রাখে কতদূর যাওয়া হলো। for লুপ, sum, list(), max, unpacking — সবাই ভেতরে এই একই দুই-ধাপের চুক্তি ব্যবহার করে। তাই আপনি নিজের class-কে এই চুক্তি মানালে সেটা সাথে সাথে Python-এর সব টুলের সাথে কাজ করবে। এটা পর্ব ৩০-এর data model আর পর্ব ৩৩-এর duck typing-এর একটি চমৎকার উদাহরণ: “for লুপে চললে সেটাই iterable”.

২ · iter(), next() আর StopIteration — for লুপ খুলে দেখা

চুক্তিটা হাতে-কলমে দেখা যাক। for x in [10, 20]: print(x) — এই দুই লাইনের লুপ Python ভেতরে মোটামুটি নিচের while লুপের মতো চালায়। ধাপে ধাপে চালিয়ে দেখুন, বুকমার্ক কীভাবে এগোয় আর লুপ কখন জানতে পারে যে শেষ:

1it = iter([10, 20])      # বুকমার্ক নাও2while True:3    try:4        x = next(it)     # পরেরটা দাও5    except StopIteration:6        break            # শেষ — লুপ থামাও7    print(x)             # লুপের body
নাম → মান (সবুজ লাইন এইমাত্র চলেছে)
it (বুকমার্ক)
x
next(it)-এর উত্তর
  1. বুকমার্ক নেওয়া। লুপ শুরুর আগে Python একবারই iter([10, 20]) ডাকে। list নিজে বদলায় না; তৈরি হয় একটি ছোট list_iterator object, যার ভেতরে শুধু দুটি জিনিস: list-টির দিকে একটি reference আর “এখন ঘর 0” — এই সংখ্যাটি।
  2. প্রথম next(). iterator ঘর 0-এর জিনিস দিল — 10 — আর নিজের ঘর-সংখ্যা এক বাড়াল। মানটি বসল x-এ। কোনো exception আসেনি, তাই except অংশ চলল না।
  3. body চলল। print(x) ছাপল 10. সাধারণ for লুপে আপনি শুধু এই অংশটিই লেখেন; বাকি সব Python আড়ালে করে।
  4. দ্বিতীয় next(). আবার লুপের শুরু, আবার “পরেরটা দাও”. এবার 20, আর বুকমার্ক list-এর শেষ প্রান্তে।
  5. body আবার। 20 ছাপা হলো। লক্ষ করুন, iterator কোথাও আগেভাগে বলেনি “আমার কাছে আর একটিই আছে”. সে শুধু চাইলে দেয়।
  6. তৃতীয় next() — StopIteration. বুকমার্ক শেষে, দেওয়ার মতো কিছু নেই। তাই সে কোনো মান ফেরত না দিয়ে StopIteration raise করল। x-এ নতুন কিছু বসেনি, আগের 20-ই রয়ে গেছে — এজন্যই সাধারণ for লুপের পরেও loop variable শেষ মান নিয়ে টিকে থাকে।
  7. break — লুপ শেষ। except ধরল সংকেতটি, break লুপ থেকে বের করে দিল। আসল for লুপে এখানে কোনো error দেখা যায় না — StopIteration হলো “স্বাভাবিক শেষ”-এর সংকেত, দুর্ঘটনা নয়। আর it এখন ফুরানো বুকমার্ক; আবার next(it) দিলে আবার StopIteration.

REPL-এ নিজে এই কথোপকথন চালাতে পারেন:

>>> it = iter("হাই")
>>> next(it), next(it)
('হ', 'া')
>>> next(it)
'ই'
>>> next(it)
Traceback (most recent call last):
  ...
StopIteration
>>> next(it, "শেষ")     # দ্বিতীয় argument = শেষ হলে এটা দাও
'শেষ'

দুটি জিনিস খেয়াল করুন। প্রথমত, string-এর iterator একটি একটি Unicode code point দেয় — তাই “হাই”-এর “া” কারটি আলাদা হয়ে এসেছে (পর্ব ০৯). দ্বিতীয়ত, next(it, default): শেষে পৌঁছালে exception না দিয়ে default মানটি দেয়। “প্রথম যে জিনিসটা শর্ত মানে, না পেলে None” — এই কাজের জন্য এটা খুব কাজের: next((m for m in marks if m < 33), None).

for-এর else কেন চলে: পর্ব ১২-এ দেখেছেন, for লুপের else চলে যখন লুপ break ছাড়া শেষ হয়। এখন কারণটা পরিষ্কার: “স্বাভাবিক শেষ” মানে iterator StopIteration দিয়েছে। আপনার নিজের break সেই পথে যায় না, তাই else বাদ পড়ে।

৩ · iterator একবারই হাঁটা যায় — “দ্বিতীয় লুপে কিছুই নেই” বাগ

বুকমার্কের উপমার সবচেয়ে বড় শিক্ষা এখানে। বুকমার্ক শেষ পাতায় পৌঁছালে সেখানেই থেকে যায়। একে বলে iterator-এর exhaustion — ফুরিয়ে যাওয়া। list-এর ওপর আপনি যতবার খুশি লুপ চালাতে পারেন, কারণ প্রতিবার for লুপ iter(list) ডেকে একটি নতুন বুকমার্ক নেয়। কিন্তু একটি iterator-এর ওপর iter() ডাকলে সে নতুন কিছু বানায় না — নিজেকেই ফেরত দেয়, সেই পুরোনো ফুরানো বুকমার্ক। ফল: দ্বিতীয় লুপ সাথে সাথে StopIteration পায়, একবারও ঘোরে না, আর কোনো error-ও দেখায় না।

এই বাগ বাস্তবে খুব প্রচলিত, কারণ চোখে দেখে বোঝা যায় না কোনটি list আর কোনটি iterator. ধাপে ধাপে দেখুন:

1sales = (int(s) for s in ["500", "300"])2print("মোট:", sum(sales))3for s in sales:4    print("বিক্রি:", s)5sales = [int(s) for s in ["500", "300"]]  # সমাধান
sales-এর অবস্থা
sales কী
iter(sales) কী দেয়
প্রথম next()-এর উত্তর
  1. generator তৈরি। পর্ব ১৮-এর generator expression — দেখতে list comprehension-এর মতো, শুধু প্রথম বন্ধনী। এখনো কোনো int() চলেনি; এটা একটি বুকমার্ক, যা চাইলে মান বানাবে।
  2. sum পুরোটা হাঁটল। sum ভেতরে iter(sales) নিল (পেল generator নিজেকেই), তারপর next, next, next — 500, 300, StopIteration. যোগফল 800 ছাপা হলো। এখন বুকমার্ক শেষ পাতায়।
  3. দ্বিতীয় লুপ — নিঃশব্দ ব্যর্থতা। for লুপ iter(sales) চাইল, পেল সেই একই ফুরানো generator. প্রথম next()-এই StopIteration, তাই লুপ “স্বাভাবিকভাবে” শেষ — একটি লাইনও ছাপা হলো না। Python-এর দৃষ্টিতে কোনো ভুল ঘটেনি, তাই কোনো traceback নেই। এটাই এই বাগকে বিপজ্জনক করে: রিপোর্টে শূন্য সারি আসে, আর আপনি ভাবেন ডেটাই নেই।
  4. সমাধান — কখন কোনটি। ডেটা একাধিকবার লাগলে list বানান (বা প্রতিবার নতুন generator তৈরি করুন, যেমন একটি ফাংশন ডেকে)। list একটি iterable, iterator নয় — তাই প্রতিটি লুপ নিজের নতুন বুকমার্ক পায়। ডেটা বিশাল আর একবারই লাগলে generator রাখুন, কিন্তু দ্বিতীয়বার ব্যবহারের চেষ্টা করবেন না।

কারা iterator ফেরত দেয়?

Python 3-এ অনেক পরিচিত built-in ফাংশন list নয়, iterator ফেরত দেয় — ঠিক এই কারণে যে বড় ডেটায় পুরো list বানানো অপচয়। তাই এদের ফল একবারই হাঁটা যায়:

লেখাফলদুবার হাঁটা যায়?
zip(a, b), map(f, xs), filter(f, xs)iteratorনা
enumerate(xs), reversed(xs)iteratorনা
open("file.txt")ফাইল object — নিজেই iterator, লাইন দেয়না (আবার পড়তে seek(0) বা নতুন করে খুলুন)
(x for x in xs), generator ফাংশনের ফলgenerator — একটি iteratorনা
range(10)অলস sequence, iterator নয়হ্যাঁ — len() আর index-ও চলে
d.keys(), d.items()dict view — iterable, iterator নয়হ্যাঁ
sorted(xs), [... for ...]নতুন listহ্যাঁ

পরীক্ষার পরিচিত ফাঁদ: pairs = zip(names, marks), তারপর print(list(pairs)) ঠিকঠাক ছাপে, কিন্তু তার পরের লাইনে dict(pairs) দেয় {}. প্রথম list() পুরো zip-টি হেঁটে ফেলেছে। ডিবাগ করার সময় শুধু “দেখার জন্য” print(list(it)) বসালেও একই কাণ্ড হয় — দেখতে গিয়েই ডেটা শেষ! একটি দ্রুত পরীক্ষা: iter(x) is x সত্য হলে x একটি iterator, একবারই হাঁটা যাবে।

৪ · নিজের iterator class — একটি লঞ্চ Countdown

চুক্তিটা যেহেতু শুধু দুটি dunder method, তাই নিজের class-কেও iterator বানানো যায়। ধরুন একটি অ্যাপে নতুন অফার চালুর আগে “৩… ২… ১…” কাউন্টডাউন দেখাতে হবে। চাই এমন একটি object, যার ওপর সরাসরি for লুপ চালানো যায়:

  • __iter__(self) — “বুকমার্ক দাও”. iterator নিজেই বুকমার্ক, তাই শুধু return self.
  • __next__(self) — “পরেরটা দাও”. অবস্থা (self.current) এক ধাপ এগিয়ে মান ফেরত দেয়; দেওয়ার কিছু না থাকলে raise StopIteration.

নিচের কোডে প্রথমে list-এর ওপর হাতে next() চালানো, তারপর Countdown class, তারপর exhaustion-এর প্রমাণ — সব একসাথে:

nums = [10, 20]
it = iter(nums)
print(next(it), next(it))
print(next(it, "শেষ"))

class Countdown:
    def __init__(self, start):
        self.current = start
    def __iter__(self):
        return self
    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        self.current -= 1
        return self.current + 1

launch = Countdown(3)
print(list(launch), "উড়ল!")
print(list(launch))
pairs = zip("ab", [1, 2])
print(list(pairs), list(pairs))
print(iter(nums) is iter(nums), iter(it) is it)

প্রথম দুই লাইন: 10 20 আর শেষ — তৃতীয় next()-এ জিনিস ছিল না, তাই default মান এল। তারপর [3, 2, 1] উড়ল! — list() ভেতরে for লুপের মতোই iter/next চালিয়েছে, আর আমাদের raise StopIteration পেয়ে থেমেছে। পরের লাইন []: একই launch object দ্বিতীয়বার খালি, কারণ self.current এখন 0 — বুকমার্ক শেষে। zip-এর লাইনে [('a', 1), ('b', 2)] [] — বাঁয়ের list(pairs) আগে চলে zip-টি ফুরিয়ে ফেলেছে, ডানেরটি কিছু পায়নি। শেষ লাইন False True: list-এর ওপর প্রতিবার iter() নতুন iterator দেয় (তাই দুটি আলাদা object), কিন্তু iterator-এর ওপর iter() নিজেকেই ফেরত দেয়।

বারবার চালানো যায় এমন Countdown

উপরের Countdown একবার ব্যবহারেই শেষ। যদি চান c = Countdown(3)-এর ওপর যতবার খুশি লুপ চালানো যাক, তাহলে বই আর বুকমার্ক আলাদা করুন: class-টি হবে শুধু iterable, যার __iter__ প্রতিবার একটি নতুন iterator ফেরত দেয়, আর __next__ থাকবেই না। সেই নতুন iterator বানানোর সবচেয়ে সহজ উপায় হলো পরের অংশের generator:

class Countdown:
    def __init__(self, start):
        self.start = start
    def __iter__(self):                 # প্রতিবার নতুন generator
        for n in range(self.start, 0, -1):
            yield n

c = Countdown(3)
print(list(c), list(c))   # [3, 2, 1] [3, 2, 1]

list, dict, str — Python-এর নিজের সংগ্রহগুলো ঠিক এভাবে কাজ করে: এরা বই, প্রতিটি লুপ নিজের বুকমার্ক পায়। নিয়মটি মনে রাখুন: সংগ্রহ (container) হবে iterable; iterator হবে আলাদা, ক্ষণস্থায়ী object.

৫ · generator — yield দিয়ে থেমে থেমে চলা ফাংশন

প্রথম Countdown class লিখতে আমাদের লেগেছে ১০ লাইন, একটি __init__, অবস্থা রাখার জন্য self.current, আর হাতে raise StopIteration. প্রতিটি নতুন ধরনের ধারার জন্য এত কিছু লেখা ক্লান্তিকর, আর ভুলের সুযোগও বেশি (শেষ মানটি একবার বেশি বা কম দেওয়া — পর্ব ১২-এর off-by-one). Python এর একটি অনেক সহজ উপায় দেয়: generator function.

def countdown(n):
    while n > 0:
        yield n
        n -= 1

for x in countdown(3):
    print(x)          # 3, 2, 1

দেখতে সাধারণ ফাংশন, শুধু return-এর জায়গায় yield. কিন্তু এই একটি শব্দ ফাংশনের আচরণ পুরোপুরি বদলে দেয়:

  • ডাকলে body চলে না। countdown(3) লিখলে ভেতরের একটি লাইনও চলে না। ফেরত আসে একটি generator object — একটি iterator, যার __iter__ আর __next__ Python নিজে বানিয়ে দিয়েছে।
  • প্রতিটি next() পরের yield পর্যন্ত চালায়। yield n-এ পৌঁছে generator মানটি দেয় আর থেমে যায় — শেষ হয় না। সব local variable (n), এমনকি কোন লাইনে থেমেছে সেটাও, জমা থাকে।
  • ফাংশন শেষ হলে StopIteration. body-র শেষে পৌঁছালে (বা return চললে) Python নিজেই StopIteration দেয়। আপনাকে হাতে raise করতে হয় না।

এই “থামা আর আবার চলা”-ই generator-এর জাদু। একটি print যোগ করে ধাপে ধাপে দেখুন, কোন লাইন কখন চলে:

1def countdown(n):2    print("শুরু")3    while n > 0:4        yield n          # থামো, মান দাও5        n -= 1           # এখান থেকে আবার6    print("শেষ")7g = countdown(2)8next(g)                  # কয়েকবার ডাকা
generator g-এর ভেতরের অবস্থা
g-এর অবস্থা
n (local)
next(g) পেল
  1. ডাকা হলো, কিন্তু কিছুই চলল না। countdown(2) একটি generator object দিল। “শুরু” এখনো ছাপা হয়নি! Python শুধু একটি frame (ফাংশনের কাজের টেবিল) তৈরি করে n = 2 সেখানে রেখেছে, আর অপেক্ষা করছে কেউ কখন প্রথম মান চাইবে।
  2. প্রথম next() — প্রথম yield পর্যন্ত। এবার body চলা শুরু: “শুরু” ছাপা হলো, while 2 > 0 সত্য, তারপর yield n — মান 2 ফেরত গেল next()-এর কাছে, আর generator ঠিক এই লাইনেই জমে গেল। return-এর মতো frame মুছে যায়নি; সব রাখা আছে।
  3. দ্বিতীয় next() — যেখানে থেমেছিল সেখান থেকে। শুরু থেকে নয়! চলা শুরু হলো yield-এর পরের লাইন থেকে: n -= 1, n এখন 1. লুপ ঘুরে আবার yield — মান 1. “শুরু” আবার ছাপা হয়নি, কারণ সেই লাইন আর চলেনি। generator মনে রেখেছে সে লুপের মাঝখানে ছিল।
  4. তৃতীয় next() — ফাংশনের শেষ। n -= 1, n = 0, while 0 > 0 মিথ্যা, লুপ থেকে বের হয়ে “শেষ” ছাপা হলো। এরপর আর yield নেই — ফাংশন শেষ, তাই Python নিজেই StopIteration দিল। for লুপ হলে এখানেই চুপচাপ থামত।
  5. আবার next() — ফুরানো generator. generator শেষ হয়ে গেছে, তার frame মুছে গেছে। যতবারই next() দিন, সাথে সাথে StopIteration — কোনো লাইন আর চলে না। অংশ ৩-এর exhaustion ঠিক এটাই।

চায়ের দোকানের কারিগরের কথা ভাবুন, যিনি অর্ডার এলে তবেই এক কাপ চা বানান। প্রথম খদ্দের এলে চুলা জ্বালান (“শুরু”), এক কাপ দেন, তারপর হাত গুটিয়ে বসে থাকেন — কিন্তু চুলা নেভান না, কেটলিতে কতটা পানি বাকি তাও মনে রাখেন। পরের খদ্দের এলে আবার চুলা জ্বালানোর দরকার নেই, যেখানে ছিলেন সেখান থেকেই আরেক কাপ। আগেভাগে ১০০ কাপ বানিয়ে টেবিলে সাজিয়ে রাখা হলো list; চাওয়া মাত্র এক কাপ বানানো হলো generator. পানি ফুরালে তিনি দোকান বন্ধ করেন — StopIteration.

return, yield আর generator object

একটি ফাংশনের ভেতরে কোথাও একটি yield থাকলেই — এমনকি এমন জায়গায়, যা কখনো চলে না — পুরো ফাংশনটি generator function হয়ে যায়। Python এটা ঠিক করে ফাংশন compile করার সময়ই, চলার সময় নয়। generator-এর ভেতরে return লেখা যায়; সেটা শুধু generator শেষ করে (আর ফেরত মানটি StopIteration-এর ভেতরে লুকানো থাকে, যা পরে yield from কাজে লাগায়)। কিন্তু for লুপ বা list() সেই return মান দেখতে পায় না।

বিষয়সাধারণ ফাংশন (return)generator function (yield)
ডাকলে কী পানফলাফল, body পুরো চলেgenerator object, body একটুও চলে না
কতগুলো মান দেয়একটিযতবার yield চলে ততগুলো, একটি একটি করে
local variablereturn-এর সাথে মুছে যায়yield-এর পর জমা থাকে
শেষ হলেমান ফেরতStopIteration (Python নিজেই দেয়)

৬ · মেমরির হিসাব — list বনাম generator

generator শুধু কম লাইনের সুবিধা নয়। এর আসল শক্তি মেমরিতে। একটি list তার সব জিনিস একসাথে মেমরিতে রাখে। generator একসাথে রাখে শুধু নিজের ছোট frame — জিনিস যতই হোক।

  • range(10**9): একশো কোটি সংখ্যার range object-এর আকার মাত্র কয়েক ডজন বাইট, কারণ সে শুধু start, stop আর step মনে রাখে। list(range(10**9)) লিখলে প্রতিটি সংখ্যার জন্য list-এ একটি reference আর একটি int object লাগবে — সাধারণ ল্যাপটপের মেমরিতে আঁটবেই না, প্রোগ্রাম MemoryError দেবে বা কম্পিউটার জমে যাবে।
  • বিশাল লগ ফাইল: একটি পেমেন্ট সার্ভারের ১০ GB-র লেনদেন-লগ। f.readlines() পুরো ফাইল একবারে list-এ তোলে — ৮ GB RAM-এর মেশিনে অসম্ভব। কিন্তু for line in f: ফাইল object-কে iterator হিসেবে ব্যবহার করে, একসাথে মেমরিতে থাকে প্রায় একটি লাইন (আর ছোট একটি buffer). ১০ GB হোক বা ১০০ GB, মেমরি একই।
  • প্রথম উত্তর দ্রুত: list আগে পুরোটা বানায়, তারপর প্রথম জিনিস দেয়। generator প্রথম জিনিস সাথে সাথে দেয়। ১০ লাখ রেকর্ডের মধ্যে প্রথম সন্দেহজনক লেনদেন খুঁজতে generator ৫০তম রেকর্ডেই থেমে যেতে পারে — বাকিগুলো কখনো পড়াই হয় না।
list = আগে সব বানাও, তারপর দাও (মেমরি ∝ জিনিসের সংখ্যা)
generator = চাওয়া মাত্র একটি বানাও (মেমরি প্রায় স্থির)

এটাই পর্ব ১৮-এ দেখা lazy evaluation. তাহলে কি সবসময় generator? না। generator-এর সীমাবদ্ধতাগুলোও জানুন — একবারই হাঁটা যায়, len() নেই, g[5] নেই, আর ছাপালে শুধু <generator object ...> দেখায়। সিদ্ধান্তের একটি সহজ নিয়ম:

পরিস্থিতিফেরত দিনকারণ
ফল ছোট, বারবার ব্যবহার হবে, len বা index লাগবেlistসরল, নিরাপদ, ডিবাগ করা সহজ
ডেটা বিশাল বা আকার অজানা (ফাইল, ডেটাবেস, API-র পাতা)generatorমেমরি স্থির থাকে
ধারা অসীম (ID, সময়ের টিক, সেন্সর)generatorlist অসীম হতে পারে না
ব্যবহারকারী হয়তো প্রথম কয়েকটিই নেবেনgeneratorবাকিগুলোর কাজ বাঁচে
ফল সাজাতে (sort) হবে, বা দুবার হাঁটতে হবেlistsort-এর জন্য সব একসাথে লাগেই

একটি মাঝামাঝি পথ: ফাংশন generator ফেরত দিক, আর যে ডাকছে তার যদি list লাগে, সে নিজে list(gen()) লিখুক। উল্টোটা সম্ভব নয় — list ফেরত দিলে মেমরির খরচ আগেই হয়ে গেছে। অনেক লাইব্রেরি এই কারণে list নয়, iterator (প্রায়ই generator) দেয়: যেমন pathlib.Path.glob() (পর্ব ২৫), os.walk(), csv.reader (পর্ব ২৬)।

৭ · generator pipeline, yield from আর অসীম generator

generator-এর সবচেয়ে সুন্দর ব্যবহার হলো কয়েকটিকে জুড়ে একটি pipeline বানানো। প্রতিটি ধাপ একটি generator, যে আগের ধাপ থেকে একটি করে জিনিস নেয় আর পরের ধাপকে একটি করে জিনিস দেয়। Linux-এর terminal-এ যাঁরা cat sales.log | grep bKash | wc -l লিখেছেন, তাঁরা ধারণাটি চেনেন — | চিহ্ন (pipe) এক প্রোগ্রামের আউটপুট আরেকটির ইনপুটে জোড়ে।

একটি বাস্তব কাজ: দোকানের বিক্রির লগ থেকে শুধু bKash লেনদেনের মোট টাকা। তিনটি ধাপ — লাইন পড়া, bKash-গুলো ছাঁকা, টাকার অঙ্ক বের করা — তারপর sum:

def read_lines(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            yield line.strip()

def only_bkash(lines):
    for ln in lines:
        if ln.startswith("bKash,"):
            yield ln

def amounts(lines):
    for ln in lines:
        yield int(ln.split(",")[1])

total = sum(amounts(only_bkash(read_lines("sales.log"))))

শেষ লাইনে কোনো কাজই হয় না, যতক্ষণ না sum প্রথম মান চায়। তারপর টানটা শেষ থেকে শুরুর দিকে চলে। ছোট একটি লগ দিয়ে ধাপে ধাপে দেখুন — একসাথে মেমরিতে কতগুলো লাইন থাকে:

  1. read_linesফাইল → লাইনউৎস
  2. only_bkashছাঁকনিbKash ছাড়া বাদ
  3. amountsলেখা → intরূপান্তর
  4. sumটানেশেষ ভোক্তা
লগ: bKash,500 / Nagad,300 / bKash,1200 — কে এইমাত্র কী দিল
read_lines দিল
only_bkash
amounts দিল
sum-এর যোগফল
মেমরিতে লাইন
  1. pipeline তৈরি, কোনো লাইন পড়া হয়নি। read_lines(...), only_bkash(...), amounts(...) — তিনটি generator object তৈরি হলো, একটি আরেকটির ভেতরে। ফাইলটি এখনো খোলাই হয়নি! শুধু sum শুরু হলেই প্রথম টান আসবে।
  2. প্রথম টান — পুরো পাইপ বেয়ে। sum বলল amounts-কে “পরেরটা দাও”; amounts বলল only_bkash-কে; only_bkash বলল read_lines-কে। read_lines ফাইল খুলে প্রথম লাইন পড়ল — 'bKash,500'. ছাঁকনি পার হলো, amounts একে 500 বানাল, sum যোগ করল। একটি মাত্র লাইন পুরো পাইপ পার হয়েছে, দ্বিতীয় লাইন এখনো ডিস্কে।
  3. দ্বিতীয় লাইন ছাঁকনিতে আটকাল। sum আবার টানল। read_lines দিল 'Nagad,300'. only_bkash এটা বাদ দিল — কিন্তু তার কাছে তো sum-কে দেওয়ার মতো কিছু নেই, তাই সে নিজের for লুপে আবার read_lines-এর কাছে চাইল। sum এই আটকে যাওয়া লাইন দেখতেই পায়নি।
  4. তৃতীয় লাইন পার। একই টানের ভেতরে read_lines দিল 'bKash,1200'; ছাঁকনি পার, amounts দিল 1200, sum এখন 1700. পুরো সময়ে মেমরিতে একসাথে একটির বেশি লাইন ছিল না — লগ ১০ কোটি লাইনের হলেও একই কথা।
  5. শেষ — StopIteration শেষ থেকে শুরুতে নয়, শুরু থেকে শেষে। read_lines-এর ফাইল ফুরাল, তার for লুপ শেষ, with ফাইল বন্ধ করল, generator শেষ — StopIteration. only_bkash-এর for লুপ সেটা পেয়ে শেষ, তারপর amounts-এর লুপ, শেষে sum থেমে 1700 ফেরত দিল। প্রতিটি ধাপ ছোট, আলাদাভাবে পরীক্ষা করা যায়, আর যেকোনো ক্রমে জোড়া যায়।

২০০৮ সালের PyCon সম্মেলনে David Beazley “Generator Tricks for Systems Programmers” নামে একটি টিউটোরিয়াল দেন। সেখানে তিনি দেখান, ওয়েব সার্ভারের বিশাল লগ ফাইল বিশ্লেষণের মতো “systems” কাজ শুধু ছোট ছোট generator জুড়ে করা যায় — ঠিক Unix pipe-এর মতো, কিন্তু একটি Python প্রোগ্রামের ভেতরে। এই ধারণা পরে Python সম্প্রদায়ে generator ব্যবহারের ধরন অনেকটা বদলে দেয় (ভাবনার প্রশ্ন ৩)।

yield from — কাজ অন্য generator-কে দিয়ে দেওয়া

কখনো একটি generator-কে আরেকটি iterable-এর সব মান হুবহু পার করে দিতে হয়। ধরুন একটি দোকানের দুটি শাখার লগ পরপর পড়বেন। লেখা যায় for x in sub: yield x, কিন্তু Python 3.3 থেকে (PEP 380) এর সংক্ষিপ্ত ও শক্তিশালী রূপ আছে — yield from:

def all_branches(paths):
    for p in paths:
        yield from read_lines(p)     # এই শাখার সব লাইন, তারপর পরের শাখা

def flatten(items):                  # যেকোনো গভীরতার nested list
    for x in items:
        if isinstance(x, list):
            yield from flatten(x)    # recursion
        else:
            yield x

print(list(flatten([1, [2, [3, 4]], 5])))   # [1, 2, 3, 4, 5]

yield from শুধু লেখা ছোট করে না। ভেতরের generator return দিয়ে যে মান দেয়, result = yield from sub() লিখলে সেটা result-এ আসে; আর পরের অংশের send() ও close()-ও সরাসরি ভেতরের generator-এ পৌঁছে যায়। সাধারণ for-yield লুপে এগুলো হাতে লিখতে হতো।

অসীম generator আর islice

generator একটি একটি মান দেয় বলে সে চিরকাল চলতে পারে — যতক্ষণ কেউ টানছে। ইনভয়েস নম্বর (INV-1001, INV-1002, …), অর্ডার ID, Fibonacci সংখ্যা — এগুলোর “শেষ” নেই। কিন্তু অসীম generator-এর ওপর সরাসরি list() বা for চালালে প্রোগ্রাম কখনো থামবে না (infinite loop). তাই দরকারমতো কয়েকটি কেটে নিতে হয়। এর জন্য standard library-র itertools module-এ আছে islice — iterator-এর slicing: islice(gen, 5) প্রথম ৫টি দিয়ে থামে, বাকিগুলো কখনো তৈরিই হয় না।

নিচের কোডে তিনটি বাস্তব কাজ: অসীম ইনভয়েস-নম্বর generator থেকে প্রথম তিনটি; bKash লেনদেনের যোগফল, যেখানে ভাঙা লাইন (abc) নিঃশব্দে বাদ যায়; আর ২৩টি রেকর্ডকে ১০টি করে পাতায় ভাগ করা (chunk বা pagination):

from itertools import count, islice

def invoice_numbers(start=1001):
    for n in count(start):            # count: 1001, 1002, ... অসীম
        yield f"INV-{n}"
def amounts(lines):
    for ln in lines:
        if (amt := ln.split(",")[1]).isdigit():
            yield int(amt)
def chunks(items, size):
    it = iter(items)
    while batch := list(islice(it, size)):
        yield batch

log = ["bKash,500", "Nagad,300", "bKash,1200", "card,700", "bKash,abc"]
bkash = (row for row in log if row.startswith("bKash,"))
print(list(islice(invoice_numbers(), 3)))
print("bKash মোট:", sum(amounts(bkash)))
for page in chunks(range(1, 24), 10):
    print(len(page), page[0], "...", page[-1])

প্রথম লাইন ['INV-1001', 'INV-1002', 'INV-1003'] — invoice_numbers() অসীম, কিন্তু islice(..., 3) তিনটি নিয়েই থেমেছে; চতুর্থ নম্বরটি কখনো বানানো হয়নি। দ্বিতীয় লাইন bKash মোট: 1700 — একটি generator expression (ছাঁকনি) আর একটি generator function (রূপান্তর) জুড়ে pipeline; bKash,abc ছাঁকনি পার হলেও isdigit() পরীক্ষায় বাদ গেছে। শেষ তিন লাইন 10 1 ... 10, 10 11 ... 20, 3 21 ... 23 — chunks একটি iterator নিয়ে প্রতিবার islice দিয়ে ১০টি করে কেটে নেয়। এখানে iterator একবারই হাঁটা যায় বলেই কৌশলটি কাজ করে: প্রতিটি islice ঠিক সেখান থেকে শুরু করে, যেখানে আগেরটি থেমেছিল। শেষ পাতায় ৩টি, তারপর খালি list এলে walrus-এর শর্ত মিথ্যা হয়ে লুপ থামে।

একই কৌশলে অসীম Fibonacci: def fib(): a, b = 0, 1; while True: yield a; a, b = b, a + b (আলাদা লাইনে লিখে), তারপর list(islice(fib(), 10)). Python 3.12 থেকে pagination-এর জন্য তৈরি itertools.batched(items, 10)-ও আছে, যা tuple দেয়। count, islice, chain, groupby সহ itertools-এর পুরো ভ্রমণ পর্ব ৩৮-এ।

৮ · আরও গভীরে — send, close আর PEP 479

send() আর close() — দুই দিকের কথা

এতক্ষণ generator শুধু মান দিয়েছে। কিন্তু yield আসলে একটি expression — এর নিজেরও একটি মান আছে। g.send(value) ডাকলে generator থেমে থাকা yield থেকে আবার চলে, আর সেই yield-এর মান হয় value. অর্থাৎ বাইরে থেকে generator-এর ভেতরে তথ্য পাঠানো যায়:

def running_average():
    total, count, avg = 0, 0, None
    while True:
        x = yield avg            # avg দাও, তারপর পরের x-এর অপেক্ষা
        total, count = total + x, count + 1
        avg = total / count

avg = running_average()
next(avg)                        # প্রথম yield পর্যন্ত চালু করা
print(avg.send(10), avg.send(20))   # 10.0 15.0
avg.close()                      # ভেতরে GeneratorExit, generator শেষ

যে generator এভাবে বাইরে থেকে মান গ্রহণ করে, তাকে বলে generator-ভিত্তিক coroutine. শুরুতে একবার next() (বা send(None)) দিয়ে প্রথম yield পর্যন্ত নিয়ে যেতে হয় — নইলে TypeError. close() থেমে থাকা yield-এর জায়গায় GeneratorExit exception তোলে; ভেতরে try/finally বা with থাকলে পরিষ্কারের কাজ (যেমন ফাইল বন্ধ) তখন চলে। একটি generator-কে মাঝপথে ছেড়ে দিলে garbage collector-ও শেষে এটাই করে।

এই ধারণা থেকেই আধুনিক Python-এর async/await জন্মেছে (ভাবনার প্রশ্ন ৫)। আজ নতুন কোডে send-ভিত্তিক coroutine খুব কম লেখা হয় — একসাথে অনেক কাজের জন্য asyncio আছে, যা পর্ব ৪২-এ দেখবেন। এখানে শুধু জেনে রাখুন যে generator দুই দিকে কথা বলতে পারে।

generator-এর ভেতরে StopIteration — এখন RuntimeError

একটি সূক্ষ্ম ফাঁদ। generator-এর ভেতরে আপনি অন্য কোনো iterator-এ next() ডাকলেন, আর সেটা ফুরিয়ে StopIteration দিল। আগে এই StopIteration generator থেকে বেরিয়ে বাইরের for লুপকে বলত “আমি শেষ” — ফলে ডেটা মাঝপথে কাটা পড়ত, কোনো error ছাড়াই। Python 3.7 থেকে (PEP 479) generator-এর body থেকে বেরিয়ে আসা StopIteration স্বয়ংক্রিয়ভাবে RuntimeError: generator raised StopIteration-এ বদলে যায়। নিঃশব্দ বাগের বদলে এখন জোরালো error.

নিয়ম: generator শেষ করতে চাইলে return লিখুন, কখনো raise StopIteration নয়। আর ভেতরে next(it) ডাকলে হয় next(it, None) দিয়ে default দিন, নয়তো try/except StopIteration: return লিখুন। (iterator class-এর __next__-এ কিন্তু raise StopIteration-ই সঠিক — PEP 479 শুধু generator-এর জন্য।)

generator expression — পর্ব ১৮-এর পুনরালোচনা

(x * x for x in nums) হলো generator function-এর এক লাইনের সংক্ষিপ্ত রূপ — ভেতরে এটা ঠিক একটি for-yield লুপওয়ালা লুকানো generator. একটি ছাঁকনি বা রূপান্তরে genexp যথেষ্ট; একাধিক ধাপ, try/except, অবস্থা মনে রাখা (যেমন আগের লাইন) বা yield from লাগলে generator function লিখুন। দুটিই একই iterator protocol মানে, তাই একই pipeline-এ মিশিয়ে ব্যবহার করা যায় — উপরের চালানো কোডে ঠিক তাই হয়েছে।

C/C++ জানলে

C-তে কোনো iterator protocol নেই — index বা pointer বাড়িয়ে হাঁটতে হয়, আর শেষ কোথায় তা আকার বা sentinel দিয়ে আপনিই ঠিক করেন। C++-এর iterator একটি অবস্থান (position) object: begin() থেকে শুরু করে end()-এর সাথে তুলনা করে থামে, আর *it দিয়ে মান পড়ে। Python-এ iterator একটি “পরেরটা দাও” object, আর শেষের সংকেত তুলনা নয়, একটি exception — তাই Python-এর iterator পেছনে যেতে বা লাফ দিতে পারে না। C++20-এর coroutine আর C++23-এর std::generator (co_yield) Python-এর yield-এর কাছাকাছি। বিস্তারিত দেখুন C++ কোর্সের iterator পর্বে।

পরীক্ষা ও ইন্টারভিউতে
  • iterable আর iterator-এর পার্থক্য? — iterable-এ __iter__ থাকে, যা iterator দেয়; iterator-এ __next__ থাকে, আর তার __iter__ নিজেকেই দেয়। list iterable, iter(list) iterator.
  • yield আর return-এর পার্থক্য? — return ফাংশন শেষ করে একটি মান দেয়; yield মান দিয়ে ফাংশনকে থামিয়ে রাখে, local অবস্থাসহ, পরের next()-এ আবার চলে।
  • g = (x for x in range(3)); list(g) দুবার ডাকলে? — প্রথমবার [0, 1, 2], দ্বিতীয়বার []; generator একবারই হাঁটা যায়।
  • generator মেমরি বাঁচায় কেন? — সব মান একসাথে রাখে না; চাওয়া মাত্র একটি বানায়, তাই মেমরি জিনিসের সংখ্যার ওপর নির্ভর করে না।
  • StopIteration কে ধরে? — for লুপ, list(), sum()-এর মতো ভোক্তারা নিজেরাই ধরে লুপ থামায়; সাধারণ কোডে হাতে ধরতে হয় না।

৯ · AI-এর যুগে — AI-এর লেখা iterator-কোড যাচাই

AI কোড-সহকারীরা ছোট উদাহরণে প্রায় নিখুঁত কোড দেয়, কারণ ছোট ডেটায় list আর generator-এর পার্থক্য চোখে পড়ে না। সমস্যা দেখা দেয় আসল ডেটায় — ১০ GB লগ, ১০ লাখ রেকর্ড, বা একই iterator দুই জায়গায়। AI-এর লেখা কোডে বারবার যেসব ভুল দেখা যায়:

AI প্রায়ই যা লেখেসমস্যাভালো রূপ
lines = f.readlines(), তারপর লুপপুরো ফাইল মেমরিতে; বড় ফাইলে ক্র্যাশfor line in f:
ফাংশনে result = [] … append … return result, শুধু একবার হাঁটার জন্যবিশাল list অকারণেgenerator function (yield)
একই zip/map/generator দুটি লুপেদ্বিতীয় লুপ খালি, কোনো error নেইlist বানানো, বা প্রতিবার নতুন generator
len(gen) বা gen[0]TypeErrorsum(1 for _ in ...), next(gen), বা list
generator-এর ভেতরে raise StopIterationPython 3.7 থেকে RuntimeErrorreturn
try: ... except StopIteration: pass পুরো লুপ ঘিরেআসল বাগ চাপা পড়েnext(it, default) বা for লুপ

AI-কে ডেটা-প্রক্রিয়ার কোড লিখতে বললে ডেটার আকার আর ব্যবহারের ধরন আগেই বলে দিন। নিচের কাঠামোটি কপি করে ব্যবহার করুন:

আমি Python 3.12 বা নতুন version ব্যবহার করি। একটি CSV-ধাঁচের বিক্রির লগ ফাইল আছে (প্রতি লাইনে: পেমেন্ট-পদ্ধতি,টাকা), আকার ৫ GB পর্যন্ত হতে পারে, আর মেশিনে RAM ৪ GB. শুধু bKash লেনদেনের মোট টাকা আর লেনদেনের সংখ্যা বের করার কোড দাও। শর্ত: ১. পুরো ফাইল কখনো মেমরিতে তুলবে না — readlines(), read() বা বড় list চলবে না; ফাইলের ওপর সরাসরি for লুপ বা generator ব্যবহার করো। ২. কাজটা ছোট generator ধাপে ভাগ করো (লাইন পড়া → ছাঁকা → সংখ্যা বের করা), প্রতিটি আলাদা ফাংশনে। ৩. কোনো iterator বা generator দুবার হাঁটবে না; মোট আর সংখ্যা এক লুপেই হিসাব করো। ৪. generator-এর ভেতরে raise StopIteration লিখবে না, আর len() কোনো generator-এ ডাকবে না। ৫. ভাঙা লাইন (টাকার জায়গায় লেখা) পেলে বাদ দিয়ে গুনে রাখো, আর শেষে কতগুলো বাদ গেল জানাও। ৬. প্রতিটি ধাপের নিচে এক লাইনে বাংলায় বলো, একসাথে মেমরিতে কতটুকু ডেটা থাকে।

AI-এর লেখা iterator/generator কোড পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Python 2.2-এর আগে iterator protocol ছিল না, তবু for লুপ চলত। তখন কীভাবে চলত, PEP 234 কী বদলাল, আর শেষের সংকেত হিসেবে Python কেন একটি exception বেছে নিল — Java-র hasNext() বা C++-এর end()-তুলনা নয়?

Python 2.2-এর আগে for লুপ একটি পুরোনো নিয়ম মানত, যাকে বলা হয় sequence protocol. লুপ object-কে index দিয়ে চাইত — obj[0], obj[1], obj[2] — অর্থাৎ ভেতরে __getitem__ ডাকত, যতক্ষণ না IndexError আসে। list আর string-এ এটা চমৎকার কাজ করে। কিন্তু ফাইলের লাইন, নেটওয়ার্ক থেকে আসা ডেটা বা dict-এর চাবির মতো জিনিসে “৫ নম্বর জিনিসটা দাও” প্রশ্নটার কোনো স্বাভাবিক মানে নেই। ফাইলের ওপর লুপ চালাতে তাই readlines() দিয়ে পুরো ফাইল list-এ তুলতে হতো, বা ভান করতে হতো যে object-টি index বোঝে।

২০০১ সালের শেষে Python 2.2-এ PEP 234 (লেখক Ka-Ping Yee ও Guido van Rossum) নতুন চুক্তি আনল: iter() আর next() (তখন method-টির নাম ছিল next, Python 3-এ হলো __next__). ফাইল, dict — সবাই নিজের iterator দিতে শিখল, আর for line in f সম্ভব হলো। পুরোনো sequence protocol বাদ যায়নি — যে class-এ শুধু __getitem__ আছে, iter() আজও তার জন্য একটি iterator বানিয়ে দেয়, যাতে পুরোনো কোড না ভাঙে।

শেষের সংকেত নিয়ে তিনটি নকশা সম্ভব ছিল। Java-র মতো hasNext() আর next() — দুটি method: প্রতিটি জিনিসের জন্য দুবার ডাকা, আর অনেক ধারায় (যেমন নেটওয়ার্ক) “আর আছে কি?” জানতে হলে আসলে পরের জিনিসটি পড়েই ফেলতে হয়, তাই iterator-কে সেটা লুকিয়ে জমা রাখতে হয়। C++-এর মতো শেষ-অবস্থানের সাথে তুলনা: এতে দুটি object লাগে, আর অসীম বা অজানা-দৈর্ঘ্যের ধারায় “শেষ অবস্থান” ধারণাটাই খাপ খায় না। একটি বিশেষ sentinel মান (যেমন None) ফেরত দেওয়া: তাহলে None নিজে কখনো ধারার জিনিস হতে পারত না।

Python বেছে নিল একটি method আর একটি exception. প্রতিটি জিনিসে একবার ডাকা, যেকোনো মান (None-সহ) বৈধ, আর শেষটি এমন একটি সংকেত যা কোনো মানের সাথে গুলিয়ে যায় না। Python-এ exception তুলনামূলক সস্তা, আর ধারার শেষে মাত্র একবার ঘটে — তাই খরচ নগণ্য। এটা Python-এর EAFP দর্শনের সাথেও মেলে: আগে জিজ্ঞেস না করে চেষ্টা করো, না হলে exception সামলাও।

মূল উপলব্ধি: একটি ছোট, সাধারণ চুক্তি পুরো ভাষাকে একসাথে বাঁধতে পারে। iterator protocol আসার পর for লুপ, sum, unpacking, comprehension আর আপনার নিজের class — সবাই একই ভাষায় কথা বলে। ভালো interface-এর মাপকাঠি হলো, সেটা কত কম জিনিস দাবি করে।

প্র ০২generator-এর ধারণা Python-এর নিজের নয় — PEP 255 নিজেই Icon আর CLU ভাষার কথা বলে। এই ধারণা কোথা থেকে এল, আর Python 2.2-তে যোগ করার সময় কোন বিতর্ক হয়েছিল?

১৯৭০-এর দশকে MIT-তে Barbara Liskov-এর দল CLU নামে একটি ভাষা বানায়। CLU-তে ছিল “iterator” নামে বিশেষ ধরনের procedure, যা yield দিয়ে একটি একটি মান দিত, আর for লুপ তা গ্রহণ করত — প্রায় আজকের Python generator-এর মতো, যদিও সেটা for লুপের বাইরে আলাদা object হিসেবে রাখা যেত না। Liskov পরে ২০০৮ সালে Turing Award পান, মূলত data abstraction নিয়ে কাজের জন্য। একই সময়ে University of Arizona-তে Ralph Griswold-এর তৈরি Icon ভাষায় generator ছিল ভাষার কেন্দ্রে: সেখানে প্রায় যেকোনো expression একাধিক মান দিতে পারত, আর ভাষা নিজেই দরকারমতো পরের মান চাইত।

Python-এ generator যোগ হয় Python 2.2-এ (২০০১ সালের শেষে), PEP 255 অনুযায়ী — লেখক Neil Schemenauer, Tim Peters আর Magnus Lie Hetland. Tim Peters দীর্ঘদিন ধরে Icon-এর generator-এর ভক্ত ছিলেন, আর PEP-এর প্রেরণা অংশে স্পষ্ট বলা আছে: অনেক কাজে একটি ফাংশনকে মান দিয়েও নিজের অবস্থা না হারিয়ে চালিয়ে যেতে হয়, আর callback বা class দিয়ে সেটা লেখা কষ্টকর। যেহেতু yield একটি নতুন keyword, যা পুরোনো কোডে variable-এর নাম হিসেবে থাকতে পারত, তাই 2.2-তে এটা ব্যবহার করতে from __future__ import generators লিখতে হতো; Python 2.3 থেকে এটা সবসময় চালু।

সবচেয়ে বড় বিতর্ক ছিল একটি শব্দ নিয়ে। generator function আচরণে সাধারণ ফাংশন থেকে একদম আলাদা — ডাকলে body চলে না। অনেকে চাইলেন আলাদা keyword, যেমন gen বা generator, যাতে প্রথম লাইনেই বোঝা যায়। Guido শেষে সিদ্ধান্ত দেন (PEP-এ “BDFL Pronouncement” হিসেবে লেখা) যে def-ই থাকবে: দুটোই কোড লেখার একক, আর ভেতরে yield আছে কি না সেটাই আসল পরিচয়। আজও এর দাম দিতে হয় — বড় ফাংশনের মাঝখানে একটি yield ঢুকিয়ে দিলে পুরো ফাংশনের আচরণ নিঃশব্দে বদলে যায়, যা নতুনদের অবাক করে।

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

প্র ০৩David Beazley ২০০৮ সালে দেখান যে generator জুড়ে Unix pipe-এর মতো pipeline বানানো যায়। একটি সিস্টেম-নকশার দৃষ্টিতে এই ধরন কেন এত শক্তিশালী, আর এর সীমাবদ্ধতা কোথায়?

১৯৭৩ সালে Bell Labs-এ Doug McIlroy-এর প্রস্তাবে Ken Thompson Unix-এ pipe যোগ করেন। ধারণাটা ছিল: প্রতিটি প্রোগ্রাম একটি কাজ ভালোভাবে করবে, আর এক প্রোগ্রামের আউটপুট আরেকটির ইনপুটে জুড়ে বড় কাজ হবে — cat | grep | sort | uniq. এটাই পরে “Unix দর্শন” নামে পরিচিত হয়। প্রতিটি ধাপ ডেটার একটি স্রোত (stream) নিয়ে কাজ করে, পুরো ফাইল একবারে নয়।

২০০৮ সালের PyCon-এ David Beazley “Generator Tricks for Systems Programmers” টিউটোরিয়ালে দেখান যে Python generator ঠিক এই কাজটিই একটি প্রোগ্রামের ভেতরে করে। তাঁর উদাহরণ ছিল ওয়েব সার্ভারের বিশাল লগ: ফাইল খোঁজা → খোলা → লাইন পড়া → regex দিয়ে ভাঙা → dict বানানো → ছাঁকা → যোগ করা, প্রতিটি একটি ছোট generator. তিনি দেখান, এমনকি একটি সাধারণ ল্যাপটপে কয়েক GB লগ এভাবে কম মেমরিতে প্রক্রিয়া করা যায়, আর একটি ক্রমাগত বাড়তে থাকা লগ (tail -f-এর মতো) পড়ার generator জুড়ে দিলে একই pipeline লাইভ ডেটাতেও চলে।

কেন শক্তিশালী? প্রথমত, মেমরি স্থির — ধারা যত বড়ই হোক। দ্বিতীয়ত, প্রতিটি ধাপ আলাদা: ছাঁকনি জানে না ডেটা ফাইল থেকে এল না ডেটাবেস থেকে, তাই ছোট list দিয়ে আলাদাভাবে পরীক্ষা করা যায় (পর্ব ৩৯-এর unit test). তৃতীয়ত, জোড়া-খোলা সহজ — নতুন একটি ছাঁকনি মাঝখানে বসানো এক লাইনের কাজ। আর চতুর্থত, অলসতা: শেষ ভোক্তা যদি প্রথম ১০টি ফল নিয়ে থেমে যায়, উৎস আর একটি লাইনও পড়ে না।

সীমাবদ্ধতাও আছে। generator pipeline একটি একক thread-এ চলে; Unix pipe-এর প্রতিটি প্রোগ্রাম আলাদা process, যা একসাথে একাধিক CPU core ব্যবহার করতে পারে। sort-এর মতো কাজ, যার জন্য সব ডেটা একসাথে দেখতে হয়, স্বভাবতই অলস হতে পারে না। কোনো ধাপে error হলে traceback কয়েক স্তর গভীর হয়, আর ডিবাগ করতে গিয়ে print(list(stage)) লিখলে স্রোতই ফুরিয়ে যায়। খুব বড় ডেটায় পরে লোকে pandas বা বিশেষ টুলে যায়, কিন্তু সেগুলোর অনেকেও ভেতরে “chunk ধরে ধরে” এই একই ভাবনা ব্যবহার করে।

মূল উপলব্ধি: ভালো নকশা প্রায়ই পুরোনো, প্রমাণিত ধারণার নতুন জায়গায় প্রয়োগ। Beazley নতুন কোনো ভাষা-সুবিধা আবিষ্কার করেননি — তিনি দেখিয়েছিলেন, যা আগে থেকেই ছিল, সেটা দিয়ে কীভাবে ৩৫ বছরের পুরোনো Unix দর্শন একটি Python প্রোগ্রামের ভেতরে আনা যায়।

প্র ০৪Python 3.7-এর আগে generator-এর ভেতরে StopIteration raise করা বৈধ ছিল, এমনকি generator শেষ করার একটি প্রচলিত উপায় ছিল। PEP 479 কেন এটা বদলাল, আর পরিবর্তনটি কয়েক বছর ধরে ধাপে ধাপে কেন আনতে হলো?

সমস্যাটা বুঝতে একটি ছোট উদাহরণ ভাবুন। একটি generator দুটি ধারা থেকে জোড়া বানায়: প্রতিটি ধাপে প্রথম ধারা থেকে একটি জিনিস নেয়, তারপর ভেতরে next(other) দিয়ে দ্বিতীয় থেকে আরেকটি। দ্বিতীয় ধারাটি আগে ফুরালে next(other) StopIteration দেয়। পুরোনো Python-এ এই exception generator-এর body থেকে বেরিয়ে যেত — আর বাইরের for লুপের কাছে এটা দেখতে হুবহু “generator স্বাভাবিকভাবে শেষ” সংকেতের মতো। ফল: ডেটার একটি অংশ নিঃশব্দে হারিয়ে যেত। কোনো error নেই, কোনো traceback নেই, শুধু রিপোর্টে কম সারি।

আসল দোষ হলো একই সংকেতের দুই অর্থ: “আমি ইচ্ছে করে শেষ করছি” আর “ভেতরে কোথাও একটি iterator দুর্ঘটনায় ফুরিয়েছে”. Python-এর দর্শনে (Zen of Python) বলা আছে, “errors should never pass silently” — ভুল কখনো নিঃশব্দে পার পাওয়া উচিত নয়। কিন্তু এখানে ঠিক তাই হচ্ছিল।

২০১৪ সালে Chris Angelico আর Guido van Rossum PEP 479 প্রস্তাব করেন: generator-এর body থেকে StopIteration বেরিয়ে এলে সেটা RuntimeError-এ বদলে যাবে। ইচ্ছে করে শেষ করতে চাইলে return — যা Python 3.3 থেকেই generator-এ মান-সহ লেখা যেত। এখন দুই অর্থ আলাদা: return মানে শেষ, StopIteration ফাঁস হওয়া মানে বাগ।

কিন্তু হাজার হাজার পুরোনো লাইব্রেরি ইচ্ছে করেই raise StopIteration লিখত। হঠাৎ বদলালে সেগুলো ভেঙে যেত। তাই পরিবর্তন এল ধাপে ধাপে, Python-এর পরিচিত পদ্ধতিতে: Python 3.5-এ ঐচ্ছিক — from __future__ import generator_stop লিখলে নতুন আচরণ; 3.6-এ পুরোনো আচরণে সতর্কবার্তা (DeprecationWarning); আর Python 3.7-এ (২০১৮) নতুন আচরণ সবার জন্য। লাইব্রেরি-লেখকেরা কয়েক বছর সময় পেলেন।

মূল উপলব্ধি: একটি সংকেতের যদি দুটি অর্থ থাকে, বাগ সেই ফাঁক দিয়ে ঢুকবেই। আর একটি জনপ্রিয় ভাষায় সঠিক পরিবর্তনও একদিনে আনা যায় না — __future__, সতর্কবার্তা, তারপর বাধ্যতামূলক: এই তিন ধাপ বড় সিস্টেমে যেকোনো ভাঙন-ঘটানো পরিবর্তনের একটি ভালো আদর্শ।

প্র ০৫একটি সাধারণ “মান দেওয়ার” যন্ত্র — generator — থেকে কীভাবে আজকের async/await এল? PEP 342, PEP 380 আর PEP 492-এর যাত্রাপথ কী, আর শেষে আলাদা syntax কেন দরকার হলো?

PEP 255-এর generator ছিল একমুখী: শুধু বাইরে মান দেয়। কিন্তু থামা আর আবার চলার ক্ষমতাটি আসলে আরও বড় কিছুর বীজ — coroutine, অর্থাৎ এমন ফাংশন যা নিজের ইচ্ছায় থেমে অন্যকে চলতে দেয়, পরে আবার চলে। এই ধারণা ১৯৫০-এর দশকের শেষে Melvin Conway-এর কাজে প্রথম নাম পায়।

২০০৬ সালে Python 2.5-এ PEP 342 (Guido van Rossum ও Phillip J. Eby) generator-কে দুইমুখী করে: yield হলো একটি expression, আর এল send(), throw() ও close(). এখন একটি generator থেমে থাকা অবস্থায় বাইরে থেকে মান বা exception পেতে পারে। Twisted-এর মতো লাইব্রেরি আর David Beazley-র ২০০৯ সালের “A Curious Course on Coroutines and Concurrency” দেখাল, এ দিয়ে একটি thread-এ বহু কাজ পালা করে চালানো যায় — যখন একটি কাজ নেটওয়ার্কের উত্তরের অপেক্ষায়, তখন অন্যটি চলে।

সমস্যা ছিল, coroutine-এর ভেতর থেকে আরেকটি coroutine ডাকা কষ্টকর ছিল — send, throw, close সব হাতে পার করতে হতো। ২০১২ সালে Python 3.3-এ PEP 380 (Gregory Ewing) আনল yield from, যা এই সবকিছু স্বয়ংক্রিয়ভাবে করে আর ভেতরের coroutine-এর return মানও ফিরিয়ে দেয়। এর ওপর ভিত্তি করে Python 3.4-এ (২০১৪) এল standard library-র asyncio (PEP 3156), যেখানে coroutine মানে ছিল yield from লেখা generator.

কিন্তু এতে বিভ্রান্তি থেকে গেল: একটি ফাংশন generator না coroutine, তা দেখে বোঝা যেত না; ভুল করে coroutine-এর ওপর for লুপ চালানো যেত। তাই ২০১৫ সালে Python 3.5-এ PEP 492 (Yury Selivanov) আলাদা syntax আনল: async def আর await. ভেতরের যন্ত্র একই রকম — থামা আর আবার চলা — কিন্তু এখন coroutine একটি আলাদা ধরনের object, generator-এর সাথে গুলিয়ে যায় না। Python 3.7-এ async আর await পূর্ণ keyword হয়, আর পুরোনো generator-ভিত্তিক coroutine-এর সাজসজ্জা (@asyncio.coroutine) Python 3.11-এ সরিয়ে ফেলা হয়।

মূল উপলব্ধি: একটি সরল যন্ত্র — “থামো, অবস্থা রাখো, পরে চলো” — একাধিক বড় ধারণার ভিত্তি হতে পারে। কিন্তু একই syntax দুটি ভিন্ন উদ্দেশ্যে ব্যবহার হলে পাঠক বিভ্রান্ত হয়; তাই শেষে Python উদ্দেশ্য অনুযায়ী আলাদা নাম দিয়েছে। asyncio-র বিস্তারিত পর্ব ৪২-এ।

অনুশীলন

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

  1. হাতে চালান: চালানোর আগে খাতায় লিখুন প্রতিটি লাইন কী দেবে — it = iter([7, 8]); (ক) next(it); (খ) next(it); (গ) next(it, 0); (ঘ) list(it); (ঙ) list(iter([7, 8])). তারপর REPL-এ মিলিয়ে নিন।

    (ক) 7, (খ) 8 — বুকমার্ক এখন শেষে। (গ) 0 — জিনিস নেই, তাই StopIteration-এর বদলে default মান। (ঘ) [] — একই ফুরানো iterator, list() প্রথম next()-এই StopIteration পায়। (ঙ) [7, 8] — এখানে list থেকে একটি নতুন iterator নেওয়া হয়েছে, তাই শুরু থেকে। শিক্ষা: ফুরিয়ে যায় iterator, list নয়।

  2. বাগ খুঁজুন: একটি রিপোর্ট-স্ক্রিপ্টে লেখা আছে prices = map(float, raw_prices), তারপর total = sum(prices), তারপর avg = total / len(list(prices)). চালালে ZeroDivisionError আসে, অথচ raw_prices-এ ৫টি দাম আছে। কেন? দুটি ভিন্ন সমাধান বলুন।

    Python 3-এ map একটি iterator দেয়। sum(prices) সেটা পুরো হেঁটে ফুরিয়ে ফেলেছে; তাই list(prices) হলো [], দৈর্ঘ্য 0, আর 0 দিয়ে ভাগ। সমাধান ১: শুরুতেই list বানান — prices = [float(p) for p in raw_prices] — তারপর যতবার খুশি ব্যবহার করুন। সমাধান ২ (ডেটা বিশাল হলে): এক লুপেই যোগফল আর সংখ্যা গুনুন — for p in prices: total += p; n += 1 (আলাদা লাইনে) — যাতে iterator একবারই হাঁটা হয়। লক্ষ করুন, ভুলটি দেখা দিয়েছে একদম অন্য জায়গায় (ভাগে), আসল কারণ থেকে দুই লাইন দূরে — exhaustion বাগ প্রায়ই এভাবেই লুকায়।

  3. generator লিখুন (একমাত্র কোড-লেখার কাজ): একটি অসীম generator fib() লিখুন, যা 0, 1, 1, 2, 3, 5, … দেয়। তারপর islice দিয়ে প্রথম ১০টি ছাপান। বোনাস: প্রথম যে Fibonacci সংখ্যা ১০০০-এর বেশি, সেটা next() আর একটি generator expression দিয়ে এক লাইনে বের করুন।
    from itertools import islice
    
    def fib():
        a, b = 0, 1
        while True:
            yield a
            a, b = b, a + b
    
    print(list(islice(fib(), 10)))      # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
    print(next(x for x in fib() if x > 1000))   # 1597

    while True থাকা সত্ত্বেও প্রোগ্রাম আটকায় না, কারণ generator শুধু চাওয়া মাত্র একটি মান বানায়। islice ১০টি নিয়ে থামে; বোনাসে next() প্রথম মিলেই থামে। কিন্তু list(fib()) লিখলে প্রোগ্রাম কখনো শেষ হবে না, আর মেমরি ভরে যাবে।

  4. AI অডিট: একটি AI এই ফাংশন দিয়েছে: def bkash_rows(path): rows = open(path).readlines(); out = []; for r in rows: if "bKash" in r: out.append(r); return out. তারপর কোডে rows = bkash_rows("sales.log"), print(len(rows)), আর দুটি আলাদা লুপ। লগ ফাইল ৬ GB. কী কী সমস্যা, আর generator-এ বদলালে কোন লাইনগুলোও বদলাতে হবে?

    সমস্যা তিনটি। (১) readlines() পুরো ৬ GB মেমরিতে তোলে, তারপর out আরও একটি বড় list — ছোট মেশিনে ক্র্যাশ। (২) open() কোনো with ছাড়া — ফাইল কখন বন্ধ হবে নিশ্চিত নয় (পর্ব ২৫)। (৩) "bKash" in r লাইনের যেকোনো জায়গায় খোঁজে — নোটে “bKash” লেখা থাকলেও মিলবে; r.startswith("bKash,") বেশি নির্ভুল।

    generator রূপ: with open(path, encoding="utf-8") as f:, তারপর for r in f: if r.startswith("bKash,"): yield r. কিন্তু সাবধান — ডাকার দিকের কোডও বদলাতে হবে। len(rows) এখন TypeError দেবে; দুটি আলাদা লুপের দ্বিতীয়টি কিছুই পাবে না। তাই সংখ্যা গোনা আর অন্য কাজ এক লুপে করুন, বা দরকার হলে ফাংশনটি দুবার ডাকুন (প্রতিবার ফাইল নতুন করে পড়া হবে, কিন্তু মেমরি স্থির থাকবে)। শিক্ষা: list থেকে generator-এ যাওয়া শুধু ফাংশনের ভেতরের পরিবর্তন নয় — যারা ফলটি ব্যবহার করে, তাদের চুক্তিও বদলায়।

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

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

ক্রমPython-এ একটি generator function ডাকা থেকে শেষ হওয়া পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📦 ফাংশন ডাকলে একটি generator object তৈরি হয় — body একটুও চলে না
  2. ▶️ প্রথম next()-এ body শুরু থেকে চলে প্রথম yield পর্যন্ত
  3. ⏸️ yield মানটি ফেরত দেয়, আর generator local অবস্থাসহ থেমে থাকে
  4. 🔁 পরের next()-এ yield-এর ঠিক পরের লাইন থেকে আবার চলে
  5. 🛑 body শেষ হলে (বা return) Python নিজেই StopIteration দেয়

Python-এ g = (x * x for x in range(3)), তারপর print(list(g), list(g)) চালালে কী ছাপা হবে?

  • [0, 1, 4] [0, 1, 4]
  • [0, 1, 4] []
  • [0, 1, 4] তারপর StopIteration traceback
  • <generator object ...> দুবার

Python 3-এ নিচের কোনটির ফল একটি iterator (অর্থাৎ iter(x) is x সত্য, আর একবারই হাঁটা যায়)?

  • [1, 2, 3]
  • range(3)
  • {"a": 1}.items()
  • zip("ab", [1, 2])

একটি Python ফাংশনের body-তে yield আছে। সেই ফাংশনটি ডাকার সাথে সাথে কী ঘটে?

  • একটি generator object ফেরত আসে; body-র কোনো লাইন এখনো চলে না
  • body প্রথম yield পর্যন্ত চলে, আর প্রথম মানটি ফেরত আসে
  • body পুরো চলে, আর সব yield করা মানের একটি list ফেরত আসে
  • None ফেরত আসে, কারণ কোনো return নেই

Python-এ len(x for x in range(5)) চালালে কী হবে?

  • 5
  • 0
  • TypeError
  • StopIteration

Python 3.7 বা নতুন version-এ একটি generator function-এর body থেকে StopIteration বেরিয়ে এলে (যেমন ভেতরে raise StopIteration লিখলে) কী হয়?

  • generator চুপচাপ স্বাভাবিকভাবে শেষ হয়
  • সেটা RuntimeError-এ বদলে যায়
  • SyntaxError হয়
  • exception উপেক্ষা করে generator পরের yield-এ চলে যায়

Python-এ def g(): yield 1; yield from [2, 3]; yield 4 (প্রতিটি statement আলাদা লাইনে) — list(g())-এর মান কী?

  • [1, 2, 3, 4]
  • [1, [2, 3], 4]
  • [1, 2, 3]
  • TypeError

শব্দকোষ

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

মনে রাখুন

  1. for লুপ = iter() একবার + next() বারবার + StopIteration-এ থামা। sum, list, unpacking, comprehension — সবাই এই একই চুক্তি মানে।
  2. iterable বই, iterator বুকমার্ক। list, dict, range বারবার হাঁটা যায়; generator, zip, map, filter, enumerate, ফাইল একবারই — দ্বিতীয় লুপ নিঃশব্দে খালি।
  3. নিজের iterator: __iter__ (return self) আর __next__ (শেষে raise StopIteration). সংগ্রহ বানালে __iter__-কে generator বানান, যাতে প্রতিবার নতুন iterator আসে।
  4. yield ফাংশনকে থামায়, শেষ করে না — local অবস্থা জমা থাকে, পরের next()-এ সেখান থেকে চলে। তাই বিশাল বা অসীম ধারা স্থির মেমরিতে; অসীম হলে islice দিয়ে কাটুন।
  5. generator pipeline — ছোট ধাপ জুড়ে লাইন একটি একটি টানা; yield from দিয়ে কাজ ভাগ; generator শেষ করতে return, কখনো raise StopIteration নয় (3.7 থেকে RuntimeError).

পরের পর্বে: এই পর্বে দেখলেন, একটি ফাংশন থেমে থেমে চলতে পারে। পরের পর্বে দেখবেন, একটি ফাংশনকে আরেকটি ফাংশন দিয়ে মুড়িয়ে দেওয়া যায় — না বদলেই তার আগে-পরে সময় মাপা, log লেখা বা ফল মনে রাখার কাজ জুড়ে দেওয়া। @ চিহ্নের রহস্য — decorator.

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

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