مبانی ثانویه‌ی پایتون - حلقه‌ی while و توابع

Professional Python: While Loops + Functions

12 اسفند 1399
درسنامه درس 9 از سری پایتون حرفه‌ای
Python حرفه ای: مبانی ثانویه - حلقه ی while و توابع (قسمت 09)

iterable چیست؟

همانطور که گفتم در این قسمت نوبت به بررسی مفهوم iterable یا «گردش پذیر» می رسد. در ابتدا باید بدانیم چه عناصری در پایتون iterable هستند:

  • list ها
  • dictionary ها
  • tuple ها
  • set ها
  • string ها

ما در جلسه قبل گردش پذیری موارد بالا به جز دیکشنری ها را بررسی کردیم و برای این کار دلیل خاصی داشتیم. گردش روی دیکشنری ها با گردش روی دیگر عناصر لیست بالا متفاوت است. به این کد نمونه توجه کنید:

user = {
  'name' : 'Amir',
  'age' : 25,
  'can_code' : True
}

for item in user:
  print(item)

ما در این کد یک دیکشنری به نام user را داریم و من از حلقه for برای گردش روی آن استفاده کرده و item در هر گردش را چاپ کرده ام. به نظر شما با اجرای این کد چه نتیجه ای می گیریم؟

name
age
can_code

همانطور که می بینید ما کلیدهای دیکشنری را چاپ کرده ایم اما خبری از مقادیر نیست. آیا انتظار چنین نتیجه ای را داشتید؟ خوشبختانه دیکشنری ها سه متد کاربردی برای گردش در حلقه ها دارند. اولین متد، متد items است:

user = {
  'name' : 'Amir',
  'age' : 25,
  'can_code' : True
}

for item in user.items():
  print(item)

با اجرای این کد، کل آیتم (کلید و مقدار کلید) را در قالب یک tuple دریافت می کنید:

('name', 'Amir')
('age', 25)
('can_code', True)

به همین سادگی یک tuple حاوی تمام کلیدها و مقادیر را داریم اما اگر بخواهیم فقط مقادیر را دریافت کنیم چطور؟ در این صورت از متد values استفاده می کنیم:

user = {
  'name' : 'Amir',
  'age' : 25,
  'can_code' : True
}

for item in user.values():
  print(item)

با اجرای این کد فقط بین مقادیر گردش می کنیم و طبیعتا فقط مقادیر را دریافت خواهیم کرد:

Amir
25
True

از طرفی اگر بخواهیم فقط کلیدها را داشته باشیم می توانیم بدون صدا زدن هیچ متدی از خود شیء user استفاده کنیم:

user = {
  'name' : 'Amir',
  'age' : 25,
  'can_code' : True
}

for item in user:
  print(item)

اما اگر در حال انجام یک کار تیمی باشیم بهتر است از متد keys استفاده کنیم تا بقیه افراد در شرکت نیز بتوانند کد ما را به راحتی بخوانند:

user = {
  'name' : 'Amir',
  'age' : 25,
  'can_code' : True
}

for item in user.keys():
  print(item)

هر دوی این کدها کلیدها را برمی گردانند اما استفاده از ()keys برای خوانا تر شدن کد بهتر است.

سوال بعدی من این است که اگر بخواهیم مقادیر و کلیدها را به صورت جداگانه دریافت کنیم چطور؟ در حال حاضر استفاده از متد ()items ما را مجبور می کند که از هر دو مقدار را در قالب یک tuple دریافت کنیم اما من نمی خواهم چنین کاری را انجام بدهم. من می خواهم هر دو کلید و مقدار را دریافت کنم اما حتما از هم جدا باشند. اگر یادتان باشد عملیاتی به نام unpacking داشتیم که برای tuple ها هم مورد استفاده قرار می گرفت و در چنین حالتی نامش tuple unpacking بود. من همین کار را در کد زیر انجام داده ام:

user = {
  'name' : 'Amir',
  'age' : 25,
  'can_code' : True
}

for item in user.items():
  key, value = item
  print(key, value)

ما می توانیم به شکل بالا کلیدها و مقادیر را از items دریافت کنیم و سپس هر دو را چاپ کنیم. با انجام این کار نتیجه زیر را می گیرید:

name Amir
age 25
can_code True

این روش به خوبی جواب می دهد اما روش خلاصه تر و راحت تری برای انجام این کار وجود دارد. آیا می دانید چه روشی؟ ما می توانیم item را در همان ابتدای حلقه unpack کنیم:

user = {
  'name' : 'Amir',
  'age' : 25,
  'can_code' : True
}

for key,value in user.items():
  print(key, value)

با انجام این کار باز هم همان نتیجه قبلی را می گیریم. باز هم یادآوری می کنیم که key و value نام دلخواه متغیرها هستند و لزومی در استفاده از این نام ها نیست. شما می توانید کد بالا را به شکل زیر بنویسید:

user = {
  'name' : 'Amir',
  'age' : 25,
  'can_code' : True
}

for aaaaaaaa,bbbbbbbb in user.items():
  print(aaaaaaaa, bbbbbbbb)

بنابراین می توانید هر نامی را برایشان انتخاب کنید.

تمرینی ساده

حالا که تا حد قابل قبولی با iterable ها آشنا شده ایم باید یک تمرین ساده را حل کنیم تا توانایی هایمان را محک بزنیم. ما می خواهیم یک شمارنده ساده را ایجاد کنیم. فرض کنید ما لیستی به شکل زیر داشته باشیم:

my_list = [1,2,3,4,5,6,7,8,9,10]

من از شما می خواهم که با استفاده از حلقه for روی اعضای آن گردش کرده و تعداد این اعضا را بشمارید. به عبارت دیگر کدی بنویسید که بتواند تعداد اعضای هر لیستی که به آن می دهیم را بشمارد. شاید بگویید ما که با چشم خود می بینید این لیست ۱۰ عضو دارد اما این فقط یک مثال است. در برنامه های واقعی زمانی که کدنویسی می کنید همیشه همه چیز در کدهای خودتان تعریف نمی شود و در بسیاری از اوقات نمی دانید که تعداد اعضای درون یک لیست یا دیکشنری چقدر است. پیشنهاد می کنم که حتما خودتان این تمرین را حل کنید و سپس به پاسخ من نگاه کنید. پاسخ من به شکل زیر است:

my_list = [1,2,3,4,5,6,7,8,9,10]

counter = 0;

for item in my_list:
  counter = counter + 1
  
print(counter)

من در ابتدا یک متغیر به نام counter (شمارنده) را ایجاد کرده و مقدارش را روی صفر گذاشته ام. در مرحله بعدی در یک حلقه ساده قرار می گیریم که در هر گردش ۱ واحد به متغیر counter اضافه می کند. تنها خط کدی که در این حلقه است به زبان ساده می گوید: متغیر counter برابر است با خود متغیر counter به علاوه ۱. در نهایت زمانی که از حلقه خارج می شویم می توانیم این مقدار را print کنیم. با اجرای کد بالا عدد ۱۰ را دریافت می کنیم بنابراین کدهای ما به خوبی کار می کنند. شاید برخی از شما متغیر counter را درون حلقه گذاشته باشید:

my_list = [1,2,3,4,5,6,7,8,9,10]


for item in my_list:
  counter = 0;
  counter = counter + 1
  
print(counter)

اگر counter را داخل حلقه قرار بدهید چه می شود؟ ما توضیح دادیم که در هر بار گردش یک حلقه، کدهای درون آن اجرا می شوند. با این حساب در کد بالا و در هر گردش، متغیر counter دوباره روی صفر تنظیم می شود و تنها در دور آخر است که از حلقه خارج می شویم و counter روی عدد 1 باقی می ماند (با اجرای کد بالا عدد ۱ را دریافت می کنید). این مسئله برای دستور print نیز صحیح است و اگر آن را با indentation یا فرورفتگی می نوشتید، جزئی از حلقه loop به حساب می آمد و نتیجه زیر را می گرفتید:

my_list = [1,2,3,4,5,6,7,8,9,10]

counter = 0;
for item in my_list:
  counter = counter + 1
  
  print(counter)

با اجرای این کد نتیجه زیر را می گیرید:

1
2
3
4
5
6
7
8
9
10

چرا؟ به دلیل اینکه دستور print در هر گردش دوباره اجرا می شود.

حالا یک سوال دیگر از شما دارم. اگر بخواهیم به جای شمارش تعداد اعضا، جمع کل تمامی اعضا را به دست بیاوریم چطور؟ در این صورت می توانیم به روش زیر عمل کنیم:

my_list = [1,2,3,4,5,6,7,8,9,10]

counter = 0;

for item in my_list:
  counter = counter + item
  
print(counter)

با اجرای این کد عدد ۵۵ را دریافت می کنیم بنابراین حلقه ما تمام اعداد داخل لیست را با هم جمع کرده است. ما در این کد به جای اضافه کردن یک واحد (به عنوان یک عضو از لیست) از مقدار خود آن عضو استفاده کرده ایم و آن را با متغیر counter جمع زده ایم. با انجام این کار جمع کل اعضای لیست به دست می آید.

استفاده از range در حلقه ها

زمانی که بحث گردش و حلقه ها در زبان پایتون می شود، یکی از شناخته شده ترین ابزارهای توسعه دهندگان تابع range است. ما قبلا با range آشنا شده ایم و می دانیم که یک بازه عددی به ما می دهد:

print(range(100))

با اجرای این کد نتیجه زیر را می گیریم:

range(0, 100)

شاید در نگاه اول این نتیجه شبیه یک tuple در پایتون باشد اما اینطور نیست. دقت کنید که در اینجا کلمه range را داریم بنابراین یک tuple نیست. این نتیجه یک شیء مخصوص range است بنابراین می توان گفت که range نیز یک iterable است چرا که می توانیم روی آن گردش کنیم. به کد زیر توجه کنید:

for number in range(30):
  print(number)

ما در این حلقه از range استفاده کرده و روی آن گردش کرده ایم. با اجرای این کد نتیجه زیر را می گیریم:

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

همانطور که می بینید از صفر تا ۲۹ را دریافت کرده ایم که یعنی یک بازه ۳۰ تایی از اعداد را داریم. شما نباید از صفر بودن عدد اول تعجب کنید چرا که ما بار ها در این دوره توضیح داده ایم که اعداد در علوم کامپیوتر معمولا از صفر شروع می شوند. بنابراین با استفاده از range می توانیم به تعداد دلخواه خودمان گردش انجام بدهیم. حالا به این کد نگاه کنید:

for number in range(30):
  print("ROXO")

این کد کلمه ROXO را ۳۰ بار چاپ می کند اما اگر به کد بالا نگاه کنید متوجه خواهید شد که ما واقعا نیازی به number نداریم و در هیچ جای حلقه از آن استفاده نکرده ایم. در چنین حالتی برنامه نویسان پایتون قرارداد دارند که از علامت _ (علامت underscore) استفاده کنند:

for _ in range(30):
  print("ROXO")

این یک قرارداد است و پیروی از آن تفاوتی در روند اجرای کد به وجود نمی آورد بلکه تنها برای خواناتر شدن کد و توضیح به دیگر توسعه دهندگان پایتون است تا آن ها نیز بفهمند در این قسمت از کدهای شما نیازی به متغیر نبوده است. شما در پیروی از این قرارداد مختار هستید اما پیشنهاد می کنم به تمام قراردادهای زبان پایتون پایبند باشید. البته می توان کارهای پیشرفته تری نیز با range انجام داد که به وقت خودش با آن آشنا خواهیم شد اما همین درک ساده از range برای استفاده در حلقه ها کافی است.

آشنایی با enumerate

enumerate مانند range یکی از توابع پرکاربرد در برنامه نویسی پایتون است. شما می توانید از این تابع در حلقه ها استفاده کنید و روش کار آن نیز جالب است. ابتدا باید یک iterable را به آن پاس بدهیم و سپس در هر گردش ایندکس را به شما می دهد. به مثال زیر توجه کنید:

for index,char in enumerate('ROXO DEVELOPMENT'):
  print(index, char)

من از unpacking استفاده کرده ام تا داده های پاس داده شده توسط enumerate را از همان ابتدا جدا کنیم. اولین داده index و دومین داده مقدار اصلی خواهد بود. با اجرای این کد نتیجه زیر را دریافت می کنید:

0 R
1 O
2 X
3 O
4  
5 D
6 E
7 V
8 E
9 L
10 O
11 P
12 M
13 E
14 N
15 T

همانطور که می بینید ما ایندکس هر حرف را نیز نوشته ایم. بنابراین زمانی که به شمارنده index نیاز دارید، enumerate بسیار کاربردی خواهد بود.

حلقه‌ while در پایتون

تا این قسمت با حلقه for در پایتون آشنا شده بودیم اما حالا نوبت به حلقه while در پایتون می رسد که دومین حلقه در زبان پایتون است. حلقه while (به معنی «تا زمانی که») همانطور که از نامش پیداست تا زمانی که شرطی صحیح باشد به گردش ادامه می دهد. ساختار کلی حلقه while در پایتون به شکل زیر است:

while condition_is_true:
  do_something

یک مثال ساده از حلقه while در پایتون را در مثال زیر می بییند:

i = 0

while i < 5:
  print(i)

به نظر شما اگر کد بالا را اجرا کنم چه اتفاقی می افتد؟ در صورتی که این کد را اجرا کنید وارد یک «حلقه نامتناهی» یا infinite loop می شوید! یعنی چه؟ در کد بالا متغیر i مقدار صفر را دارد و گفته ایم تا زمانی که i کمتر از ۵ می باشد باید گردش کنیم. از طرفی هیچ مکانیسمی برای افزایش مقدار i وجود ندارد بنابراین i (عدد صفر) همیشه و تا ابد از ۵ کمتر خواهد بود. این مسئله یعنی شرط ما تا ابد True خواهد بود و این حلقه تا ابد اجرا خواهد شد (در کنسول خودمان عدد صفر را بی نهایت بار چاپ خواهیم کرد). اگر اشتباها چنین حلقه ای را روی سیستم خودتان اجرا کنید، نزدیک به ۱۰۰% توانش پردازشی CPU را درگیر خواهید کرد و سیستم شما هنگ می کند.

حلقه های نامتناهی یکی از رایج ترین اشتباهات در بین افراد مبتدی هستند. نوشتن چنین کدی روی یک سرور می تواند آن سرور را به صورت کامل فلج کند بنابراین باید بسیار مراقب باشید تا چنین اشتباهی را نکنید. نهایتا سوالی که مطرح می شود این است که چطور این مشکل را حل کنیم؟ در حلقه های while دستور خاصی به نام break وجود دارد و هر زمانی که اجرا شود، ‌از حلقه خارج می شویم. مثال:

i = 0

while i < 5:
  print(i)
  break

با اجرای این کد فقط یک بار عدد صفر چاپ خواهد شد. چرا؟ به دلیل اینکه ابتدا شرط حلقه بررسی می شود (i از ۵ کوچکتر باشد) که صحیح است بنابراین وارد حلقه می شویم. درون این حلقه یک دستور print را داریم که اجرا می شود و سپس به دستور break می رسیم و از حلقه خارج می شویم. مسئله اینجاست که اگر بخواهیم ۵ بار گردش کنیم چطور؟ در این صورت باید مکانیسمی را ایجاد کنیم که در هر گردش حلقه یک واحد به آن اضافه کند:

i = 0

while i < 5:
  print(i)
  i = i + 1

با اجرای کد بالا نتیجه زیر را می گیریم:

0
1
2
3
4

همانطور که می بینید ما از صفر شروع کرده و تا ۴ پیش رفته ایم (۵ بار گردش). چطور؟ در گردش اول وارد حلقه شده و i را چاپ می کنیم و در انتها یک واحد به i اضافه کرده ایم. در گردش دوم مقدار i برابر با عدد ۱ است اما هنوز کمتر از ۵ است بنابراین باز هم وارد حلقه می شویم، i را چاپ کرده و یک واحد به آن اضافه می کنیم. این بار i برابر با ۲ است اما هنوز کمتر از ۵ می باشد و الی آخر. این فرآیند آنقدر ادامه پیدا می کند تا i برابر ۵ بشود. در این حالت ۵ کمتر از ۵ نیست بلکه مساوی هستند بنابراین شرط برقرار نبوده و دیگر وارد حلقه نخواهیم شد.

شما می توانید با مسائلی که در جلسات قبل توضیح داده بودیم، این کد را به سلیقه خودتان ویرایش کنید. به طور مثال اگر می خواهید عدد ۵ را نیز در نتیجه داشته باشیم (۶ بار گردش) باید از اپراتور کمتر مساوی استفاده کنیم:

i = 0

while i <= 5:
  print(i)
  i = i + 1

نتیجه:

0
1
2
3
4
5

همچنین به جای i = i + 1 می توانید از حالت خلاصه آن استفاده کنید که قبلا هم برایتان توضیح داده بودم:

i = 0

while i <= 5:
  print(i)
  i += 1

برای جمع بندی این موضوع می توان گفت که فقط دو راه برای خارج شدن از حلقه وجود دارد:

  • False کردن شرط (مثال بالا افزایش i)
  • break

نکته بعدی اینجاست که حلقه while در پایتون از else هم پشتیبانی می کنند:

i = 0

while i <= 5:
  print(i)
  i += 1
else:
  print("All finished")

اگر از حلقه های for یادتان باشد می دانید که else زمانی اجرا می شود که شرط اصلی حلقه صحیح نباشد. با این حساب با اجرای کد بالا نتیجه زیر را می گیریم:

0
1
2
3
4
5
All finished

یعنی دقیقا زمانی که i برابر با ۶ می شود (شرط حلقه false می شود) وارد else شده و یک دستور print را اجرا می کنیم. یادتان باشد که قسمت else در این مثال گردش نمی کند بلکه فقط یک بار اجرا خواهد شد. همچنین در صورتی که دستور break ای در قسمت اصلی حلقه وجود داشته باشد دیگر else اجرا نخواهد شد. مثال:

i = 0

while i <= 5:
  print(i)
  i += 1
  break
else:
  print("All finished")

نتیجه یک عدد صفر است و دیگر عبارت All finished را نداریم چرا که هر زمان به دستور break برسیم از کل حلقه خارج می شویم.

حلقه for یا حلقه while؟

سوال بعدی ما این است که چه زمانی از حلقه for و چه زمانی از حلقه while در پایتون استفاده کنیم. ما می توانیم اکثر مسائل برنامه نویسی را با هر دو حلقه انجام بدهیم. به طور مثال فرض کنید لیستی داشته باشیم و بخواهیم اعضای آن را print کنیم. این کار با هر دو حلقه به سادگی انجام می شود:

my_list = ["Amir", "Roxo", "Development"]

for item in my_list:
  print(item)
  

i = 0
while i < len(my_list):
  print(my_list[i])
  i += 1

من در شرط حلقه while گفته ام تا زمانی که i کمتر از طول لیست (تعداد اعضای آن) باشد آن عضو را پرینت کنیم. در ضمن از i به عنوان ایندکس استفاده کرده ایم. نتیجه اجرای این کد به شکل زیر خواهد بود:

Amir
Roxo
Development
Amir
Roxo
Development

بنابراین هر دو کد دقیقا یک کار را انجام می دهند اما نکاتی در مورد هر کدام وجود دارد. همانطور که مشاهده می کنید حلقه های while انعطاف پذیرتر هستند چرا که تعداد دفعات گردش دقیقا به خودمان بستگی دارد. به طور مثال ما می توانیم به جای ۳ بار ۱۰ بار یا ۱۰۰ بار گردش کنیم. در چنین حالتی کنترل کامل حلقه به دست خود ما است و در عین حال باعث ایجاد خطراتی مانند حلقه های نامتناهی می شود. از طرف دیگر حلقه های for ساده تر هستند بنابراین راحت تر نوشته شده و راحت تر نیز خوانده می شوند. من شخصا چنین قانونی را به شما پیشنهاد می دهم: در صورتی که می خواهید روی یک شیء یا لیست یا هر iterable دیگری گردش کنید از حلقه های for استفاده کنید اما اگر در موقعیتی هستید که نمی دانید چند بار باید گردش انجام بدهید یا به کنترل بیشتری نیاز دارید از حلقه while در پایتون استفاده کنید. یک مثال ساده از انعطاف پذیری حلقه while در پایتون را در کد زیر مشاهده می کنید:

while True:
  response = input("Your name?")
  if (response == "bye"):
    print("Bye")
    break
  elif(response):
    print("Hello " + response)

شرط این کد را True گذاشته ام بنابراین همیشه اجرا می شود. زمانی که وارد حلقه می شویم نام کاربر از او پرسیده می شود و در متغیری به نام reponse ذخیره می شود. اگر کاربر عبارت Bye را وارد کند break کرده و از حلقه خارج می شویم اما اگر نام خودش را وارد کند نامش را برایش چاپ کرده و دوباره حلقه اجرا می شود (یعنی دوباره می پرسیم که نامش چیست) و این مسئله تا بی نهایت بار ادامه دارد مگر اینکه کاربر bye را تایپ کند. در چنین حالتی اصلا نمی دانیم قرار است حلقه چند بار اجرا شود بنابراین استفاده از while انتخاب درست است.

تفاوت دستورات break و continue و pass

تا این بخش از دوره با دستور break آشنا شدیم و می دانیم که این دستور ما را از حلقه خارج می کند. باید برایتان توضیح بدهم که این دستور مختص while نیست بلکه می توانید از آن درون for نیز استفاده کنید اما دستورات دیگری در نیز در این زمینه وجود دارند که گاهی باعث سردرگمی برنامه نویسان تازه کار می شوند: continue و pass.

دستور اول continue است که به فارسی معنی «ادامه بده» می باشد و به حلقه می گوید که به کارش ادامه بدهد. چطور؟ زمانی که در حلقه به دستور continue برسیم کدهای پس از آن نادیده گرفته شده و مستقیما به گردش بعدی حلقه می رویم. به مثال زیر توجه کنید:

my_list = ["Academy", "Roxo", "DevOps"]

for item in my_list:
  print(item)
  continue
  print("After Continue")

با اجرای کد بالا نتیجه زیر را دریافت می کنیم:

Academy
Roxo
DevOps

همانطور که می بینید هیچ خبری از عبارت After Continue نیست چرا که دستور پرینتِ آن بعد از continue آمده است و کد هیچ گاه به این قسمت نمی رسد.

اما دستور pass چطور؟ این دستور کاربرد زیادی ندارد و عملا کاری انجام نمی دهد! احتمالا می گویید پس چه استفاده ای دارد؟ دستور pass معمولا به عنوان یک placeholder مطرح می شود یعنی آن را در کدهای خود قرار می دهیم تا بعدا آن را با کد اصلی جایگزین کنیم، با این کار یادمان نمی رود که قرار است در این قسمت کد خاصی را بنویسیم. فرض کنید یک حلقه داشته باشیم اما فعلا ندانیم که قرار است چه کاری را با آن انجام بدهیم:

my_list = ["Academy", "Roxo", "DevOps"]

for item in my_list:
  # بعدا کدهای این قسمت را بررسی می کنیم

با اجرای این کد خطای unexpected EOF while parsing را دریافت می کنیم. چرا؟ به دلیل اینکه انتهای این حلقه for مشخص نیست. اگر یادتان باشد گفتم که پایتون بر اساس فرورفتگی ها، بلوک کدها را تشخیص می دهد و اگر فقط یک کامنت را برای یادآوری خودمان قرار بدهیم بقیه کدها به درستی اجرا نمی شوند. برای حل این مشکل از pass استفاده می کنیم:

my_list = ["Academy", "Roxo", "DevOps"]

for item in my_list:
  # بعدا کدهای این قسمت را بررسی می کنیم
  pass

با اجرای این کد هیچ نتیجه ای نمی گیریم. بنابراین pass برای پُر کردن جای کدهای واقعی به کار می رود، چیزی شبیه به قراردادن زنبیل در صف نانوایی! در نهایت دستور break را داریم که قبلا کارش را برایتان توضیح دادم؛ این دستور ما را به طور کامل از حلقه خارج می کند در حالی که continue به گردش بعدی از حلقه می رود.

تمرینی ساده: ایجاد تصویر با حلقه

ما در این قسمت می خواهیم یک تمرین بسیار ساده را داشته باشیم. در این تمرین یک ماتریس را داریم که به شکل زیر است:

picture = [
  [0,0,0,1,0,0,0],
  [0,0,1,1,1,0,0],
  [0,1,1,1,1,1,0],
  [1,1,1,1,1,1,1],
  [0,0,0,1,0,0,0],
  [0,0,0,1,0,0,0]
]

من از شما می خواهم که با یک حلقه در این ماتریس گردش کنید و به ازای هر عدد صفر، یک اسپیس و به ازای هر عدد ۱، یک علامت ستاره (*) نمایش بدهید. اگر این کار را درست انجام بدهید، در انتهای کار باید تصویری شبیه به یک درخت داشته باشید. این یک تمرین بسیار عالی برای شماست تا سطح آشنایی خود با حلقه ها را بسنجید و عین حال درک خود را نیز ارتقاء بدهید. پیشنهاد می کنم حتما خودتان این تمرین را حل کنید حتی اگر یک ساعت از وقت شما را بگیرد، سپس به پاسخ و توضیحات من مراجعه کنید.

شما می توانید از روش های مختلفی برای حل این مسئله استفاده کنید. من دو روش ممکن برای حل این مسئله را برایتان قرار می دهم و در هر کدام از تکنیک های متفاوتی استفاده می کنم. در روش اول از یک رشته ساده استفاده کرده ایم:

picture = [
  [0,0,0,1,0,0,0],
  [0,0,1,1,1,0,0],
  [0,1,1,1,1,1,0],
  [1,1,1,1,1,1,1],
  [0,0,0,1,0,0,0],
  [0,0,0,1,0,0,0]
]

for index,row in enumerate(picture):
  rowString = ""
  for number in picture[index]:
    if number == 0:
      rowString += " "
    if number == 1:
      rowString += "*"
  print(rowString)

من در ابتدا از enumerate استفاده کرده ام تا به ایندکس هر لیست از لیست های درون picture دسترسی داشته باشم. حالا وارد اولین حلقه for شده و این بار یک متغیر به نام rowString را ایجاد می کنم که در ابتدا خالی است، سپس وارد حلقه دوم می شویم. این حلقه روی تک تک اعداد هر لیست گردش می کند در حالی که حلقه اول روی هر لیست (مجموعا ۶ گردش) گردش می کرد. حالا اگر عدد ما برابر با صفر بود یک اسپیس و در صورتی که عدد ۱ بود یک علامت ستاره را به rowString اضافه می کنیم. در مرحله نهایی نیز این ردیف را پرینت می کنیم که می شود اولین ردیف ما. در مرحله بعدی وارد دور دوم از حلقه اول می شویم (یعنی لیست دوم درون pictures) و دوباره یک rowString جدید را تعریف می کنیم. سپس وارد حلقه دوم می شویم (تک تک اعداد درون لیست دوم از pictures) و همان کار قبلی را تکرار می کنیم تا یک ردیف دیگر را نیز به دست بیاوریم. با انجام این کار نتیجه زیر را می گیریم:

   *   
  ***  
 *****
*******
   *   
   *   

همانطور که می بینید یک درخت ساده را دریافت کرده ایم. روش دوم انجام این کار کمی پیشرفته تر است و با استفاده از کد زیر انجام می شود:

picture = [
  [0,0,0,1,0,0,0],
  [0,0,1,1,1,0,0],
  [0,1,1,1,1,1,0],
  [1,1,1,1,1,1,1],
  [0,0,0,1,0,0,0],
  [0,0,0,1,0,0,0]
]


for row in picture:
  for number in row:
    if (number):
      print('*', end ="")
    else:
      print(' ', end ="")
  print('')

بگذارید این کد را برایتان توضیح بدهم. ما در اینجا به جای استفاده از index از نام متغیر استفاده کرده ایم که کار بهتری است و کد ما را خواناتر می کند. نکته اصلی در این روش این است که باید بدانید دستور print به صورت پیش فرض آرگومانی به نام end دارد که روی n\ تنظیم شده است. کاراکتر n\ یعنی newline یا به عبارت ساده تر کاراکتری است که باعث رفتن به خط بعدی می شود (معادل کلید enter روی کیبورد شما). ما در کد بالا end را برابر با یک رشته خالی گذاشته ایم تا دیگر برابر با n\ نباشد و به خط بعدی نرویم بلکه همه چیز پشت سر هم و در یک خط باشد. در انتهای حلقه دوم نیز یک دستور print خالی را داریم تا باعث n\ شود و به خط بعد برویم.

مهارت یادگیری ۴#

در شماره چهارم از مهارت یادگیری در مورد مبحث «کد خوب» صحبت می کنیم. کد خوب چیست؟ کد خوب چندین مولفه مهم دارد که همگی تا حد زیادی به هم وابسته هستند:

اولین مولفه کد خوب تمیز بودن کد (clean code) است. تمیز بودن کد یعنی از اسپیس های اضافه استفاده نکنیم، از قرارداد های زبان پایتون و توسعه دهندگان آن استفاده کنیم، کدها مرتب شده باشند، یکپارچگی کد را رعایت کنیم (به طور مثال یک اشتباه: در یک قسمت از کد بین اعضای لیست اسپیس بگذاریم اما در قسمت دیگر اعضا را بدون اسپیس بنویسیم)، از کامنت های زیاد و تکراری و توضیحات بی جا استفاده نکنیم و الی آخر.

مولفه دوم خوانایی کد (code readability) است. خوانایی کد یعنی دیگر افراد بتوانند به راحتی کد ما را خوانده و متوجه آن بشوند یا به زبان ساده تر کدهای خود را با منطقی عجیب و غریب ننوشته باشید. یک جنبه دیگر خوانایی کد این است که اگر کدهای خودتان را پس از یک سال به شما نشان دادند بتوانید سریعا متوجه بشوید که کارکرد آن چه بوده است. به طور مثال استفاده از نام های عجیب برای متغیر ها کار بسیار بدی است (استفاده از variable1 به جای user_name). به طور مثال به کد خودمان از قسمت قبل توجه کنید:

for row in picture:
  for number in row:
    if (number):
      print('*', end ="")
    else:
      print(' ', end ="")
  print('')

اگر کسی این کد را بخواند شاید متوجه نشود که دستور print آخر (رشته خالی) برای چه هدفی نوشته شده است و این کد گُنگ می باشد بنابراین بهتر است یک کامنت را در کنار آن اضافه کنیم:

for row in picture:
  for number in row:
    if (number):
      print('*', end ="")
    else:
      print(' ', end ="")
  print('') # این قسمت برای رفتن به خط بعدی در هر ردیف است

مولفه سوم کد خوب «پیش بینی پذیری» یا predictibilaty می باشد. برخی از اوقات برنامه نویسان سعی می کنند فشرده ترین کدهای ممکن را بنویسند، برخی اوقات سعی می کنند از اپراتور های کمتر شناخته شده استفاده کنند، برخی اوقات از ابزار های عجیب و غریبی استفاده می کنند که کسی با آن ها آشنا نیست. معمولا هدف از این کار این است که خودی نشان بدهند و به خود افتخار کنند اما در جامعه برنامه نویسان این کار به شدت اشتباهی است چرا که دیگران نمی توانند هدف و کارکرد کد شما را پیش بینی کنند، آن ها نمی توانند مقدار برگردانده شده توسط تابع عجیب و غریب شما را پیش بینی کنند، آن ها نمی توانند کدهایی بنویسند که با کد شما سازگار باشد و الی آخر. بنابراین سعی کنید کدهایتان ساده و قابل فهم باشد و از روش های میانبر عجیب و غریب استفاده نکنید.

مولفه نهایی کد خوب DRY نامیده می شود که مخفف Do not Repeat Yourself (به معنی «حرف خودت را تکرار نکن») می باشد. زمانی که به کدهایتان نگاه می کنید نباید کدهای تکراری را زیاد ببینید. اگر در طول توسعه از کلیدهای Ctrl + C برای کپی کردن بلوک های کد استفاده می کنید یعنی یک قسمت از کار اشکال دارد و code duplication (تکرار کد) رخ داده است. شما باید با استفاده از توابع که بعدا با آن ها آشنا می شویم یا با استفاده از روش های دیگر جلوی تکرار کدها را بگیرید. در مثال خودمان اگر به جای یک درخت، دو درخت بخواهیم نباید کل حلقه for را دوباره کپی کنیم.

تمرینی ساده: بررسی مقادیر تکراری در لیست

فرض کنید ما لیست زیر را داشته باشیم و بخواهیم مقادیر تکراری در آن را چاپ کنیم:

some_list = ['a', 'b', 'c', 'b', 'd', 'm', 'n', 'n']

اگر یادتان باشد یک روش بسیار ساده برای انجام این کار استفاده از set ها بود و کافی بود تنها لیست خود را به set تبدیل کنیم اما بحث ما فعلا روی حلقه ها است بنابراین شما اجازه استفاده از set ها برای حل این تمرین را ندارید و باید با حلقه ها آن را حل کنید. ما در لیست بالا b و n را به صورت تکراری داریم بنابراین باید آن ها را نمایش بدهیم. سعی کنید خودتان به پاسخ برسید و سپس پاسخ من را بررسی کنید:

some_list = ['a', 'b', 'c', 'b', 'd', 'm', 'n', 'n']

duplicates = []
for value in some_list:
    if some_list.count(value) > 1:
        if value not in duplicates:
            duplicates.append(value)

print(duplicates)

ما در کد بالا ابتدا یک متغیر به نام duplicates را داریم که یک لیست است. در مرحله بعدی وارد حلقه for شده و یک شرط if را در آن تعریف می کنیم. این شرط یک موضوع خاص را بررسی می کند؛ آیا می توانید خودتان متوجه آن شوید؟ اگر یادتان باشد به شما گفته بودم که متد count تعداد دفعات تکرار یک عضو از لیست را نمایش می دهد بنابراین ما آن را صدا زده ایم و value (مقدار هر عضو در هر گردش) را به آن پاس داده ایم. در صورتی که تعداد برگردانده شده بیشتر از ۱ باشد می دانیم که آن عضو چند بار تکرار شده است (۲ بار یا ۳ بار یا ۱۰۰ بار و...) بنابراین آن را با متد appent به لیست خودمان اضافه می کنیم. البته قبل از انجام این کار یک شرط دیگر را نیز داریم؛ value نباید از قبل درون duplicates باشد یا به زبان ساده تر نباید درون duplicates مقدار تکراری داشته باشیم و از هر نسخه تکراری فقط یک عدد می خواهیم. اگر یادتان باشد not به معنی نفی یک مقدار یا شرط بود و از طرفی in مشخص می کرد که آیا مقداری درون لیست ما وجود دارد یا خیر. با ترکیب این دو می توانیم عدم وجود یک مقدار در لیست را به سادگی مشخص کنیم.

آشنایی با توابع

ما در طول این دوره با تابع یا function کار کرده ایم؛ به طور مثال تابع input به ما اجازه می داد که از کاربر مقداری را دریافت کنیم یا print به ما اجازه چاپ یک مقدار را می داد. با اینکه این توابع بسیار کاربردی هستند اما باید در نظر داشته باشید که قدرت اصلی توابع هنگامی است که خودمان آن ها را تعریف کنیم. ما می توانیم خودمان توابع مورد نظر خودمان را تعریف کرده و هر جایی که خواستیم از آن استفاده کنیم. اگر بخواهم به زبان ساده بگویم، توابع مجموعه ای از کدها هستند که به صورت یکجا اجرا می شوند. به مثال زیر توجه کنید:

def say_hello():
  print('Hello Roxo')

مثال بالا یک تابع ساده است. برای تعریف توابع باید از کلیدواژه def (مخفف define به معنی «تعریف کردن») استفاده کرده و سپس نامی را برای تابع انتخاب کنیم که من say_hello را انتخاب کرده ام. برای نام گذاری توابع خود از همان قوانین مربوط به نام گذاری متغیر ها استفاده کنید. در مرحله بعدی باید حتما پرانتزها را نیز پس از نام تابع قرار بدهید چرا که پرانتزها قسمت جدانشدنی توابع هستند. حالا وارد بلوک کد این تابع شده و مشخص می کنیم که وظیفه این تابع چیست. تابع ما در مثال بالا بسیار ساده است و قرار است فقط عبارت hello roxo را چاپ کند اما با اجرای کد بالا چنین اتفاقی نمی افتد. چرا؟ شما نباید دو مبحث زیر را با هم اشتباه بگیرید:

  • تعریف تابع: در این قسمت تابع را تعریف می کنیم (مانند کشیدن نقشه یک ساختمان)
  • اجرای تابع: در این قسمت کدهای تابع را اجرا می کنیم (مانند ساختن یک ساختمان از روی نقشه). به این مرحله «صدا زدن» (در انگلیسی call) یا «فراخوانی» (در انگلیسی invoke) تابع گفته می شود.

بنابراین اگر می خواهیم عبارت hello roxo چاپ شود باید به شکل زیر عمل کنیم:

def say_hello():
  print('Hello Roxo')
  
say_hello()

برای اجرای توابع همیشه باید نام آن ها را به همراه علامت های پرانتز بیاورید. دقت کنید که فراخوانی تابع ما خارج از بلوک کد def است. با اجرای این کد نتیجه Hello Roxo را دریافت می کنید. در ضمن ما می توانیم توابع را چندین بار صدا کنیم:

def say_hello():
  print('Hello Roxo')
  
say_hello()
say_hello()
say_hello()

با این کار سه بار عبارت Hello Roxo برایمان نمایش داده می شود. حالا از شما سوالی دارم: اگر فراخوانی تابع را قبل از تعریف آن انجام بدهیم چه اتفاقی می افتد؟

say_hello()

def say_hello():
  print('Hello Roxo')

با اجرای این کد خطای زیر را می گیرید:

Traceback (most recent call last):
  File "./prog.py", line 3, in <module>
NameError: name 'say_hello' is not defined

یعنی say_hello تعریف نشده است. همانطور که نمی توانیم یک ساختمان را قبل از کشیدن نقشه اش بسازیم، نمی توانیم تابع را قبل از تعریف شدنش اجرا کنیم. این یکی از نکات مهمی است که باید به یاد داشته باشید.

همچنین اگر یادتان باشد در مهارت یادگیری مربوط به «کد خوب» توضیح دادم که قانونی به نام DRY وجود دارد که به تکرار نکردن کدها اشاره می کند. توابع یکی از بهترین راه های جلوگیری از تکرار کدها هستند چرا که می توانیم یک مکانیسم خاص را در آن ها کدنویسی کرده و سپس در تمام قسمت های برنامه از آن استفاده کنیم. حتما مثال چاپ درخت را یادتان است. اگر در آن مثال بخواهیم سه درخت داشته باشیم باید آن کد را سه بار تکرار کنیم:

picture = [
  [0,0,0,1,0,0,0],
  [0,0,1,1,1,0,0],
  [0,1,1,1,1,1,0],
  [1,1,1,1,1,1,1],
  [0,0,0,1,0,0,0],
  [0,0,0,1,0,0,0]
]


for image in picture:
  for pixel in image:
    if (pixel):
      print('*', end ="")
    else:
      print(' ', end ="")
  print('')

for image in picture:
  for pixel in image:
    if (pixel):
      print('*', end ="")
    else:
      print(' ', end ="")
  print('')

for image in picture:
  for pixel in image:
    if (pixel):
      print('*', end ="")
    else:
      print(' ', end ="")
  print('')

طبیعتا انجام چنین کاری باعث شلوغ شدن کدهای ما می شودو فایل های برنامه ما را سنگین تر می کند. برای حل این مشکل می توانیم از توابع استفاده کنیم:

picture = [
  [0,0,0,1,0,0,0],
  [0,0,1,1,1,0,0],
  [0,1,1,1,1,1,0],
  [1,1,1,1,1,1,1],
  [0,0,0,1,0,0,0],
  [0,0,0,1,0,0,0]
]


def printTree():
  for image in picture:
    for pixel in image:
      if (pixel):
        print('*', end ="")
      else:
        print(' ', end ="")
    print('')


printTree()
printTree()
printTree()
printTree()

با اجرای این کد و بدون تکرار کردن و شلوغ کردن کدها، چهار درخت را چاپ می کنیم.

پارامتر یا آرگومان؟

زمانی که بحث توابع مطرح می شود، حتما دو مفهوم پارامتر (parameter) و آرگومان (argument) نیز حضور خواهند داشت. پارامترها مقادیری هستند که تابع برای اجرا به آن ها نیاز دارد بنابراین آن ها را در زمان تعریف یک تابع به آن پاس می دهیم. به مثال زیر توجه کنید:

name = 'Amir'
print('hello ' + name)

در اینجا متغیری با نام Amir را داریم و آن را به رشته hello چسبانده ام تا عبارت hello Amir چاپ شود. ما اگر بخواهیم این کار را در قالب یک تابع انجام بدهیم باید از پارامترها استفاده کنیم:

def say_hello(name):
  print(f'Hello {name}')


say_hello("Amir")
say_hello("Ahmad")
say_hello("Nastaran")

همانطور که می بینید پارامتر یک نام دلخواه است که در هنگام تعریف تابع درون پرانتزها قرار می گیرد. من نام name را برای این پارامتر انتخاب کرده ام اما شما می توانید هر نام دیگری را انتخاب کنید (حتی یک رشته بی معنی مانند adkuasdiuh اما چنین کاری از نظر خوانایی کد بسیار بد است). در کد بالا مشخص است که تنها کار انجام شده توسط این تابع چاپ رشته Hello name است به طوری که name باید به آن پاس داده شود. من سه بار این تابع را با نام های مختلف صدا زده ام. به این رشته های پاس داده شده آرگومان می گویند! با اجرای این کد، نتیجه زیر را نیز می گیریم:

Hello Amir

Hello Ahmad

Hello Nastaran

همانطور که می بینید آرگومان همان مقدار واقعی است که جای پارامتر را در تعریف تابع می گیرد. با این حساب به طور خلاصه می توان گفت که:

  • پارامتر: نام متغیر پاس داده شده به تابع در هنگام تعریف آن.
  • آرگومان: مقدار واقعی پاس داده شده به تابع در هنگام فراخوانی آن.

در نظر داشته باشید که هیچ محدودیتی در تعداد پارامترهای تعریف شده برای یک تابع وجود ندارد. مثال:

def say_hello(username, status):

  print(f'Hello {username} your status is: {status}')




say_hello("Amir", "developer")

say_hello("Ahmad", "manager")

say_hello("Nastaran", "PR")

با اجرای کد بالا نتیجه زیر را می گیریم:

Hello Amir your status is: developer

Hello Ahmad your status is: manager

Hello Nastaran your status is: PR

به همین سادگی با استفاده از توابع باعث جلوگیری از تکرار کد شده ایم و به جای اینکه هر بار دستور print را بنویسیم فقط یک بار تابع را صدا می زنیم. در ضمن این مثالی بسیار کوچک بود و تابع ما کاری به جز print انجام نمی دهد اما در برنامه های واقعی توابعی را پیدا می کنید که ده ها خط هستند و انواع کارهای مختلف را انجام می دهند. در چنین حالتی برنامه را از تکرار کدهای بسیار زیادی نجات داده ایم.

انواع آرگومان

تا این بخش فقط با یک نوع پارامتر و آرگومان آشنا شدید که به آن ها پارامترها یا آرگومان های positional (موقعیتی) می گویند. چرا؟ به دلیل اینکه در این نوع آرگومان ها، موقعیت اهمیت دارد؛ اینکه چه مقداری اول بدهید و چه مقداری را دوم بدهید نتیجه را تغییر می دهد. دوباره به مثال قبلی خودمان نگاهی بیندازید:

def say_hello(username, status):

  print(f'Hello {username} your status is: {status}')




say_hello("Amir", "developer")

say_hello("Ahmad", "manager")

say_hello("Nastaran", "PR")

اگر در هنگام فراخوانی این تابع رشته ها را با ترتیب اشتباهی پاس بدهیم چطور؟

def say_hello(username, status):

  print(f'Hello {username} your status is: {status}')




say_hello("developer", "Amir")

say_hello("manager", "Ahmad")

say_hello("PR", "Nastaran")

با اجرای این کد چنین چیزی دریافت می کنیم:

Hello developer your status is: Amir

Hello manager your status is: Ahmad

Hello PR your status is: Nastaran

در واقع جای username و status فقط و فقط به ترتیب آن ها بستگی دارد بنابراین به آن ها پارامتر موقعیتی می گوییم.

در عین حال دسته دومی از آرگومان ها به نام keyword argument (آرگومان های کلیدواژه ای) وجود دارند. در این دسته از آرگومان ها می توانیم با اشاره به نام پارامتر، مستقیما مقداری را به آن بدهیم:

def say_hello(username, status):

  print(f'Hello {username} your status is: {status}')



say_hello(status="developer", username="Amir")

say_hello(status="manager", username="Ahmad")

say_hello(status="PR", username="Nastaran")

همانطور که می بینید در هنگام پاس دادن آرگومان ها، به نام پارامتر آن ها اشاره کرده ام تا پایتون به صورت صریح متوجه شود که هر مقدار برای کدام پارامتر است. با اجرای این کد نتیجه صحیح را می گیریم:

Hello Amir your status is: developer

Hello Ahmad your status is: manager

Hello Nastaran your status is: PR

با اینکه ترتیب آرگومان های پاس داده شده صحیح نبود اما این بار پایتون تشخیص داد که کدام مقدار مربوط به کدام پارامتر است. به صورت کلی توصیه می شود از آرگومان های کلیدواژه ای استفاده نکنید چرا که بی جهت کدهایتان را شلوغ و پیچیده می کنید.

پارامترهای پیش فرض

پارامترهای پیش فرض به فرآیند تعیین یک مقدار پیش فرض برای یک پارامتر گفته می شود. دوباره به کد قبلی خود برمی گردیم:

def say_hello(username, status):

  print(f'Hello {username} your status is: {status}')




say_hello()

همانطور که می بینید من این تابع را بدون پاس دادن آرگومان صدا زده ام. به نظر شما با انجام این کار چه اتفاقی می افتد؟ بله یک خطا دریافت می کنیم چرا که تابع از ما دو مقدار (دو آرگومان) می خواهد اما ما چیزی را به آن پاس نمی دهیم. برای حل این مشکل می توانیم پارامترهای پیش فرضی را تعیین کنیم:

def say_hello(username='Amir', status='developer'):

  print(f'Hello {username} your status is: {status}')




say_hello()

یعنی اگر مقداری برای username پاس داده نشده بود از رشته Amir استفاده می کنیم و اگر مقداری برای status پاس داده نشده بود از رشته developer استفاده می کنیم. با اجرای این کد به جای دریافت خطا، مقدار زیر را می گیریم:

Hello Amir your status is: developer

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

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری پایتون حرفه‌ای توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.