# Python-এ জেনারেটর: yield, iterator এবং list ও dict জেনারেটর

URL: https://skillaria.top/bn/guides/python/generators-python

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:
        yield start
        start -= 1

gen = countdown(3)
print(gen)          #
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-এ সূচক নেই।

## অনুশীলন

1. `fib()` নামে একটি generator লিখুন, যা অসীমভাবে Fibonacci সংখ্যা দেয়, এবং `islice` দিয়ে প্রথম 10টি নিন।
2. এক লাইনে «শব্দ → দৈর্ঘ্য» dictionary বানান, তিন অক্ষরের বেশি শব্দগুলোর জন্য।
3. তালিকা না বানিয়ে দশ লাখ পর্যন্ত জোড় সংখ্যার বর্গের যোগফল বের করুন।
4. `chunks(seq, n)` generator লিখুন, যা তালিকাকে `n` উপাদানের টুকরোয় ভাগ করে দেয়।

## সারসংক্ষেপ

Iterator একটি একটি করে উপাদান দেয় `next()`-এর মাধ্যমে; generator সেটি লেখার সবচেয়ে সহজ উপায়: python yield-সহ ফাংশন বা গোল বন্ধনীর এক্সপ্রেশন। List, dict ও set comprehension এক লাইনে তৈরি কালেকশন দেয়; ফিল্টার `if` যায় শেষে, বাছাই `if else` যায় শুরুতে। মেমোরি বাঁচাতে বা ডেটার স্রোত একবার প্রক্রিয়া করতে generator নিন; ফলাফল বারবার লাগলে তালিকা। Comprehension, generator এবং Python 3.15-এর নতুন আনপ্যাক কোর্সে খরচের ট্র্যাকারের উদাহরণে বাস্তবে শেখানো হয়েছে।

## FAQ

**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 দেয়, যা চাওয়া মতো উপাদান হিসাব করে এবং একবারই পার হওয়া যায়।


---
Source: Skillaria.Top (https://skillaria.top)
