تکرارگرها در پایتون

تکرار گرها یا iterator ها در زبان های برنامه نویسی استفاده های زیادی دارند. برنامه نویسان در هنگام استفاده از ساختمان داده های آماده مکرراً از تکرارگرها استفاده میکنند. به عبارت ساده تر، با وجود iterator هاست که میتوانیم یک لیست، آرایه یا ساختمان داده خاص دیگری را پیمایش کرده و تک تک عناصر آنرا درون یک حلقه بررسی کنیم.

 

تکرارگر یا iterator چیست ؟

شاید درک مفهوم تکرارگر یا iterator کمی سخت و گیج کننده باشد!

بگذارید با یک مثال بسیار ساده در زبان برنامه نویسی پایتون، مفهوم و موقعیت استفاده از تکرارگرها را ببینیم.

فرض کنید یک لیست از تعدادی عدد را در اختیار دارید و میخواهید روی این لیست پیمایش انجام دهید. همانطور که میدانید به سادگی، با استفاده از یک حلقه for این کار امکان پذیر است.

my_list = [1, 2, 3, 4]

for x in my_list:
    print(x) #or do something ...

 

اما در پشت پرده اجرای حلقه فوق چه اتفاقی می افتد ؟

در حالت بسیار ساده، فرض کنید یک متغیر اشاره گر به اولین عنصر این لیست (در ساختار ساختمان داده لیست پایتون) داریم. وقتی لیست را درون یک حلقه for استفاده میکنیم، در اولین اجرای حلقه، اولین عنصر مورد بررسی و پیمایش قرار میگیرد.

در تکرار دوم حلقه، می بایست یک عنصر به جلو حرکت کنیم، یعنی اشاره گر ما باید به عنصر دوم (عنصر بعد از عنصر فعلی) اشاره کند.

فرآیند گفته شده به تعداد عناصر موجود در لیست تکرار شده تا دیگر عنصری برای جایگزینی با عنصر قبلی نداشته باشیم؛ در این صورت حلقه ما به اتمام می رسد. به همین سادگی!

مفهوم تکرارگر (iterator) در یک شئ به همین سادگی است که گفته شد!!

در تصویر زیر فرآیند کلی و ترتیبی تکرارگرها ترسیم شده است.

تکرارگر یا iterator چگونه کار میکند؟

 

هنگامی که در حال ایجاد یک ساختمان داده دلخواه در زبان python هستیم، در صورتی که بخواهیم ساختمان داده ما ویژگی iterate شدن را داشته باشند، می بایست دو متد مرتبط با تکرارگرها را در کلاس خود پیاده سازی کنیم.

تابع __iter__

این تابع هنگامی صدا زده میشود که یک شئ از ساختمان داده ما در محلی برای پیمایش قرار گرفته باشد. (مثلا در حلقه for)

متد __iter__ در حقیقت یک شئ را به ما بر میگرداند که دارای یک متد به نام __next__ است.

تابع __next__

در متد __next__ فرآیندی انجام میشود که اشاره گر پیمایش عناصر ما یک گام به جلو حرکت کرده، به عنصر بعدی اشاره کند و در نهایت عنصر فعلی را به عنوان خروجی بدهد.

 

یک مثال برای درک ساختار iterator ها در پایتون

برای درک بهتر ساختار iterator ها، فرض کنید یک کلاس با یک متغیر به اسم current داریم و مقدار اولیه آن را برابر با 1 می گذاریم.

class Numbers:
    def __init__(self):
        self.current = 1

 

حال اگر از کلاس آزمایشی خود یک نمونه ایجاد کرده و بخواهیم در یک حلقه ساده for استفاده کنیم، با خطا مواجه خواهیم شد!

obj = Numbers()

for x in obj:
    print(x)

#Run Result:
#Traceback (most recent call last):
#File "/home/SabzElco/Files/py/test.py", line 6, in <module>
#    for x in obj:
#TypeError: 'Numbers' object is not iterable

 

همانطور که از آخرین خط ارور داده شده مشخص است، شئ های کلاس Numbers قابلیت iterate شدن را ندارند یا به عبارتی iterable نیستند.

همانطور که پیش تر نیز گفته شد، برای iterable کردن کلاس مورد نظر، نیاز به دو تابع کمکی دیگر داریم. در ادامه دو متد لازم را به کلاس خود اضافه میکنیم. یعنی کلاس ما چیزی شبیه کد زیر خواهد شد.

class Numbers:
    def __init__(self):
        self.current = 1

    def __iter__(self):
        return self

    def __next__(self): #have infinit end!
        output = self.current
        self.current += 1
        return output

 

اکنون اگر همان حلقه قبلی را اجرا کنیم، در ابتدا عدد 1 را به صورت چاپ شده در خروجی میبینیم و در خط بعدی عدد 2، و این فرآیند تا بینهایت ادامه پیدا میکند.

معمولاً در ساختمان داده هایی که داریم، یک محدودیت خاصی وجود دارد؛ برای مثال، تعداد قابل شمارشی از عناصر در آن قرار داشته و یا در همین مثال آموزشی ما (کلاس Numbers) میتوان در نظر گرفت که پیمایش تا سقف خاصی پیشروی کند.

 

برای اینکه سقف مورد انتظار را به عنوان ورودی بگیریم، سازنده (constructor) کلاس را تغییر میدهیم و دو مقدار low و high را به عنوان ورودی در هنگام ایجاد شئ خواهیم گرفت.

همچنین برای کنترل عدم سرریز شدن از سقف مورد انتظار، می بایست شرطی جهت چک کردن رسیدن به حد مورد انتظار در متد __next__ قرار داد. در صورتی که به سقف خود رسیدیم، با استفاده از عبارت StopIteration میتوانیم به حلقه for اعلام کنیم که پیمایش به پایان رسیده است.

class Numbers:
    def __init__(self, low, high):
        self.current = low
        self.limit = high

    def __iter__(self):
        return self

    def __next__(self):
        if self.current > self.limit:
            raise StopIteration
        output = self.current
        self.current += 1
        return output

 

در نتیجه ی اجرای قطعه کد تست زیر، اعداد 1 تا 10 را در خروجی خواهیم داشت.

obj = Numbers(1, 10)

for x in obj:
    print(x)

 

حال اگر همین شئ obj را در حلقه ای دیگر استفاده کنیم، خواهید دید که هیچ نتیجه ای حاصل نخواهد شد!

دلیل این امر این است که متغیر مربوط به current با یکبار اجرای عملیات پیمایش، به حداکثر خود رسیده و در دفعات دیگر پیمایش، به دلیل اینکه به limit رسیده، با اعلام StopIteration حلقه را در ابتدای اجرا خاتمه میدهد.

 

برای جلوگیری از این کار، میتوان در متد __iter__ متغیرهایی که به عنوان اشاره گر (پیمایش گر) استفاده کرده ایم را به مقدار اولیه آنها بازگردانیم.

در اینجا صرفاً مقدار متغیر current تغییر میکند و مشکل ساز ما دقیقا همین متغیر است.

با نگهداری مقدار low در هنگام ساخت شئ از کلاس و انتساب آن در هنگام اجرای متد __iter__ به متغیر current میتوان مشکل را به راحتی حل کرد.

class Numbers:
    def __init__(self, low, high):
        self.low = low
        self.limit = high

    def __iter__(self):
        self.current = self.low
        return self

    def __next__(self):
        if self.current > self.limit:
            raise StopIteration
        output = self.current
        self.current += 1
        return output

 

بعضاً به دلیل پیچیده بودن پروسه نگهداری عنصر فعلی و پرش به عنصر بعدی آن در هر گامِ پیمایش، کلاس iterable را به عنوان یک کلاس جداگانه تعریف میکنند و در کلاس اصلی صرفا متد __iter__ را مورد استفاده قرار میدهند.

در صورت صدا زده شدن __iter__ ، یک شئ از کلاس کمکی ساخته شده و بازگردانده میشود؛ لازم به ذکر است که کلاس کمکی حتما باید دو تابع __iter__ و __next__ را دارا باشد.

در مثال آموزشی ما، خود کلاس اصلی را iterable کردیم و به همین دلیل در متد __iter__ خود شئ را به عنوان یک شئی iterable بازمیگردانیم.