본문으로 건너뛰기
Advertisement

5.2 매개변수 심화 — 기본값, 키워드 인수, *args, **kwargs

Python의 함수 매개변수 시스템은 매우 유연합니다. 위치 인수, 키워드 인수, 가변 인수, 기본값까지 다양한 방식으로 인자를 전달할 수 있습니다. 이를 제대로 이해하면 유연하고 강력한 API를 설계할 수 있습니다.


위치 인수(Positional Argument)

# 기본: 정의된 순서대로 값을 전달
def describe_point(x: float, y: float, label: str) -> str:
return f"점 {label}: ({x}, {y})"

# 위치 인수로 호출
print(describe_point(3.0, 4.0, "A")) # 점 A: (3.0, 4.0)

# 순서를 바꾸면 다른 결과
print(describe_point(4.0, 3.0, "B")) # 점 B: (4.0, 3.0)


# 위치 인수 → 키워드 인수로 호출 가능 (가독성 향상)
print(describe_point(x=3.0, y=4.0, label="A")) # 동일 결과
print(describe_point(label="C", x=1.0, y=2.0)) # 순서 무관

기본값 매개변수

# 기본값 있는 매개변수는 뒤에 와야 함
def connect(host: str, port: int = 80, timeout: float = 30.0) -> str:
return f"연결: {host}:{port} (timeout={timeout}s)"

print(connect("example.com")) # port=80, timeout=30.0 사용
print(connect("example.com", 443)) # port=443, timeout=30.0
print(connect("example.com", 443, 10.0)) # 모두 지정


# 주의: 가변 객체를 기본값으로 사용하면 버그!
# 나쁜 예
def append_bad(item, lst=[]): # lst는 함수 정의 시 한 번만 생성됨
lst.append(item)
return lst

print(append_bad(1)) # [1]
print(append_bad(2)) # [1, 2] — 이전 호출의 리스트가 재사용됨!
print(append_bad(3)) # [1, 2, 3]


# 올바른 방법: None을 기본값으로, 내부에서 새 객체 생성
def append_good(item: int, lst: list[int] | None = None) -> list[int]:
if lst is None:
lst = []
lst.append(item)
return lst

print(append_good(1)) # [1]
print(append_good(2)) # [2] — 매번 새 리스트
print(append_good(3, [10, 20])) # [10, 20, 3]


# datetime도 같은 주의 필요
from datetime import datetime

# 나쁜 예
def log_bad(msg: str, timestamp: datetime = datetime.now()):
print(f"[{timestamp}] {msg}") # timestamp가 함수 정의 시 고정됨!

# 좋은 예
def log_good(msg: str, timestamp: datetime | None = None) -> None:
if timestamp is None:
timestamp = datetime.now()
print(f"[{timestamp.strftime('%H:%M:%S')}] {msg}")

키워드 인수(Keyword Argument)

# 이름을 명시하여 인자 전달 — 순서 무관, 가독성 향상
def create_profile(
name: str,
age: int,
email: str,
city: str = "Seoul",
is_premium: bool = False
) -> dict:
return {
"name": name,
"age": age,
"email": email,
"city": city,
"is_premium": is_premium,
}

# 키워드 인수로 명확하게 전달
profile = create_profile(
name="Alice",
age=30,
email="alice@example.com",
is_premium=True, # city는 기본값 사용
)
print(profile)

위치 전용 매개변수: / (Python 3.8+)

/ 앞의 매개변수는 반드시 위치 인수로만 전달해야 합니다.

# / 앞은 위치 전용
def power(base, exponent, /):
return base ** exponent

print(power(2, 3)) # OK: 8
# print(power(base=2, exponent=3)) # TypeError: 키워드 인수 불가

# 실제 예시: 내장 함수 len()도 위치 전용
# len(obj=["a"]) → TypeError


# 위치 전용 + 일반 + 키워드 전용 혼합
def complex_func(pos_only, /, normal, *, kw_only):
return f"pos={pos_only}, normal={normal}, kw={kw_only}"

print(complex_func(1, 2, kw_only=3)) # OK
print(complex_func(1, normal=2, kw_only=3)) # OK
# complex_func(pos_only=1, normal=2, kw_only=3) # TypeError!

키워드 전용 매개변수: *

* 뒤의 매개변수는 반드시 키워드 인수로 전달해야 합니다.

# * 뒤는 키워드 전용
def send_email(to: str, subject: str, *, body: str, html: bool = False) -> None:
format_str = "HTML" if html else "텍스트"
print(f"To: {to}, Subject: {subject} [{format_str}]")
print(f"Body: {body[:50]}...")

# body는 반드시 키워드 인수로
send_email("alice@ex.com", "안녕하세요", body="반갑습니다!", html=False)
# send_email("alice@ex.com", "제목", "본문") # TypeError: body는 키워드 전용


# 키워드 전용으로 실수 방지
def delete_records(table: str, *, confirm: bool) -> str:
"""confirm=True 없이는 실행 불가"""
if not confirm:
return "취소됨"
return f"{table} 테이블 삭제 완료"

# delete_records("users", True) # TypeError — confirm은 키워드 전용
result = delete_records("users", confirm=True)
print(result)

*args: 가변 위치 인수

# *args: 추가 위치 인수를 튜플로 수집
def sum_all(*args: int | float) -> float:
return sum(args)

print(sum_all(1, 2, 3)) # 6
print(sum_all(1, 2, 3, 4, 5)) # 15
print(sum_all()) # 0


def log_messages(level: str, *messages: str) -> None:
for msg in messages:
print(f"[{level}] {msg}")

log_messages("INFO", "서버 시작", "포트 8080 바인딩", "준비 완료")
# [INFO] 서버 시작
# [INFO] 포트 8080 바인딩
# [INFO] 준비 완료


# args는 튜플
def inspect_args(*args):
print(f"타입: {type(args)}, 값: {args}")

inspect_args(1, "hello", True)
# 타입: <class 'tuple'>, 값: (1, 'hello', True)


# 기존 리스트를 *로 언패킹하여 전달
numbers = [3, 1, 4, 1, 5, 9]
print(sum_all(*numbers)) # 23 — 리스트 언패킹

**kwargs: 가변 키워드 인수

# **kwargs: 추가 키워드 인수를 딕셔너리로 수집
def create_html_tag(tag: str, content: str, **attrs) -> str:
attr_str = " ".join(f'{k}="{v}"' for k, v in attrs.items())
if attr_str:
return f"<{tag} {attr_str}>{content}</{tag}>"
return f"<{tag}>{content}</{tag}>"

print(create_html_tag("a", "클릭", href="https://example.com", target="_blank"))
# <a href="https://example.com" target="_blank">클릭</a>

print(create_html_tag("p", "안녕하세요"))
# <p>안녕하세요</p>


# kwargs는 딕셔너리
def inspect_kwargs(**kwargs):
print(f"타입: {type(kwargs)}")
for key, value in kwargs.items():
print(f" {key} = {value}")

inspect_kwargs(name="Alice", age=30, city="Seoul")


# 기존 딕셔너리를 **로 언패킹하여 전달
options = {"color": "red", "size": "large", "bold": "true"}
print(create_html_tag("span", "텍스트", **options))

매개변수 순서 규칙

# Python 매개변수 순서: 위치전용 / 일반 / *args / 키워드전용 / **kwargs
def full_example(
pos_only, # 위치 전용 (/ 앞)
/,
regular, # 일반 (위치 or 키워드)
*args, # 가변 위치
keyword_only, # 키워드 전용 (* 뒤)
**kwargs # 가변 키워드
) -> dict:
return {
"pos_only": pos_only,
"regular": regular,
"args": args,
"keyword_only": keyword_only,
"kwargs": kwargs,
}

result = full_example(
1, # pos_only
2, # regular
3, 4, 5, # *args
keyword_only="kw", # keyword_only
extra1="a", # **kwargs
extra2="b" # **kwargs
)
print(result)
# {'pos_only': 1, 'regular': 2, 'args': (3, 4, 5),
# 'keyword_only': 'kw', 'kwargs': {'extra1': 'a', 'extra2': 'b'}}

실전 예제: 유연한 API 설계

from typing import Any


class QueryBuilder:
"""SQL 쿼리 빌더 (시뮬레이션)"""

def __init__(self, table: str):
self.table = table
self._conditions: list[str] = []
self._order: str | None = None
self._limit: int | None = None

def where(self, **conditions: Any) -> "QueryBuilder":
"""WHERE 조건 추가 — 키워드 인수로 조건 전달"""
for col, val in conditions.items():
self._conditions.append(f"{col} = '{val}'")
return self

def order_by(self, column: str, *, desc: bool = False) -> "QueryBuilder":
"""ORDER BY 절 — desc는 키워드 전용"""
direction = "DESC" if desc else "ASC"
self._order = f"ORDER BY {column} {direction}"
return self

def limit(self, n: int, /) -> "QueryBuilder":
"""LIMIT 절 — n은 위치 전용"""
self._limit = n
return self

def build(self) -> str:
query = f"SELECT * FROM {self.table}"
if self._conditions:
query += " WHERE " + " AND ".join(self._conditions)
if self._order:
query += " " + self._order
if self._limit:
query += f" LIMIT {self._limit}"
return query


# 유창한 API
query = (
QueryBuilder("users")
.where(city="Seoul", is_active=True)
.order_by("created_at", desc=True)
.limit(10)
.build()
)
print(query)
# SELECT * FROM users WHERE city = 'Seoul' AND is_active = 'True'
# ORDER BY created_at DESC LIMIT 10

고수 팁

1. * 없이 키워드 전용 강제

# 가변 인수가 필요 없지만 키워드 강제하고 싶을 때
def configure(*, host: str, port: int, debug: bool = False) -> dict:
return {"host": host, "port": port, "debug": debug}

# configure("localhost", 8080) # TypeError
config = configure(host="localhost", port=8080, debug=True)
print(config)

2. 함수 시그니처 검사

import inspect

def example(a: int, b: str = "hello", *args, kw_only: bool = False, **kwargs):
pass

sig = inspect.signature(example)
for name, param in sig.parameters.items():
kind = param.kind.name
default = param.default if param.default != inspect.Parameter.empty else "없음"
annotation = param.annotation if param.annotation != inspect.Parameter.empty else "없음"
print(f" {name}: kind={kind}, default={default}, type={annotation}")

3. 인수 전달 패턴: 함수 래핑

# 함수 래핑 시 *args, **kwargs로 모든 인수 투명하게 전달
import time
import functools

def timing_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs) # 원래 함수에 모든 인수 그대로 전달
elapsed = time.perf_counter() - start
print(f"{func.__name__} 실행 시간: {elapsed:.4f}s")
return result
return wrapper

@timing_decorator
def slow_function(n: int, multiplier: int = 1) -> int:
total = sum(range(n)) * multiplier
return total

result = slow_function(1_000_000, multiplier=2)
print(f"결과: {result}")
Advertisement