در این آموزش، مفصل دربارهی فرایند پیمایش دیتا تایپهایی مانند لیست و دیکشنری در پایتون توضیح داده و کاربرد Iterator و Generator در پایتون برای پیمایش در لیستها را آموزش دادهایم. در پایان، شما چرایی استفاده از Iterator ها و Generator ها را به خوبی متوجه میشوید. بنابراین اگر در یادگیری پایتون، مبتدی هستید؛ این آموزش را تا انتها با دقت مطالعه کنید.
Iterator در زبان پایتون
معرفی دیتا تایپ لیست در پایتون برای آموزش Iterator
هر خط کدی که در فرایند توسعهی نرمافزار نوشته میشود، از دو حالت زیر خارج نیست:
- یا به منظور ایجاد و یا لود نوعی دیتا نوشته میشود،
- و یا عملیات خاصی بر روی دیتا است!
با این تفاسیر باید این نکته را همواره مد نظر داشته باشیم که هر دیتایی که تعریف میکنیم، خود انواع مختلفی دارد که به آن Data Type (دیتا تایپ) گفته میشود و بخش عمدهای از آنها در همهی زبانهای برنامهنویسی یکسان میباشند. از دیتا تایپ های معروف میتوان به موارد زیر اشاره کرد:
- اعداد (که شامل انواع مختلفی از جمله اعداد صحیح، اعداد اعشاری، اعداد مختلط و ... است)،
- کاراکترها (یا همان حروف)،
- بولین (یا همان دادههای منطقی)
اما آنچه در این آموزش قصد داریم مورد بررسی قرار دهیم، دیتا تایپهایی است که از ترکیب چند دیتا تایپ یکسان و یا غیریکسان تشکیل میشوند که در زبانهای برنامهنویسی معمولا آرایه (Array) نامیده میشوند ولی در زبان برنامهنویسی پایتون (Python) به آنها List میگوییم.
توجه: از دیگر دیتا تایپهای معروف در پایتون که شبیه لیست هستند میتوان به dictionary ،tuple ،set و forzenset اشاره کرد اما در عین حال هر آبجکت دیگری که حتی خودتان تعریف کرده باشید و شامل مجموعهای از عناصر به هم پیوسته باشد را میتوان در قالب لیست قرار داد.
فانکشن dir برای دریافت فهرست متدها و خصوصیات یک لیست
هنگامی که یک لیست در پایتون تعریف میکنیم، با استفاده از فانکشن ()dir میتوانیم فهرستی از تمامی متدها و خصوصیات کلاس لیست را ببینیم اما نکتهای که در اینجا باید حتماً مد نظر داشته باشیم این است که فانکشن ()dir یک آبجکت را به عنوان آرگومان ورودی گرفته و لیستی از متدها و اَتریبیوتهایی (attributes) که اصطلاحاً private نیستند را برمیگرداند (چه آبجکتی که خودتان تعریف کرده باشید و چه آبجکتی که جزو دیتا تایپهای به اصطلاح Built-in پایتون است، هر دو را میتوان به عنوان پارامتر ورودی به ()dir پاس داد).
مثال برای فانکشن dir
برای روشنتر شدن این مسئله، مثالی میزنیم. فرض کنید لیست l را تعریف کرده و از آن ()dir گرفته باشیم:
>>> l = [1, 2, 3]
>>> dir(l)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__',
'__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear',
'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']متد iter برای لیستها در پایتون
همانطور که در فهرست متدهای لیست مثال بالا مشاهده میشود، متدی به اسم __iter__ در آن قرار دارد که بعد از فراخوانی کردن آن (اصطلاحاً Call کردن)، یک متغیر جدید از لیست ساخته میشود:
>>> i = l.__iter__()
>>> iتایپ این متغیر از جنس list_iterator و در اصل یک Iterator در پایتون است مضاف بر اینکه متد __iter__ جزو فانکشن های Built-in در پایتون (یا «از پیش تعریفشده») بوده و میتوان به شکل زیر هم از آن استفاده کرد:
>>> iter(l)حال در ادامه قصد داریم متغیر i که جدیداً ساختهایم را به عنوان پارامتر ورودی مجدداً به فانکشن ()dir پاس دهیم:
>>> dir(i)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
'__init__', '__init_subclass__', '__iter__', '__le__', '__length_hint__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__']متدهای مختلفی مشاهده میشوند که برای ما __next__ حائز اهمیت است به طوری که با فراخوانی این متد داریم:
>>> i.__next__()
1
>>> i.__next__()
2
>>> i.__next__()
3ضمناً از __next__ به صورت زیر هم میتوان استفاده کرد:
>>> next(i)
1
>>> next(i)
2
>>> next(i)
3احتمالاً پس از این فرایند، باید متوجه مفهوم Iterator در پایتون و عملکرد متد ()next شده باشید.
در واقع، فانکشن __iter__ از دیتا تایپی همچون لیست متغیری ساخته است که به عنصر اول لیست اشاره کرده و آدرس عنصر بعدی را نیز حفظ میباشد و اصطلاحاً گفته میشود که به آن لینک شده است و هنگامی که فانکشن ()next را فراخوانی میکنیم، مقدار Iterator به عنصر بعدی که آدرسش را حفظ است تغییر کرده و آدرس را به عنصر بعدی آپدیت میکند.
یعنی در یک کلام، هر Iterator در پایتون، شامل یک مقدار و یک پوینتر میباشد که آن پوینتر به عنصر بعدی لیست اشاره میکند (این نکته را همواره مد نظر داشته باشید کسانی که با زبانهایی مانند C یا ++C کد زده باشند، با مفهوم پوینتر آشنایی دارند و هر پوینتر در واقع آدرس یک متغیر دیگر را درون خودش ذخیر میکند و به همین دلیل به آن پوینتر به معنی «اشارهگر» میگویند).
متغیرهای Iterable
به طور خلاصه، به متغیرهایی مانند لیست که امکان پیمایش در آنها وجود دارد متغیرهای Iterable و به عملیات پیمایش اصطلاحاً Iteration میگویند. ضمناً در پایتون هر آبجکتی که دارای متد __iter__ باشد نیز Iterable گفته شده و هر آبجکتی که متد __next__ را داشته باشد Iterator است. در مثالهای زیر، شخصاً __iter__ و __next__ را برای کلاسها تعریف کردهایم:
class Fib:
def __init__(self, max):
self.max = max
def __iter__(self):
self.a = 0
self.b = 1
return self
def next(self):
fib = self.a
if fib > self.max:
raise StopIteration
self.a, self.b = self.b, self.a + self.b
return fib
>>> for i in Fib(10):
print i
0
1
1
2
3
5
8
class CustomRange:
def __init__(self, max):
self.max = max
def __iter__(self):
self.curr = 0
return self
def next(self):
numb = self.curr
if self.curr >= self.max:
raise StopIteration
self.curr += 1
return numb
>>> for i in CustomRange(10):
print i
0
1
2
3
4
5
6
7
8
9هنگامی که به انتهای متغیر رسیدیم، هیچ متغیر بعدی وجود نخواهد داشت و از همین روی اگر متد ()next فراخوانی شود، ارور StopIteration تولید میشود که در گامهای بعدی میتوانید آن را هندل کنید. همچنین در تکمیل این مبحث به یاد داشته باشید هنگامی که از دستور for استفاده می کنیم، دو متغیر به آن پاس داده میشود به طوری که برای مثال داریم:
for i in lمتغیر اول در واقع همان Iterator و متغیر دوم Iterable است. متد __iter__ یک بار و فقط هنگام ایجاد for برای ساخت Iterator فراخوانی میشود و بعد به ازای هر بار تکرار حلقه، متد __next__ در ابتدای for کال میشود تا به عنصر بعدی دسترسی پیدا کنیم و از همین روی عملیات پیمایش همینطور انجام میشود تا به انتهای لیست رسیده و ارور StopIteration تولید شود. در اینجا و در ابتدای for، به محض رؤیت این ارور، دیگر عملیات درون حلقه تکرار نمیشود و به اولین خط بعد از حلقه میرویم:
i = iter(l)
while True:
try:
print next(i)
except StopIteration:
breakبرای درک بهتر عملکرد for در پایتون، همانطور که ملاحظه میشود، آن را به صورت جزئیتر با استفاده از while به صورت فوق بازنویسی کردهایم.
Generator در زبان پایتون
هنگام تعریف یک فانکشن در زبان برنامهنویسی پایتون، به جای return میتوان از کلیدواژهی yield نیز استفاده کرد و به تابعی که از yield درون خودش استفاده کند Generator گفته میشود. yield و return دو تفاوت عمده با یکدیگر دارند، که درک تفاوت آن ارتباط تنگاتنگی با مفهوم Iterator در پایتون دارد که پیش از این با آن آشنا شدید، که عبارتند از:
- وقتی تابع به
yieldمیرسد، برخلافreturnاز تابع بیرون نمیآید و ادامهی آن را اجرا میکند تا به انتهای تابع و یا عبارتreturnبرسد. - کلیدواژهی
yieldیک ایتریتور از متغیری که به آن پاس میدهیم ساخته و آن را به جایی که دوبارهyieldتکرار شده باشد لینک میکند (زیرا همانطور که در مورد اول ذکر شد، یک فانکشن میتواند شامل چندین دستورyieldباشد.)
در ادامه، برای اینکه این مطلب کاملاً روشن شود، مثالی میزنیم:
def mygen(a, b):
yield a
yield b
>>> mygen(1, 2)
>>> i=mygen(1,2)
>>> next(i)
1
>>> next(i)
2
>>> next(i)
Traceback (most recent call last):
File "", line 1, in
StopIterationهمانطور که مشاهده میشود، هنگام فراخوانی این فانکشن، یک Iterator (ایتریتور) به ما پاس داده شده که به اولین جایی که عبارت yield را استفاده کردهایم اشاره میکند و به دومین جایی که yield استفاده شده است لینک شده و همینطور این فرایند تا آخرین yield در این فانکشن (تابع) ادامه مییابد. حال مثال زیر را در نظر بگیرید:
def mygen(n):
for i in range(n):
yield iدر این مثال، از یک حلقه استفاده شده است. دقت کنید که اگر به جای yield از return استفاده شده بود، این تابع به محض ورود به حلقۀ for، اولین مقدار را ریترن کرده و خارج میشد اما در اینجا که از yield استفاده کردهایم، تابع تا انتها اجرا شده و به ازای هر باری که yield استفاده شده است، یک مقدار جدید تولید و به مقدار قبلی لینک میکند. همچنین به یاد داشته باشیم که تعریف یک Generator در زبان برنامهنویسی پایتون را با استفاده از لیست میتوان در یک خط انجام داد؛ به عنوان مثال داریم:
>>> squares = (i**2 for i in range(10))
>>> squares
at 0x1069a6d70>i**2 در واقع مقداری است که به yield در تابع جنراتور پاس داده میشود. ضمناً در این حالت، آبجکت متفاوتی توسط پایتون تولید میشود که به آن Generator Expression یا به اختصار genexpr میگویند.
جمعبندی
شاید از خودتان بپرسید که پرداختن به این مبحث اساساً قرار است چه دردی را از یک توسعهدهنده پایتون دوا کند و در واقع ایتریتورها و جنریتورها را کجا میتوان به صورت کاربردی مورد استفاده قرار داد؟
در پاسخ به سؤالاتی از این دست، میتوان گفت که تفاوت عمدهای که یک Iterator در یک متغیر Iterable (همانند لیست) دارد در میزان مصرف memory (مِموری) است. شما برای بررسی یک لیست، به طور معمول باید ابتدا آن را کاملاً در مِموری لود کرده سپس شروع به پیمایش آن کنید ولی هنگامی که از یک Iterator استفاده میکنید، دیگر نگران منابعی مانند مِموری نخواهید بود و این تفاوت در اِسکیلهای بزرگ کاملاً محسوس است.
مثلاً فرض کنید قرار است هر بار دیتاهایی که نمیدانید بزرگی آنها چقدر است را از دیتابیس بخوانید و ممکن است تعداد آنها در برخی موارد خیلی زیاد باشد که در چنین شرایطی اصلاً معقول نیست که هر بار نتیجه را ابتدا در یک متغیر Iterable همانند یک لیست یا تاپل در مِموری لود کرده و از آن استفاده کنید بلکه در اینجا بهتر است که از یک Iterator استفاده کنید و پیمایش را از طریق آن انجام دهید.
به عبارتی، مواقعی که اندازهی دیتا را نمیدانیم و یا مطمئن هستیم دیتا خیلی بزرگ است، استفاده از Iterator در پایتون را میتوان به عنوان راهحل بهینه قلمداد کرد.
