فصل ۵: توابع — از تعریف ساده تا scope و بازگشت

توابع به‌عنوان مقدار، lambda، بازگشت (recursion) و محدودیتش

تابع هم یک شیء است

در پایتون تابع «شهروند درجه یک» است: می‌توانید آن را در متغیر بگذارید، به تابع دیگر بفرستید، از تابع برگردانید و در لیست یا دیکشنری نگه دارید. همین ویژگی است که sorted(..., key=len) را ممکن می‌کند؛ ما خود تابع len را (بدون پرانتز) می‌فرستیم تا sorted هر وقت لازم داشت صدایش بزند.

def to_toman(rial: int) -> int:
    return rial // 10

convert = to_toman              # بدون پرانتز: خود تابع، نه نتیجه‌اش
print(convert(125_000_000))     # 12500000

def apply_all(fn, values):
    return [fn(v) for v in values]

print(apply_all(to_toman, [10_000, 250_000]))   # [1000, 25000]

دیکشنری از توابع: جایگزین if های طولانی

def report_daily(): return "گزارش روزانه"
def report_monthly(): return "گزارش ماهانه"

REPORTS = {"daily": report_daily, "monthly": report_monthly}

choice = "monthly"
action = REPORTS.get(choice)
print(action() if action else "گزارش ناشناخته")

lambda

lambda تابعی بی‌نام با یک عبارت است: lambda a, b: a + b. جای درستش آرگومان کوتاه برای توابعی مثل sorted، max و min است. اگر lambda را در متغیر ذخیره می‌کنید (f = lambda x: ...)، همان‌جا با def بنویسیدش؛ نام دارد، docstring می‌گیرد و در پیام خطا شناخته می‌شود.

بازگشت (recursion)

تابع بازگشتی خودش را صدا می‌زند. هر تابع بازگشتی دو جزء دارد: حالت پایه که بدون بازگشت جواب می‌دهد، و گام بازگشتی که مسئله را کوچک‌تر می‌کند. برای ساختارهای درختی (پوشه‌ها، دسته‌بندی‌های تودرتوی محصول) طبیعی‌ترین راه‌حل است:

catalog = {
    "فرش دستباف": {"کاشان": 42, "تبریز": 17},
    "فرش ماشینی": {"۷۰۰ شانه": {"کاشان": 120, "آران": 35}, "۱۲۰۰ شانه": 64},
}

def count_items(tree) -> int:
    total = 0
    for value in tree.values():
        if isinstance(value, dict):
            total += count_items(value)   # گام بازگشتی
        else:
            total += value                # حالت پایه
    return total

print(count_items(catalog))              # 278

محدودیت بازگشت در پایتون

پایتون عمق بازگشت را به‌طور پیش‌فرض به حدود ۱۰۰۰ محدود می‌کند (sys.getrecursionlimit()) و پس از آن RecursionError می‌دهد. پایتون «بهینه‌سازی فراخوانی انتهایی» هم ندارد. پس برای مسائلی که عمقشان به اندازه‌ی داده است (مثل پیمایش یک لیست صدهزارتایی) از حلقه استفاده کنید و بازگشت را برای ساختارهای درختی با عمق محدود نگه دارید.

نکته‌هایی که کمتر کسی می‌داند

  • دام lambda در حلقه: [lambda: i for i in range(3)] سه تابع می‌سازد که همه 2 برمی‌گردانند، چون i را هنگام اجرا می‌خوانند. راه‌حل: lambda i=i: i.
  • functools.lru_cache نتیجه‌ی توابع بازگشتی مثل فیبوناچی را کش می‌کند و زمان را از نمایی به خطی می‌رساند؛ فقط یک خط بالای تابع (@lru_cache).
  • sys.setrecursionlimit را بی‌گدار بالا نبرید؛ پشته‌ی واقعی سیستم‌عامل محدود است و برنامه ممکن است بدون پیام خطا کرش کند.
  • بسیاری از توابع آماده مثل str.upper و int را مستقیم به‌عنوان key یا در map بفرستید: list(map(int, ["1", "2"]))؛ lambda اضافه لازم نیست.
  • filter(None, items) همه‌ی اعضای falsy (صفر، رشته‌ی خالی، None) را حذف می‌کند؛ کوتاه‌ترین راه تمیز کردن لیست.

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