একটি generator হলো এমন একটি ফাংশন বা এক্সপ্রেশন, যা এক বিশেষ ধরনের ইটারেটর রিটার্ন করে, যাকে বলা হয় জেনারেটর ইটারেটর।
Generator-iterators হলো লেজি: এরা তাদের values মেমরিতে জমা রাখে না, বরং দরকার হলে তাদের মান তৈরি করে।
একটি জেনারেটর ফাংশন দেখতে অন্য যেকোনো ফাংশনের মতোই, তবে এতে এক বা একাধিক yield এক্সপ্রেশন থাকে।
প্রতিটি yield কোড এক্সিকিউশন স্থগিত করে এবং বর্তমান এক্সিকিউশন স্টেট (সব লোকাল ভ্যারিয়েবল ও try স্টেটমেন্টসহ) সংরক্ষণ করে।
জেনারেটর আবার চালু হলে সে স্থগিত হওয়ার জায়গা থেকেই স্টেট নিয়ে এগিয়ে যায়, যা সাধারণ ফাংশনের মতো নয়; সাধারণ ফাংশন প্রতিবার কল করলে রিসেট হয়ে যায়।
জেনারেটর তৈরি করা হয় অনেকটা অন্য লুপিং বা রিকার্সিভ ফাংশনের মতোই, তবে এর জন্য একটি yield এক্সপ্রেশন দরকার, যা নিয়ে আমরা একটু পরে বিস্তারিত আলোচনা করব।
এর একটি উদাহরণ হলো এমন একটি ফাংশন, যা প্রদত্ত সংখ্যার অ্যারে থেকে বর্গগুলো রিটার্ন করে।
এখনকার লেখা অনুযায়ী, কোনো মান রিটার্ন করার আগে সব ইনপুট প্রসেস করতে হয়:
>>> def squares(list_of_numbers):
... squares = []
... for number in list_of_numbers:
... squares.append(number ** 2)
... return squares
আপনি ওই ফাংশনটিকে এভাবে একটি জেনারেটরে রূপান্তর করতে পারেন:
>>> def squares_generator(list_of_numbers):
... for number in list_of_numbers:
... yield number ** 2
এর যুক্তি হলো, যখন আপনার সব মান একসাথে দরকার হয় না, তখন আপনি একটি জেনারেটর ব্যবহার করেন।
এতে মেমরি ও প্রসেসিং পাওয়ার বাঁচে, কারণ কেবল যে মানটি নিয়ে আপনি এখন কাজ করছেন সেটিই হিসাব করা হয়।
জেনারেটর (কারিগরি দিক থেকে generator-iteratorগুলো, নিচের নোটটি দেখুন) এক ধরনের iterator এবং Python-এ যেখানে iterator বা iterable প্রত্যাশিত, সেখানে যেকোনো জায়গায় ব্যবহার করা যায়।
এর মধ্যে পড়ে এমন ফাংশন বা অবজেক্ট, যেগুলো আর্গুমেন্ট হিসেবে একটি iterable/iterator চায়।
আরও গভীরে যেতে চাইলে দেখুন Python-এ কীভাবে একটি ইটারেটর তৈরি করবেন।
জেনারেটর-ইটারেটর হলো ইটারেটরগুলোর এক বিশেষ উপসেট।
Iterators হলো সেই মেকানিজম/প্রোটোকল, যার মাধ্যমে ইটারেবলগুলোর ওপর লুপ চালানো যায়।
জেনারেটর-ইটারেটর আর সাধারণ Python iterables থেকে পাওয়া ইটারেটর প্রায় একইভাবে কাজ করে, তবে কয়েকটি গুরুত্বপূর্ণ পার্থক্য খেয়াল করার মতো:
indexes থাকে না।
যোগ বা বিয়োগ করে আগের বা পরের কোনো মান উল্লেখ করা যায় না, বন্ধনী ([]) নোটেশন বা স্লাইসিংও ব্যবহার করা যায় না।len() ফাংশনের সাথে ব্যবহার করা যায় না, কারণ এগুলোর কোনো দৈর্ঘ্য নেই।generator-iterator থেকে সব মান সংগ্রহের সময় সাবধান থাকুন!squares_generator() জেনারেটরটি ব্যবহার করতে:
>>> squared_numbers = squares_generator([1, 2, 3, 4])
>>> for square in squared_numbers:
... print(square)
...
1
4
9
16
জেনারেটরের ভেতরের মানগুলো next() ফাংশনের মাধ্যমেও তৈরি বা অ্যাক্সেস করা যায়।
next() একটি জেনারেটর অবজেক্টের __next__() মেথড ডাকে, জেনারেটরের কোডকে তার yield এক্সপ্রেশন পর্যন্ত "এগিয়ে নেয়" বা মূল্যায়ন করে, যা তখন মানটি "yield করে" বা রিটার্ন করে।
>>> squared_numbers = squares_generator([1, 2])
>>> next(squared_numbers)
1
>>> next(squared_numbers)
4
কোনো generator পুরোপুরি শেষ হয়ে গেলে এবং রিটার্ন করার মতো আর কোনো মান না থাকলে এটি একটি StopIteration এরর ছোড়ে।
>>> next(squared_numbers)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
জেনারেটর হলো ইটারেটরগুলোর এক বিশেষ উপসেট।
Iterators হলো সেই মেকানিজম/প্রোটোকল, যার মাধ্যমে ইটারেবলগুলোর ওপর লুপ চালানো যায়।
জেনারেটর আর সাধারণ Python iterables থেকে পাওয়া ইটারেটর প্রায় একইভাবে কাজ করে, তবে কয়েকটি গুরুত্বপূর্ণ পার্থক্য খেয়াল করার মতো:
জেনারেটর একমুখী; আগের কোনো মানে "ফিরে যাওয়ার" সুযোগ নেই।
জেনারেটরের ওপর ইটারেশন করলে রিটার্ন করা মানগুলো শেষ হয়ে যায়; কোনো রিসেট নেই।
জেনারেটর (যেহেতু লেজি উপায়ে মূল্যায়িত হয়) সাজানো যায় না এবং উল্টানোও যায় না।
জেনারেটরের indexes থাকে না, তাই যোগ বা বিয়োগ করে আগের বা পরের কোনো মান উল্লেখ করা যায় না।
জেনারেটর len() ফাংশনের সাথে ব্যবহার করা যায় না।
জেনারেটর সসীম বা অসীম হতে পারে, একটি অসীম জেনারেটর থেকে সব মান সংগ্রহের সময় সাবধান থাকুন।
yield এক্সপ্রেশন আর return এক্সপ্রেশনের সাথে অনেকটাই মিল।
return এক্সপ্রেশনের বিপরীতে yield একেবারে নির্দিষ্ট একটি জায়গায় কলকারীকে মান দিয়ে দেয়, আর বাকি মানগুলোর মূল্যায়ন বা রিটার্ন স্থগিত রাখে যতক্ষণ না সেগুলো চাওয়া হয়।
yield মূল্যায়িত হলে এটি যে ফাংশনের ভেতরে আছে তার এক্সিকিউশন থামিয়ে দেয় এবং সেই মুহূর্তে ফাংশনের যেকোনো মান রিটার্ন করে।
এরপর ফাংশনটি স্কোপে টিকে থাকে, এবং __next__() কল করা হলে আবার yield-এর দেখা না পাওয়া পর্যন্ত এক্সিকিউশন চলতে থাকে।
ফাংশনের বাইরে yield এক্সপ্রেশন ব্যবহার করা নিষিদ্ধ।
>>> def infinite_sequence():
... current_number = 0
... while True:
... yield current_number
... current_number += 1
>>> lets_try = infinite_sequence()
>>> lets_try.__next__()
0
>>> lets_try.__next__()
1
অনেক ধরনের কাজেই জেনারেটর দরকারি।
বড় কোনো সংগ্রহ নিয়ে কাজ করার সময় আপনি হয়তো তার সব মান memory-তে রাখতে চাইবেন না।
বড় ডেটার ওপর টুকরো টুকরো করে কাজ করতে জেনারেটর ব্যবহার করা যায়, যা মেমরি বাঁচায় ও পারফরম্যান্স বাড়ায়।
কোনো প্রসেস বা গণনা যখন জটিল, ব্যয়বহুল বা অসীম হয়, তখনও জেনারেটর খুব কাজে লাগে:
>>> def infinite_sequence():
... current_number = 0
... while True:
... yield current_number
... current_number += 1
এখন infinite_sequence অবজেক্টে যখনই __next__() কল করা হবে, এটি আগের সংখ্যা + 1 রিটার্ন করবে।