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

مقدارهای بازگشتی (فصل ۶ Think Python)

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

منبع اصلی: Think Python, Chapter 6: Return Values — Allen B. Downey

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

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

در فصل‌های قبل از توابع داخلی – مثل abs و round – و توابع ماژول math – مثل sqrt و pow – استفاده کرده‌ایم. وقتی یکی از این توابع را فراخوانی می‌کنید، مقداری برمی‌گرداند که می‌توانید آن را به یک متغیر نسبت دهید یا به‌عنوان بخشی از یک عبارت به کار ببرید.

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

در این فصل می‌بینیم چگونه توابعی بنویسیم که مقدار برمی‌گردانند.

۶.۱. بعضی توابع مقدار بازگشتی دارند

وقتی تابعی مثل math.sqrt را فراخوانی می‌کنید، نتیجه مقدار بازگشتی (return value) نامیده می‌شود. اگر فراخوانی تابع در انتهای یک سلول باشد، Jupyter مقدار بازگشتی را بلافاصله نمایش می‌دهد.

import math

math.sqrt(42 / math.pi)
3.656366395715726

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

radius = math.sqrt(42 / math.pi)

اما می‌توانید بعداً آن را نمایش دهید.

radius
3.656366395715726

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

radius + math.sqrt(42 / math.pi)
7.312732791431452

این مثالی از تابعی است که مقدار برمی‌گرداند.

def circle_area(radius):
    area = math.pi * radius**2
    return area

circle_area شعاع radius را به‌عنوان پارامتر می‌گیرد و مساحت دایره‌ای با آن شعاع را حساب می‌کند.

خط آخر یک دستور return است که مقدار area را برمی‌گرداند.

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

circle_area(radius)
42.00000000000001

می‌توانیم مقدار بازگشتی را به یک متغیر نسبت دهیم.

a = circle_area(radius)

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

circle_area(radius) + 2 * circle_area(radius / 2)
63.000000000000014

بعداً می‌توانیم مقدار متغیری را که نتیجه را به آن نسبت داده‌ایم نمایش دهیم.

a
42.00000000000001

اما نمی‌توانیم به area دسترسی داشته باشیم.

area
NameError: name 'area' is not defined

area یک متغیر محلی در یک تابع است، پس نمی‌توانیم از بیرون تابع به آن دسترسی داشته باشیم.

۶.۲. و بعضی None برمی‌گردانند

اگر تابعی دستور return نداشته باشد، None برمی‌گرداند، که مقداری خاص مثل True و False است. برای مثال، این تابع repeat از فصل ۳ است.

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

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

repeat('Hello, ', 3)
Hello, Hello, Hello, 

این تابع از تابع print برای نمایش یک رشته استفاده می‌کند، اما از دستور return برای برگرداندن مقدار استفاده نمی‌کند. اگر نتیجه را به یک متغیر نسبت دهیم، باز هم رشته را نمایش می‌دهد.

result = repeat('Hello, ', 3)
Hello, Hello, Hello, 

و اگر مقدار متغیر را نمایش دهیم، چیزی دریافت نمی‌کنیم.

result

result در واقع مقداری دارد، اما Jupyter آن را نشان نمی‌دهد. با این حال می‌توانیم آن را این‌طور نمایش دهیم.

print(result)
None

مقدار بازگشتی repeat برابر None است.

حالا تابعی شبیه repeat داریم که مقدار بازگشتی دارد.

def repeat_string(word, n):
    return word * n

دقت کنید که در یک دستور return می‌توانیم از یک عبارت استفاده کنیم، نه فقط یک متغیر.

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

line = repeat_string('Tea, ', 4)

اما بعداً می‌توانیم مقدار نسبت داده‌شده به line را نمایش دهیم.

line
'Tea, Tea, Tea, Tea, '

تابعی مثل این تابع خالص (pure function) نامیده می‌شود، چون چیزی نمایش نمی‌دهد و هیچ اثر دیگری هم ندارد – جز برگرداندن یک مقدار.

۶.۳. مقدارهای بازگشتی و دستورهای شرطی

اگر پایتون abs را فراهم نکرده بود، می‌توانستیم آن را این‌طور بنویسیم.

def absolute_value(x):
    if x < 0:
        return -x
    else:
        return x

اگر x منفی باشد، دستور return اول -x را برمی‌گرداند و تابع بلافاصله تمام می‌شود. در غیر این صورت، دستور return دوم x را برمی‌گرداند و تابع تمام می‌شود. پس این تابع درست است.

با این حال، اگر دستورهای return را در یک شرط بگذارید، باید مطمئن شوید که هر مسیر ممکن در برنامه به یک دستور return می‌رسد. برای مثال، این یک نسخه‌ی نادرست از absolute_value است.

def absolute_value_wrong(x):
    if x < 0:
        return -x
    if x > 0:
        return x

ببینید اگر این تابع را با 0 به‌عنوان آرگومان فراخوانی کنیم چه می‌شود.

absolute_value_wrong(0)

چیزی نمی‌گیریم! مشکل این است: وقتی x برابر 0 است، هیچ‌کدام از شرط‌ها درست نیست و تابع بدون رسیدن به دستور return تمام می‌شود؛ یعنی مقدار بازگشتی None است و به همین دلیل Jupyter چیزی نمایش نمی‌دهد.

به‌عنوان مثالی دیگر، این نسخه‌ای از absolute_value است که یک دستور return اضافه در انتها دارد.

def absolute_value_extra_return(x):
    if x < 0:
        return -x
    else:
        return x
    
    return 'This is dead code'

اگر x منفی باشد، دستور return اول اجرا می‌شود و تابع تمام می‌شود. در غیر این صورت دستور return دوم اجرا می‌شود و تابع تمام می‌شود. در هر دو حالت هرگز به دستور return سوم نمی‌رسیم – پس هرگز نمی‌تواند اجرا شود.

کدی که هرگز نمی‌تواند اجرا شود کد مرده (dead code) نامیده می‌شود. به‌طور کلی کد مرده ضرری ندارد، اما اغلب نشانه‌ی یک سوءتفاهم است و ممکن است برای کسی که می‌خواهد برنامه را بفهمد گیج‌کننده باشد.

۶.۴. توسعه‌ی تدریجی

هرچه توابع بزرگ‌تری بنویسید، ممکن است زمان بیشتری را صرف اشکال‌زدایی کنید. برای مقابله با برنامه‌های پیچیده‌تر و پیچیده‌تر، می‌توانید توسعه‌ی تدریجی (incremental development) را امتحان کنید؛ روشی که در آن در هر زمان فقط مقدار کمی کد اضافه و آزمایش می‌شود.

به‌عنوان مثال، فرض کنید می‌خواهید فاصله‌ی بین دو نقطه را که با مختصات $(x_1, y_1)$ و $(x_2, y_2)$ نمایش داده می‌شوند پیدا کنید. طبق قضیه‌ی فیثاغورس، فاصله این است:

$$\mathrm{distance} = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$$

اولین قدم این است که ببینیم تابع distance در پایتون باید چه شکلی داشته باشد – یعنی ورودی‌ها (پارامترها) چیست و خروجی (مقدار بازگشتی) چیست؟

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

def distance(x1, y1, x2, y2):
    return 0.0

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

برای آزمودن تابع جدید، آن را با آرگومان‌های نمونه فراخوانی می‌کنیم:

distance(1, 2, 4, 6)
0.0

این مقدارها را طوری انتخاب کردم که فاصله‌ی افقی 3 و فاصله‌ی عمودی 4 باشد. به این ترتیب نتیجه 5 می‌شود، وتر یک مثلث قائم‌الزاویه‌ی 3-4-5. هنگام آزمودن یک تابع، مفید است جواب درست را بدانید.

در این نقطه تأیید کرده‌ایم که تابع اجرا می‌شود و یک مقدار برمی‌گرداند و می‌توانیم شروع به اضافه کردن کد به بدنه کنیم. قدم بعدی خوب این است که تفاضل‌های x2 - x1 و y2 - y1 را پیدا کنیم. این نسخه‌ای است که آن مقدارها را در متغیرهای موقت ذخیره می‌کند و نمایش می‌دهد.

def distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    print('dx is', dx)
    print('dy is', dy)
    return 0.0

اگر تابع درست کار کند، باید dx is 3 و dy is 4 را نمایش دهد. اگر این‌طور باشد، می‌دانیم تابع آرگومان‌های درست را می‌گیرد و اولین محاسبه را درست انجام می‌دهد. اگر نه، فقط چند خط برای بررسی هست.

distance(1, 2, 4, 6)
dx is 3
dy is 4
0.0

تا اینجا خوب است. بعد مجموع مربعات dx و dy را حساب می‌کنیم:

def distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    dsquared = dx**2 + dy**2
    print('dsquared is: ', dsquared)
    return 0.0

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

distance(1, 2, 4, 6)
dsquared is:  25
0.0

سرانجام، می‌توانیم از math.sqrt برای محاسبه‌ی فاصله استفاده کنیم:

def distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    dsquared = dx**2 + dy**2
    result = math.sqrt(dsquared)
    print("result is", result)

و آزمایشش کنیم.

distance(1, 2, 4, 6)
result is 5.0

نتیجه درست است، اما این نسخه از تابع نتیجه را نمایش می‌دهد نه اینکه برگرداند، پس مقدار بازگشتی None است.

می‌توانیم با جایگزین کردن تابع print با یک دستور return این را درست کنیم.

def distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    dsquared = dx**2 + dy**2
    result = math.sqrt(dsquared)
    return result

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

distance(1, 2, 4, 6)
5.0

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

d = distance(1, 2, 4, 6)

دستورهای print که نوشتیم برای اشکال‌زدایی مفیدند، اما وقتی تابع درست کار کرد می‌توانیم آن‌ها را حذف کنیم. به کدی مثل این داربست (scaffolding) می‌گویند، چون برای ساختن برنامه مفید است اما بخشی از محصول نهایی نیست.

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

  1. با یک برنامه‌ی کارآمد شروع کنید، تغییرات کوچک بدهید و بعد از هر تغییر آزمایش کنید.

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

  3. وقتی برنامه کار کرد، داربست را حذف کنید.

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

۶.۵. توابع بولی

توابع می‌توانند مقدارهای بولی True و False را برگردانند که اغلب برای کپسوله کردن یک آزمون پیچیده در یک تابع راحت است. برای مثال، is_divisible بررسی می‌کند که آیا x بدون باقی‌مانده بر y بخش‌پذیر است یا نه.

def is_divisible(x, y):
    if x % y == 0:
        return True
    else:
        return False

این‌طور از آن استفاده می‌کنیم.

is_divisible(6, 4)
False
is_divisible(6, 3)
True

داخل تابع، نتیجه‌ی عملگر == یک مقدار بولی است، پس می‌توانیم تابع را با برگرداندن مستقیم آن مختصرتر بنویسیم.

def is_divisible(x, y):
    return x % y == 0

توابع بولی اغلب در دستورهای شرطی به کار می‌روند.

if is_divisible(6, 2):
    print('divisible')
divisible

ممکن است وسوسه شوید چیزی شبیه این بنویسید:

if is_divisible(6, 2) == True:
    print('divisible')
divisible

اما این مقایسه غیرضروری است.

۶.۶. بازگشت با مقدارهای بازگشتی

حالا که می‌توانیم توابعی با مقدار بازگشتی بنویسیم، می‌توانیم توابع بازگشتی با مقدار بازگشتی هم بنویسیم، و با این توانایی از یک آستانه‌ی مهم عبور کرده‌ایم – زیرمجموعه‌ای از پایتون که داریم اکنون تورینگ‌کامل (Turing complete) است، یعنی می‌توانیم هر محاسبه‌ای را که با یک الگوریتم قابل توصیف است انجام دهیم.

برای نشان دادن بازگشت با مقدارهای بازگشتی، چند تابع ریاضی را که به‌صورت بازگشتی تعریف شده‌اند ارزیابی می‌کنیم. تعریف بازگشتی شبیه تعریف دوری است، به این معنا که تعریف به چیزی که تعریف می‌شود ارجاع می‌دهد. یک تعریف واقعاً دوری چندان مفید نیست:

vorpal: صفتی که برای توصیف چیزی که vorpal است به کار می‌رود.

اگر این تعریف را در فرهنگ لغت ببینید، شاید عصبانی شوید. از طرف دیگر، اگر تعریف تابع فاکتوریل را که با نماد $!$ نشان داده می‌شود جست‌وجو کنید، ممکن است چیزی شبیه این ببینید:

$$\begin{aligned} 0! &= 1 \ n! &= n~(n-1)! \end{aligned}$$

این تعریف می‌گوید فاکتوریل $0$ برابر $1$ است و فاکتوریل هر مقدار دیگر، $n$، برابر $n$ ضرب در فاکتوریل $n-1$ است.

اگر بتوانید یک تعریف بازگشتی از چیزی بنویسید، می‌توانید برنامه‌ی پایتونی برای ارزیابی آن بنویسید. با پیروی از فرایند توسعه‌ی تدریجی، با تابعی شروع می‌کنیم که n را به‌عنوان پارامتر می‌گیرد و همیشه 0 برمی‌گرداند.

def factorial(n):
    return 0

حالا بخش اول تعریف را اضافه کنیم – اگر آرگومان 0 بود، فقط باید 1 را برگردانیم:

def factorial(n):
    if n == 0:
        return 1
    else:
        return 0

حالا بخش دوم را کامل کنیم – اگر n برابر 0 نبود، باید یک فراخوانی بازگشتی انجام دهیم تا فاکتوریل n-1 را پیدا کنیم و بعد نتیجه را در n ضرب کنیم:

def factorial(n):
    if n == 0:
        return 1
    else:
        recurse = factorial(n-1)
        return n * recurse

جریان اجرای این برنامه شبیه جریان countdown در فصل ۵ است. اگر factorial را با مقدار 3 فراخوانی کنیم:

چون 3 برابر 0 نیست، شاخه‌ی دوم را می‌گیریم و فاکتوریل n-1 را حساب می‌کنیم…

چون 2 برابر 0 نیست، شاخه‌ی دوم را می‌گیریم و فاکتوریل n-1 را حساب می‌کنیم…

چون 1 برابر 0 نیست، شاخه‌ی دوم را می‌گیریم و فاکتوریل n-1 را حساب می‌کنیم…

چون 0 برابر 0 است، شاخه‌ی اول را می‌گیریم و بدون انجام فراخوانی بازگشتی دیگری 1 را برمی‌گردانیم.

مقدار بازگشتی، 1، در n که 1 است ضرب می‌شود و نتیجه برگردانده می‌شود.

مقدار بازگشتی، 1، در n که 2 است ضرب می‌شود و نتیجه برگردانده می‌شود.

مقدار بازگشتی 2 در n که 3 است ضرب می‌شود و نتیجه، 6، به مقدار بازگشتی فراخوانی تابعی تبدیل می‌شود که کل این فرایند را شروع کرد.

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

(در کتاب اصلی این‌جا نمودار رسم شده است: قاب __main__ و زیرش قاب‌های factorial با n برابر ۳، ۲، ۱ و ۰، که مقدارهای بازگشتی ۶، ۲، ۱ و ۱ کنارشان نوشته شده است.)

مقدارهای بازگشتی در حال برگشتن از پشته نشان داده شده‌اند. در هر قاب، مقدار بازگشتی حاصل‌ضرب n و recurse است.

در آخرین قاب، متغیر محلی recurse وجود ندارد، چون شاخه‌ای که آن را می‌سازد اجرا نمی‌شود.

۶.۷. جهش ایمان

دنبال کردن جریان اجرا یک راه برای خواندن برنامه‌هاست، اما می‌تواند به‌سرعت طاقت‌فرسا شود. یک جایگزین چیزی است که من آن را «جهش ایمان» (leap of faith) می‌نامم. وقتی به یک فراخوانی تابع می‌رسید، به‌جای دنبال کردن جریان اجرا، فرض می‌کنید که تابع درست کار می‌کند و نتیجه‌ی درست را برمی‌گرداند.

در واقع وقتی از توابع داخلی استفاده می‌کنید، همین جهش ایمان را تمرین می‌کنید. وقتی abs یا math.sqrt را فراخوانی می‌کنید، بدنه‌ی آن توابع را بررسی نمی‌کنید – فقط فرض می‌کنید کار می‌کنند.

درباره‌ی فراخوانی توابع خودتان هم همین‌طور است. برای مثال، پیش‌تر تابعی به نام is_divisible نوشتیم که تعیین می‌کند آیا یک عدد بر عدد دیگر بخش‌پذیر است. وقتی خودمان را قانع کردیم که این تابع درست است، می‌توانیم بدون نگاه دوباره به بدنه‌اش از آن استفاده کنیم.

درباره‌ی برنامه‌های بازگشتی هم همین‌طور است. وقتی به فراخوانی بازگشتی می‌رسید، به‌جای دنبال کردن جریان اجرا، باید فرض کنید فراخوانی بازگشتی کار می‌کند و بعد از خودتان بپرسید: «با فرض اینکه بتوانم فاکتوریل $n-1$ را حساب کنم، آیا می‌توانم فاکتوریل $n$ را حساب کنم؟» تعریف بازگشتی فاکتوریل نشان می‌دهد که می‌توانید، با ضرب در $n$.

البته کمی عجیب است که فرض کنید تابع درست کار می‌کند در حالی که هنوز نوشتنش را تمام نکرده‌اید، اما برای همین به آن «جهش ایمان» می‌گویند!

۶.۸. فیبوناچی

بعد از factorial، رایج‌ترین مثال یک تابع بازگشتی fibonacci است که تعریفش این است:

$$\begin{aligned} \mathrm{fibonacci}(0) &= 0 \ \mathrm{fibonacci}(1) &= 1 \ \mathrm{fibonacci}(n) &= \mathrm{fibonacci}(n-1) + \mathrm{fibonacci}(n-2) \end{aligned}$$

وقتی به پایتون ترجمه می‌شود، این‌طور به نظر می‌رسد:

def fibonacci(n):
    if n == 0:
        return 0
    elif  n == 1:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)

اگر سعی کنید جریان اجرا را این‌جا دنبال کنید، حتی برای مقدارهای کوچک $n$، سرتان منفجر می‌شود. اما طبق جهش ایمان، اگر فرض کنید دو فراخوانی بازگشتی درست کار می‌کنند، می‌توانید مطمئن باشید دستور return آخر درست است.

به‌عنوان نکته‌ی جانبی، این روش محاسبه‌ی اعداد فیبوناچی بسیار ناکارآمد است. در فصل ۱۰ توضیح می‌دهم چرا و راهی برای بهبودش پیشنهاد می‌کنم.

[fibonacci(i) for i in range(10)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

(این خروجی نمونه از اجرای تابع برای ۱۰ مقدار اول است؛ در کتاب نیامده است.)

۶.۹. بررسی نوع‌ها

اگر factorial را فراخوانی کنیم و 1.5 را به‌عنوان آرگومان بدهیم چه می‌شود؟

factorial(1.5)
RecursionError: maximum recursion depth exceeded

شبیه یک بازگشت بی‌نهایت است. چطور ممکن است؟ تابع حالت‌های پایه‌ای دارد، وقتی n == 1 یا n == 0. اما اگر n عدد صحیح نباشد، می‌توانیم حالت پایه را رد کنیم و برای همیشه بازگشت کنیم.

در این مثال، مقدار اولیه‌ی n برابر 1.5 است. در اولین فراخوانی بازگشتی مقدار n برابر 0.5 است. در بعدی -0.5 است. از آن‌جا کوچک‌تر (منفی‌تر) می‌شود، اما هرگز 0 نمی‌شود.

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

isinstance(3, int)
True
isinstance(1.5, int)
False

حالا این نسخه‌ای از factorial با بررسی خطاست.

def factorial(n):
    if not isinstance(n, int):
        print('factorial is only defined for integers.')
        return None
    elif n < 0:
        print('factorial is not defined for negative numbers.')
        return None
    elif n == 0:
        return 1
    else:
        return n * factorial(n-1)

اول بررسی می‌کند که n عدد صحیح است یا نه. اگر نباشد، پیام خطا نمایش می‌دهد و None برمی‌گرداند.

factorial('hello')
factorial is only defined for integers.

بعد بررسی می‌کند که n منفی است یا نه. اگر باشد، پیام خطا نمایش می‌دهد و None برمی‌گرداند.

factorial(-2)
factorial is not defined for negative numbers.

اگر از هر دو بررسی بگذریم، می‌دانیم n یک عدد صحیح نامنفی است، پس می‌توانیم مطمئن باشیم بازگشت تمام می‌شود. بررسی پارامترهای یک تابع برای اطمینان از اینکه نوع و مقدار درستی دارند اعتبارسنجی ورودی (input validation) نامیده می‌شود.

۶.۱۰. اشکال‌زدایی

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

  • مشکلی در آرگومان‌هایی که تابع می‌گیرد وجود دارد – یعنی یک پیش‌شرط نقض شده است.

  • مشکلی در خود تابع وجود دارد – یعنی یک پس‌شرط نقض شده است.

  • فراخواننده با مقدار بازگشتی کار اشتباهی می‌کند.

برای رد کردن احتمال اول، می‌توانید یک دستور print در ابتدای تابع اضافه کنید که مقدار پارامترها (و شاید نوعشان) را نمایش دهد. یا می‌توانید کدی بنویسید که پیش‌شرط‌ها را صریحاً بررسی کند.

اگر پارامترها خوب به نظر می‌رسند، می‌توانید قبل از هر دستور return یک دستور print اضافه کنید و مقدار بازگشتی را نمایش دهید. در صورت امکان، تابع را با آرگومان‌هایی فراخوانی کنید که بررسی نتیجه را آسان می‌کنند.

اگر تابع به نظر درست کار می‌کند، فراخوانی تابع را نگاه کنید تا مطمئن شوید مقدار بازگشتی درست استفاده می‌شود – یا اصلاً استفاده می‌شود!

اضافه کردن دستورهای print در ابتدا و انتهای یک تابع می‌تواند به قابل‌مشاهده‌تر شدن جریان اجرا کمک کند. برای مثال، این نسخه‌ای از factorial با دستورهای print است:

def factorial(n):
    space = ' ' * (4 * n)
    print(space, 'factorial', n)
    if n == 0:
        print(space, 'returning 1')
        return 1
    else:
        recurse = factorial(n-1)
        result = n * recurse
        print(space, 'returning', result)
        return result

space رشته‌ای از نویسه‌های فاصله است که تورفتگی خروجی را کنترل می‌کند. این نتیجه‌ی factorial(3) است:

factorial(3)
             factorial 3
         factorial 2
     factorial 1
 factorial 0
 returning 1
     returning 1
         returning 2
             returning 6

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

۶.۱۱. واژه‌نامه

مقدار بازگشتی (return value): نتیجه‌ی یک تابع. اگر فراخوانی تابع به‌عنوان یک عبارت به کار رود، مقدار بازگشتی مقدار آن عبارت است.

تابع خالص (pure function): تابعی که چیزی نمایش نمی‌دهد و هیچ اثر دیگری هم جز برگرداندن یک مقدار بازگشتی ندارد.

کد مرده (dead code): بخشی از یک برنامه که هرگز نمی‌تواند اجرا شود، اغلب چون بعد از یک دستور return آمده است.

توسعه‌ی تدریجی (incremental development): طرح توسعه‌ی برنامه‌ای که قصد دارد با افزودن و آزمودن مقدار کمی کد در هر زمان از اشکال‌زدایی دوری کند.

داربست (scaffolding): کدی که در طول توسعه‌ی برنامه به کار می‌رود اما بخشی از نسخه‌ی نهایی نیست.

تورینگ‌کامل (Turing complete): یک زبان، یا زیرمجموعه‌ای از یک زبان، تورینگ‌کامل است اگر بتواند هر محاسبه‌ای را که با یک الگوریتم قابل توصیف است انجام دهد.

اعتبارسنجی ورودی (input validation): بررسی پارامترهای یک تابع برای اطمینان از اینکه نوع و مقدار درستی دارند.

۶.۱۲. تمرین‌ها

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

%xmode Verbose

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

در این فصل تابعی نادرست دیدیم که می‌تواند بدون برگرداندن مقدار تمام شود.

def absolute_value_wrong(x):
    if x < 0:
        return -x
    if x > 0:
        return x

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

def absolute_value_extra_return(x):
    if x < 0:
        return -x
    else:
        return x
    
    return 'This is dead code.'

و مثال زیر را دیدیم که درست است اما سبک پایتونی (idiomatic) ندارد.

def is_divisible(x, y):
    if x % y == 0:
        return True
    else:
        return False

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

بعد بپرسید «تابعی بنویس که مختصات دو نقطه را بگیرد و فاصله‌ی بینشان را حساب کند.» (Write a function that takes coordinates of two points and computes the distance between them.) ببینید نتیجه شبیه نسخه‌ی distance که در این فصل نوشتیم هست یا نه.

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

از توسعه‌ی تدریجی برای نوشتن تابعی به نام hypot استفاده کنید که طول وتر یک مثلث قائم‌الزاویه را، با گرفتن طول دو ضلع دیگر به‌عنوان آرگومان، برگرداند.

نکته: تابعی در ماژول math به نام hypot وجود دارد که همین کار را می‌کند، اما نباید در این تمرین از آن استفاده کنید!

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

# Solution goes here

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

تابع بولی is_between(x, y, z) را بنویسید که اگر $x < y < z$ یا $z < y < x$ باشد True و در غیر این صورت False برگرداند.

# Solution goes here

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

is_between(1, 2, 3)  # should be True
is_between(3, 2, 1)  # should be True
is_between(1, 3, 2)  # should be False
is_between(2, 3, 1)  # should be False

۶.۱۲.۴. تمرین

تابع آکرمان (Ackermann)، $A(m, n)$، این‌طور تعریف می‌شود:

$$\begin{aligned} A(m, n) = \begin{cases} n+1 & \mbox{if } m = 0 \ A(m-1, 1) & \mbox{if } m > 0 \mbox{ and } n = 0 \ A(m-1, A(m, n-1)) & \mbox{if } m > 0 \mbox{ and } n > 0. \end{cases} \end{aligned}$$

تابعی به نام ackermann بنویسید که تابع آکرمان را ارزیابی کند. اگر ackermann(5, 5) را فراخوانی کنید چه می‌شود؟

# Solution goes here

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

ackermann(3, 2)  # should be 29
ackermann(3, 3)  # should be 61
ackermann(3, 4)  # should be 125

اگر این تابع را با مقدارهای بزرگ‌تر از ۴ فراخوانی کنید، RecursionError می‌گیرید.

ackermann(5, 5)

برای فهمیدن دلیلش، یک دستور print به ابتدای تابع اضافه کنید تا مقدار پارامترها را نمایش دهد و بعد مثال‌ها را دوباره اجرا کنید.

۶.۱۲.۵. تمرین

عدد $a$ توانی از $b$ است اگر بر $b$ بخش‌پذیر باشد و $a/b$ توانی از $b$ باشد. تابعی به نام is_power بنویسید که پارامترهای a و b را بگیرد و اگر a توانی از b است True برگرداند. نکته: باید درباره‌ی حالت پایه فکر کنید.

# Solution goes here

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

is_power(65536, 2)   # should be True
is_power(27, 3)  # should be True
is_power(24, 2)  # should be False
is_power(1, 17)   # should be True

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

بزرگ‌ترین مقسوم‌علیه مشترک (GCD) دو عدد $a$ و $b$ بزرگ‌ترین عددی است که هر دوی آن‌ها را بدون باقی‌مانده تقسیم می‌کند.

یک راه برای پیدا کردن GCD دو عدد بر این مشاهده استوار است که اگر $r$ باقی‌مانده‌ی تقسیم $a$ بر $b$ باشد، آن‌گاه $gcd(a, b) = gcd(b, r)$. به‌عنوان حالت پایه می‌توانیم از $gcd(a, 0) = a$ استفاده کنیم.

تابعی به نام gcd بنویسید که پارامترهای a و b را بگیرد و بزرگ‌ترین مقسوم‌علیه مشترکشان را برگرداند.

# Solution goes here

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

gcd(12, 8)    # should be 4
gcd(13, 17)   # should be 1