منابع ترجمه · Think Python · فصل ۳

توابع (فصل ۳ Think Python)

۱۴ دقیقه مطالعه · به‌روزرسانی ۲ آبان ۱۴۰۵

منبع اصلی: Think Python, Chapter 3: Functions — Allen B. Downey

💡 این صفحه مثل یک نوت‌بوک Jupyter است: متغیر و تابعی که در یک سلول تعریف شود، در سلول‌های بعدی همین صفحه در دسترس است. سلول‌ها را به ترتیب اجرا کنید، یا با «⏮ تا اینجا» همهٔ سلول‌های قبلیِ اجرانشده را یک‌جا اجرا کنید.

دو نکته درباره‌ی این ترجمه: (۱) کتاب اصلی در این فصل از متن ترانه‌های گروه Monty Python به‌عنوان رشته‌های نمونه استفاده می‌کند. چون متن ترانه‌ها حق نشر جداگانه‌ای دارد، در این ترجمه آن رشته‌ها را با جمله‌های ساده‌ی خودم جایگزین کرده‌ام و مثال‌ها و توضیح‌ها را مطابق آن‌ها تنظیم کرده‌ام؛ ساختار کدها و مفاهیم دقیقاً همان است. (۲) خروجی‌های نمایش‌داده‌شده‌ی زیر کدها را با اجرای همین کدها به دست آورده‌ام.

در فصل قبل از چند تابع که پایتون فراهم می‌کند استفاده کردیم، مثل int و float، و چند تابع از ماژول math، مثل sqrt و pow. در این فصل یاد می‌گیرید چگونه توابع خودتان را بسازید و اجرا کنید. و می‌بینیم چگونه یک تابع می‌تواند تابع دیگری را فراخوانی کند. به‌عنوان مثال، چند خط متن ساده را نمایش می‌دهیم. این مثال‌های ساده یک ویژگی مهم را نشان می‌دهند – توانایی نوشتن توابع خودتان پایه‌ی برنامه‌نویسی است.

این فصل یک دستور جدید هم معرفی می‌کند: حلقه‌ی for، که برای تکرار یک محاسبه به کار می‌رود.

۳.۱. تعریف توابع جدید

تعریف تابع (function definition) نام یک تابع جدید و دنباله‌ی دستورهایی را که هنگام فراخوانی تابع اجرا می‌شوند مشخص می‌کند. این یک مثال است:

def print_message():
    print("Functions let us name a group of statements.")
    print("Then we can run them whenever we like.")

def کلمه‌ی کلیدی‌ای است که نشان می‌دهد این یک تعریف تابع است. نام تابع print_message است. هر چیزی که نام مجاز برای متغیر باشد، نام مجاز برای تابع هم هست.

پرانتزهای خالی بعد از نام نشان می‌دهند که این تابع آرگومانی نمی‌گیرد.

خط اول تعریف تابع سرآیند (header) نامیده می‌شود – بقیه‌ی آن بدنه (body) است. سرآیند باید با دونقطه (colon) تمام شود و بدنه باید تورفتگی (indentation) داشته باشد. طبق قرارداد، تورفتگی همیشه چهار فاصله است. بدنه‌ی این تابع دو دستور print است؛ به‌طور کلی بدنه‌ی یک تابع می‌تواند هر تعداد دستور از هر نوع داشته باشد.

تعریف یک تابع یک شیء تابع (function object) می‌سازد که می‌توانیم آن را این‌طور نمایش دهیم.

print_message
<function __main__.print_message()>

خروجی نشان می‌دهد که print_message تابعی است که آرگومانی نمی‌گیرد. __main__ نام ماژولی است که print_message را در خود دارد.

حالا که یک تابع تعریف کرده‌ایم، می‌توانیم آن را به همان روشی که توابع داخلی را فراخوانی می‌کنیم فراخوانی کنیم.

print_message()
Functions let us name a group of statements.
Then we can run them whenever we like.

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

۳.۲. پارامترها

بعضی از توابعی که دیده‌ایم آرگومان لازم دارند؛ برای مثال، وقتی abs را فراخوانی می‌کنید یک عدد به‌عنوان آرگومان می‌دهید. بعضی توابع بیش از یک آرگومان می‌گیرند؛ برای مثال math.pow دو آرگومان می‌گیرد، پایه و توان.

این تعریف تابعی است که یک آرگومان می‌گیرد.

def print_twice(string):
    print(string)
    print(string)

نام متغیر داخل پرانتز یک پارامتر (parameter) است. وقتی تابع فراخوانی می‌شود، مقدار آرگومان به پارامتر نسبت داده می‌شود. برای مثال، می‌توانیم print_twice را این‌طور فراخوانی کنیم.

print_twice('Hello, ')
Hello, 
Hello, 

اجرای این تابع همان اثری را دارد که نسبت دادن آرگومان به پارامتر و سپس اجرای بدنه‌ی تابع، مثل این:

string = 'Hello, '
print(string)
print(string)
Hello, 
Hello, 

می‌توانید از یک متغیر هم به‌عنوان آرگومان استفاده کنید.

line = 'Hello, '
print_twice(line)
Hello, 
Hello, 

در این مثال، مقدار line به پارامتر string نسبت داده می‌شود.

۳.۳. فراخوانی توابع

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

با تابع زیر شروع می‌کنیم که دو پارامتر می‌گیرد.

def repeat(word, n):
    print(word * n)

می‌توانیم از این تابع برای چاپ خط اول استفاده کنیم، این‌طور.

spam = 'Spam, '
repeat(spam, 4)
Spam, Spam, Spam, Spam, 

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

def first_two_lines():
    repeat(spam, 4)
    repeat(spam, 4)

و بعد آن را این‌طور فراخوانی کنیم.

first_two_lines()
Spam, Spam, Spam, Spam, 
Spam, Spam, Spam, Spam, 

برای نمایش سه خط آخر، می‌توانیم تابع دیگری تعریف کنیم که آن هم از repeat استفاده می‌کند.

def last_three_lines():
    repeat(spam, 2)
    print('(Hooray for Spam!)')
    repeat(spam, 2)
last_three_lines()
Spam, Spam, 
(Hooray for Spam!)
Spam, Spam, 

سرانجام، می‌توانیم همه را با یک تابع که کل بند را چاپ می‌کند کنار هم بیاوریم.

def print_verse():
    first_two_lines()
    last_three_lines()
print_verse()
Spam, Spam, Spam, Spam, 
Spam, Spam, Spam, Spam, 
Spam, Spam, 
(Hooray for Spam!)
Spam, Spam, 

وقتی print_verse را اجرا می‌کنیم، first_two_lines را فراخوانی می‌کند، که repeat را فراخوانی می‌کند، که print را فراخوانی می‌کند. این تعداد زیادی تابع است.

البته می‌شد همین کار را با توابع کمتری انجام داد، اما هدف این مثال نشان دادن این است که توابع چگونه می‌توانند با هم کار کنند.

۳.۴. تکرار

اگر بخواهیم بیش از یک بند را نمایش دهیم، می‌توانیم از دستور for استفاده کنیم. این یک مثال ساده است.

for i in range(2):
    print(i)
0
1

خط اول یک سرآیند است که به دونقطه ختم می‌شود. خط دوم بدنه است که باید تورفتگی داشته باشد.

سرآیند با کلمه‌ی کلیدی for، یک متغیر جدید به نام i و کلمه‌ی کلیدی دیگری، in، شروع می‌شود. از تابع range برای ساختن دنباله‌ای از دو مقدار استفاده می‌کند که 0 و 1 هستند. در پایتون، وقتی شروع به شمردن می‌کنیم، معمولاً از 0 شروع می‌کنیم.

وقتی دستور for اجرا می‌شود، اولین مقدار از range را به i نسبت می‌دهد و بعد تابع print را در بدنه اجرا می‌کند که 0 را نمایش می‌دهد.

وقتی به انتهای بدنه می‌رسد، دوباره به سرآیند برمی‌گردد؛ به همین دلیل به این دستور حلقه (loop) می‌گویند. بار دوم که حلقه اجرا می‌شود، مقدار بعدی را از range به i نسبت می‌دهد و آن را نمایش می‌دهد. سپس، چون آن آخرین مقدار از range است، حلقه تمام می‌شود.

این‌طور می‌توانیم از یک حلقه‌ی for برای چاپ دو بند استفاده کنیم.

for i in range(2):
    print("Verse", i)
    print_verse()
    print()
Verse 0
Spam, Spam, Spam, Spam, 
Spam, Spam, Spam, Spam, 
Spam, Spam, 
(Hooray for Spam!)
Spam, Spam, 

Verse 1
Spam, Spam, Spam, Spam, 
Spam, Spam, Spam, Spam, 
Spam, Spam, 
(Hooray for Spam!)
Spam, Spam, 

می‌توانید یک حلقه‌ی for را داخل یک تابع بگذارید. برای مثال، print_n_verses پارامتری به نام n می‌گیرد که باید عدد صحیح باشد و تعداد بندهای داده‌شده را نمایش می‌دهد.

def print_n_verses(n):
    for i in range(n):
        print_verse()
        print()

در این مثال از i در بدنه‌ی حلقه استفاده نمی‌کنیم، اما به هر حال باید یک نام متغیر در سرآیند باشد.

۳.۵. متغیرها و پارامترها محلی‌اند

وقتی داخل یک تابع متغیری می‌سازید، آن متغیر محلی (local) است؛ یعنی فقط داخل همان تابع وجود دارد. برای مثال، تابع زیر دو آرگومان می‌گیرد، آن‌ها را الحاق می‌کند و نتیجه را دو بار چاپ می‌کند.

def cat_twice(part1, part2):
    cat = part1 + part2
    print_twice(cat)

این هم مثالی که از آن استفاده می‌کند:

line1 = 'Python is '
line2 = 'fun to learn.'
cat_twice(line1, line2)
Python is fun to learn.
Python is fun to learn.

وقتی cat_twice اجرا می‌شود، یک متغیر محلی به نام cat می‌سازد که وقتی تابع تمام می‌شود از بین می‌رود. اگر سعی کنیم آن را نمایش دهیم، NameError می‌گیریم:

print(cat)
NameError: name 'cat' is not defined

بیرون از تابع، cat تعریف نشده است.

پارامترها هم محلی‌اند. برای مثال، بیرون از cat_twice چیزی به نام part1 یا part2 وجود ندارد.

۳.۶. نمودارهای پشته

برای دنبال کردن اینکه کدام متغیرها را کجا می‌توان به کار برد، گاهی مفید است یک نمودار پشته (stack diagram) رسم کنیم. مثل نمودارهای حالت، نمودارهای پشته مقدار هر متغیر را نشان می‌دهند، اما تابعی را که هر متغیر به آن تعلق دارد هم نشان می‌دهند.

هر تابع با یک قاب (frame) نمایش داده می‌شود. قاب جعبه‌ای است که نام تابع بیرون آن و پارامترها و متغیرهای محلی تابع داخل آن نوشته شده‌اند.

(در کتاب اصلی این‌جا نمودار پشته‌ی مثال قبل رسم شده است: چهار قاب روی هم، از پایین __main__ با متغیرهای line1 و line2، سپس cat_twice با part1، part2 و cat، سپس print_twice با string، و بالای همه print.)

قاب‌ها در یک پشته چیده شده‌اند که نشان می‌دهد کدام تابع کدام را فراخوانی کرده و همین‌طور تا آخر. اگر از پایین بخوانید، print را print_twice فراخوانی کرده، که cat_twice آن را فراخوانی کرده، که __main__ آن را فراخوانی کرده – __main__ نام خاصی برای بالاترین قاب است. وقتی بیرون از هر تابعی یک متغیر می‌سازید، آن متغیر به __main__ تعلق دارد.

در قاب print، علامت سؤال نشان می‌دهد که نام پارامتر را نمی‌دانیم. اگر کنجکاوید، از یک دستیار مجازی بپرسید «پارامترهای تابع print در پایتون چیست؟» (What are the parameters of the Python print function?)

۳.۷. ردیابی خطا (Traceback)

وقتی در یک تابع خطای زمان اجرا رخ می‌دهد، پایتون نام تابعی را که در حال اجرا بوده، نام تابعی که آن را فراخوانی کرده و همین‌طور بالاتر در پشته نمایش می‌دهد. برای دیدن یک مثال، نسخه‌ای از print_twice را تعریف می‌کنم که خطا دارد – سعی می‌کند cat را چاپ کند که یک متغیر محلی در تابع دیگری است.

def print_twice(string):
    print(cat)            # NameError
    print(cat)

حالا ببینید وقتی cat_twice را اجرا کنیم چه می‌شود.

# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs, including a traceback.

%xmode Verbose
cat_twice(line1, line2)
NameError: name 'cat' is not defined

پیام خطا شامل یک ردیابی (traceback) است که تابعی را که هنگام بروز خطا در حال اجرا بود، تابعی که آن را فراخوانی کرده بود و همین‌طور بالاتر نشان می‌دهد. در این مثال نشان می‌دهد که cat_twice تابع print_twice را فراخوانی کرده و خطا در print_twice رخ داده است.

ترتیب توابع در ردیابی با ترتیب قاب‌ها در نمودار پشته یکی است. تابعی که در حال اجرا بوده در پایین است.

۳.۸. چرا توابع؟

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

  • ساختن یک تابع جدید به شما فرصت می‌دهد گروهی از دستورها را نام‌گذاری کنید که برنامه‌تان را برای خواندن و اشکال‌زدایی آسان‌تر می‌کند.

  • توابع می‌توانند با حذف کد تکراری برنامه را کوچک‌تر کنند. بعداً اگر تغییری بدهید، فقط باید آن را در یک جا انجام دهید.

  • تقسیم یک برنامه‌ی بلند به توابع به شما اجازه می‌دهد بخش‌ها را یکی‌یکی اشکال‌زدایی کنید و بعد آن‌ها را به یک کل کارآمد وصل کنید.

  • توابعی که خوب طراحی شده باشند اغلب برای برنامه‌های زیادی مفیدند. وقتی یکی را نوشتید و اشکال‌زدایی کردید، می‌توانید دوباره از آن استفاده کنید.

۳.۹. اشکال‌زدایی

اشکال‌زدایی می‌تواند ناامیدکننده باشد، اما چالش‌برانگیز، جالب و گاهی حتی سرگرم‌کننده هم هست. و یکی از مهم‌ترین مهارت‌هایی است که می‌توانید یاد بگیرید.

از جهاتی اشکال‌زدایی شبیه کار کارآگاهی است. سرنخ‌هایی به شما داده می‌شود و باید رویدادهایی را که به نتایجی که می‌بینید منجر شده‌اند استنباط کنید.

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

برای بعضی‌ها برنامه‌نویسی و اشکال‌زدایی یکی هستند؛ یعنی برنامه‌نویسی فرایند تدریجیِ اشکال‌زدایی یک برنامه است تا زمانی که کاری را که می‌خواهید انجام دهد. ایده این است که باید با یک برنامه‌ی کارآمد شروع کنید و تغییرات کوچک بدهید و در حین کار اشکال‌زدایی‌شان کنید.

اگر می‌بینید زمان زیادی را صرف اشکال‌زدایی می‌کنید، این اغلب نشانه‌ی آن است که پیش از شروع آزمایش، کد زیادی می‌نویسید. اگر قدم‌های کوچک‌تری بردارید، ممکن است ببینید سریع‌تر پیش می‌روید.

۳.۱۰. واژه‌نامه

تعریف تابع (function definition): دستوری که یک تابع می‌سازد.

سرآیند (header): خط اول یک تعریف تابع.

بدنه (body): دنباله‌ی دستورهایی که داخل یک تعریف تابع هستند.

شیء تابع (function object): مقداری که یک تعریف تابع می‌سازد. نام تابع متغیری است که به یک شیء تابع اشاره می‌کند.

پارامتر (parameter): نامی که داخل یک تابع برای اشاره به مقداری که به‌عنوان آرگومان داده شده به کار می‌رود.

حلقه (loop): دستوری که یک یا چند دستور را، اغلب به‌صورت تکراری، اجرا می‌کند.

متغیر محلی (local variable): متغیری که داخل یک تابع تعریف شده و فقط داخل همان تابع قابل دسترسی است.

نمودار پشته (stack diagram): نمایشی گرافیکی از پشته‌ای از توابع، متغیرهایشان و مقدارهایی که به آن‌ها اشاره می‌کنند.

قاب (frame): جعبه‌ای در نمودار پشته که یک فراخوانی تابع را نشان می‌دهد. متغیرهای محلی و پارامترهای تابع را در خود دارد.

ردیابی (traceback): فهرستی از توابعی که در حال اجرا هستند و هنگام رخ دادن یک استثنا چاپ می‌شود.

۳.۱۱. تمرین‌ها

# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.

%xmode Verbose

۳.۱۱.۱. از یک دستیار مجازی بپرسید

طبق قرارداد، دستورهای داخل یک تابع یا حلقه‌ی for با چهار فاصله تورفتگی می‌گیرند. اما همه با این قرارداد موافق نیستند. اگر درباره‌ی تاریخچه‌ی این مناقشه‌ی بزرگ کنجکاوید، از یک دستیار مجازی بخواهید «درباره‌ی فاصله و تب در پایتون به من بگو» (tell me about spaces and tabs in Python).

دستیارهای مجازی در نوشتن توابع کوچک خوب‌اند.

  1. از دستیار مجازی محبوب‌تان بخواهید «تابعی به نام repeat بنویس که یک رشته و یک عدد صحیح بگیرد و رشته را به تعداد داده‌شده چاپ کند.» (Write a function called repeat that takes a string and an integer and prints the string the given number of times.)

  2. اگر نتیجه از حلقه‌ی for استفاده کرد، می‌توانید بپرسید «می‌شود بدون حلقه‌ی for انجامش داد؟» (Can you do it without a for loop?)

  3. هر تابع دیگری از این فصل را انتخاب کنید و از دستیار بخواهید آن را بنویسد. چالش این است که تابع را آن‌قدر دقیق توصیف کنید که چیزی را که می‌خواهید بگیرید. از واژگانی که تا اینجا در این کتاب یاد گرفته‌اید استفاده کنید.

دستیارهای مجازی در اشکال‌زدایی توابع هم خوب‌اند.

  1. از یک دستیار بپرسید این نسخه از print_twice چه ایرادی دارد.

    def print_twice(string):
        print(cat)
        print(cat)

و اگر در هر یک از تمرین‌های زیر گیر کردید، از دستیار کمک بخواهید.

۳.۱۱.۲. تمرین

تابعی به نام print_right بنویسید که یک رشته به نام text را به‌عنوان پارامتر بگیرد و رشته را با آن‌قدر فاصله‌ی ابتدایی چاپ کند که آخرین حرف رشته در ستون چهلم صفحه قرار بگیرد.

# Solution goes here

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

این هم مثالی که نشان می‌دهد باید چگونه کار کند.

                                   Monty
                                Python's
                           Flying Circus
print_right("Monty")
print_right("Python's")
print_right("Flying Circus")

۳.۱۱.۳. تمرین

تابعی به نام triangle بنویسید که یک رشته و یک عدد صحیح بگیرد و یک هرم با ارتفاع داده‌شده را که با نسخه‌های رشته ساخته شده رسم کند. این مثالی از یک هرم با 5 سطح با رشته‌ی 'L' است.

L
LL
LLL
LLLL
LLLLL
# Solution goes here
triangle('L', 5)

۳.۱۱.۴. تمرین

تابعی به نام rectangle بنویسید که یک رشته و دو عدد صحیح بگیرد و یک مستطیل با عرض و ارتفاع داده‌شده را که با نسخه‌های رشته ساخته شده رسم کند. این مثالی از یک مستطیل با عرض 5 و ارتفاع 4 است که از رشته‌ی 'H' ساخته شده.

HHHHH
HHHHH
HHHHH
HHHHH
# Solution goes here
rectangle('H', 5, 4)

۳.۱۱.۵. تمرین

ترانه‌ی سنتی «99 Bottles of Beer» (۹۹ بطری آبجو) با این بند شروع می‌شود:

99 bottles of beer on the wall
99 bottles of beer
Take one down, pass it around
98 bottles of beer on the wall

بند دوم مثل همین است، با این تفاوت که با ۹۸ بطری شروع می‌شود و به ۹۷ ختم می‌شود. ترانه – برای مدتی بسیار طولانی – ادامه پیدا می‌کند تا به ۰ بطری آبجو برسد.

تابعی به نام bottle_verse بنویسید که یک عدد را به‌عنوان پارامتر بگیرد و بندی را که با تعداد بطری داده‌شده شروع می‌شود نمایش دهد.

راهنمایی: ایده‌ی خوب این است که با تابعی شروع کنید که بتواند خط اول، دوم یا آخر بند را چاپ کند، و بعد از آن برای نوشتن bottle_verse استفاده کنید.

# Solution goes here
# Solution goes here

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

bottle_verse(99)

اگر می‌خواهید کل ترانه را چاپ کنید، می‌توانید از این حلقه‌ی for استفاده کنید که از 99 تا 1 به‌صورت نزولی می‌شمارد. لازم نیست این مثال را کامل بفهمید – درباره‌ی حلقه‌های for و تابع range بعداً بیشتر یاد می‌گیریم.

for n in range(99, 0, -1):
    bottle_verse(n)
    print()