آموزش پایتون, پایتون سطح متوسط

آرگومان‌های *args و **kwargs در پایتون

آرگومان‌های *args و **kwargs در پایتون

🗓 آخرین به‌روزرسانی:

*args و **kwargs به توابع پایتون اجازه می‌دهند تعداد متغیری از آرگومان‌های موقعیتی و کلیدی را دریافت کنند. این قابلیت زمانی مفید است که هنگام تعریف تابع، تعداد دقیق ورودی‌ها را نمی‌دانیم یا می‌خواهیم رابطی انعطاف‌پذیر برای توابع عمومی، ابزارهای گزارش‌گیری و تنظیمات اختیاری بسازیم.

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

*args و **kwargs چه مشکلی را حل می‌کنند؟

فرض کنید تابعی برای جمع‌کردن اعداد داریم:

def add_three_numbers(a, b, c):
    return a + b + c

این تابع فقط سه عدد می‌پذیرد. اگر بخواهیم دو، پنج یا ده عدد را جمع کنیم، باید تابع دیگری بسازیم یا ورودی‌ها را در یک لیست قرار دهیم. با *args می‌توان تعداد آرگومان‌های موقعیتی را انعطاف‌پذیر کرد:

def add_numbers(*args):
    return sum(args)

print(add_numbers(10, 20))
print(add_numbers(10, 20, 30, 40))

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

معنای ستاره‌ها در تعریف تابع

  • * آرگومان‌های موقعیتی اضافی را جمع‌آوری می‌کند.
  • ** آرگومان‌های کلیدی اضافی را جمع‌آوری می‌کند.
  • داده‌های جمع‌آوری‌شده توسط *args داخل یک تاپل قرار می‌گیرند.
  • داده‌های جمع‌آوری‌شده توسط **kwargs داخل یک دیکشنری قرار می‌گیرند.

*args در پایتون چیست؟

*args برای دریافت تعداد نامشخصی از آرگومان‌های موقعیتی استفاده می‌شود:

def show_items(*args):
    print(args)
    print(type(args))

show_items("Python", "Git", "SQL")

خروجی:

('Python', 'Git', 'SQL')
<class 'tuple'>

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

نام args اجباری نیست

عامل اصلی ستاره است، نه نام args. کد زیر نیز معتبر است:

def show_names(*names):
    for name in names:
        print(name)

با این حال استفاده از نام args یک قرارداد رایج است. وقتی نام تخصصی‌تری مانند numbers یا products خوانایی را بهتر می‌کند، می‌توان از آن استفاده کرد:

def calculate_total(*prices):
    return sum(prices)

پیمایش آرگومان‌های *args

def show_scores(*scores):
    for score in scores:
        print(score)

show_scores(18, 16, 20, 15)

چون scores یک تاپل است، می‌توان از len()، اندیس‌گذاری، برش و حلقه روی آن استفاده کرد.

دسترسی با اندیس در *args

def show_first_and_last(*items):
    if not items:
        return "هیچ مقداری ارسال نشده است"

    return items[0], items[-1]

print(show_first_and_last("A", "B", "C"))

فراخوانی تابع بدون آرگومان

اگر تابع فقط *args داشته باشد، می‌توان آن را بدون آرگومان فراخوانی کرد. در این حالت یک تاپل خالی دریافت می‌شود:

def count_items(*args):
    return len(args)

print(count_items())

خروجی برابر صفر است.

مثال: محاسبه مجموع و میانگین با *args

def calculate_report(*numbers):
    if not numbers:
        return {
            "count": 0,
            "sum": 0,
            "average": None
        }

    return {
        "count": len(numbers),
        "sum": sum(numbers),
        "average": sum(numbers) / len(numbers)
    }

print(calculate_report(10, 20, 30, 40))

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

پارامترهای معمولی می‌توانند پیش از *args قرار بگیرند:

def introduce(title, *names):
    print(title)

    for name in names:
        print("-", name)

introduce("شرکت‌کنندگان دوره", "مهران", "سارا", "علی")

آرگومان موقعیتی نخست به title اختصاص داده می‌شود و آرگومان‌های باقی‌مانده داخل names قرار می‌گیرند.

پارامترهای پس از *args فقط کلیدی هستند

پارامترهایی که پس از *args تعریف می‌شوند، باید با نام ارسال شوند:

def calculate_total(*prices, discount=0):
    total = sum(prices)
    return total - (total * discount / 100)

print(calculate_total(100000, 250000, discount=10))

در این مثال، discount یک پارامتر فقط کلیدی یا Keyword-only است.

چرا آرگومان کلیدی پس از *args لازم است؟

اگر مقدار تخفیف را بدون نام ارسال کنیم، پایتون آن را یکی از قیمت‌ها در نظر می‌گیرد:

calculate_total(100000, 250000, 10)

در این فراخوانی هر سه عدد داخل prices قرار می‌گیرند و مقدار پیش‌فرض تخفیف استفاده می‌شود. بنابراین برای پارامترهای پس از *args باید نام آرگومان نوشته شود.

بازکردن لیست و تاپل هنگام فراخوانی با *

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

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

numbers = [10, 20, 30]
print(add(*numbers))

این فراخوانی معادل کد زیر است:

add(10, 20, 30)

تعداد عناصر داده بازشده باید هماهنگ باشد

def create_point(x, y):
    return (x, y)

values = [10, 20, 30]
create_point(*values)

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

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

def show_values(a, b, c, d):
    print(a, b, c, d)

more_values = [30, 40]
show_values(10, 20, *more_values)

**kwargs در پایتون چیست؟

**kwargs تعداد نامشخصی از آرگومان‌های کلیدی را دریافت می‌کند و آن‌ها را در یک دیکشنری قرار می‌دهد:

def show_profile(**kwargs):
    print(kwargs)
    print(type(kwargs))

show_profile(name="مهران", city="استانبول", active=True)

خروجی:

{'name': 'مهران', 'city': 'استانبول', 'active': True}
<class 'dict'>

نام kwargs نیز اجباری نیست

مانند args، نام kwargs قراردادی است. عامل اصلی دو ستاره است:

def create_settings(**options):
    return options

پیمایش **kwargs

برای دریافت هم‌زمان کلید و مقدار از متد items() استفاده می‌کنیم:

def show_details(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

show_details(name="سارا", age=27, city="تهران")

دسترسی امن به مقدارهای kwargs

def create_user(**kwargs):
    name = kwargs.get("name", "بدون نام")
    city = kwargs.get("city", "ثبت نشده")
    active = kwargs.get("active", True)

    return {
        "name": name,
        "city": city,
        "active": active
    }

print(create_user(name="مهران", city="استانبول"))

استفاده از get() مانع ایجاد KeyError برای گزینه‌های اختیاری می‌شود.

فراخوانی **kwargs بدون آرگومان

اگر هیچ آرگومان کلیدی ارسال نشود، دیکشنری خالی دریافت می‌شود:

def count_options(**kwargs):
    return len(kwargs)

print(count_options())

مثال: ساخت محصول با تنظیمات اختیاری

def create_product(name, price, **options):
    product = {
        "name": name,
        "price": price
    }

    product.update(options)
    return product

product = create_product(
    "هدفون",
    850000,
    color="مشکی",
    in_stock=True,
    warranty=12
)

print(product)

نام و قیمت الزامی هستند و بقیه ویژگی‌ها به‌صورت انعطاف‌پذیر دریافت می‌شوند.

بازکردن دیکشنری هنگام فراخوانی با **

دو ستاره هنگام فراخوانی، جفت‌های کلید–مقدار یک دیکشنری را به آرگومان‌های کلیدی تبدیل می‌کند:

def introduce(name, city, age):
    return f"{name}، {age} ساله، ساکن {city}"

user = {
    "name": "مهران",
    "city": "استانبول",
    "age": 30
}

print(introduce(**user))

کلیدهای دیکشنری باید با نام پارامترهای تابع هماهنگ باشند.

خطای کلید اضافی هنگام بازکردن دیکشنری

def introduce(name, city):
    return f"{name} - {city}"

user = {
    "name": "مهران",
    "city": "استانبول",
    "age": 30
}

introduce(**user)

چون تابع پارامتری به نام age ندارد، خطای زیر ایجاد می‌شود:

TypeError: introduce() got an unexpected keyword argument 'age'

حل کلیدهای اضافی با **kwargs

def introduce(name, city, **extra):
    print(f"{name} - {city}")
    print("اطلاعات اضافی:", extra)

introduce(**user)

ترکیب *args و **kwargs در یک تابع

def inspect_inputs(*args, **kwargs):
    print("آرگومان‌های موقعیتی:", args)
    print("آرگومان‌های کلیدی:", kwargs)

inspect_inputs(10, 20, 30, name="مهران", active=True)

خروجی:

آرگومان‌های موقعیتی: (10, 20, 30)
آرگومان‌های کلیدی: {'name': 'مهران', 'active': True}

ترتیب استاندارد پارامترها در تعریف تابع

ترتیب کامل پارامترها معمولاً به شکل زیر است:

def example(required, default=0, *args, keyword_only=True, **kwargs):
    pass
  1. پارامترهای موقعیتی یا کلیدی الزامی
  2. پارامترهای دارای مقدار پیش‌فرض
  3. *args
  4. پارامترهای فقط کلیدی
  5. **kwargs

نمونه کامل ترتیب پارامترها

def create_order(
    customer,
    currency="TRY",
    *items,
    discount=0,
    send_email=True,
    **metadata
):
    subtotal = sum(items)
    total = subtotal - (subtotal * discount / 100)

    return {
        "customer": customer,
        "currency": currency,
        "items": items,
        "subtotal": subtotal,
        "discount": discount,
        "total": total,
        "send_email": send_email,
        "metadata": metadata
    }

order = create_order(
    "مهران",
    "TRY",
    250,
    400,
    150,
    discount=10,
    send_email=False,
    source="website",
    campaign="summer"
)

print(order)

ترکیب * و ** هنگام فراخوانی

def register_user(name, age, city="نامشخص", active=True):
    return {
        "name": name,
        "age": age,
        "city": city,
        "active": active
    }

main_data = ["مهران", 30]
options = {
    "city": "استانبول",
    "active": True
}

user = register_user(*main_data, **options)
print(user)

ارسال دوباره مقدار برای یک پارامتر

نباید یک پارامتر را هم مستقیم و هم از دیکشنری مقداردهی کنیم:

def greet(name):
    return f"سلام {name}"

options = {"name": "سارا"}
greet("مهران", **options)

این کد خطای زیر را ایجاد می‌کند:

TypeError: greet() got multiple values for argument 'name'

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

  • در تعریف تابع، *args چند آرگومان را داخل یک تاپل جمع‌آوری می‌کند.
  • در فراخوانی تابع، *data عناصر یک داده قابل پیمایش را باز می‌کند.
  • در تعریف تابع، **kwargs آرگومان‌های کلیدی را داخل یک دیکشنری جمع‌آوری می‌کند.
  • در فراخوانی تابع، **data جفت‌های دیکشنری را به آرگومان‌های کلیدی تبدیل می‌کند.

آیا همیشه باید از *args و **kwargs استفاده کنیم؟

خیر. اگر ورودی‌های تابع مشخص‌اند، پارامترهای نام‌گذاری‌شده معمولاً خواناتر هستند:

def calculate_rectangle_area(width, height):
    return width * height

نسخه زیر انعطاف‌پذیر است، اما هدف تابع را مبهم می‌کند:

def calculate_rectangle_area(*args):
    return args[0] * args[1]

*args و **kwargs زمانی مناسب‌اند که تعداد ورودی‌ها واقعاً متغیر باشد یا تابع نقش واسط عمومی داشته باشد.

کاربرد در توابع واسط یا Wrapper

یکی از کاربردهای حرفه‌ای، دریافت آرگومان‌ها و انتقال آن‌ها به تابع دیگر است:

def log_call(function, *args, **kwargs):
    print("در حال اجرای تابع:", function.__name__)
    result = function(*args, **kwargs)
    print("نتیجه:", result)
    return result


def calculate_price(price, quantity=1, discount=0):
    total = price * quantity
    return total - (total * discount / 100)

log_call(
    calculate_price,
    500,
    quantity=3,
    discount=10
)

کاربرد در توابع داخلی و کتابخانه‌ها

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

اعتبارسنجی گزینه‌های kwargs

پذیرفتن هر کلید بدون بررسی ممکن است خطای تایپی را پنهان کند. بهتر است کلیدهای مجاز مشخص باشند:

def create_report(**options):
    allowed_options = {
        "title",
        "show_date",
        "show_total"
    }

    unknown_options = set(options) - allowed_options

    if unknown_options:
        unknown_text = ", ".join(unknown_options)
        raise ValueError(f"گزینه نامعتبر: {unknown_text}")

    return {
        "title": options.get("title", "گزارش"),
        "show_date": options.get("show_date", True),
        "show_total": options.get("show_total", True)
    }

Type Hint برای *args و **kwargs

می‌توان نوع عناصر *args و مقدارهای **kwargs را مشخص کرد:

def add_numbers(*numbers: float) -> float:
    return sum(numbers)


def show_options(**options: str) -> None:
    for key, value in options.items():
        print(key, value)

در مثال دوم انتظار می‌رود تمام مقدارهای دیکشنری رشته باشند. Type Hint به خوانایی و ابزارهای تحلیل کد کمک می‌کند، اما به‌صورت خودکار مانع ارسال نوع دیگر نمی‌شود.

خطاهای رایج در *args و **kwargs

۱. فراموش‌کردن ستاره

def show_items(args):
    print(args)

show_items(1, 2, 3)

این تابع فقط یک آرگومان می‌پذیرد. برای دریافت چند مقدار باید بنویسیم *args.

۲. تصور اینکه args یک لیست است

args تاپل است و متد append() ندارد:

def add_item(*args):
    args.append("new")

برای تغییر باید آن را به لیست تبدیل کرد:

items = list(args)

۳. استفاده از اندیس بدون بررسی

def show_first(*args):
    return args[0]

فراخوانی بدون آرگومان باعث IndexError می‌شود. ابتدا خالی‌بودن تاپل را بررسی کنید.

۴. دسترسی مستقیم به کلید اختیاری

def show_name(**kwargs):
    return kwargs["name"]

اگر name ارسال نشود، KeyError ایجاد می‌شود. برای گزینه اختیاری از get() استفاده کنید.

۵. ترتیب نادرست پارامترها

**kwargs باید در انتهای فهرست پارامترها باشد:

def invalid(**kwargs, value):
    pass

این تعریف از نظر نحوی نادرست است.

۶. استفاده بی‌دلیل و کاهش خوانایی

تابعی که فقط با *args و **kwargs تعریف شده، ممکن است مشخص نکند چه ورودی‌هایی لازم‌اند. پارامترهای اصلی را صریح تعریف کنید و فقط بخش متغیر را با args یا kwargs دریافت کنید.

پروژه اول: گزارش نمرات با *args

در این پروژه، نام دانش‌آموز الزامی است و تعداد نمرات می‌تواند متغیر باشد:

def create_grade_report(student_name, *scores):
    if not scores:
        return {
            "student": student_name,
            "count": 0,
            "average": None,
            "highest": None,
            "lowest": None,
            "status": "بدون نمره"
        }

    for score in scores:
        if not isinstance(score, (int, float)):
            raise TypeError("تمام نمرات باید عدد باشند")

        if score < 0 or score > 20:
            raise ValueError("هر نمره باید بین صفر تا بیست باشد")

    average = sum(scores) / len(scores)
    status = "قبول" if average >= 10 else "مردود"

    return {
        "student": student_name,
        "count": len(scores),
        "average": round(average, 2),
        "highest": max(scores),
        "lowest": min(scores),
        "status": status
    }

report = create_grade_report(
    "مهران",
    18,
    16.5,
    20,
    14
)

for key, value in report.items():
    print(f"{key}: {value}")

پروژه دوم: سیستم ساخت سفارش با args و kwargs

در این پروژه قیمت کالاها به‌صورت موقعیتی و تنظیمات سفارش به‌صورت کلیدی دریافت می‌شوند:

def create_order(customer, *prices, **options):
    if not customer.strip():
        raise ValueError("نام مشتری نمی‌تواند خالی باشد")

    if not prices:
        raise ValueError("حداقل یک قیمت باید ارسال شود")

    for price in prices:
        if not isinstance(price, (int, float)):
            raise TypeError("تمام قیمت‌ها باید عدد باشند")

        if price < 0:
            raise ValueError("قیمت نمی‌تواند منفی باشد")

    allowed_options = {
        "discount",
        "tax",
        "shipping",
        "currency",
        "paid"
    }

    unknown_options = set(options) - allowed_options

    if unknown_options:
        invalid = ", ".join(unknown_options)
        raise ValueError(f"گزینه نامعتبر: {invalid}")

    discount = options.get("discount", 0)
    tax = options.get("tax", 0)
    shipping = options.get("shipping", 0)
    currency = options.get("currency", "TRY")
    paid = options.get("paid", False)

    if not 0 <= discount <= 100:
        raise ValueError("تخفیف باید بین صفر تا صد باشد")

    if tax < 0 or shipping < 0:
        raise ValueError("مالیات و هزینه ارسال نمی‌توانند منفی باشند")

    subtotal = sum(prices)
    discounted_total = subtotal - (subtotal * discount / 100)
    tax_amount = discounted_total * tax / 100
    final_total = discounted_total + tax_amount + shipping

    return {
        "customer": customer,
        "items_count": len(prices),
        "subtotal": subtotal,
        "discount": discount,
        "tax": tax,
        "shipping": shipping,
        "final_total": round(final_total, 2),
        "currency": currency,
        "paid": paid
    }

order = create_order(
    "مهران",
    350,
    480,
    120,
    discount=10,
    tax=20,
    shipping=50,
    currency="TRY",
    paid=True
)

for key, value in order.items():
    print(f"{key}: {value}")

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

  1. تابعی با *args بسازید که بزرگ‌ترین و کوچک‌ترین عدد را برگرداند.
  2. تابعی بنویسید که فقط رشته‌های ارسال‌شده در *args را به یک متن تبدیل کند.
  3. تابعی با **kwargs بسازید که یک پروفایل کاربر تولید کند.
  4. تابعی بنویسید که کلیدهای ناشناخته در kwargs را گزارش دهد.
  5. یک لیست سه‌عنصری را با * به تابعی با سه پارامتر ارسال کنید.
  6. یک دیکشنری را با ** به تابعی با آرگومان‌های کلیدی ارسال کنید.
  7. تابعی بسازید که یک عنوان الزامی، چند مقدار موقعیتی و چند گزینه کلیدی دریافت کند.
  8. تابعی برای محاسبه فاکتور چند کالا با تخفیف و مالیات اختیاری بنویسید.
  9. تابعی طراحی کنید که تابع دیگری را همراه با تمام args و kwargs اجرا کند.
  10. پروژه سفارش را توسعه دهید تا کد تخفیف و روش ارسال را نیز مدیریت کند.

📌 همچنین بخوانید: بازار کار پایتون در ایران، دستورهای break، continue و pass در پایتون و ساخت ربات تلگرام با پایتون

پرسش‌های متداول

آیا نام args و kwargs اجباری است؟

خیر. ستاره‌ها رفتار را تعیین می‌کنند. با این حال نام‌های args و kwargs قراردادهای شناخته‌شده و خوانا هستند.

*args چه نوع داده‌ای می‌سازد؟

آرگومان‌های موقعیتی اضافی را داخل یک تاپل قرار می‌دهد.

**kwargs چه نوع داده‌ای می‌سازد؟

آرگومان‌های کلیدی اضافی را داخل یک دیکشنری قرار می‌دهد.

آیا می‌توان *args و **kwargs را هم‌زمان استفاده کرد؟

بله. ترتیب معمول این است که *args پیش از پارامترهای فقط کلیدی و **kwargs در انتها قرار گیرد.

تفاوت ستاره در تعریف و فراخوانی چیست؟

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

چه زمانی نباید از kwargs استفاده کنیم؟

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

جمع‌بندی

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

استفاده درست از این قابلیت‌ها به ساخت توابع عمومی و انعطاف‌پذیر کمک می‌کند؛ اما استفاده بی‌دلیل می‌تواند رابط تابع را مبهم کند. پارامترهای اصلی را صریح تعریف کنید، گزینه‌های اختیاری را اعتبارسنجی کنید و از args و kwargs فقط زمانی استفاده کنید که تعداد ورودی‌ها واقعاً متغیر است.

درس بعدی: توابع Lambda در پایتون — در حال آماده‌سازی

ادامه مسیر یادگیری پایتون

برای یادگیری منظم، آموزش‌های مرتبط زیر را به‌ترتیب دنبال کنید:

می‌خواهی پیشرفتت ذخیره شود؟با ورود یا ثبت‌نام رایگان، درس‌های تمام‌شده را علامت بزن و درصد پیشرفتت را در حساب کاربری ببین.ورود / ثبت‌نام برای ذخیره پیشرفت
درس بعدی · دوره پایتون متوسط (8 از 18)توابع Lambda در پایتون؛ آموزش توابع ناشناس با مثال و پروژه ←
آموزش‌های تازه، کد آماده و رفع خطاهر هفته در کانال Dev Source؛ رایگان.
🧭 مسیر یادگیری پایتون از صفر
ماژول 6: توابع — درس 27 از 44

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

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *