Python में जनरेटर: yield, इटरेटर और सूची व डिक्शनरी के जनरेटर
Python में जनरेटर एक ऑब्जेक्ट है, जो मान एक-एक करके और माँगे जाने पर देता है, पूरी श्रृंखला मेमोरी में नहीं रखता; इसे yield वाले फ़ंक्शन या कोष्ठक में लिखे जनरेटर व्यंजन से बनाया जाता है। सूची, डिक्शनरी और सेट के कॉम्प्रिहेंशन एक पंक्ति में तैयार संग्रह बनाने का छोटा तरीका हैं: [x * 2 for x in xs], {k: v for k, v in pairs}, {x for x in xs}।
Python में जनरेटर एक ऑब्जेक्ट है, जो मान एक-एक करके देता है, और तभी जब उससे माँगा जाए। पूरी श्रृंखला वह मेमोरी में नहीं रखता। इसे दो तरीकों से बनाया जाता है: yield कीवर्ड वाले फ़ंक्शन से, या कोष्ठक में लिखे जनरेटर व्यंजन से। इनके साथ सूची, डिक्शनरी और सेट के «जनरेटर» (comprehensions) भी हैं — एक पंक्ति में पूरा संग्रह तैयार करने का छोटा तरीका। यहाँ देखते हैं कि python yield कैसे काम करता है, जनरेटर इटरेटर से कैसे अलग है, और comprehension को अपठनीय बनने से कैसे बचाएँ।
इटरेटर: for लूप की बुनियाद
जनरेटर समझने से पहले इटरेटर समझना ज़रूरी है। इटरेबल (iterable) वह चीज़ है जिस पर लूप चलाया जा सकता है: सूची, स्ट्रिंग, डिक्शनरी, फ़ाइल। इटरेटर वह ऑब्जेक्ट है जो अपनी स्थिति याद रखता है और next() कॉल होने पर अगला तत्व देता है; तत्व ख़त्म होने पर StopIteration उठाता है।
nums = [10, 20, 30]
it = iter(nums) # सूची से इटरेटर लेना
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
next(it) # StopIteration
लूप for ठीक यही करता है: iter() बुलाता है, फिर StopIteration तक next()। अपना इटरेटर __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]
यह काम करता है, पर इसमें बहुत दोहराव है। जनरेटर वही काम तीन पंक्तियों में कर देते हैं।
Python में जनरेटर क्या है और yield कैसे काम करता है
जनरेटर फ़ंक्शन एक साधारण फ़ंक्शन है, जिसकी बॉडी में yield है। उसे बुलाने पर कोड नहीं चलता, बल्कि एक जनरेटर ऑब्जेक्ट लौटता है। हर 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 करता है), तो जनरेटर अपने-आप StopIteration उठाता है, और for लूप शांति से ख़त्म हो जाता है। यहीं yield और return का मुख्य फ़र्क है: return फ़ंक्शन ख़त्म करके एक मान लौटाता है, जबकि yield फ़ंक्शन को रोकता है और चाहे जितने मान दे सकता है।
जनरेटर और इटरेटर में फ़र्क
हर जनरेटर एक इटरेटर है: उसमें __iter__ और __next__ होते हैं। पर हर इटरेटर जनरेटर नहीं होता। फ़र्क बनाने के तरीके और क्षमता में है।
| इटरेटर (क्लास) | जनरेटर | |
|---|---|---|
| कैसे बनता है | __iter__ और __next__ वाली क्लास | yield वाला फ़ंक्शन या (x for x in ...) |
| स्थिति | हाथ से एट्रिब्यूट में | लोकल वेरिएबल में अपने-आप सहेजी जाती है |
| StopIteration | आप ख़ुद उठाते हैं | फ़ंक्शन ख़त्म होने पर अपने-आप उठता है |
| अतिरिक्त | जो आप लिखें | send(), throw(), close() |
दोनों में एक अहम गुण समान है: उन पर एक ही बार चला जा सकता है। ख़त्म हो चुके जनरेटर पर दोबारा for कुछ नहीं देगा — नया बनाना होगा।
अनंत जनरेटर और बड़ी फ़ाइलें
जनरेटर का ख़त्म होना ज़रूरी नहीं। अनंत श्रृंखला तब तक सुरक्षित है, जब तक उसमें से सीमित तत्व लिए जाएँ:
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']
सबसे व्यावहारिक उदाहरण है बड़ी फ़ाइल को पंक्ति-दर-पंक्ति पढ़ना। जनरेटर को पाइपलाइन में जोड़ा जा सकता है, और मेमोरी में हर समय एक ही पंक्ति रहती है, पूरी फ़ाइल नहीं:
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 किसी दूसरे इटरेबल के सारे मान सीधे आगे भेजता है, और भीतरी लूप की ज़रूरत नहीं पड़ती:
def flatten(groups):
for g in groups:
yield from g
print(list(flatten([[1, 2], [3], [4, 5]]))) # [1, 2, 3, 4, 5]
send() जनरेटर के भीतर मान भेजता है: वह मान yield व्यंजन का परिणाम बन जाता है। पहले send से पहले जनरेटर को next() से चालू करना होता है। उदाहरण के लिए औसत निकालने वाला:
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 जनरेटर है?
नहीं। range एक आलसी (lazy) अनुक्रम है: वह भी सारी संख्याएँ नहीं रखता, पर उसमें इंडेक्स, len(), in की जाँच है और उस पर कितनी भी बार चला जा सकता है। जनरेटर में यह कुछ भी नहीं है।
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)
# वही कॉम्प्रिहेंशन से
with_markup = [a * 1.2 for a in amounts]
print(with_markup) # [120.0, 300.0, 480.0]
नाम «सूची जनरेटर» के बावजूद परिणाम एक तैयार सूची है, जनरेटर नहीं: सारे तत्व तुरंत निकाल लिए जाते हैं।
सूची जनरेटर में if और if else
यहीं सबसे ज़्यादा उलझन होती है। फ़िल्टर-शर्त (if) अंत में आती है और तत्व हटाती है। चयन-शर्त (टर्नरी) की जगह आरंभ में होती है और हर तत्व को दो में से एक मान बना देती है, कुछ नहीं हटाती।
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 सिर्फ़ आरंभ वाली शर्त के साथ आता है।
नेस्टेड कॉम्प्रिहेंशन और द्वि-आयामी सूची
दोहरा 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]
द्वि-आयामी सूची (मैट्रिक्स) कॉम्प्रिहेंशन के भीतर कॉम्प्रिहेंशन से बनती है। सूची को गुणा करने की आम गलती पर ध्यान दें: पंक्तियाँ एक ही ऑब्जेक्ट बन जाती हैं।
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]]
डिक्शनरी और सेट का जनरेटर
यही तरीका दूसरे संग्रहों पर भी चलता है। कोलन वाले कर्ली ब्रैकेट से डिक्शनरी बनती है (dict comprehension), और बिना कोलन के कर्ली ब्रैकेट से सेट (set comprehension)।
spent = [("खाना", 500), ("टैक्सी", 300), ("खाना", 200)]
categories = {cat for cat, _ in spent}
print(categories) # {'खाना', 'टैक्सी'} — सेट का क्रम तय नहीं होता
last = {cat: amount for cat, amount in spent}
print(last) # {'खाना': 200, 'टैक्सी': 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 पर ध्यान दें: कुंजी दोहराने पर आख़िरी मान जीतता है, और 500 का जोड़ खो गया। जोड़ करना हो तो comprehension काम नहीं आता — लूप लिखें या collections.Counter/defaultdict लें।
टपल का जनरेटर?
Python में अलग से «tuple comprehension» नहीं है। कोष्ठक से जनरेटर व्यंजन बनता है, और टपल पाने के लिए उसे tuple() में दिया जाता है:
t = tuple(n * 2 for n in range(3)) # (0, 2, 4)
जनरेटर व्यंजन: जमा न करके एक-एक देना
सूची के चौकोर ब्रैकेट की जगह गोल कोष्ठक लिखें, तो जनरेटर व्यंजन बनता है। यह पहले से कुछ नहीं गिनता, और मान एक-एक करके देता है। दस लाख तत्वों पर मेमोरी का फ़र्क बहुत बड़ा है:
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)) # लगभग 8 MB
print(sys.getsizeof(as_gen)) # कुछ सौ बाइट
जनरेटर व्यंजन ज़्यादातर एग्रीगेट फ़ंक्शन के भीतर रहते हैं। अगर जनरेटर अकेला आर्गुमेंट हो, तो अतिरिक्त कोष्ठक ज़रूरी नहीं। any() और all() पहले उत्तर पर ही रुक जाते हैं और बाक़ी को नहीं देखते।
spent = [("खाना", 500), ("टैक्सी", 300), ("खाना", 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]) # ('खाना', 500)
चुनने का सरल नियम: परिणाम कई बार चाहिए, इंडेक्स या len() चाहिए — तो सूची लें। एक बार चलकर कुछ गिनना है — जनरेटर लें।
दायरा (scope): वेरिएबल बाहर नहीं निकलता
यह सवाल अनुभवी लोगों को भी उलझा देता है:
x = 10
squares = [x for x in range(3)]
print(x) # 10
कॉम्प्रिहेंशन के भीतर लूप का वेरिएबल अपने दायरे में रहता है और बाहर के वेरिएबल को नहीं बदलता (Python 3 में ऐसा ही है; Python 2 में अलग था)। साधारण for लूप के बाद x 2 होता। Python 3.12 (PEP 709) से कॉम्प्रिहेंशन घेरने वाले फ़ंक्शन में समाहित हो गए और तेज़ चलते हैं, पर दायरे का नियम वही रहा।
कॉम्प्रिहेंशन में * और ** की अनपैकिंग: Python 3.15 (PEP 798)
फ़ंक्शन कॉल और सूची-लिटरल में तारे से अनपैकिंग लंबे समय से लिखी जा सकती है, पर कॉम्प्रिहेंशन के भीतर यह SyntaxError था। Python 3.15 में यह रोक हट गई: *तत्व का मतलब है «यहाँ उसके सारे तत्व डालो»। सूचियों, सेटों और जनरेटरों में, अतुल्यकालिक (async) वालों समेत, यह काम करता है।
# 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} # सेट
lazy = (*g for g in groups) # आलसी जनरेटर
डबल तारा डिक्शनरी के लिए यही करता है। यह परतदार सेटिंग्स के लिए सुविधाजनक है — «डिफ़ॉल्ट → फ़ाइल → फ़्लैग»: हर अगली परत पिछली की कुंजियों को ढक देती है।
# 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] सिंटैक्स एरर है, शर्त को कोष्ठक में लिखें। और यह कोड 3.14 या उससे पुराने पर नहीं चलेगा: अगर लाइब्रेरी पुराने वर्ज़न सपोर्ट करती है, तो डबल for या itertools.chain.from_iterable रखें।
अतुल्यकालिक जनरेटर
अगर async def फ़ंक्शन में yield है, तो वह अतुल्यकालिक जनरेटर बनता है। उस पर async for चलता है, और भीतर नेटवर्क या डिस्क के लिए await किया जा सकता है। अतुल्यकालिक कॉम्प्रिहेंशन भी होते हैं।
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())
आम गलतियाँ
- तीन मंज़िला कॉम्प्रिहेंशन। दो
for, दोifऔर टर्नरी एक पंक्ति में — एक हफ़्ते बाद कोई नहीं पढ़ पाएगा। पंक्ति में न समाए तो सामान्य लूप लिखें: PEP 8 पठनीयता को संक्षिप्तता से ऊपर रखता है। - जनरेटर पर दोबारा चलाना। दूसरा
sum(gen)0 देगा, क्योंकि जनरेटर ख़त्म हो चुका है। - साइड-इफ़ेक्ट के लिए कॉम्प्रिहेंशन।
[print(x) for x in xs]बेकारNoneकी सूची बनाता है। कार्रवाई के लिएforलूप लें। - जनरेटर व्यंजन से सूची की उम्मीद।
(x for x in xs)[0]—TypeError, जनरेटर में इंडेक्स नहीं होते।
अभ्यास के लिए जनरेटर के सवाल
fib()जनरेटर लिखें, जो फ़िबोनाची संख्याएँ अनंत तक दे, औरisliceसे पहले 10 लें।- एक पंक्ति में वह डिक्शनरी बनाएँ, जिसमें तीन अक्षरों से लंबे शब्द «शब्द → लंबाई» के रूप में हों।
- एक लाख तक की सम संख्याओं के वर्गों का योग निकालें, बिना सूची बनाए।
- जनरेटर
chunks(seq, n)लिखें, जो सूची केnतत्वों के टुकड़े दे।
निष्कर्ष
इटरेटर next() से एक-एक तत्व देता है, और जनरेटर उसे लिखने का सबसे सरल तरीका है: yield वाला फ़ंक्शन या कोष्ठक वाला व्यंजन। सूची, डिक्शनरी और सेट के जनरेटर एक पंक्ति में संग्रह बनाते हैं: फ़िल्टर if अंत में, चयन if else आरंभ में। मेमोरी बचानी हो या डेटा-प्रवाह को एक बार संसाधित करना हो, तो जनरेटर लें; परिणाम बार-बार चाहिए तो सूची। कॉम्प्रिहेंशन, जनरेटर और Python 3.15 की नई अनपैकिंग कोर्स में ख़र्च ट्रैकर के उदाहरण पर व्यवहार में सिखाए जाते हैं।
अक्सर पूछे जाने वाले प्रश्न
क्या जनरेटर फ़ंक्शन में return इस्तेमाल किया जा सकता है?
हाँ। बिना मान का return सिर्फ़ जनरेटर ख़त्म करता है, और मान वाला return उसे StopIteration अपवाद के value एट्रिब्यूट में डालता है; इसे yield from वाला व्यंजन प्राप्त करता है।
जनरेटर को सूची में कैसे बदलें?
उसे list() में दें: list(gen)। इसके बाद जनरेटर ख़त्म हो जाता है, और सारे मान मेमोरी में आ जाते हैं।
list comprehension और append वाला लूप, कौन तेज़ है?
आम तौर पर comprehension थोड़ा तेज़ है, क्योंकि हर चक्र में append मेथड नहीं बुलाता। पर चुनाव पठनीयता से करें; फ़र्क शायद ही कभी निर्णायक होता है।
कैसे पता करें कि ऑब्जेक्ट जनरेटर है?
ऑब्जेक्ट के लिए inspect.isgenerator(obj), और फ़ंक्शन के लिए inspect.isgeneratorfunction(f)। यह जाँचने के लिए कि ऑब्जेक्ट इटरेटर है, isinstance(obj, collections.abc.Iterator) लें।
जनरेटर व्यंजन और list comprehension में क्या फ़र्क है?
चौकोर ब्रैकेट तुरंत पूरी सूची मेमोरी में बनाता है, जबकि कोष्ठक जनरेटर बनाता है, जो मान माँगे जाने पर निकालता है और सिर्फ़ एक बार चलता है।
यह विषय “Python 3.15: от новичка до профи” कोर्स में अभ्यास के साथ विस्तार से समझाया गया है।
कोर्स पर जाएँ