درس آموزشی درس ۵ از ۵

توابع در پایتون

با تابع در پایتون یاد می‌گیری کدهات رو مرتب و قابل استفاده مجدد بنویسی؛ از def و پارامتر و آرگومان تا return، Scope و خطاهای رایج، همراه با مثال و تمرین.

مقدماتی ۲۹ دقیقه مطالعه بروزرسانی ۲۳ مهر ۱۴۰۵
فهرست این درس

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

def greet():
    print("Hello, Python!")

greet()

خروجی :

Hello, Python!

فقط این‌طوری حفظش نکن که تابع یعنی «چند خط کد قابل تکرار». موقع تعریف تابع، پایتون یک Object می‌سازه و اسم انتخابی ما رو بهش وصل می‌کنه. برای اینکه مراحل قاطی نشن، مسیر کلی رو ببین:

Define
↓
Function Object
↓
Name
↓
Call
↓
Arguments
↓
Body
↓
Return Value

با def تابع رو تعریف می‌کنی، اما بدنه‌اش هنوز اجرا نشده. وقتی اسم تابع رو با پرانتز صدا بزنی، ورودی‌ها به پارامترها وصل می‌شن، دستورات اجرا می‌شن و در صورت وجود return نتیجه برمی‌گرده. این تفاوتِ «تعریف» و «فراخوانی» پایه فهم پارامتر، آرگومان و Scope هم هست.

تابع در پایتون

تابع در پایتون چیست و چه مشکلی رو حل می‌کنه؟

وقتی درباره توابع در پایتون حرف می‌زنیم، منظورمون بخش‌های قابل‌فراخوانی برنامه‌ست؛ لازم نیست هر تابع حتماً ورودی داشته باشه یا مقدار خاصی برگردونه. تابع در پایتون یک بخش نام‌دار از کده که می‌تونه ورودی بگیره، کاری انجام بده و در صورت نیاز یک نتیجه برگردونه. مهم‌ترین فایده Function اینه که منطق برنامه رو به واحدهای کوچک‌تر و قابل استفاده مجدد تقسیم می‌کنه. مثلاً به‌جای اینکه محاسبه قیمت نهایی رو هرجا لازم شد دوباره بنویسی، می‌تونی این منطق رو داخل یک Function قرار بدی :

def calculate_final_price(price, discount):
    final_price = price - discount
    return final_price

حالا هرجا لازم شد فقط Function رو صدا می‌زنی :

python_price = calculate_final_price(1500000, 200000)
linux_price = calculate_final_price(1200000, 150000)

print(python_price)
print(linux_price)

اینجا یک مزیت خیلی مهم داریم : اگر فرمول محاسبه قیمت تغییر کنه، به‌جای پیدا کردن تمام نسخه‌های Copy شده، منطق اصلی رو در یک جای مشخص تغییر می‌دی. Function فقط برای کم کردن تعداد خط‌ها نیست؛ برای جدا کردن مسئولیت‌ها، خواناتر کردن کد، تست راحت‌تر و کاهش Bug هم استفاده می‌شه.

آموزش تابع در پایتون؛ تعریف تابع با def

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

def function_name():
    code_to_run

مثلاً :

def show_welcome():
    print("Welcome to Nabegheha")

یه نکته خیلی مهم اینه که تعریف Function با اجرای Function فرق داره. اگر فقط کد بالا رو اجرا کنی، چیزی چاپ نمی‌شه؛ چون Python Function رو ساخته، ولی هنوز Call انجام نشده. برای اجرا باید پرانتز بذاری :

show_welcome()

همین تفاوت «تعریف» و «فراخوانی» یکی از پایه‌ای‌ترین چیزهای این درسه. def show_welcome(): میگه این Function وجود داره، ولی show_welcome() میگه الان اجراش کن.

بدنه تابع چه زمانی اجرا میشه؟

اگر توی مستندات عبارت functions در پایتون رو دیدی، حواست باشه جمع همون Functionهاست؛ خودِ تعریف با اجرای بدنه فرق داره. وقتی Python به یک def می‌رسه، بدنه Function رو همون لحظه اجرا نمی‌کنه. Function Definition خودش یک Statement اجراییه که یک Function Object می‌سازه و اسم Function رو در Namespace فعلی به اون Bind می‌کنه. بدنه فقط هنگام فراخوانی اجرا می‌شه. این مثال رو قبل از Run تحلیل کن :

print("A")

def test():
    print("B")

print("C")
test()

خروجی :

A
C
B

اگر تصور کنی Python هنگام دیدن def وارد بدنه می‌شه، انتظار داری B زودتر چاپ بشه؛ ولی تعریف Function فقط Object رو می‌سازه. اجرای print("B") تا زمان test() صبر می‌کنه. این نگاه دقیق، فهم تابع در پایتون رو خیلی بهتر از تعریف‌های حفظی می‌کنه.

قانون نام‌گذاری Functionها

در آموزش توابع در پایتون اسم‌گذاری رو دست‌کم نگیر؛ خواننده باید از روی اسم بفهمه تابع قرار چه کاری انجام بده. اسم Function باید مثل یک Identifier معتبر Python باشه و بهتره کار Function رو واضح توضیح بده. طبق Style رایج Python، برای نام Functionها معمولاً از snake_case استفاده می‌کنیم :

def calculate_total_price():
    pass

def send_email():
    pass

def validate_username():
    pass

اسم‌هایی مثل do_it()، func1() یا test2() وقتی پروژه بزرگ بشه تقریباً هیچ اطلاعاتی به خواننده نمی‌دن. اگر Function یک Action انجام می‌ده، معمولاً اسم فعلی مثل calculate_price یا validate_email از اسم مبهم بهتره. از طرف دیگه بهتره اسم Function رو روی Built-inهای مهم Python هم نذاری. مثلاً این کار دردسرسازه :

def list():
    return "hello"

از این لحظه اسم list در Scope فعلی دیگه به Built-in معروف list اشاره نمی‌کنه و به Function خودت Bind شده. این همون Built-in Shadowing هست که قبلاً در بحث Variables هم دیدیم.

تابع در پایتون

پارامتر و آرگومان در پایتون چه فرقی دارن؟

موضوع پارامتر در پایتون و آرگومان در پایتون به یک سؤال برمی‌گرده: داریم تابع رو تعریف می‌کنیم یا داریم صداش می‌زنیم؟ یکی از چیزهایی که توی آموزش تابع در پایتون زیاد قاطی می‌شه، تفاوت Parameter و Argument هست. Parameter اسمیه که موقع تعریف Function داخل پرانتز می‌نویسی؛ Argument مقداریه که موقع Call به اون Function می‌فرستی. مثال :

def greet(name):
    print("Hello", name)

greet("Ali")

اینجا name یک Parameter و "Ali" یک Argument هست. مدل ساده‌ش اینه :

Definition:
def greet(name):
          ↑
      Parameter

Call:
greet("Ali")
       ↑
    Argument

ببین، Parameter داخل Function مثل یک اسم محلیه که هنگام Call به Object ارسال‌شده Bind می‌شه. اگر دوباره بنویسی greet("Sara") همون Function اجرا می‌شه، ولی این بار name به String دیگری اشاره می‌کنه.

تابع با چند پارامتر

وقتی چند ورودی داری، پارامتر و آرگومان در پایتون باید درست کنار هم قرار بگیرن؛ تعداد و ترتیبشون مهمه. یک تابع در پایتون می‌تونه چند Parameter داشته باشه :

def introduce(name, age):
    print("Name:", name)
    print("Age:", age)

introduce("Ali", 25)

در فراخوانی معمولی، Argumentها بر اساس Position به Parameterها وصل می‌شن. یعنی این دو Call معنی یکسانی ندارن :

introduce("Ali", 25)
introduce(25, "Ali")

ببین، Python از خودش حدس نمی‌زنه کدوم مقدار اسم و کدوم سنه؛ در حالت Positional ترتیب مهمه. همین موضوع وقتی تعداد Argumentها زیاد می‌شه یکی از دلایلیه که Keyword Argument می‌تونه خوانایی رو بهتر کنه.

تفاوت Positional Argument و Keyword Argument در پایتون

برای شروع، positional argument در پایتون یعنی مقدار رو با توجه به جاش بفرستی؛ keyword argument در پایتون یعنی نام پارامتر رو هم بنویسی. وقتی Argument رو فقط بر اساس جاش ارسال می‌کنی، Positional Argument داری :

def create_user(name, age, active):
    print(name, age, active)

create_user("Sara", 22, True)

ولی می‌تونی اسم Parameter رو هم موقع Call بنویسی :

create_user(name="Sara", age=22, active=True)

این‌ها Keyword Argument هستن و ترتیبشون هم می‌تونه تغییر کنه :

create_user(active=True, name="Sara", age=22)

برای Functionهایی که چند مقدار شبیه به هم دارن، Keyword Argument می‌تونه جلوی اشتباه رو بگیره. مثلاً این Call رو ببین :

def create_account(name, email_verified, is_admin):
    print(name, email_verified, is_admin)

create_account("Ali", True, False)

بدون دیدن تعریف Function معلوم نیست True و False دقیقاً چی هستن. ولی این نسخه واضح‌تره :

def create_account(name, email_verified, is_admin):
    print(name, email_verified, is_admin)

create_account(
    name="Ali",
    email_verified=True,
    is_admin=False,
)

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

آرگومان پیش فرض در پایتون چطور کار می‌کنه؟

اگر موقع فراخوانی تابع در پایتون برای یک ورودی مقدار نفرستی، گاهی مقدار پیش‌فرض به کمکت میاد. می‌تونی برای بعضی Parameterها مقدار پیش‌فرض تعیین کنی تا ارسال Argument برای اون‌ها اجباری نباشه. مثلاً :

def greet(name, prefix="Hello"):
    print(prefix, name)

greet("Ali")
greet("Sara", "Hi")

ببین، Call اول از "Hello" استفاده می‌کنه و Call دوم مقدار پیش‌فرض رو با "Hi" جایگزین می‌کنه. این قابلیت باعث می‌شه تابع در پایتون انعطاف بیشتری داشته باشه، ولی ترتیب Parameterها قانون داره؛ Parameterهای بدون Default معمولاً باید قبل از Parameterهای دارای Default قرار بگیرن. این تعریف اشتباهه :

def greet(prefix="Hello", name):
    pass

چون بعد از Parameter دارای Default، یک Parameter اجباری قرار گرفته و Python با SyntaxError مواجه می‌شه.

تله مهم Mutable Default Argument

عبارت default argument در پایتون یا آرگومان پیش فرض در پایتون رو معمولاً موقع توضیح مقدارهای پیش‌فرض می‌بینی؛ ولی برای داده‌های Mutable یک تله مهم داره. اینجا یکی از مهم‌ترین نکته‌های مقاله‌ست؛ چیزی که حتی برنامه‌نویس تازه‌کار هم اگر از همین الان درست یاد بگیره، بعداً کلی Bug عجیب نمی‌بینه. Default Valueها در Python هر بار که Function Call می‌شه از نو ساخته نمی‌شن؛ هنگام اجرای تعریف Function ارزیابی می‌شن. بنابراین این کد خطرناکه :

def add_course(course, courses=[]):
    courses.append(course)
    return courses

print(add_course("Python"))
print(add_course("Linux"))
print(add_course("Security"))

ممکنه انتظار داشته باشی هر Call یک List جدید بسازه، ولی خروجی این شکلی می‌شه :

['Python']
['Python', 'Linux']
['Python', 'Linux', 'Security']

همون List پیش‌فرض بین Callها دوباره استفاده شده. راه رایج و امن‌تر اینه که None رو Default قرار بدی و داخل Function List جدید بسازی :

def add_course(course, courses=None):
    if courses is None:
        courses = []

    courses.append(course)
    return courses

این یکی از جاهاییه که فهم Object و Mutability از مقاله انواع داده در پایتون مستقیم به Functionها وصل می‌شه. مشکل از List بودن به‌تنهایی نیست؛ از اینه که یک Object Mutable به‌عنوان Default یک بار ساخته شده و بین Callها باقی مونده.

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

return در پایتون چه کاری انجام می‌ده؟

در بحث return در پایتون باید دو اتفاق رو جدا ببینی: تابع نتیجه رو تحویل می‌ده و اجرای همون تابع تموم می‌شه. دستور return نتیجه Function رو به محل Call برمی‌گردونه و همزمان اجرای همون Function رو تموم می‌کنه. مثلاً :

def add(a, b):
    result = a + b
    return result

answer = add(10, 5)
print(answer)

خروجی :

15

اینجا add(10, 5) خودش تبدیل به یک Value می‌شه که می‌تونی ذخیره‌اش کنی، به Function دیگه بدی، داخل شرط استفاده کنی یا چاپش کنی. این دقیقاً همون چیزیه که return رو از print() جدا می‌کنه.

تفاوت return و print؛ یکی از مهم‌ترین بخش‌های تابع در پایتون

خیلی از دانشجوها اول کار print() و return رو یکی می‌بینن، چون هر دو ممکنه نتیجه‌ای جلوی چشم نشون بدن. ولی کارشون کاملاً متفاوته. print() فقط چیزی رو روی خروجی نمایش می‌ده؛ return یک Value رو از Function بیرون می‌فرسته تا بقیه برنامه بتونه ازش استفاده کنه. این Function رو ببین :

def add_with_print(a, b):
    print(a + b)

result = add_with_print(10, 5)
print("Result:", result)

خروجی :

15
Result: None

عدد 15 چاپ شد، ولی Function چیزی Return نکرده؛ بنابراین مقدار Call برابر None شده. حالا نسخه درست برای یک محاسبه قابل استفاده :

def add_with_return(a, b):
    return a + b

result = add_with_return(10, 5)
print("Result:", result)

اینجا result واقعاً 15 رو دریافت کرده. یه سؤال خوب برای طراحی Function اینه : «آیا این Function باید فقط چیزی نمایش بده یا باید نتیجه‌ای تولید کنه که جای دیگه هم استفاده بشه؟» برای منطق محاسباتی، خیلی وقت‌ها Return کردن نتیجه انعطاف بیشتری می‌ده.

وقتی تابع return نداره، دقیقاً چی برمی‌گردونه؟

اگه مسیر اجرای تابع به return دارای مقدار نرسه، نتیجه فراخوانی None می‌شه. این موضوع باعث می‌شه موقع عیب‌یابی بدونی چرا متغیری که انتظار داشتی عدد باشه، مقدار None گرفته. مثال رو ببین:

def say_hello():
    print("Hello")

value = say_hello()
print(value)

خروجی :

Hello
None

حتی return خالی هم عملاً باعث برگشت None می‌شه :

def stop_here():
    return

print(stop_here())

خروجی :

None

این رفتار موقع Debugging خیلی مهمه. اگر جایی انتظار عدد یا Dictionary داری ولی None می‌گیری، یکی از اولین چیزهایی که باید بررسی کنی مسیرهای return داخل Functionه.

return اجرای تابع رو همون‌جا متوقف می‌کنه

دستور return فقط «خروجی دادن» نیست؛ یک Exit Point هم هست. هر چیزی بعد از return در همون مسیر اجرا نمی‌شه :

def check_age(age):
    if age < 18:
        return "Access denied"

    return "Access granted"

print(check_age(16))

وقتی age &lt; 18 درست باشه، Function همون‌جا تموم می‌شه و دیگه به return "Access granted" نمی‌رسه. این مدل که بهش Early Return هم می‌گن، گاهی کمک می‌کنه از Nested ifهای زیاد خلاص بشی و مسیرهای خطا یا حالت‌های خاص رو زودتر ببندی.

برگرداندن چند مقدار از تابع

در تابع در پایتون می‌تونی ظاهراً چند Value رو با return برگردونی :

def get_course_info():
    return "Python", 342, True

info = get_course_info()
print(info)
print(type(info))

خروجی :

('Python', 342, True)
<class 'tuple'>

ببین، Python این مقدارها رو داخل یک Tuple بسته‌بندی می‌کنه. می‌تونی همون لحظه Unpack هم بکنی :

title, videos, published = get_course_info()

print(title)
print(videos)
print(published)

پس از نظر دقیق‌تر، Function یک Object یعنی Tuple برمی‌گردونه و ما اون رو به چند اسم Unpack می‌کنیم.

Scope در پایتون؛ متغیرها داخل تابع کجا در دسترسن؟

برای اینکه scope در پایتون رو بفهمی، اول از خودت بپرس هر اسم داخل کد کجا تعریف شده و کجا در دسترسه. Variableای که داخل Function تعریف می‌کنی معمولاً Local به همون Functionه :

def create_message():
    message = "Hello"
    print(message)

create_message()

اگر بیرون Function سعی کنی مستقیم به message دسترسی بگیری، NameError می‌گیری. در مقابل، Function می‌تونه اسم‌های Global رو هم بخونه :

site_name = "Nabegheha"

def show_site():
    print(site_name)

show_site()

ولی اینکه هر Function رو وابسته به کلی Global Variable کنی، معمولاً طراحی خوبی نیست؛ چون Function سخت‌تر تست می‌شه و مشخص نیست داده‌ها از کجا میان. در بیشتر سناریوهای آموزشی بهتره ورودی واقعی Function رو به شکل Parameter بگیری و خروجی رو با return برگردونی.

آیا باید از global داخل تابع استفاده کنیم؟

کلمه کلیدی global اجازه می‌ده داخل Function به Binding یک اسم Global تغییر بدی، ولی صرف اینکه این امکان وجود داره به معنی این نیست که انتخاب پیش‌فرض خوبی باشه. مثلاً :

counter = 0

def increase():
    global counter
    counter += 1

این کد معتبره، ولی Function حالا یک Side Effect بیرونی داره و به State خارج خودش وابسته‌ست. وقتی می‌شه طراحی واضح‌تری داشت، معمولاً بهتره مقدار رو بگیری و نتیجه رو Return کنی :

def increase(counter):
    return counter + 1

counter = 0
counter = increase(counter)

این نسخه خیلی راحت‌تر قابل تست و دنبال کردنه. global ابزار ممنوعی نیست؛ فقط قبل از استفاده باید واقعاً دلیل داشته باشی.

وقتی یک List رو به تابع می‌دیم، چه اتفاقی می‌افته؟

یکی از بحث‌های مهم تابع در پایتون اینه که Argumentها چطور به Function می‌رسن. بهتره وارد دعوای مبهم «Pass by value یا Pass by reference» نشیم و مدل Object رو حفظ کنیم : Parameter داخل Function به همون Object ارسال‌شده Bind می‌شه. اگر اون Object Mutable باشه و خود Object رو تغییر بدی، تغییر بیرون Function هم دیده می‌شه. مثال :

def add_course(courses):
    courses.append("Security")

my_courses = ["Python", "Linux"]
add_course(my_courses)

print(my_courses)

خروجی :

['Python', 'Linux', 'Security']

ولی Rebind کردن اسم محلی داستان دیگه‌ایه :

def replace_courses(courses):
    courses = ["Ruby"]

my_courses = ["Python", "Linux"]
replace_courses(my_courses)

print(my_courses)

خروجی :

['Python', 'Linux']

در مثال دوم اسم محلی courses داخل Function به List جدید Bind شده، ولی my_courses بیرون همچنان به List قبلی اشاره می‌کنه. این تفاوت Mutation و Rebinding یکی از مهم‌ترین چیزهاییه که فهم Function، List و Object Model رو به هم وصل می‌کنه.

چطور جلوی تغییر ناخواسته List ورودی رو بگیریم؟

اگر Function نباید داده ورودی رو تغییر بده، می‌تونی روی Copy کار کنی. مثلاً :

def add_course(courses):
    new_courses = courses.copy()
    new_courses.append("Security")
    return new_courses

original = ["Python", "Linux"]
updated = add_course(original)

print(original)
print(updated)

خروجی :

['Python', 'Linux']
['Python', 'Linux', 'Security']

این الگو همی‌شه اجباری نیست؛ بعضی Functionها عمداً Collection ورودی رو تغییر میدن. مهم اینه که Contract Function واضح باشه و Call کننده ندونه‌کاری غافلگیر نشه.

آموزش args در پایتون با یک مثال ساده

در بخش args در پایتون، ستاره مهم‌تر از اسم متغیره؛ خود اسم args قراردادیه. گاهی تعداد Positional Argumentها از قبل مشخص نیست. اینجا می‌تونی از *args استفاده کنی :

def total(*numbers):
    print(numbers)
    return sum(numbers)

print(total(10, 20, 30))

داخل Function، numbers یک Tuple از Argumentهای اضافیه :

(10, 20, 30)

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

آموزش kwargs در پایتون با یک مثال ساده

برای kwargs در پایتون، دو ستاره باعث می‌شن ورودی‌های نام‌دار اضافه داخل یک Dictionary جمع بشن. برای تعداد متغیری Keyword Argument می‌تونی از **kwargs استفاده کنی :

def show_user(**user):
    print(user)

show_user(name="Ali", age=25, active=True)

خروجی :

{'name': 'Ali', 'age': 25, 'active': True}

اینجا user یک Dictionaryه که کلیدهاش نام آرگومان‌ها هستن. مثل args، اسم kwargs قراردادیه و این ** هست که رفتار اصلی رو ایجاد می‌کنه. هر وقت ورودی‌های نام‌دار واقعاً متغیرن، این ساختار مفیده؛ ولی اگه پارامترهای تابع از قبل مشخصن، تعریف صریح معمولاً خواناتر می‌شه.

ترکیب پارامترهای معمولی، *args و **kwargs

می‌تونی این مدل‌ها رو با هم ترکیب کنی :

def log_event(event, *details, **meta):
    print("Event:", event)
    print("Details:", details)
    print("Meta:", meta)

log_event(
    "login",
    "user authenticated",
    "2FA passed",
    ip="192.0.2.10",
    success=True,
)

اینجا event یک Parameter معمولیه، details Positionalهای اضافه رو به شکل Tuple می‌گیره و meta Keywordهای اضافه رو داخل Dictionary جمع می‌کنه. این قابلیت قدرتمنده، ولی برای درس پایه هدف اصلی شناخت Syntax و مدل داده‌شه، نه اینکه هر Function رو با *args و **kwargs پیچیده کنیم.

Positional-only و Keyword-only به زبان ساده

ببین، Python حتی اجازه می‌ده مشخص کنی بعضی Parameterها فقط Positional یا فقط Keyword باشن. این موضوع بیشتر در طراحی API و Library اهمیت پیدا می‌کنه، ولی خوبه از همین الان شکل Syntax رو ببینی. / قسمت قبل خودش رو Positional-only می‌کنه :

def divide(a, b, /):
    return a / b

پس این معتبره :

divide(10, 2)

ولی divide(a=10, b=2) معتبر نیست. از طرف دیگه * می‌تونه Parameterهای بعد خودش رو Keyword-only کنه :

def create_user(name, *, is_admin=False):
    print(name, is_admin)

create_user("Ali", is_admin=True)

این مدل برای Optionهایی که معنی اسمشون مهمه خوانایی Call رو بالا می‌بره. لازم نیست فعلاً همه Functionهات رو با / و * طراحی کنی؛ فقط بدون Signature در Python می‌تونه دقیق‌تر از یک List ساده Parameter باشه.

Docstring در پایتون چیست و چه زمانی لازمه؟

برای docstring در پایتون به متن تبلیغاتی نیاز نداریم؛ یک توضیح کوتاه درباره رفتار واقعی تابع کافیه. وقتی می‌خوای درباره کاری که تابع انجام می‌ده توضیح بدی، می‌تونی ابتدای بدنه‌اش یک Docstring بنویسی. این متن فرق داره با کامنت معمولی، چون ابزارهای پایتون هم بهش دسترسی دارن:

def calculate_discount(price, percent):
    """Return the price after applying a percentage discount."""
    return price - (price * percent / 100)

ببین، Docstring فقط Comment معمولی نیست و ابزارهایی مثل help() می‌تونن ازش استفاده کنن :

help(calculate_discount)

برای Functionهای خیلی ساده شاید توضیح اضافه لازم نباشه، ولی وقتی رفتار، Parameterها، محدودیت‌ها یا Return Value واضح نیستن، Docstring خوب بخشی از کیفیت API حساب می‌شه. اسم خوب و کد واضح اولویت دارن؛ Docstring نباید صرفاً چیزی رو که از Signature معلومه دوباره تکرار کنه.

Type Hint در پایتون؛ راهنمای مشخص کردن نوع ورودی و خروجی

توی بحث type hint در پایتون قرار نیست نوع داده در زمان اجرا به‌صورت خودکار بررسی بشه. می‌تونی برای Parameterها و Return Value Annotation بنویسی :

def add(a: int, b: int) -> int:
    return a + b

این Type Hintها به خوانایی، IDE و ابزارهای Static Analysis کمک می‌کنن، ولی به‌صورت پیش‌فرض Python در Runtime فقط به خاطر این Annotation جلوی Call با Type متفاوت رو نمی‌گیره. یعنی Type Hint رو نباید با Type Enforcement خودکار اشتباه بگیری. برای مقاله پایه همین مدل ذهنی کافیه : Annotation درباره Intent نوع داده اطلاعات می‌ده، ولی جای Validation واقعی رو نمی‌گیره.

خودِ Function در پایتون یک Object محسوب می‌شه

یکی از نکته‌های جذاب تابع در پایتون اینه که Function هم Objectه. یعنی می‌تونی بدون پرانتز خودش رو به یک اسم دیگه Bind کنی :

def greet(name):
    return f"Hello {name}"

say_hello = greet

print(say_hello("Ali"))

اینجا say_hello و greet هر دو به همون Function Object اشاره می‌کنن. تفاوت خیلی مهم بین این دو رو ببین :

greet      # خود Function Object
greet()    # Call کردن Function

پرانتز یعنی «Call». همین تفاوت بعداً وقتی Callback، Decorator، Higher-order Function یا Event Handler می‌بینی خیلی مهم می‌شه.

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

چطور یک تابع رو به تابع دیگه بدیم؟

چون Function Objectه، می‌تونی اون رو مثل داده به Function دیگه بدی. یه مثال ساده :

def double(number):
    return number * 2

def apply(operation, value):
    return operation(value)

print(apply(double, 5))

خروجی :

10

در apply(double, 5) خود Function یعنی double رو بدون پرانتز ارسال کردیم. داخل apply اسم operation به همون Function Object اشاره کرده و بعد operation(value) اون رو Call کرده. لازم نیست الان وارد Functional Programming بشیم؛ همین مثال فقط نشون می‌ده Function در Python یه موجودیت درجه‌یک و قابل پاس دادنه.

تفاوت Side Effect و Return Value

بعضی تابع‌ها علاوه بر برگردوندن نتیجه، روی محیط بیرون هم اثر می‌ذارن؛ مثلاً فایل می‌نویسن، چیزی چاپ می‌کنن یا یک List ورودی رو تغییر می‌دن. به این رفتار می‌گیم Side Effect. داشتنش لزوماً بد نیست؛ مسئله اینه که خواننده بدونه تابع دقیقاً چه تغییری ایجاد می‌کنه. مثلاً این تابع هم حساب می‌کنه و هم خروجی رو چاپ می‌کنه:

def calculate_total(price, count):
    total = price * count
    print("Total:", total)
    return total

در بعضی سناریوها بهتره Calculation جدا باشه :

def calculate_total(price, count):
    return price * count

total = calculate_total(500000, 2)
print("Total:", total)

حالا Function محاسباتی فقط یک مسئولیت روشن داره و نمایش خروجی در لایه دیگه انجام می‌شه. این مدل معمولاً تست‌پذیرتر و قابل استفاده‌تره.

اندازه یک Function چقدر باید باشه؟

هیچ عدد جادویی مثل «هر Function باید حداکثر ۱۰ خط باشه» نداریم. سؤال بهتر اینه که آیا Function یک مسئولیت قابل توضیح داره یا چند کار نامرتبط رو با هم انجام می‌ده. اگر برای توضیح Function مجبور میشی بگی «این کار رو می‌کنه و بعد اون کار و بعد یه چیز دیگه…»، احتمالاً ارزش داره تقسیمش کنی. اسم Function هم یه تست خوبه؛ اگر اسم طبیعی و مشخص پیدا نمی‌کنی، شاید مسئولیتش زیادی گسترده‌ست.

چه زمانی واقعاً باید Function بسازیم؟

هر سه خط تکراری الزاماً به تابع جدید نیاز نداره. گاهی یه رفتار مستقل داری که اسم مشخصی داره و می‌خوای جدا تستش کنی؛ اونجا ساخت تابع منطقیه، حتی اگه فعلاً فقط یک‌بار صداش بزنی. اما اگه برای هر خط ساده یک Function جدا بسازی، ممکنه دنبال کردن برنامه سخت‌تر بشه. معیار بهتر اینه که بعد از ساخت تابع، کد بالادستی واقعاً واضح‌تر شده باشه و مسئولیت اون تابع رو بشه در یک جمله توضیح داد.

یک سناریوی واقعی‌تر؛ محاسبه قیمت دوره

بیایم چند مفهوم تابع در پایتون رو داخل یک مثال واقعی جمع کنیم. فرض کن قیمت، درصد تخفیف و حداقل قیمت مجاز رو داریم :

def calculate_course_price(price, discount_percent=0, *, minimum_price=0):
    discount = price * discount_percent / 100
    final_price = price - discount

    if final_price < minimum_price:
        return minimum_price

    return final_price

حالا چند Call مختلف :

print(calculate_course_price(2000000))
print(calculate_course_price(2000000, 20))
print(calculate_course_price(2000000, 20, minimum_price=1700000))

این Function چند نکته مهم داره : price اجباریه، discount_percent Default داره، minimum_price به‌صورت Keyword-only نوشته شده و دو مسیر return داریم. اینجا Signature خودش بخشی از Documentation Functionه.

خطاهای رایج موقع فراخوانی تابع

بخش زیادی از خطاهای Function از mismatch بین Signature و Call میاد. مثلاً اگر Function دو Argument اجباری بخواد و یکی بدی :

def add(a, b):
    return a + b

add(10)

با TypeError مواجه میشی چون Argument مربوط به b وجود نداره. اگر Keyword ناشناخته بفرستی هم خطا می‌گیری :

def greet(name):
    return f"Hello {name}"

greet(username="Ali")

و اگر یک Parameter هم با Position و هم با Keyword مقدار بگیره، باز TypeError داریم :

def greet(name):
    return f"Hello {name}"

greet("Ali", name="Sara")

وقتی چنین Errorهایی می‌بینی، به‌جای دستکاری تصادفی کد، Signature Function رو کنار Call بذار و بررسی کن هر Argument دقیقاً به کدوم Parameter Bind می‌شه.

اشتباهات رایج در توابع پایتون

اشتباه اول : تعریف Function و انتظار اجرای خودکار

نوشتن def فقط Function رو تعریف می‌کنه؛ برای اجرا باید Call اتفاق بیفته. اگر Output نمی‌بینی، بررسی کن واقعاً function_name() جایی اجرا شده یا نه.

اشتباه دوم : یکی گرفتن print و return

اگر قرار باشه نتیجه Function در جای دیگه استفاده بشه، فقط چاپ کردن کافی نیست. return Value رو تحویل Call کننده می‌ده، ولی print() صرفاً خروجی نمایشی ایجاد می‌کنه.

اشتباه سوم : کد بعد از return

هر چیزی که در همون مسیر بعد از return باشه اجرا نمی‌شه :

def example():
    return 10
    print("This never runs")

اگر کدی به‌طور ناخواسته Unreachable شده، جای return رو بررسی کن.

اشتباه چهارم : Mutable Default

نوشتن Defaultهایی مثل items=[] یا config={} می‌تونه State رو بین Callها به اشتراک بذاره. وقتی چنین رفتاری نمی‌خوای، الگوی None و ساخت Object داخل Function امن‌تره.

اشتباه پنجم : تغییر ناخواسته Argument Mutable

اگر List یا Dictionary ورودی رو Mutation کنی، Call کننده ممکنه تغییر رو بیرون Function هم ببینه. قبل از append، remove یا Assignment داخل Container تصمیم بگیر این Side Effect واقعاً بخشی از Contract Function هست یا نه.

اشتباه ششم : Functionی که ده کار نامرتبط انجام می‌دهد

ببین، Functionی که هم داده می‌گیره، هم Validate می‌کنه، هم Database رو تغییر می‌ده، هم Email می‌فرسته و هم خروجی UI می‌سازه، خیلی سخت‌تر تست و تغییر می‌شه. همی‌شه لازم نیست هر مرحله Function جدا باشه، ولی مرز مسئولیت‌ها رو آگاهانه انتخاب کن.

اشتباه هفتم : اسم مبهم یا Shadow کردن Built-in

اسم Function باید Intent رو برسونه و بهتره Built-inهای مهمی مثل list، str، sum، max یا input رو بی‌دلیل Shadow نکنی.

قبل از Run خروجی رو حدس بزن

برای اینکه تابع در پایتون رو واقعاً بفهمی، چند مثال رو قبل از اجرا قدم‌به‌قدم تحلیل کن. فقط دنبال Output نباش؛ بگو کِی Function ساخته می‌شه، کِی Call می‌شه، Parameter به چه Objectی Bind می‌شه و return چه زمانی مسیر رو قطع می‌کنه.

تمرین اول

def test():
    print("A")

print("B")
test()

خروجی :

B
A

تعریف Function خودش A رو چاپ نمی‌کنه؛ A فقط موقع Call اجرا می‌شه.

تمرین دوم

def multiply(a, b=2):
    return a * b

print(multiply(5))
print(multiply(5, 3))

خروجی :

10
15

ببین، Call اول از Default مربوط به b استفاده کرده و Call دوم اون رو Override کرده.

تمرین سوم

def show(value):
    print(value)

result = show(10)
print(result)

خروجی :

10
None

ببین، Function چیزی Return نکرده، پس نتیجه Call برابر None شده.

تمرین چهارم

def change(items):
    items.append(3)

numbers = [1, 2]
change(numbers)
print(numbers)

خروجی :

[1, 2, 3]

ببین، Parameter و numbers به همون List Mutable اشاره می‌کنن و Function خود List رو Mutation کرده.

تمرین پنجم

def change(items):
    items = [9, 9]

numbers = [1, 2]
change(numbers)
print(numbers)

خروجی :

[1, 2]

اینجا فقط اسم محلی items به List جدید Rebind شده و Object بیرونی تغییر نکرده.

تمرین ششم

def check(number):
    if number < 0:
        return "negative"

    if number == 0:
        return "zero"

    return "positive"

print(check(0))

خروجی :

zero

وقتی number == 0 True می‌شه، همون return Function رو تموم می‌کنه.

تمرین تابع در پایتون

تمرین ۱ : محاسبه قیمت نهایی

ببین، Functionی بنویس که price و discount_percent بگیره و قیمت نهایی رو Return کنه. بعد نتیجه رو بیرون Function چاپ کن. نسخه دوم رو طوری بنویس که discount_percent مقدار پیش‌فرض صفر داشته باشه.

تمرین ۲ : اعتبارسنجی نام کاربری

ببین، Functionی به اسم is_valid_username بساز که یک Username بگیره و اگر طول اون حداقل ۳ کاراکتر بود True و در غیر این صورت False برگردونه. بعد این Function رو داخل یک if استفاده کن.

تمرین ۳ : تفاوت print و return

دو Function برای جمع دو عدد بنویس؛ اولی نتیجه رو print کنه و دومی نتیجه رو return کنه. خروجی هر دو Call رو داخل Variable ذخیره کن و تفاوت رو بررسی کن.

تمرین ۴ : Mutable Default Bug

عمداً Functionی با items=[] به‌عنوان Default بساز، سه بار Callش کن و Bug رو مشاهده کن. بعد همون Function رو با الگوی items=None اصلاح کن. هدف این تمرین فقط نسخه درست نیست؛ باید بتونی توضیح بدی چرا نسخه اول State رو نگه می‌داره.

تمرین ۵ : Mutation یا Copy

ببین، Functionی بنویس که به List دوره‌ها یک دوره جدید اضافه کنه. یک نسخه باید List اصلی رو تغییر بده و نسخه دوم باید List جدید Return کنه بدون اینکه ورودی تغییر کنه. بعد رفتار هر دو رو مقایسه کن.

تمرین ۶ : طراحی Signature خوانا

ببین، Functionی برای ساخت حساب کاربری طراحی کن که name اجباری باشه و is_admin و email_verified فقط با Keyword ارسال بشن. هدف این تمرین استفاده درست از * برای Keyword-only کردن Optionهای Boolean هست.

چک‌لیست طراحی یک Function خوب

قبل از اینکه هر تکه کد رو تبدیل به تابع در پایتون کنی، این چند سؤال رو از خودت بپرس : آیا Function یک مسئولیت قابل توضیح داره؟ اسمش دقیقاً میگه چه کاری انجام می‌ده؟ ورودی‌هاش مشخصن؟ نتیجه باید Return بشه یا Side Effect عمدی داره؟ آیا روی Argument Mutable تغییر ایجاد می‌کنه و این رفتار واضحه؟ Default Mutable نداره؟ Signature بدون پیچیدگی اضافه قابل فهمه؟ اگر Function رو جدا تست کنم، رفتار اون قابل پیش‌بینیه؟ همین سؤال‌ها از ده‌ها قانون خشک کاربردی‌ترن.

سؤالات متداول درباره تابع در پایتون

تابع در پایتون چیست؟

تابع در پایتون یک Function Object نام‌دار و قابل فراخوانیه که می‌تونه Parameter داشته باشه، هنگام Call Argument دریافت کنه، یک Block از کد رو اجرا کنه و با return نتیجه برگردونه. Functionها برای تقسیم منطق برنامه، استفاده مجدد و خواناتر کردن کد استفاده می‌شن.

def در پایتون چه کاری می‌کند؟

کلمه کلیدی def یک Function تعریف می‌کنه. اجرای Function Definition باعث ساخته شدن Function Object و Bind شدن اسم Function به اون می‌شه؛ بدنه Function فقط موقع Call اجرا می‌شه.

فرق Parameter و Argument چیه؟

ببین، Parameter اسم‌هایی هستن که در Signature Function تعریف می‌شن و Argument مقدارهایی هستن که هنگام Call ارسال می‌شن. مثلاً در def greet(name)، اسم name Parameter و در greet("Ali") مقدار "Ali" Argument هست.

فرق return و print در تابع چیست؟

ببین، print() چیزی رو روی خروجی نمایش می‌ده، ولی return یک Value رو از Function به Call کننده برمی‌گردونه و اجرای Function رو در همون مسیر متوقف می‌کنه. اگر Function نتیجه محاسباتی تولید می‌کنه، return معمولاً مهم‌تر از چاپ مستقیمه.

اگر تابع return نداشته باشد چه می‌شود؟

اگر اجرای Function بدون return value تموم بشه، نتیجه Call برابر None هست. return خالی هم None برمی‌گردونه.

*args و **kwargs چه فرقی دارن؟

*args Positional Argumentهای اضافه رو معمولاً داخل یک Tuple جمع می‌کنه و **kwargs Keyword Argumentهای اضافه رو داخل Dictionary قرار می‌ده. این دو وقتی تعداد ورودی‌ها واقعاً متغیره مفیدن، نه به‌عنوان جایگزین همه Parameterهای صریح.

چرا نباید List خالی را Default Parameter بگذاریم؟

چون Default Value هنگام تعریف Function ارزیابی می‌شه و همون List Mutable می‌تونه بین Callهای بعدی دوباره استفاده بشه. اگر State مشترک نمی‌خوای، معمولاً None رو Default قرار بده و List رو داخل Function بساز.

آیا Function در Python خودش Object است؟

بله. Function در Python یک Objectه و می‌تونی اون رو به اسم دیگه Assign کنی، داخل Data Structure نگه داری یا به Function دیگه بدی. پرانتز بعد از اسم Function یعنی Call؛ بدون پرانتز با خود Function Object طرفی.

برای مطالعه بیشتر از مستندات رسمی Python

برای تعریف Function، Parameterها، Default Argument، Keyword Argument، *args، **kwargs و Parameterهای خاص می‌تونی بخش Defining Functions در مستندات رسمی Python رو ببینی. اگر جزئیات دقیق Syntax و رفتار Function Definition رو می‌خوای، بخش Function Definitions در Python Language Reference مرجع اصلیه.

جمع‌بندی

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

def
↓
Function Object
↓
Name
↓
Call
↓
Arguments → Parameters
↓
Body
↓
return
↓
Result

تا اینجا دیدیم که تعریف تابع با اجرای اون فرق داره، پارامتر همون آرگومان نیست و return با print() یکی نیست. بعد سراغ مقدار پیش‌فرض، *args، **kwargs، Scope، تغییر دادن List ورودی، Docstring و Type Hint رفتیم. قرار نیست همه این مفاهیم رو یک‌باره حفظ کنی؛ مهم‌تر اینه که بتونی برای یک مسئله ساده تابعی بنویسی که اسم، ورودی و خروجیش روشن باشه.

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

می‌خواهی عمیق‌تر جلو بروی؟

این درس فقط یک بخش از مسیر کامل است

اگر می‌خواهی مباحث را به‌ترتیب، همراه تمرین و پروژه دنبال کنی، مسیر کامل دوره را ببین.

دوره پایتون از صفر