در این آموزش قصد داریم تا به تفصیل دربارۀ فرایند پیمایش دیتا تایپهایی مانند لیست، دیکشنری و غیره از یکسو و همچنین مباحث مرتبط با آنها مثل Iterator و Generator از سوی دیگر نکاتی را برای دولوپرهای تازهکار بیان کنیم که قصد دارند دانش خود را در حوزهٔ یادگیری زبان پایتون عمیقتر سازند.
هر خط کدی که در فرایند توسعهٔ نرمافزار نوشته میشود، از دو حالت زیر خارج نیست؛ یا به منظور ایجاد و یا لود نوعی دیتا نوشته میشود و یا عملیات خاصی بر روی دیتا است که با این تفاسیر باید این نکته را همواره مد نظر داشته باشیم دیتایی که تعریف میکنیم، خود انواع مختلفی دارد که به آن Data Type گفته میشود و بخش عمدهای از آنها در همۀ زبانهای برنامهنویسی یکسان میباشند و از جمله دیتا تایپهای معروف میتوان به اعداد (که خود شامل انواع مختلفی از جمله اعداد صحیح، اعداد اعشاری، اعداد مختلط و ... است)، کاراکترها (یا همان حروف)، بولین (یا همان دادههای منطقی) و ... اشاره کرد.
اما آنچه در این پست قصد داریم مورد بررسی قرار دهیم، دیتا تایپهایی است که از اِجماع چند دیتا تایپ یکسان و یا غیریکسان تشکیل میشوند که در زبانهای برنامهنویسی عمدتاً Array (آرایه) نامیده میشوند ولی در زبان برنامهنویسی پایتون (Python) به آنها List میگوییم (از دیگر دیتا تایپهای معروف که شبیه لیست هستند میتوان به dictionary ،tuple ،set و forzenset اشاره کرد اما در عین حال هر آبجکت دیگری که حتی خودتان تعریف کرده باشید و شامل مجموعهای از عناصر به هم پیوسته باشد را میتوان در این قالب قرار داد.)
آشنایی با مفهوم Iterator در زبان پایتون
هنگامی که یک لیست در پایتون تعریف میکنیم، با استفاده از فانکشن ()dir
میتوانیم فهرستی از تمامی متدها و خصوصیات کلاس لیست را ببینیم اما نکتهای که در اینجا باید حتماً مد نظر داشته باشیم این است که فانکشن ()dir
یک آبجکت را به عنوان آرگومان ورودی گرفته و لیستی از متدها و اَتریبیوتهایی که اصطلاحاً private
نیستند را برمیگرداند (چه آبجکتی که خودتان تعریف کرده باشید و چه آبجکتی که جزو دیتا تایپهای به اصطلاح Built-in پایتون است، هر دو را میتوان به عنوان پارامتر ورودی به ()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__
در آن قرار دارد که بعد از اصطلاحاً 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 و به عملیات پیمایش اصطلاحاً 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 همانند لیست دارد در میزان مصرف مِموری است. شما برای بررسی یک لیست، به طور معمول باید ابتدا آن را کاملاً در مِموری لود کرده سپس شروع به پیمایش آن کنید ولی هنگامی که از یک Iterator استفاده میکنید، دیگر نگران منابعی مانند مِموری نخواهید بود و این تفاوت در اِسکیلهای بزرگ کاملاً محسوس است.
مثلاً فرض کنید قرار است هر بار دیتاهایی که نمیدانید بزرگی آنها چقدر است را از دیتابیس بخوانید و ممکن است تعداد آنها در برخی موارد خیلی زیاد باشد که در چنین شرایطی اصلاً معقول نیست که هر بار نتیجه را ابتدا در یک متغیر Iterable همانند یک لیست یا تاپل در مِموری لود کرده و از آن استفاده کنید بلکه در اینجا بهتر است که از یک Iterator استفاده کنید و پیمایش را از طریق آن انجام دهید. به عبارتی، مواقعی که اندازۀ دیتا را نمیدانیم و یا مطمئن هستیم دیتا خیلی بزرگ است، استفاده از Iterator را میتوان به عنوان راهحل بهینه قلمداد کرد.