تکرارها و حلقه ها (Iterations and Loops) در پایتون – به زبان ساده


در این نوشته با طرز کار تکرار (iteration) در پایتون آشنا میشوید. همچنین iterable-ها و iterator-ها و شیوه ایجاد آنها معرفی شدهاند. در ادامه مفهوم iterator protocol توضیح داده شده و به معرفی lazy evaluation پرداختهایم. همچنین تابعهای generator و عبارتهای generator توصیف شدهاند.
حلقه در پایتون
پایتون از حلقههای سنتی for استفاده نمیکند.
در شبه کد زیر میتوانید حلقه سنتی for که در اغلب زبانهای برنامهنویسی دیگر مورد استفاده قرار میگیرد را ملاحظه کنید:
for (initializer; condition; iterator) body
- بخش مقداردهی اولیه ، یک بار و پیش از ورود به حلقه اجرا میشود.
- بخش شرطی حلقه باید یک عبارت بولی باشد. اگر این عبارت به صورت True ارزیابی شود، تکرار بعدی حلقه اجرا میشود.
- بخش تکرار کننده (iterator) حلقه، آن چه را پس از هر بار تکرار (iteration) رخ میدهد تعریف میکند.
اکنون به بررسی روش تعریف یک حلقه سنتی در زبان جاوا اسکریپت میپردازیم.
let numbers = [10, 12, 15, 18, 20]; for (let i = 0; i < numbers.length; i += 1) { console.log(numbers[i]) }
خروجی
10 12 15 18 20
بسیاری از زبانهای برنامهنویسی دیگر این نوع از حلقه را دارند؛ اما پایتون آن را ندارد. با این وجود، پایتون چیزی به نام حلقه for دارد که مانند یک حلقه foreach عمل میکند:
numbers = [10, 12, 15, 18, 20] for number in numbers: print(number)
خروجی
10 12 15 18 20
چنان که در مثال فوق ملاحظه میکنید، در حلقههای for پایتون ، هیچ بخشی که قبلاً شاهد بودیم را نمیبینیم.
موارد تکرار شونده (Iterables)
تکرار شونده شیئی است که قابلیت بازگشت دادن اعضای خود به صورت یک به یک را دارد. به بیان دیگر یک Iterable به هر چیزی گفته میشود که بتوان روی آن یک حلقه for تعریف کرد.
دنبالهها
دنبالهها نوع بسیار متداولی از موارد تکرار شونده هستند. برخی نمونههای انواع داخلی از دنبالهها شامل لیست، رشته و چندتایی (tuple) هستند.
numbers = [10, 12, 15, 18, 20] fruits = ("apple", "pineapple", "blueberry") message = "I love Python ❤️"
دنباله ها دسترسی کارآمدی در سطح عنصر ارائه می کنند که با استفاده از اندیسها از طریق متد خاص __()getitem__ (اندیسگذاری) و متد __()length__ که طول دنباله را بازمیگرداند، صورت میپذیرد.
# Element access using integer indices print(numbers[0]) print(fruits[2]) print(message[-2])
خروجی
10 blueberry ❤
همچنین میتوانیم از تکنیک برش دادن (slicing) روی آنها استفاده کنیم. منظور از برش دادن، در واقع ایجاد لیستهای زیرمجموعه است.
# Slicing the sequences print(numbers[:2]) print(fruits[1:]) print(message[2:])
خروجی
[10, 12] ('pineapple', 'blueberry') love Python ❤️
موارد تکرار شونده دیگر
بسیاری از چیزها در پایتون قابل تکرار هستند؛ اما همه آنها دنباله نیستند. دیکشنریها، اشیای فایل، مجموعهها و generator-ها همگی موارد تکرار شوندهای هستند که هیچ یک دنباله محسوب نمیشوند.
my_set = {2, 3, 5} my_dict = {"name": "Ventsislav", "age": 24} my_file = open("file_name.txt") squares = (n**2 for n in my_set)
در حلقههای for پایتون از اندیس استفاده نمیشود
اگر در مورد روش تعریف حلقه روی یک مورد تکرار شونده بدون استفاده از حلقه for در پایتون تأمل کنید، ممکن است فکر کنید که میتوانیم از یک حلقه while استفاده کرده و اندیسهایی برای رسیدن به آنها ایجاد نماییم.
index = 0 numbers = [1, 2, 3, 4, 5] while index < len(numbers): print(numbers[index]) index += 1
خروجی
1 2 3 4 5
به نظر میرسد که این رویکرد برای لیستها و دیگر شیءهای دنباله به خوبی کار میکند. اما در مورد اشیای غیر دنبالهای چه میتوان گفت؟ این اشیا از اندیسسازی پشتیبانی نمیکنند و از این رو این رویکرد برای آنها پاسخ نمیدهد.
index = 0 numbers = {1, 2, 3, 4, 5} while index < len(numbers): print(numbers[index]) index += 1
خروجی
-------------------------------------------------------------------- TypeError Traceback (most recent call last) in () 2 numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 3 while index < len(numbers): ----> 4 print(numbers[index]) 5 index += 1 TypeError: 'set' object does not support indexing
در ادامه به طور عملی کارکرد حلقههای for در پایتون را بررسی میکنیم:
numbers = {1, 2, 3, 4, 5} for number in numbers: print(number)
خروجی
1 2 3 4 5
تکرار کنندهها (Iterators)
یک تکرار کننده، شیئی نماینده یک جریان داده (Stream of Data) است. شیء تکرار کننده را میتوان با بهکارگیری تابع داخلی ()iter روی تکرار شونده ایجاد کرد.
numbers = [10, 12, 15, 18, 20] fruits = ("apple", "pineapple", "blueberry") message = "I love Python ❤️" print(iter(numbers)) print(iter(fruits)) print(iter(message))
خروجی
<list_iterator object at 0x000001DBCEC33B70> <tuple_iterator object at 0x000001DBCEC33B00> <str_iterator object at 0x000001DBCEC33C18>
میتوانید از یک تکرار کننده به طور دستی برای تعریف حلقه روی تکرار شونده آن استفاده کنید. ارسال تکراری یک تکرار کننده به تابع داخلی ()next باعث بازگشت دادن آیتمهای متوالی از این جریان میشود. زمانی که یک آیتم از تکرار کننده مصرف میشود، حذف میگردد. زمانی که هیچ دادهای باقی نماند یک استثنای StopIteration ایجاد میشود.
values = [10, 20, 30] iterator = iter(values) print(next(iterator)) print(next(iterator)) print(next(iterator)) print(next(iterator))
خروجی
10 20 30 -------------------------------------------------------------------- StopIteration Traceback (most recent call last) in () 4 print(next(iterator)) 5 print(next(iterator)) ----> 6 print(next(iterator)) StopIteration:
حلقه for پایتون در پسزمینه از تکرار کنندهها استفاده میکنند.
درک کارکرد حلقههای for در پایتون
اینک میدانیم که تکرار شوندهها و تکرار کنندهها چه هستند و چگونه میتوانیم از آنها استفاده کنیم. بنابراین میتوانیم اقدام به تعریف یک تابع بکنیم که بدون استفاده از یک حلقه for شروع به چرخش روی یک تکرار شونده بکند.
بدین منظور به موارد زیر نیاز داریم:
- ایجاد یک تکرار کننده از تکرار شونده مفروض
- دریافت مکرر آیتم بعدی از تکرار کننده
- اجرای عمل مورد نظر
- متوقفسازی حلقه هنگامی که با استثنای StopIteration مواجه میشویم.
def custom_for_loop(iterable, action_to_do): iterator = iter(iterable) done_looping = False while not done_looping: try: item = next(iterator) except StopIteration: done_looping = True else: action_to_do(item)
در ادامه از این تابع روی مجموعهای از اعداد و تابع داخلی print بهره میگیریم:
numbers = {1, 2, 3, 4, 5} custom_for_loop(numbers, print)
خروجی
1 2 3 4 5
میبینیم که تابعی که با استفادهع از کلمه کلیدی def در پایتون تعریف کردهایم، روی مجموعهها نیز که دنباله نیستند به خوبی عمل میکند. این بار میتوانیم هر تکرار شونده را به این تابع ارسال کنیم و به خوبی کار میکند. اساساً همه شکلهای تعریف حلقه روی موارد تکرار شونده در پایتون به همین ترتیب عمل میکنند.
پروتکل تکرار کننده
شیءهای تکرار شونده برای پشتیبانی از دو متد زیر لازم هستند و در مجموع پروتکل تکرار کننده (iterator) را تشکیل میدهند.
- ()iterator.__iter__
این متد خود شیء تکرار کننده را باز می گرداند. این حالت برای ایجاد امکان استفاده از کانتینرها (که مجموعه نیز نامیده میشوند) و تکرار کنندهها به همراه گزارههای for و in ضروری است.
- ()iterator.__next__
این متد آیتم بعدی را از کانتینر باز میگذارند. اگر هیچ آیتمی موجود نباشد، استثنای StopIteration رخ میدهد.
از روی متدهایی که در بخش فوق تعریف کردیم، میفهمیم که میتوان روی یک تکرار کننده نیز حلقه تعریف کرد. بنابراین تکرار کنندهها نیز خود تکرار شونده هستند.
به خاطر داشته باشید که وقتی از تابع ()iter روی یک تکرار شونده استفاده میکنیم، یک تکرار کننده به دست میآوریم. حال اگر تابع ()iter را روی یک تکرار کننده فراخوانی کنیم، همواره خود آن را به دست خواهیم آورد.
numbers = [100, 200, 300] iterator1 = iter(numbers) iterator2 = iter(iterator1) # Check if they are the same object print(iterator1 is iterator2) for number in iterator1: print(number)
خروجی
True 100 200 300
نکات دیگر در خصوص تکرار کنندهها
شاید این موضوع تا حدودی گیجکننده باشد؛ اما اگر فکر میکنید در برخورد اول همه مفاهیم را درک نمیکنید، جای نگرانی نیست چون فرصت زیادی برای تمرین و تکرار دارید.
- یک تکرار شونده (iterable) چیزی است که میتوان روی آن حلقه تعریف کرد.
- یک تکرار کننده (iterator) شیئی است که نماینده جریان داده است. این شیء عمل تکرار روی یک تکرار شونده را اجرا میکند.
به علاوه در پایتون خود تکرار کنندهها نیز جزو اشیای تکرار شونده هستند و به عنوان تکرار کننده خود عمل میکنند.
با این وجود، تفاوت در این نکته است که تکرار کنندهها برخی از ویژگیهای موجود در بعضی از تکرار شوندهها را ندارند. برای نمونه آنها طول ندارند و نمیتوانند اندیسگذاری شوند.
مثال
numbers = [100, 200, 300] iterator = iter(numbers) print(len(iterator))
خروجی
-------------------------------------------------------------------- TypeError Traceback (most recent call last) in () 1 numbers = [100, 200, 300] 2 iterator = iter(numbers) ----> 3 print(len(iterator)) TypeError: object of type 'list_iterator' has no len()
مثال
numbers = [100, 200, 300] iterator = iter(numbers) print(iterator[0])
خروجی
-------------------------------------------------------------------- TypeError Traceback (most recent call last) in () 1 numbers = [100, 200, 300] 2 iterator = iter(numbers) ----> 3 print(iterator[0]) TypeError: 'list_iterator' object is not subscriptable
تکرار کنندهها کُند هستند
تکرار کنندهها امکان ایجاد و کار با تکرار کنندههای کند (lazy iterables) را فراهم میسازند. این نوع از تکرار کنندهها تا زمانی که آیتم بعدی را درخواست نکنیم، هیچ کاری انجام نمیدهند.
به دلیل همین کند بودن این نوع تکرار کنندهها، میتوان از آنها در مورد تکرار شوندههای بسیار طولانی استفاده کرد و در برخی موارد نمیتوان همه اطلاعات را در حافظه ذخیره کرد. از این رو میتوان از یک تکرار کننده استفاده کرد که در هر بار درخواست ، تنها آیتم بعدی را در اختیار ما قرار دهد. بدین ترتیب چنین تکرار کنندههایی میتوانند صرفهجویی زیادی در میزان حافظه و زمان پردازشی سیستم ایجاد کنند. این رویکرد به نام ارزیابی کُند (lazy evaluation) نامیده میشود.
تکرار کنندهها همه جا هستند
در بخشهای پیشین نمونههایی از تکرار کنندهها را داریم. به علاوه پایتون کلاسهای درونی زیادی در مورد تکرار کنندهها دارد. برای مثال شیءهای enumerate و reversed در واقع تکرار کننده هستند.
مثالی از Enumerate
fruits = ("apple", "pineapple", "blueberry") iterator = enumerate(fruits) print(type(iterator)) print(next(iterator))
خروجی
<class 'enumerate'> (0, 'apple')
مثالی از Reversed
fruits = ("apple", "pineapple", "blueberry") iterator = reversed(fruits) print(type(iterator)) print(next(iterator))
خروجی
<class 'reversed'> Blueberry
شیءهای zip, map و Filter نیز تکرار شونده هستند.
مثال zip
numbers = [1, 2, 3] squares = [1, 4, 9] iterator = zip(numbers, squares) print(type(iterator)) print(next(iterator)) print(next(iterator))
خروجی
<class 'zip'> (1, 1) (2, 4)
مثال Map
numbers = [1, 2, 3, 4, 5] squared = map(lambda x: x**2, numbers) print(type(squared)) print(next(squared)) print(next(squared))
خروجی
<class 'map'> 1 4
مثال Filter
numbers = [-1, -2, 3, -4, 5] positive = filter(lambda x: x > 0, numbers) print(type(positive)) print(next(positive))
خروجی
<class 'filter'> 3
به علاوه، شیءهای file در پایتون نیز تکرار کننده هستند.
file = open("example.txt") print(type(file)) print(next(file)) print(next(file)) print(next(file)) file.close()
خروجی
<class '_io.TextIOWrapper'> This is the first line. This is the second line. This is the third line.
همچنین میتوانیم روی جفتهای کلید-مقدار یک دیکشنری پایتون نیز با استفاده از متد ()items حلقه تکرار تعریف کنیم:
my_dict = {"name": "Ventsislav", "age": 24} iterator = my_dict.items() print(type(iterator)) for key, item in iterator: print(key, item)
خروجی
<class 'dict_items'> name Ventsislav age 24
افراد زیادی از پایتون برای حل مسائل مرتبط با علم داده استفاده میکنند. در برخی موارد دادههایی که با آنها سر و کار داریم بسیار بزرگ هستند. در چنین حالاتی نمیتوانیم همه دادهها را در حافظه بارگذاری کنیم.
راهحل این است که دادهها را به صورت بخشبندی شده در حافظه بارگذاری کنیم و سپس عملیات مورد نیز را روی هر بخش اجرا نماییم و سپس آن بخش را کنار گذاشته و بخش بعدی دادهها را در حافظه بارگذاری کنیم. به بیان دیگر باید یک تکرار کننده ایجاد کنیم. این کار با استفاده از تابع read_csv در pandas ممکن است. کافی است که اندازه بخشها (chunksize) را تعیین کنیم.
نمونههایی از مجموعه دادههای بزرگ
در مثال زیر ایده فوق را روی مجموعه داده کوچکی به نام «iris species» بررسی میکنیم؛ اما دقت کنید که مفاهیم اساسی در مورد مجموعه دادههای بسیار بزرگ نیز یکسان است.
import pandas as pd # Initialize an empty dictionary counts_dict = {} # Iterate over the file chunk by chunk for chunk in pd.read_csv("iris.csv", chunksize = 10): # Iterate over the "species" column in DataFrame for entry in chunk["species"]: if entry in counts_dict.keys(): counts_dict[entry] += 1 else: counts_dict[entry] = 1 # Print the populated dictionary print(counts_dict)
خروجی
{'Iris-setosa': 50, 'Iris-versicolor': 50, 'Iris-virginica': 50}
اشیای تکرار کننده بسیار دیگری نیز در کتابخانه استاندارد پایتون و کتابخانههای شخص ثالث وجود دارند.
ایجاد یک تکرار کننده سفارشی با تعریف کردن یک کلاس
در برخی موارد میتوانیم یک تکرار کننده سفارشی ایجاد کنیم. این کار از طریق تعریف کردن یک کلاس که متدهای __init__، __next__ و __iter__ دارد امکانپذیر است.
در ادامه یک کلاس تکرار کننده ایجاد میکنیم که اعدادی بین مقدار کمینه و بیشینه مفروض ارائه میکند.
class generate_numbers: def __init__(self, min_value, max_value): self.current = min_value self.high = max_value def __iter__(self): return self def __next__(self): if self.current > self.high: raise StopIteration else: self.current += 1 return self.current - 1 numbers = generate_numbers(40, 50) print(type(numbers)) print(next(numbers)) print(next(numbers)) print(next(numbers))
خروجی
<class '__main__.generate_numbers'> 40 41 42
میتوانیم ببینیم که این کلاس کار میکند. با این وجود، استفاده از تابع generator و عبارت generator برای ایجاد تکرار کنندههای سفارشی بسیار آسانتر است.
تابعهای Generator و عبارتهای Generator
در مواردی که میخواهیم یک تکرار کننده سفارشی بسازیم به طور معمول از تابعهای Generator و عبارتهای Generator استفاده میکنیم، چون استفاده از آنها سادهتر است و نیاز به کدنویسی کمتری برای رسیدن به نتایج مشابه دارد.
تابعهای Generator
در مستندات پایتون تابع Generator به صورت زیر تعریف شده است:
تابع generator ، تابعی است که یک تکرار کننده generator بازمیگرداند. این تابع مانند یک تابع معمول است؛ به جز این که شامل عبارتهای yield برای تولید یک سری از مقادیر قابل استفاده در یک حلقه for است که میتوان به صورت یک به یک با استفاده از تابع ()next بازیابی کرد.
اینک میتوانیم اقدام به ایجاد مجدد تکرار کننده سفارشی با استفاده از تابع generator بکنیم:
class generate_numbers: def __init__(self, min_value, max_value): self.current = min_value self.high = max_value def __iter__(self): return self def __next__(self): if self.current > self.high: raise StopIteration else: self.current += 1 return self.current - 1 numbers = generate_numbers(40, 50) print(type(numbers)) print(next(numbers)) print(next(numbers)) print(next(numbers))
خروجی
<class 'generator'> 10 11 12
عبارت yield چیزی است که تابع generation را از یک تابع معمولی جدا میکند. این عبارت به ما کمک میکند که از کند بودن تکرار کننده بهره بگیریم. بنا بر توضیحات مستندات پایتون:
هر yield به طور موقت با به خاطر سپاری مکان دقیق حالت اجرایی شامل متغیرهای محلی و معلقسازی گزارههای try، پردازش را به حالت مکث میبرد. وقتی که تکرار کننده generator از سر گرفته شود، کار خود را دقیقاً از همان جایی که متوقف شده بود مجدداً آغاز میکند و این حالت متضاد تابعهای معمولی است که در هر بار فراخوانی کار خود را از نو آغاز میکنند.
عبارتهای Generator
عبارتهای generator بسیار به خلاصه لیست (List Comprehension) ها شباهت دارند. این عبارتها دقیقاً همانند خلاصه لیست، بسیار فشرده هستند. در اغلب موارد این عبارتها در یک خط کد نوشته میشوند. بنا بر توضیحات مستندات پایتون:
عبارت generator عبارتی است که یک تکرار کننده بازمیگرداند. این عبارت به یک عبارت معمول شباهت دارد که پیش از یک عبارت for برای تعریف یک متغیر حلقه، محدوده و یک عبارت if اختیاری آمده باشد.
فرمول کلی آن چنین است که چنین عبارتی متغیر تکرار کننده را در تکرار شونده در خروجی ارائه می کند.
در ادامه روش تعریف یک عبارت generator ساده را میبینید:
numbers = [1, 2, 3, 4, 5] squares = (number**2 for number in numbers) print(type(squares)) print(next(squares)) print(next(squares)) print(next(squares))
خروجی
<class 'generator'> 1 4 9
میتوان یک عبارت شرطی نیز روی تکرار شونده اضافه کرد. طرز کار چنین است:
numbers = [1, 2, 3, 4, 5] squares = (number**2 for number in numbers if number% 2 == 0) print(type(squares)) print(list(squares))
خروجی
<class 'generator'> [4, 16]
امکان دارد به منظور فیلتر کردن بیشتر، چندین عبارت شرطی روی تکرار کننده وجود داشته باشد:
numbers = [1, 2, 3, 4, 5] squares = (number**2 for number in numbers if number% 2 == 0 if number% 4 == 0) print(type(squares)) print(list(squares))
خروجی
<class 'generator'> [16]
همچنین میتوانیم به صورت زیر یک بند if-else را روی عبارت خروجی اضافه کنیم:
numbers = [1, 2, 3, 4, 5] result = ("even" if number% 2 == 0 else "odd" for number in numbers) print(type(result)) print(list(result))
خروجی
<class 'generator'> ['odd', 'even', 'odd', 'even', 'odd']
سخن پایانی
در این نوشته به توضیح تکرار شوندهها و تکرار کننده در پایتون پرداختیم.
طرز کار حلقهها در پایتون با دیگر زبانهای برنامهنویسی کمی متفاوت است. در ادامه مواردی که در این مقاله مطرح شدهاند را جمعبندی کردهایم:
- تکرار شونده (iterable) چیزی است که حلقه روی آن تعریف میشود.
- دنبالهها (Sequences) نوع بسیار متداولی از تکرار شوندهها هستند.
- چیزهای زیادی در پایتون از نوع تکرار شونده هستند؛ اما همه آنها دنباله محسوب نمیشوند.
- تکرار کننده (iterator) شیئی است که نماینده جریان دادهای است. این شیء عمل تکرار را روی تکرار شونده اجرا میکند. از تکرار کننده میتوان برای دریافت مقدار بعدی یا تعریف حلقه روی آن استفاده کرد. زمانی که روی یک تکرار کننده حلقهای تعریف کنید مقادیر جریانی دیگری وجود نخواهند داشت.
- تکرارکنندهها از رویکرد ارزیابی کند (lazy evaluation) استفاده میکنند.
- در پایتون کلاسهای داخلی زیادی در مورد تکرار کنندهها وجود دارند.
- تابع generator تابعی است که یک تکرار کننده بازمیگرداند.
- عبارت generator عبارتی است که یک تکرار کننده بازمیگرداند.
اگر این مطلب برایتان مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزش های برنامه نویسی پایتون
- مجموعه آموزشهای برنامهنویسی
- متدهای وهلهای، استاتیک و کلاس در پایتون — تفاوتهای مهمی که باید بدانید
- پنج دلیل برای کاربردی بودن زبان پایتون
- آموزش نحوه نصب و راه اندازی پایتون
==