Python Generators සහ Iterators: මතකය බේරගමු, වැඩේ වේගවත් කරමු | SC Guide

Python Generators සහ Iterators: මතකය බේරගමු, වැඩේ වේගවත් කරමු | SC Guide

කොහොමද යාලුවනේ! අද අපි කතා කරන්න යන්නේ ඔයාලගේ program වල performance එකයි, memory usage එකයි දෙකම optimize කරගන්න පුළුවන් සුපිරි concepts දෙකක් ගැන. ඒ තමයි Generators සහ Iterators.

සාමාන්‍යයෙන් අපිට ලොකු data set එකක් manage කරන්න වුණාම හරි, නැත්නම් අනන්තය (infinite) වෙනකම් යන data stream එකක් handle කරන්න වුණාම හරි memory issues එන්න පුළුවන්. ඒත් Generators සහ Iterators හරියට පාවිච්චි කරොත් මේ ප්‍රශ්නයට Smart Solution එකක් ගන්න පුළුවන්.

නිකන් හිතන්නකෝ, ඔයාට movie එකක් බලන්න ඕනේ කියලා. ඔයා whole movie එකම download කරගෙන බලනවද, නැත්නම් streaming කරනවද? Streaming කරනකොට පොඩ්ඩ පොඩ්ඩ තමයි data එක load වෙන්නේ, ඒක නිසා ඔයාගේ internet එකට ලොකු බරක් වැටෙන්නේ නෑ, නේද? මේ Generators සහ Iterators වැඩ කරන්නෙත් ඒ වගේ 'lazy evaluation' කියන concept එකෙන්. ඒ කියන්නේ, අවශ්‍ය වෙලාවට විතරක් data generate කරලා දෙන එක.


Iterator කියන්නේ මොකක්ද?

සරලවම කිව්වොත්, Iterator කියන්නේ Object එකක්. මේ Object එකට පුළුවන් එකින් එකට (one by one) data elements set එකක් හරහා යන්න. ලංකාවේ බස් එකක conductor කෙනෙක් වගේ වැඩේ කරන්නේ. එයාට පුළුවන් හැම passenger කෙනෙක්ගෙන්ම ටිකට් පත් request කරලා දෙනවා වගේ, Iterator එකට පුළුවන් එකින් එකට data item request කළාම එයාලව provide කරන්න.

Python වලදී, Object එකක් Iterator එකක් වෙන්න නම්, ඒ Object එකට __iter__ සහ __next__ කියන special methods දෙක තියෙන්න ඕනේ. __iter__ method එකෙන් Iterator object එකම return කරනවා. __next__ method එකෙන් තමයි series එකේ next item එක return කරන්නේ. හැබැයි, series එක ඉවර වුණාම StopIteration කියන Error එක raise කරනවා.

Custom Iterator එකක් හදමු!

අපි හිතමු අපිට ඕනේ 0 ඉඳන් අහවල් number එකක් වෙනකම් තියෙන even numbers විතරක් ගන්න කියලා. සාමාන්‍යයෙන් list එකක් හදලා ගන්න පුළුවන්. ඒත් අපි බලමු මේකට custom iterator එකක් කොහොමද හදන්නේ කියලා. මේකෙන් අපිට තේරෙයි __iter__ සහ __next__ methods වැඩ කරන හැටි.


class EvenNumbers:
    def __init__(self, limit):
        self.limit = limit
        self.current = 0 # Starting from 0

    def __iter__(self):
        # This method should return the iterator object itself
        return self

    def __next__(self):
        # This method returns the next item in the sequence
        while self.current <= self.limit:
            if self.current % 2 == 0:
                result = self.current
                self.current += 1
                return result
            self.current += 1
        # If no more items, raise StopIteration
        raise StopIteration

# Using our custom iterator
print("Custom Iterator Example:")
evens_upto_10 = EvenNumbers(10)
for num in evens_upto_10:
    print(num)

# You can also manually call next()
print("\nManual next() calls:")
evens_upto_5 = EvenNumbers(5)
print(next(evens_upto_5)) # 0
print(next(evens_upto_5)) # 2
print(next(evens_upto_5)) # 4
# print(next(evens_upto_5)) # This would raise StopIteration

මේ code එකේදී අපි EvenNumbers කියලා Class එකක් හැදුවා. ඒකේ __init__ එකෙන් limit එකයි, current number එකයි define කරනවා. __iter__ එකෙන් self return කරනවා. __next__ එකේදී තමයි වැඩේ වෙන්නේ. while loop එකක් ඇතුලේ current number එක limit එකට වඩා අඩු නම්, ඒක even number එකක්ද කියලා බලලා, එහෙම නම් ඒක return කරනවා. ඊට පස්සේ current එක වැඩි කරනවා. Even number එකක් නොවුණොත්, current එක වැඩි කරලා අනිත් number එක බලනවා. මේක continue වෙනවා limit එකට යනකම්. limit එක පැන්නම StopIteration raise කරනවා. එතකොට for loop එක auto-break වෙනවා.


Generator එකක් කියන්නේ මොකක්ද?

Generator එකක් කියන්නේ Iterator එකක් generate කරන, ඒ කියන්නේ හදන, function එකක්. මේක සාමාන්‍ය function එකකට වඩා වෙනස් වෙන්නේ return කියන keyword එක වෙනුවට yield කියන keyword එක පාවිච්චි කරන එකෙන්. yield කියන්නේ, "මේ item එක දෙන්න, හැබැයි මාව අමතක කරන්න එපා, මට තව වැඩ තියෙනවා" වගේ දෙයක්. ඒ කියන්නේ, Generator function එකක් yield එකක් දැක්කම current value එක return කරලා, ඒක execute වෙන තැනින්ම pause වෙනවා. ඊළඟ වතාවේ call කළාම pause වුණ තැනින්ම ආයෙත් වැඩේ පටන් ගන්නවා.

මේක නිකන් paan kade (බේකරියක්) එකක් වගේ. Paan kade එකකට ගියාම ඔයාට එක පාරටම paan ගොඩක් ගන්න පුළුවන්. හැබැයි Generator එකක් නිකන් ගෙදරට පාන් ගේන uncle කෙනෙක් වගේ. ඔයාට ඕනෙ වෙලාවට එයා ගෙනල්ලා දෙනවා. අවශ්‍ය ප්‍රමාණයට විතරක් ලැබෙන නිසා, එක පාරටම ගොඩක් stock කරගන්න අවශ්‍ය වෙන්නේ නෑ. Memory එක save වෙනවා නේද?

Generator Function එකක් ලියමු!

අපි කලින් හදපු even numbers generator එකම, generator function එකක් විදිහට හදලා බලමු. මේක කලින් එකට වඩා කොච්චර සරලද කියලා බලන්නකෝ.


def generate_even_numbers(limit):
    current = 0
    while current <= limit:
        if current % 2 == 0:
            yield current # This is where the magic happens!
        current += 1

# Using our generator function
print("\nGenerator Function Example:")
for num in generate_even_numbers(10):
    print(num)

# Getting a generator object and manually iterating
print("\nManual iteration with generator:")
even_gen_5 = generate_even_numbers(5)
print(next(even_gen_5)) # 0
print(next(even_gen_5)) # 2
print(next(even_gen_5)) # 4
# print(next(even_gen_5)) # This would raise StopIteration

දැක්කනේ! මේක කලින් Custom Iterator එකට වඩා කොච්චර කෙටිද? yield keyword එක use කරපු ගමන්, Python automatically අපිට අවශ්‍ය Iterator logic එක හදලා දෙනවා. මේක තමයි Generators වල ලොකුම වාසියක්. අඩු code වලින්, කියවන්න පහසු විදිහට Iterator එකක් හදන්න පුළුවන්.

Generator Expressions (Generator Expressions)

List comprehensions ගැන ඔයාලා දන්නවා ඇති, නේද? [x for x in range(10)] වගේ. ඒ වගේම Generator expressions කියලා එකකුත් තියෙනවා. මේවා List comprehensions වගේම තමයි, හැබැයි square brackets [] වෙනුවට round brackets () පාවිච්චි කරනවා. මේවා List comprehensions වගේ එකපාරටම whole list එක memory එකට load කරන්නේ නැහැ, lazy evaluation concept එකෙන්ම වැඩ කරන්නේ.


# List comprehension (loads all into memory)
my_list = [x * x for x in range(10)]
print(f"\nList Comprehension: {my_list}")
print(f"Type: {type(my_list)}")

# Generator expression (creates a generator object, loads one by one)
my_generator = (x * x for x in range(10))
print(f"\nGenerator Expression: {my_generator}")
print(f"Type: {type(my_generator)}")

print("Iterating through Generator Expression:")
for num in my_generator:
    print(num)

# Example for large scale difference
import sys

# List approach
list_of_nums = [i for i in range(1000000)]
print(f"\nMemory used by list (1M items): {sys.getsizeof(list_of_nums)} bytes") # Will be quite large

# Generator approach
generator_of_nums = (i for i in range(1000000))
print(f"Memory used by generator (1M items): {sys.getsizeof(generator_of_nums)} bytes") # Will be much smaller

මේ උදාහරණයේදී ඔයාලට පේනවා List comprehension එකක් හැදුවම ඒකෙන් List එකක් එනවා, memory එකත් ගොඩක් ගන්නවා. ඒත් Generator expression එකෙන් එන්නේ Generator object එකක්. ඒකෙන් memory එක ගොඩක් අඩුයි. මොකද ඒක අවශ්‍ය වෙලාවට විතරක් value එක generate කරලා දෙන නිසා.


StopIteration සහ මතක කාර්යක්ෂමතාවය

කලින් කිව්වා වගේ, StopIteration කියන එක Error එකක් විදිහට දැක්කට, ඒක Iterator එකක් හෝ Generator එකක් ඉවර වුණා කියලා කියන්න python use කරන සාමාන්‍ය ක්‍රමයක්. for loop එකක් වගේ Iterator එකක් use කරනකොට, Python automatically StopIteration handle කරන නිසා අපිට ඒ ගැන වැඩිය හිතන්න අවශ්‍ය වෙන්නේ නැහැ. manual next() call කරනකොට තමයි මේක direct පේන්නේ.

දැන් අපි කතා කරමු Generators වල ප්‍රධානම වාසිය ගැන - Memory Efficiency (මතක කාර්යක්ෂමතාවය). අපි හිතමු අපිට Gigabytes ගණන් විශාල data file එකක් read කරන්න ඕනේ කියලා. මේකෙන් Line by Line (එක පේළියෙන් එක පේළිය) read කරන්න පුළුවන්. සාමාන්‍ය විදිහට නම් අපි මේ whole file එකම memory එකට load කරන්න හදනවා. ඒත් එතකොට computer එකේ memory මදි වෙන්න පුළුවන්, නැත්නම් program එක slow වෙන්න පුළුවන්.

ඒත් Generator එකක් පාවිච්චි කරනකොට, අපි file එකේ තියෙන හැම line එකක්ම memory එකට load කරන්නේ නැහැ. අවශ්‍ය වෙන එක line එක විතරක් load කරලා, process කරලා, ඊළඟ line එකට යනවා. මේක තමයි 'lazy evaluation' කියන්නේ. මේ නිසා, ඔයාගේ program එකට පුළුවන් අඩු memory එකක් පාවිච්චි කරලා, ගොඩක් විශාල data sets handle කරන්න.

හිතන්නකෝ, ඔයාට 1 සිට 1,000,000,000 (බිලියනයක්) දක්වා තියෙන ඉලක්කම් ටික processing කරන්න ඕනේ කියලා. මේවා list එකකට ගත්තොත් ඔයාට බිලියන ගණන් integers memory එකට දාන්න වෙනවා. ඒත් Generator එකක් පාවිච්චි කරොත්, එක වෙලාවක මතකයේ තියෙන්නේ එක ඉලක්කමක් විතරයි. මේකෙන් මතකය ගොඩක් බේරගන්න පුළුවන්, විශේෂයෙන්ම Big Data applications වලදී.


හොඳම Practices (Best Practices)

Generators සහ Iterators ගොඩක් powerful concepts වුණාට, හැම වෙලාවෙම මේවා පාවිච්චි කරන්න ඕනේ කියල නෑ. නිවැරදි තැනදී නිවැරදි දේ පාවිච්චි කිරීම තමයි Smart Developer කෙනෙක්ගේ ලක්ෂණය.

  • විශාල Data Sets සඳහා: ඔයාට ලොකු files read කරන්න, database records ගොඩක් process කරන්න, නැත්නම් network එකෙන් stream වෙන data handle කරන්න ඕනේ නම් Generators තමයි හොඳම විසඳුම. මේකෙන් memory එක save කරගන්න පුළුවන්.
  • Infinite Sequences සඳහා: ඔයාට අනන්තය දක්වා යන sequence එකක් (උදාහරණයක් විදිහට, Fibonacci series එක) generate කරන්න අවශ්‍ය නම්, Generator එකක් use කරන එකෙන් memory limitations නැතුව වැඩේ කරගන්න පුළුවන්.
  • Data Pipelines සඳහා: ඔයාට data එකක් අදියර කීපයකින් process කරන්න අවශ්‍ය නම් (filter කරන්න, transform කරන්න, aggregations කරන්න), Generators use කරලා data pipeline එකක් හදන්න පුළුවන්. මේකෙන් එක අදියරකින් process වුණු data, ඊළඟ අදියරට යවන්නේ, whole data set එක memory එකේ තියාගෙන ඉන්නේ නැතුව.
  • සංකීර්ණ Iteration Logic සඳහා: ඔයාට custom iteration logic එකක් අවශ්‍ය නම්, Generator functions වලින් ඒක පහසුවෙන් ලියන්න පුළුවන්. Custom Iterators ලියනවාට වඩා yield keyword එක පාවිච්චි කරන එක ගොඩක් සරලයි.

කවදාද Generators පාවිච්චි නොකරන්නේ?

  • කුඩා Data Sets සඳහා: ඔයාට තියෙන්නේ පොඩි data set එකක් නම්, list එකක් හෝ tuple එකක් use කරන එක සරලයි වගේම ප්‍රායෝගිකයි.
  • Random Access අවශ්‍ය නම්: ඔයාට sequence එකක තියෙන elements වලට random විදිහට access කරන්න ඕනේ නම් (උදාහරණයක් විදිහට, list එකක my_list[5] කියලා access කරනවා වගේ), Generator එකක් මේකට සුදුසු නැහැ. මොකද Generators එක පාරටම values generate කරන්නේ නැති නිසා.

අවසාන වශයෙන්...

Generators සහ Iterators කියන්නේ Python වල තියෙන සුපිරි features දෙකක්. මේවා හරියට තේරුම් අරගෙන පාවිච්චි කරන එකෙන් ඔයාලට පුළුවන් මතක කාර්යක්ෂම, වේගවත් applications develop කරන්න. විශාල දත්ත එක්ක වැඩ කරනකොට මේ concepts දෙක අනිවාර්යයෙන්ම ඔයාලගේ best friendsලා වෙයි.

ඉතින්, ඔයාලා මේවා ඔයාලගේ project වලට use කරලා තියෙනවද? නැත්නම් මේ ගැන අලුතෙන් මොනවා හරි දැනගත්තද? පහලින් comment එකක් දාගෙන යමු! ඔයාලගේ අදහස්, ප්‍රශ්න වගේම මේ ගැන ඔයාලගේ අත්දැකීම් ගැන දැනගන්න අපි කැමතියි. ඊළඟ article එකෙන් ආයෙත් හම්බවෙමු!