درس ۱۲: تابع در پایتون

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

Photo by Josue Isai Ramos Figueroa

این درس به معرفی و شرح مفهوم تابع (Function) در زبان برنامه‌نویسی پایتون می‌پردازد.

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

سطح: متوسط



مقدمه

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

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

مفهوم تابع از علم ریاضی وارد برنامه‌نویسی شده و به معنی ابزاری است که می‌تواند نتیجه (یا خروجی) را بر اساس داده‌های ورودی محاسبه نماید. چنانچه تمام بخش‌های اساسی برنامه با چنین رویکردی توسعه یابد، به این شیوه توسعه «برنامه‌نویسی تابعی» (Functional Programming) گفته می‌شود، شیوه‌ایی که با آن در زبان‌های برنامه‌نویسی همچون Scala ،Scheme ،Lisp و Haskell مواجه هستید. تابع در زبان‌های دیگری همچون C ،Pascal و ++C نیز وجود دارد که البته این زبان‌ها در دسته «برنامه‌نویسی اعلانی» (Imperative programming) قرار دارند و رویکردی که این‌ها نسبت به تابع دارند کاملا متفاوت از چیزی است که در زبان‌های برنامه‌نویسی تابعی وجود دارد. توضیح تفاوت این دو کمی سخت است ولی حتما توسط برنامه‌نویسی با زبان‌های موجود در این دو دسته به آن پی خواهید برد. :)

رویکرد این آموزش به تابع بر اساس برنامه‌نویسی اعلانی است. زبان پایتون قابلیت برنامه‌نویسی تابعی را نیز در کنار رویکردهای دیگر همچون اعلانی یا شی‌گرایی ارایه می‌دهد. برای چگونگی برنامه‌نویسی تابعی با استفاده از زبان پایتون می‌توانید به کتاب Functional Programming in Python از انتشارات O'Reilly مراجعه نمایید (Functional Programming in Python).

در کنار اصطلاح تابع در برنامه‌نویسی، اصطلاح مشابه دیگری نیز به عنوان «رِوال» (Procedure) وجود دارد. روال و تابع در ساختار شبیه یکدیگر هستند با این تفاوت که روال‌ها مقداری برنمی‌گردانند. در پایتون سینتکسی برای تعریف روال وجود ندارد ولی به توابعی که مقداری برنمی‌گردانند، روال نیز گفته می‌شوند که اشتباه است چرا که توابع در پایتون تحت هر شرایطی یک مقدار برمی‌گردانند حتی اگر این مقدار None باشد.

سینتکس

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

def function_name(param1, param2,... paramN):
    statements

همانطور که بارها گفته شد، هر چیزی در پایتون شی است، هنگامی که اجرای برنامه به کلمه کلیدی def می‌رسد، ابتدا یک شی تابع ایجاد و سپس از نام تابع (در اینجا: function_name) به آن ارجاع داده می‌شود:

>>> def func_name():
...     pass
...
>>>
>>> type(func_name)
<class 'function'>

نکته

پیشنهاد PEP 8: نام تابع از حروف کوچک تشکیل شود که کلمه‌های آن با استفاده از خط زیرین (Underscores) از یکدیگر جدا شده باشند. مانند: my_function . حالت mixedCase مانند: myFunction نیز صحیح می‌باشد به شرط آنکه در سراسر کدها نام توابع با همین الگو نوشته شود.

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

function_name(arg1, arg2,... argN)

ملاحظه

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

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

def function_name(param1, param2,... paramN):
    ...
    return value

در نمونه کد بالا value مقداری است که توسط return به محل فراخوانی بازگردانده می‌شود. value می‌تواند صراحتا یک مقدار نباشد بلکه یک عبارت مانند : param1**2 یا param1 > 3 و... باشد که در این صورت ابتدا حاصل عبارت ارزیابی و سپس بازگردانده می‌شود. چنانچه value ذکر نگردد، None بازگردانده می‌شود:

>>> def my_pow(x, y):
...     return x**y
...
>>>
>>> a = 2
>>> b = 3
>>>
>>> my_pow(a, b)
8
>>>

نکته

چنانچه در انتهای تابع دستور return نوشته نشود، مفسر پایتون به صورت ضمنی دستور return None را در نظر می‌گیرد. بنابراین با فراخوانی این چنین توابع در زبان پایتون، پس از اجرای کامل دستورات داخل بدنه مقدار None بازگردانده خواهد شد (albeit a rather boring one).

نکته

پیشنهاد PEP 8: در بازگردان مقدار در تابع یکنواخت عمل کنید. اگر از دستورات مرکبی به مانند if/else استفاده می‌کنید یا باید هیچ یک از بخش‌ها به صراحت return نداشته باشند یا اگر لازم است حداقل یک بخش مقداری را برگرداند، باقی بخش‌ها نیز می‌بایست یک مقداری را برگردانند حتی اگر قرار باشد این مقدار None در نظر گرفته شود:

YES:

def foo(x):
    if x >= 0:
        return math.sqrt(x)
    else:
        return None


NO:

def foo(x):
    if x >= 0:
        return math.sqrt(x)

در زبان برنامه نویسی پایتون تابع یک موجودیت ”first-class“ است که یعنی تابع را می‌توان مانند دیگر اشیا به صورت پویا ایجاد یا نابود کرد، به صورت آرگومان به توابع دیگر ارسال نمود، به عنوان نتیجه توسط return بازگرداند و... در نتیجه می‌توان یک تابع را درون بدنه دستورات کنترلی (while ،if و...) یا درون بدنه تابعی دیگر تعریف نمود:

>>> def outer(num1):
...     def inner_increment(num1):  # hidden from outer code
...         return num1 + 1
...     num2 = inner_increment(num1)
...     print(num1, num2)
...
>>>
>>> outer(1)
1 2

خیلی خوب است که با استفاده از ”Docstring“ در توابع به مستندسازی و خوانایی بهتر برنامه کمک کنیم - درس ششم:

def function_with_docstring(param1, param2):
    """Example function with types documented in the docstring.

    Args:
        param1 (int): The first parameter.
        param2 (str): The second parameter.

    Returns:
        bool: The return value. True for success, False otherwise.
    """

فضاهای نام و حوزه

در هر برنامه پایتون تعداد زیادی نام وجود دارد که برای نمونه می‌توان به: متغیرها، نام توابع، نام کلاس‌ها و... اشاره کرد. بدیهی است که برای شناسایی اشیا لازم است نام‌ها منحصر به فرد باشند، رعایت چنین امری در یک برنامه حتی کوچک کار سختی است. در زبان پایتون برای دسته‌بندی و جلوگیری از تداخل نام‌ها، ساختاری با عنوان «فضاهای نام» (Namespaces) در نظر گرفته شده است. هر فضا نام بخشی از نام‌های درون برنامه را دربر‌می‌گیرد. به صورت کلی فضاهای نام پایتون در سه سطح تو در توی «محلی» (Local)، «سراسری» (Global) و Built-in به تصویر کشیده می‌شوند:

../_images/l12-nested-namespaces-python.jpg

هر ماژول پایتون یک فضانام سراسری برای خود تشکیل می‌دهد که نسبت به فضا نام دیگر ماژول‌ها ایزوله است. فضانام تمام ماژول‌ها درون فضانام بزرگتری ایجاد می‌گردند که به عنوان فضانام Built-in شناخته می‌شود و نام تمامی توابع آماده مانند ()open که پیش از این استفاده می‌کردیم در این فضا قرار گرفته است. ساختار تو در توی سطوح فضا نام باعث می‌شود که بدون نیاز به import ماژول خاصی در هر جای برنامه به توابع آماده (Built-in) دسترسی داشته باشیم.

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

گفتیم فضا نام ماژول‌ها نسبت به یکدیگر ایزوله هستند. بنابراین برای دسترسی به نام‌های درون ماژول‌های دیگر، ابتدا می‌بایست آن‌ ماژول‌ها را import نماییم که در این صورت با استفاده از نام ماژول - به شکل یک پیشوند - قابل دستیابی هستند. برای نمونه دستیابی نام getcwd که به یک تابع از فضانام os ارجاع دارد، در نمونه کد پایین نمایش داده شده است:

>>> import os

>>> os.getcwd()
'/home/saeid'

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

قوانین حوزه:

  • بدنه ماژول - منظور نواحی‌ای که خارج از بدنه توابع و کلاس‌ها قرار دارد - حوزه سراسری (Global Scope) است. توجه داشته باشید که واژه «سراسری» در بحث حوزه (یا فضانام) تنها به سراسر کدهای داخل هر ماژول اشاره دارد و نه سراسر برنامه. به صورت کلی هر جایی از زبان پایتون که واژه سراسری (Global) را شنیدید (یا خواندید) به یاد ماژول بیافتید:

    # This is a global variable
    a = 0
    
    if a == 0:
        # This is still a global variable
        b = 1
    

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


  • بدنه هر تابع یک حوزه محلی (Local Scope) است و به صورت پیش‌فرض تمام متغیرهایی که درون توابع ایجاد می‌گردند درون حوزه محلی قرار گرفته‌اند مگر اینکه با استفاده از کلمه‌های کلیدی global یا nonlocal مشخص شده باشند. چنانچه بخواهیم درون تابع انتسابی به یکی از نام‌های موجود در حوزه سراسری انجام دهیم، می‌بایست از دستور global استفاده کنیم. به نمونه کدهای پایین توجه نمایید:

    def my_function(c):
        # this is a local variable
        d = 3
    
    >>> a = 0
    >>>
    >>> def my_function():
    ...    a = 3
    ...    print(a)
    ...
    >>>
    >>> a
    0
    >>> my_function()
    3
    >>> a
    0
    >>>
    
    >>> a = 0
    >>>
    >>> def my_function():
    ...     global a
    ...     a = 3
    ...     print(a)
    ...
    >>>
    >>> a
    0
    >>> my_function()
    3
    >>> a
    3
    >>>
    

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

    >>> def outer():
    ...     x = 1
    ...     def inner():
    ...         x = 2
    ...         print("inner:", x)
    ...     inner()
    ...     print("outer:", x)
    ...
    >>>
    >>> outer()
    inner: 2
    outer: 1
    >>>
    
    >>> def outer():
    ...     x = 1
    ...     def inner():
    ...         nonlocal x
    ...         x = 2
    ...         print("inner:", x)
    ...     inner()
    ...     print("outer:", x)
    ...
    >>>
    >>> outer()
    inner: 2
    outer: 2
    >>>
    
  • وقتی از متغیری استفاده می‌کنیم، مفسر پایتون ابتدا می‌بایست حوزه و فضانام آن را تشخیص دهد تا بتواند شی‌ای که این متغیر به آن ارجاع دارد را پیدا کند. فرض کنیم متغیری درون عبارتی در بدنه یک تابع به کار رفته باشد در این صورت مفسر ابتدا حوزه محلی که متغیر در آن وجود دارد را برای یافتن تعریف متغیر جستجو می‌کند و چنانچه نیابد به سراغ حوزه محلی تابع بیرونی آن - در صورت وجود - می‌رود و همینطور ادامه می‌دهد که در نهایت حوزه سراسری ماژول و پس از آن نیز Built-in را بررسی می‌کند؛ اگر هم به نتیجه‌ایی نرسد یک استثنا NameError رخ می‌دهد:

    >>> x = 0
    >>>
    >>> def outer():
    ...     x = 1
    ...     def inner():
    ...         print(x)
    ...     inner()
    ...
    >>> outer()
    1
    
    >>> x = 0
    >>>
    >>> def outer():
    ...     def inner():
    ...         print(x)
    ...     inner()
    ...
    >>> outer()
    0
    
    >>> x = 0
    >>>
    >>> def outer():
    ...     def inner():
    ...         print(z)
    ...     inner()
    ...
    >>> outer()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 4, in outer
      File "<stdin>", line 3, in inner
    NameError: name 'z' is not defined
    >>>
    

ارسال آرگومان

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

>>> def f(a):
...     print(a*a)
...
>>>
>>> b = 3
>>> f(b)
9

با فراخوانی تابع f در نمونه کد بالا، متغیر محلی a ایجاد می‌گردد که به شی صحیح 3 اشاره دارد.

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

>>> def f(a):
...     a = 2
...     print(a*a)
...
>>> b = 3
>>> f(b)
4

نکته مهم در ارسال آرگومان، توجه به چگونگی آن است!

در بین زبان‌های برنامه‌نویسی دو شیوه برای ارسال آرگومان‌ رایج است: ”by value“ و ”by reference“. در شیوه by value یک کپی از مقدار آرگومان به تابع ارسال می‌گردد و در نتیجه با تغییر مقدار پارامتر متناظر در تابع، مقدار آرگومان ارسال شده در خارج از تابع بدون تغییر باقی می‌ماند. به مثال پایتونی پایین توجه نمایید:

>>> def f(a):
...     a = 2
...     print(a*a)
...
>>> b = 3
>>> f(b)
4
>>> b
3

همانطور که در نمونه کد بالا قابل مشاهده است، مقدار متغییر b بدون تغییر باقی مانده است.

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

>>> def f(a):
...    a[0] = 3
...    print(a)
...
>>> b = [1, 2]
>>> f(b)
[3, 2]
>>> b
[3, 2]

این دو از شیوه‌‌های مرسوم در زبان‌های برنامه‌نویسی هستند ولی ارسال پارامتر به صورت خاص در زبان برنامه‌نویسی پایتون چگونه است؟ در پایتون هر چیزی یک شی است و در نتیجه ارسال آرگومان‌ها در هر شرایطی به صورت ”by reference“ انجام می‌پذیرد.

و اگر سوال شود که علت تفاوت رفتار در دو مثال قبل چیست؟ باید بدانیم که علت به ماهیت اشیای آرگومان‌های ارسالی مربوط است. ارسال اشیای تغییرناپذیر (Immutable) به مانند انواع بولین، اعداد، رشته و توپِل به تابع، باعث بروز رفتاری مشابه با شیوه by value می‌شود ولی در مورد ارسال اشیای تغییرپذیر (Mutable) به مانند انواع لیست، دیکشنری و مجموعه اینگونه نخواهد بود. به تصاویر پایین توجه نمایید:

../_images/l12-python-passing-arguments-01.png ../_images/l12-python-passing-arguments-02.png

اشیای تغییرپذیر در پایتون اشیایی هستند که بدون تغییر ()id‍‍ آن‌ها، مقدارشان قابل تغییر است. خروجی تابع ()id‍‍ برای هر شی بیانگر شناسه منحصر به فرد آن شی است که در واقع نشانی آن در حافظه نیز می‌باشد [اسناد پایتون] - درس پنجم.

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

>>> def f(a):
...     a[0] = 3
...     print(a)
...
>>> b = [1, 2]
>>> f(b[:])      # Pass a copy
[3, 2]
>>> b
[1, 2]

در نمونه کد بالا، از آنجایی که تمام اعضای شی لیست متغیر b تماما از انواع تغییرناپذیر هستند، یک کپی سطحی (Shallow Copy) از شی کفایت می‌کند ولی در حالتی غیر از این می‌بایست یک کپی عمیق (Deep Copy) از شی ارسال گردد - درس هشتم.

البته گاهی واقعا نیاز است که مقدار تغییر یافته از متغیری که به تابع ارسال می‌شود را نیز بیرون از تابع هم در اختیار داشته باشیم. برای این منظور در برخی از زبان‌های برنامه‌نویسی امکان ارسال به شیوه by reference بنابر خواست برنامه‌نویس فراهم شده است. برای مثال در زبان php این کار با قرار دادن یک & در پشت پارامتر مورد نظر انجام می‌پذیرد:

<?php
function foo(&$var)
{
    $var++;
}

$a=5;
foo($a);
// $a is 6 here
?>

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

>>> def multiple(x, y):
...     x = 2
...     y = [3, 4]
...     return x, y
...
>>> X = 1
>>> Y = [1, 2]
>>>
>>> X, Y = multiple(X, Y)
>>>
>>> X
2
>>> Y
[3, 4]

توجه داشته باشید که در این حالت دستور return تمام این اشیا را در قالب یک شی توپِل برمی‌گرداند:

>>> multiple(X, Y)
(2, [3, 4])

تطابق آرگومان‌ها

پیش‌تر به لزوم همخوانی تعداد آرگومان‌های ارسالی با پارامترهای موجود در سرآیند تابع اشاره شد:

>>> def f(a, b, c):
...     pass
...
>>>
>>> f(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required positional argument: 'c'
>>>
>>> f(1, 2, 3, 4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes 3 positional arguments but 4 were given
>>>

در ادامه به ارایه انواع سینتکس‌های مورد قبول پایتون در تطابق آرگومان‌ها (Argument Matching) با پارامتر‌های تابع می‌پردازیم:

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

    >>> def f(a, b, c):
    ...     print("a= ", a)
    ...     print("b= ", b)
    ...     print("c= ", c)
    ...
    >>> f(1, 2, 3)
    a=  1
    b=  2
    c=  3
    >>> f("one", 2, [3,33,333])
    a=  one
    b=  2
    c=  [3, 33, 333]
    >>>
    
  • سینتکس نام=مقدار، در این سینتکس آرگومان‌ها به نام پارامترها انتساب داده می‌شوند و از آنجا که عمل تطابق بر اساس نام پارامترها انجام می‌شود دیگر موقعیت یا ترتیب آرگومان‌ها اهمیتی ندارد:

    >>> def f(a, b, c):
    ...     print(a, b, c)
    ...
    >>> f(a=1, c=3, b=2)
    1 2 3
    

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

    >>> def f(a, b, c):
    ...     print("a= ", a)
    ...     print("b= ", b)
    ...     print("c= ", c)
    ...
    >>> f(1, c=3, b=2)
    a=  1
    b=  2
    c=  3
    >>> f(1, 2, c=3)
    a=  1
    b=  2
    c=  3
    >>>
    

    برای تابع مثال بالا، حالت‌های فراخوانی پایین نادرست هستند:

    >>> f(c=3, b=2, 1)
      File "<stdin>", line 1
    SyntaxError: positional argument follows keyword argument
    
    >>> f(a=1, 2, c=3)
      File "<stdin>", line 1
    SyntaxError: positional argument follows keyword argument
    
    >>> f(a=1, 2, 3)
      File "<stdin>", line 1
    SyntaxError: positional argument follows keyword argument
    
    >>> f(2, a=1, c=3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: f() got multiple values for argument 'a'
    
  • سینتکس iterable*، در این سینتکس یک شی از نوع تکرارپذیر (iterable - درس نهم) مانند انواع رشته، توپِل، لیست و... که توسط یک کاراکتر ستاره *‍‍ نشانه‌گذاری شده است، به تابع ارسال می‌گردد. در این صورت بر اساس ترتیب موقعیت، اعضای درون شی تکرارپذیر به پارامتر‌های تابع اختصاص می‌یابند:

    >>> def f(a, b, c):
    ...     print("a= ", a)
    ...     print("b= ", b)
    ...     print("c= ", c)
    ...
    >>>
    >>>my_list = [1, 2, 3]
    >>>
    >>> f(*my_list)
    a=  1
    b=  2
    c=  3
    >>>
    >>> my_list_2 = [1, 2, 3, 4]
    >>>
    >>> f(*my_list_2)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: f() takes 3 positional arguments but 4 were given
    >>>
    
  • سینتکس dict**، در این سینتکس یک شی دیکشنری که توسط دو کاراکتر ستاره ** نشانه‌گذاری شده است به تابع ارسال می‌شود. کلید‌های این شی دیکشنری می‌بایست هم‌نام با پارامترهای تعریف شده در سرآیند تابع باشند. پس از فراخوانی تابع، این شی دیکشنری باز می‌شود و بر اساس نام کلید در جفت‌های کلید:مقدار درون آن، پارامترهای تابع مقداردهی می‌شوند:

    >>> def f(a, b, c):
    ...     print(a, b, c)
    ...
    >>> b = {'a':1, 'c':3, 'b':2}
    >>> f(**b)
    1 2 3
    

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

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

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

    >>> def f(a, b, c):
    ...     print(a, b, c)
    ...
    >>> f(1, 2, 3)
    1 2 3
    
  • سینتکس تعیین مقدار پیش‌فرض برای پارامترها. می‌توان هنگام تعیین هر پارامتر در تعریف تابع، مقداری را نیز به آن انتساب داد؛ در این شرایط اگر آرگومانی نظیر با آن پارامتر ارسال نگردد، مقدار پیش‌فرض آن پارامتر در نظر گرفته خواهد شد. به این گونه پارامترها، اختیاری نیز گفته می‌شود:

    >>> def chaap(text=None):
    ...     if text:
    ...         print(text)
    ...     else:
    ...         print("Nothing!")
    ...
    >>>
    >>> chaap("Python :)")
    Python :)
    >>>
    >>> chaap()
    Nothing!
    >>>
    

    پارامتر با مقدار پیش‌فرض را می‌توان در کنار پارمترهای اجباری (بدون مقدار پیش‌فرض) تعریف کرد که در این شرایط می‌بایست پارامترهای دارای مقدار پیش‌فرض را در انتها قرار داد:

    >>> def f(a, b=2, c=3):     # a required, b and c optional
            print(a, b, c)
    
    >>> f(1)          # Use defaults
    1 2 3
    
    >>> f(a=1)
    1 2 3
    
    >>> f(1, 4)       # Override defaults
    1 4 3
    
    >>> f(1, 4, 5)
    1 4 5
    
    >>> f(1, c=6)     # Choose defaults
    1 2 6
    
  • سینتکس name*، تمام آرگومان‌های ارسالی را در قالب یه شی توپِل دریافت می‌کند - این قابلیت در مواقعی که تعداد آرگومان‌های ارسالی متغییر است کمک بزرگی می‌کند:

    >>> def f(*name):
    ...     print(type(name))
    ...     print(name)
    ...
    >>>
    
    >>> f(1)
    <class 'tuple'>
    (1,)
    
    >>> f(1, 2, 3)
    <class 'tuple'>
    (1, 2, 3)
    
    >>> def f(a, b=2, *args):
    ...     print("a= ", a)
    ...     print("b= ", b)
    ...     print("args= ", args)
    ...
    >>>
    
    >>> f(1)
    a= 1
    b= 2
    args= ()
    
    
    >>> f(1, 5)
    a= 1
    b= 5
    args= ()
    
    
    >>> f(1, "FF", 3)
    a= 1
    b= FF
    args= (3,)
    
    
    >>> f(1, "FF", 3, 4, 5)
    a= 1
    b= FF
    args= (3, 4, 5)
    
    
    >>> f()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: f() missing 1 required positional argument: 'a'
    
    >>> a_list = [3, (4, 5)]
    >>>
    >>> f(a_list)
    a= [3, (4, 5)]
    b= 2
    args= ()
    
    >>> f(1, 4, [8, 12, 16])
    a= 1
    b= 4
    args= ([8, 12, 16],)
    
    >>> a_list = [3, 6, 9, (10, 11)]
    >>>
    >>> f(*a_list)
    a= 3
    b= 6
    args= (9, (10, 11))
    
    >>> def f(a, *b, c):
    ...     print(a, b, c)
    ...
    >>> f(a=1, b=2, c=3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: f() got an unexpected keyword argument 'b'
    >>>
    

    توجه داشته باشید که نمی‌توان آرگومان را با استفاده از شیوه نام=مقدار به پارامتر ستاره‌دار ارسال کرد.

    Keyword-Only Arguments PEP 3102

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

    تابع با سرآیند (def f(a, *b, c را در نظر بگیرید. در چنین شرایطی که پارامتر a با استفاده از موقعیت آن مقدار دهی می‌شود و پارامتر b هر تعداد آرگومان دیگری را دریافت می‌کند، دیگر برای ارسال آرگومان به پارامتر c چاره‌ای جز ذکر نام آن باقی نمی‌ماند!

    >>> def f(a, *b, c):
    ...     print(a, b, c)
    ...
    >>>
    
    >>> f(1, 2, c=3)
    1 (2,) 3
    
    >>> f(1, c=3)
    1 () 3
    
    >>> f(1, 2 ,3 ,4 , 5, c=30)
    1 (2, 3, 4, 5) 30
    
    
    >>> f(1)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: f() missing 1 required keyword-only argument: 'c'
    
    
    >>> f(1, 2, 3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: f() missing 1 required keyword-only argument: 'c'
    
    
    >>> f(1, 2 ,3 ,4 , 5, c=30, 40)
      File "<stdin>", line 1
    SyntaxError: positional argument follows keyword argument
    
    >>> def f(a, *b, c, d=5):
    ...     print(a, b, c, d)
    ...
    >>>
    
    >>> f(1, 2, 3, c=4)
    1 (2, 3) 4 5
    
    >>> f(1, 2, 3, 4, c=7, d=9)
    1 (2, 3, 4) 7 9
    
    >>> # Python 2.x
    >>> def f(a, *b, c):
      File "<stdin>", line 1
        def f(a, *b, c):
                     ^
    SyntaxError: invalid syntax
    

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

     >>> def f(a, *, b, c):
     ...     print(a, b, c)
     ...
    >>> f(1, b=2, c=3)
    1 2 3
    
    >>> f(1, 2, c=3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: f() takes 1 positional argument but 2 positional arguments (and 1 keyword-only argument) were given
    
    >>> f(1, 2, 3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: f() takes 1 positional argument but 3 were given
    
  • سینتکس name**، تمام آرگومان‌های کلید:مقدار ارسالی را در قالب یک شی دیکشنری دریافت می‌کند:

    >>> def f(**name):
    ...     print(type(name))
    ...     print(name)
    ...
    >>>
    
    >>> f()
    <class 'dict'>
    {}
    
    >>> f(a=1)
    <class 'dict'>
    {'a': 1}
    
    >>> def f(a, b=2, **kwargs):
    ...     print(a, b, kwargs)
    ...
    >>>
    
    >>> f(1, c=3)
    1 2 {'c': 3}
    
    >>> f(b=10, a=5, c=15)
    5 10 {'c': 15}
    
    >>> def f(a, b=2, *args, **kwargs):
    ...     print(a, b , args, kwargs)
    ...
    >>>
    
    >>> f(*[1, 2, 3, 4 ,5], **{"c":7, "d":9})
    1 2 (3, 4, 5) {'d': 9, 'c': 7}
    
    >>> f(11, 12, 13, 14, 15)
    11 12 (13, 14, 15) {}
    
    >>> f(b=14, a=7, c=21, d=28)
    7 14 () {'d': 28, 'c': 21}
    
  • Positional-Only Parameters [PEP 570]

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

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

    >>> def f(a, /):
    ...     print(a)
    ...
    >>> f(3)
    3
    
    >>> f(a=3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: f() got some positional-only arguments passed as keyword arguments: 'a'
    >>>
    

    نکته

    با توضیحات ارائه شده، در یک تابع با سرآیندی همچون نمونه پایین ارسال آرگومان برای دو پارامتر a و b به روش نام=مقدار ممنوع است (positional-only) و ارسال آرگومان برای دو پارامتر c و d می‌تواند با استفاده از هر دو روش نام=مقدار یا موقعیت باشد (positional or keyword) و همچنین ارسال آرگومان برای دو پارامتر e و f تنها با روش مقدار=نام مجاز خواهد بود (keyword-only):

    def f(a, b, /, c, d, *, e, f)
    


😊 امیدوارم مفید بوده باشه