포스트

[Python 100일 챌린지] Day 27 - 가변 인자 (*args, **kwargs)

[Python 100일 챌린지] Day 27 - 가변 인자 (*args, **kwargs)

print()는 왜 1개든 10개든 마음대로 넣을 수 있을까요? 🤔 ──바로 *args를 사용하기 때문입니다! 😊

지금까지 만든 함수는 매개변수 개수가 고정이었죠? add(a, b) 함수는 딱 2개만 받을 수 있습니다.

하지만 실전에서는 “몇 개가 들어올지 모르는” 상황이 많습니다. 여러 숫자의 평균 계산, 다양한 옵션의 설정 함수…

오늘 배울 *args**kwargs로 진짜 유연한 함수를 만듭니다! 💡

(30분 완독 ⭐⭐⭐)

🎯 오늘의 학습 목표

  1. *args로 가변 위치 인자 받기
  2. 일반 매개변수와 *args 혼합하기
  3. **kwargs로 가변 키워드 인자 받기
  4. 언패킹 연산자 (*와 **) 활용하기
  5. 복합 매개변수 순서 규칙 이해하기
  6. *args와 **kwargs 함께 사용하기

📚 사전 지식

🎁 가변 인자란?

한 줄 설명

가변 인자 = 개수가 정해지지 않은 인자를 받을 수 있는 특별한 매개변수

함수를 정의할 때 몇 개의 인자를 받을지 모를 때 사용합니다.

실생활 비유

1
2
3
4
5
6
7
8
9
10
📦 택배 상자:

일반 함수: 정확히 3개만 넣을 수 있는 상자
         box(item1, item2, item3)

*args: 개수 제한 없이 넣을 수 있는 상자
       box(item1, item2, ...)  # 몇 개든 OK!

**kwargs: 라벨을 붙여서 넣을 수 있는 상자
          box(fragile=item1, heavy=item2, ...)

🎯 학습 목표 1: *args로 가변 위치 인자 받기

기본 개념

*args는 여러 개의 위치 인자를 튜플로 받습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def print_args(*args):
    """전달받은 모든 인자를 출력"""
    print(f"받은 인자 개수: {len(args)}")
    print(f"타입: {type(args)}")
    print(f"내용: {args}")

print_args(1, 2, 3)
# 받은 인자 개수: 3
# 타입: <class 'tuple'>
# 내용: (1, 2, 3)

print_args("사과", "바나나", "포도", "딸기")
# 받은 인자 개수: 4
# 타입: <class 'tuple'>
# 내용: ('사과', '바나나', '포도', '딸기')

이름은 관례일 뿐

*argsargs는 단지 관례입니다. 다른 이름도 가능합니다.

1
2
3
4
5
6
7
def sum_numbers(*numbers):  # *numbers도 OK
    return sum(numbers)

def concat_strings(*strings):  # *strings도 OK
    return " ".join(strings)

# 하지만 *args가 가장 일반적이고 읽기 쉬움!

실전 활용 예제

예제 1: 합계 계산

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def calculate_sum(*numbers):
    """개수 제한 없이 숫자를 받아 합계 계산"""
    if not numbers:
        return 0

    total = sum(numbers)
    print(f"숫자 {len(numbers)}개의 합: {total}")
    return total

# 다양한 개수로 호출
calculate_sum(1, 2, 3)                    # 6
calculate_sum(10, 20, 30, 40, 50)         # 150
calculate_sum(1, 2, 3, 4, 5, 6, 7, 8, 9)  # 45
calculate_sum()                            # 0

예제 2: 최댓값/최솟값 찾기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def find_minmax(*numbers):
    """여러 숫자 중 최솟값과 최댓값을 반환"""
    if not numbers:
        return None, None

    minimum = min(numbers)
    maximum = max(numbers)

    return minimum, maximum

# 사용
min_val, max_val = find_minmax(5, 2, 8, 1, 9, 3)
print(f"최솟값: {min_val}, 최댓값: {max_val}")
# 최솟값: 1, 최댓값: 9

min_val, max_val = find_minmax(100, 50, 75, 200, 125)
print(f"최솟값: {min_val}, 최댓값: {max_val}")
# 최솟값: 50, 최댓값: 200

예제 3: 문자열 연결

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def create_sentence(*words, separator=" "):
    """여러 단어를 받아 문장 생성 (구분자 선택 가능)"""
    if not words:
        return ""

    sentence = separator.join(words)
    return sentence

# 사용
print(create_sentence("Python", "is", "awesome"))
# Python is awesome

print(create_sentence("사과", "바나나", "포도", separator=", "))
# 사과, 바나나, 포도

print(create_sentence("HTML", "CSS", "JavaScript", separator=" + "))
# HTML + CSS + JavaScript

🎯 학습 목표 2: 일반 매개변수와 *args 혼합하기

위치 규칙

일반 매개변수는 *args 앞에 와야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def greet(greeting, *names):
    """인사말과 여러 이름을 받아 인사"""
    for name in names:
        print(f"{greeting}, {name}!")

# 사용
greet("안녕하세요", "홍길동", "김철수", "이영희")
# 안녕하세요, 홍길동!
# 안녕하세요, 김철수!
# 안녕하세요, 이영희!

greet("Hello", "John", "Jane")
# Hello, John!
# Hello, Jane!

필수 + 가변 인자 조합

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def calculate(operation, *numbers):
    """연산 종류와 숫자들을 받아 계산"""
    if not numbers:
        return 0

    if operation == "sum":
        return sum(numbers)
    elif operation == "avg":
        return sum(numbers) / len(numbers)
    elif operation == "max":
        return max(numbers)
    elif operation == "min":
        return min(numbers)
    else:
        return "지원하지 않는 연산"

# 사용
print(calculate("sum", 1, 2, 3, 4, 5))      # 15
print(calculate("avg", 10, 20, 30, 40))     # 25.0
print(calculate("max", 5, 15, 3, 20, 8))    # 20
print(calculate("min", 5, 15, 3, 20, 8))    # 3

🎯 학습 목표 3: **kwargs로 가변 키워드 인자 받기

기본 개념

**kwargs는 여러 개의 키워드 인자를 딕셔너리로 받습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def print_kwargs(**kwargs):
    """전달받은 모든 키워드 인자를 출력"""
    print(f"받은 인자 개수: {len(kwargs)}")
    print(f"타입: {type(kwargs)}")
    print(f"내용: {kwargs}")
    print()

print_kwargs(name="홍길동", age=25, city="서울")
# 받은 인자 개수: 3
# 타입: <class 'dict'>
# 내용: {'name': '홍길동', 'age': 25, 'city': '서울'}

print_kwargs(language="Python", version=3.12, year=2024)
# 받은 인자 개수: 3
# 타입: <class 'dict'>
# 내용: {'language': 'Python', 'version': 3.12, 'year': 2024}

kwargs도 관례

**kwargskwargs도 관례입니다. 다른 이름도 가능합니다.

1
2
3
4
5
def print_info(**details):  # **details도 OK
    for key, value in details.items():
        print(f"{key}: {value}")

# 하지만 **kwargs가 가장 일반적!

실전 활용 예제

예제 1: 사용자 프로필 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def create_profile(name, **details):
    """이름과 추가 정보로 프로필 생성"""
    profile = {"이름": name}

    # kwargs의 모든 항목을 프로필에 추가
    for key, value in details.items():
        profile[key] = value

    return profile

# 사용
profile1 = create_profile("홍길동", 나이=25, 직업="개발자", 도시="서울")
print(profile1)
# {'이름': '홍길동', '나이': 25, '직업': '개발자', '도시': '서울'}

profile2 = create_profile("김철수", 나이=30, 취미="독서")
print(profile2)
# {'이름': '김철수', '나이': 30, '취미': '독서'}

예제 2: 설정 관리자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def configure_app(**settings):
    """앱 설정을 관리하는 함수"""
    default_settings = {
        "theme": "light",
        "language": "ko",
        "notifications": True,
        "auto_save": False
    }

    # 전달받은 설정으로 기본값 업데이트
    default_settings.update(settings)

    print("=== 앱 설정 ===")
    for key, value in default_settings.items():
        print(f"{key}: {value}")

    return default_settings

# 사용
configure_app(theme="dark", auto_save=True)
# === 앱 설정 ===
# theme: dark
# language: ko
# notifications: True
# auto_save: True

configure_app(language="en", notifications=False)
# === 앱 설정 ===
# theme: light
# language: en
# notifications: False
# auto_save: False

예제 3: HTML 태그 생성기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def create_tag(tag_name, content="", **attributes):
    """HTML 태그를 생성하는 함수"""
    # 속성 문자열 생성
    attrs = []
    for key, value in attributes.items():
        attrs.append(f'{key}="{value}"')

    attrs_str = " " + " ".join(attrs) if attrs else ""

    # 태그 생성
    if content:
        return f"<{tag_name}{attrs_str}>{content}</{tag_name}>"
    else:
        return f"<{tag_name}{attrs_str} />"

# 사용
print(create_tag("p", "안녕하세요"))
# <p>안녕하세요</p>

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

print(create_tag("img", src="photo.jpg", alt="사진", width="400"))
# <img src="photo.jpg" alt="사진" width="400" />

print(create_tag("div", "내용", id="main", class_="container"))
# <div id="main" class_="container">내용</div>

🎯 학습 목표 4: 언패킹 연산자 (*와 **) 활용하기

리스트/튜플 언패킹 (*)

1
2
3
4
5
6
7
8
9
10
11
12
def sum_three(a, b, c):
    return a + b + c

# 리스트를 언패킹해서 전달
numbers = [1, 2, 3]
result = sum_three(*numbers)  # sum_three(1, 2, 3)과 동일
print(result)  # 6

# 튜플도 동일
numbers_tuple = (10, 20, 30)
result = sum_three(*numbers_tuple)
print(result)  # 60

딕셔너리 언패킹 (**)

1
2
3
4
5
6
7
8
9
10
11
12
def introduce(name, age, city):
    print(f"이름: {name}, 나이: {age}, 도시: {city}")

# 딕셔너리를 언패킹해서 전달
person = {
    "name": "홍길동",
    "age": 25,
    "city": "서울"
}

introduce(**person)  # introduce(name="홍길동", age=25, city="서울")과 동일
# 이름: 홍길동, 나이: 25, 도시: 서울

함수 호출 시 언패킹 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def print_info(name, *hobbies, **details):
    print(f"이름: {name}")
    print(f"취미: {', '.join(hobbies)}")
    for key, value in details.items():
        print(f"{key}: {value}")

# 언패킹을 사용한 호출
name = "홍길동"
hobbies = ["독서", "운동", "영화"]
details = {"age": 25, "city": "서울", "job": "개발자"}

print_info(name, *hobbies, **details)
# 이름: 홍길동
# 취미: 독서, 운동, 영화
# age: 25
# city: 서울
# job: 개발자

🎯 학습 목표 5: 복합 매개변수 순서 규칙 이해하기

매개변수 순서

중요: 매개변수 순서는 반드시 지켜야 합니다!

1
2
3
4
5
# ✅ 올바른 순서
def func(a, b, *args, c=0, d=0, **kwargs):
    pass

# 순서: 일반 매개변수 → *args → 기본값 매개변수 → **kwargs

실전 예제 1: 만능 로거

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def log(level, message, *tags, timestamp=True, **metadata):
    """다양한 정보를 포함하는 로그 함수"""
    from datetime import datetime

    # 로그 헤더
    if timestamp:
        time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{time_str}] [{level}]", end=" ")
    else:
        print(f"[{level}]", end=" ")

    # 메시지
    print(message, end="")

    # 태그
    if tags:
        print(f" | Tags: {', '.join(tags)}", end="")

    # 메타데이터
    if metadata:
        print(" | Metadata: {", end="")
        meta_items = [f"{k}={v}" for k, v in metadata.items()]
        print(", ".join(meta_items), end="")
        print("}", end="")

    print()  # 줄바꿈

# 사용
log("INFO", "서버 시작")
# [2024-03-27 10:30:00] [INFO] 서버 시작

log("WARNING", "메모리 부족", "system", "memory", threshold=80)
# [2024-03-27 10:30:01] [WARNING] 메모리 부족 | Tags: system, memory | Metadata: {threshold=80}

log("ERROR", "연결 실패", "network", "database", host="db.example.com", port=5432, timestamp=False)
# [ERROR] 연결 실패 | Tags: network, database | Metadata: {host=db.example.com, port=5432}

실전 예제 2: API 요청 래퍼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def api_request(method, endpoint, *params, timeout=30, **headers):
    """API 요청을 생성하는 함수 (시뮬레이션)"""
    print(f"=== API 요청 ===")
    print(f"메서드: {method}")
    print(f"엔드포인트: {endpoint}")
    print(f"타임아웃: {timeout}")

    if params:
        print(f"경로 파라미터: {params}")

    if headers:
        print("헤더:")
        for key, value in headers.items():
            print(f"  {key}: {value}")

    return {"status": 200, "message": "성공"}

# 사용
# 기본 GET 요청
api_request("GET", "/users")

# 특정 사용자 조회 (경로 파라미터)
api_request("GET", "/users", 123)

# 인증 헤더 포함
api_request("POST", "/users", Authorization="Bearer token123", Content_Type="application/json")

# 모든 옵션 포함
api_request("DELETE", "/users", 456, timeout=60, Authorization="Bearer token123", X_Request_ID="abc-123")

실전 예제 3: 데이터베이스 쿼리 빌더

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def build_query(table, *columns, limit=None, **conditions):
    """SQL SELECT 쿼리를 생성하는 함수"""
    # 컬럼 선택
    if columns:
        cols = ", ".join(columns)
    else:
        cols = "*"

    # 기본 쿼리
    query = f"SELECT {cols} FROM {table}"

    # WHERE 조건
    if conditions:
        where_clauses = []
        for key, value in conditions.items():
            if isinstance(value, str):
                where_clauses.append(f"{key} = '{value}'")
            else:
                where_clauses.append(f"{key} = {value}")
        query += " WHERE " + " AND ".join(where_clauses)

    # LIMIT
    if limit:
        query += f" LIMIT {limit}"

    return query

# 사용
print(build_query("users"))
# SELECT * FROM users

print(build_query("users", "id", "name", "email"))
# SELECT id, name, email FROM users

print(build_query("users", "name", "age", city="서울"))
# SELECT name, age FROM users WHERE city = '서울'

print(build_query("users", "id", "name", age=25, active=True, limit=10))
# SELECT id, name FROM users WHERE age = 25 AND active = True LIMIT 10

🎯 학습 목표 6: *args와 **kwargs 함께 사용하기

기본 결합 패턴

*args**kwargs를 함께 사용하면 매우 유연한 함수를 만들 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def flexible_function(*args, **kwargs):
    """위치 인자와 키워드 인자를 모두 받는 함수"""
    print(f"위치 인자 (args): {args}")
    print(f"키워드 인자 (kwargs): {kwargs}")

# 다양한 호출 방법
flexible_function(1, 2, 3)
# 위치 인자 (args): (1, 2, 3)
# 키워드 인자 (kwargs): {}

flexible_function(a=1, b=2, c=3)
# 위치 인자 (args): ()
# 키워드 인자 (kwargs): {'a': 1, 'b': 2, 'c': 3}

flexible_function(1, 2, x=10, y=20)
# 위치 인자 (args): (1, 2)
# 키워드 인자 (kwargs): {'x': 10, 'y': 20}

실전 예제: 로깅 시스템

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def log_message(level, *messages, **metadata):
    """로그 메시지를 기록하는 함수

    Args:
        level: 로그 레벨 (필수)
        *messages: 로그 메시지들 (가변)
        **metadata: 추가 메타데이터 (가변)
    """
    # 로그 레벨 출력
    print(f"[{level.upper()}]", end=" ")

    # 모든 메시지 결합
    full_message = " ".join(str(msg) for msg in messages)
    print(full_message)

    # 메타데이터가 있으면 출력
    if metadata:
        print(f"  메타데이터: {metadata}")

# 다양한 사용 예시
log_message("info", "서버 시작됨")
# [INFO] 서버 시작됨

log_message("error", "연결 실패:", "타임아웃", user="홍길동", attempt=3)
# [ERROR] 연결 실패: 타임아웃
#   메타데이터: {'user': '홍길동', 'attempt': 3}

log_message("warning", "메모리 사용량", "80%", threshold=90, action="모니터링")
# [WARNING] 메모리 사용량 80%
#   메타데이터: {'threshold': 90, 'action': '모니터링'}

실전 예제: 범용 API 호출 래퍼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def api_call(endpoint, *path_params, method="GET", **query_params):
    """API 호출을 위한 범용 래퍼 함수

    Args:
        endpoint: API 엔드포인트 (필수)
        *path_params: URL 경로 파라미터들
        method: HTTP 메서드 (기본값: GET)
        **query_params: 쿼리 파라미터들
    """
    # URL 구성
    url = f"https://api.example.com/{endpoint}"

    # 경로 파라미터 추가
    if path_params:
        url += "/" + "/".join(str(p) for p in path_params)

    # 쿼리 파라미터 추가
    if query_params:
        query_string = "&".join(f"{k}={v}" for k, v in query_params.items())
        url += f"?{query_string}"

    print(f"{method} {url}")
    return url

# 다양한 API 호출 패턴
api_call("users")
# GET https://api.example.com/users

api_call("users", 123)
# GET https://api.example.com/users/123

api_call("posts", 456, "comments", page=2, limit=10)
# GET https://api.example.com/posts/456/comments?page=2&limit=10

api_call("products", method="POST", category="books", sort="price")
# POST https://api.example.com/products?category=books&sort=price

데코레이터 패턴에서의 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def smart_cache(*args, **kwargs):
    """함수 호출 결과를 캐싱하는 데코레이터"""
    cache = {}

    def decorator(func):
        def wrapper(*func_args, **func_kwargs):
            # 캐시 키 생성
            cache_key = (func_args, tuple(sorted(func_kwargs.items())))

            # 캐시에 있으면 반환
            if cache_key in cache:
                print(f"캐시 사용: {func.__name__}{func_args}")
                return cache[cache_key]

            # 없으면 실행 후 캐시에 저장
            result = func(*func_args, **func_kwargs)
            cache[cache_key] = result
            print(f"캐시 저장: {func.__name__}{func_args}")
            return result

        return wrapper
    return decorator

@smart_cache()
def expensive_calculation(a, b, c=10):
    """시간이 오래 걸리는 계산"""
    print(f"계산 중... {a} + {b} + {c}")
    return a + b + c

# 사용
print(expensive_calculation(1, 2))
# 계산 중... 1 + 2 + 10
# 캐시 저장: expensive_calculation(1, 2)
# 13

print(expensive_calculation(1, 2))
# 캐시 사용: expensive_calculation(1, 2)
# 13

print(expensive_calculation(1, 2, c=20))
# 계산 중... 1 + 2 + 20
# 캐시 저장: expensive_calculation(1, 2)
# 23

💡 실전 팁 & 주의사항

💡 Tip 1: args는 관례일 뿐

*args**kwargs의 이름은 단지 관례입니다. *numbers, **options 등 의미 있는 이름을 사용해도 됩니다.

1
2
3
4
5
6
def sum_numbers(*numbers):  # *args보다 명확!
    return sum(numbers)

def configure(**options):  # **kwargs보다 명확!
    for key, value in options.items():
        print(f"{key}: {value}")

💡 Tip 2: 타입이 다릅니다

*args튜플, **kwargs딕셔너리로 받습니다.

1
2
3
4
5
def check_types(*args, **kwargs):
    print(f"args 타입: {type(args)}")      # <class 'tuple'>
    print(f"kwargs 타입: {type(kwargs)}")  # <class 'dict'>

check_types(1, 2, 3, a=4, b=5)

💡 Tip 3: 빈 인자도 가능

가변 인자는 0개의 인자도 받을 수 있습니다.

1
2
3
4
5
def flexible(*args, **kwargs):
    if not args and not kwargs:
        print("인자가 전달되지 않았습니다")

flexible()  # 정상 작동

💡 Tip 4: 언패킹으로 함수 호출 간소화

리스트나 딕셔너리를 언패킹하면 코드가 깔끔해집니다.

1
2
3
4
5
6
7
8
9
# 길고 복잡한 방식
print(numbers[0], numbers[1], numbers[2])

# 간결한 방식
print(*numbers)

# 딕셔너리도 마찬가지
config = {"host": "localhost", "port": 8080}
connect(**config)  # connect(host="localhost", port=8080)와 동일

💡 Tip 5: 문서화에 명시하기

가변 인자를 사용할 때는 독스트링에 명확히 설명하세요.

1
2
3
4
5
6
7
8
9
def create_user(username, *permissions, **metadata):
    """사용자를 생성합니다.

    Args:
        username (str): 사용자 이름 (필수)
        *permissions: 권한 목록 (가변, 예: 'read', 'write', 'admin')
        **metadata: 추가 사용자 정보 (가변, 예: email='...', age=25)
    """
    pass

💡 Tip 6: 데코레이터에서 필수

데코레이터를 만들 때 *args**kwargs는 거의 필수입니다.

1
2
3
4
5
6
def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"{func.__name__} 호출됨")
        result = func(*args, **kwargs)  # 원본 함수에 모든 인자 전달
        return result
    return wrapper

⚠️ 주의 1: 매개변수 순서는 엄격합니다

매개변수 순서를 지키지 않으면 SyntaxError가 발생합니다.

1
2
3
4
5
6
7
8
# ✅ 올바른 순서: 일반 → *args → 기본값 → **kwargs
def correct(a, b, *args, c=0, d=0, **kwargs):
    pass

# ❌ 잘못된 순서
# def wrong1(*args, a, b):      # *args 뒤에 필수 매개변수
# def wrong2(**kwargs, *args):  # **kwargs는 반드시 마지막
# def wrong3(a=1, b):           # 기본값 있는 것이 앞에

⚠️ 주의 2: args는 튜플이라 수정 불가

*args는 튜플이므로 요소를 직접 수정할 수 없습니다.

1
2
3
4
5
6
7
8
def modify_args(*args):
    # ❌ 에러 발생
    # args[0] = 100  # TypeError: 'tuple' object does not support item assignment

    # ✅ 리스트로 변환 후 수정
    args_list = list(args)
    args_list[0] = 100
    return args_list

⚠️ 주의 3: 언패킹 시 키 이름 일치 필수

** 언패킹 시 딕셔너리 키와 매개변수 이름이 정확히 일치해야 합니다.

1
2
3
4
5
6
7
8
9
10
def greet(name, age):
    print(f"{name}, {age}")

# ❌ 키가 일치하지 않으면 TypeError
person = {"username": "홍길동", "years": 25}
# greet(**person)  # TypeError!

# ✅ 키가 정확히 일치해야 함
person = {"name": "홍길동", "age": 25}
greet(**person)  # 정상 작동

⚠️ 주의 4: Python 예약어는 키워드 인자로 사용 불가

Python 예약어(class, def, return 등)는 키워드 인자 이름으로 사용할 수 없습니다.

1
2
3
4
5
6
7
8
9
# ❌ SyntaxError 발생
# create_user(class="student")

# ✅ 다른 이름 사용
create_user(user_class="student")

# ✅ 또는 딕셔너리로 전달 (하지만 함수 내부에서 처리 필요)
params = {"class": "student"}
# 함수 내부에서 params['class']로 접근

⚠️ 주의 5: 너무 많은 인자는 디버깅 어려움

*args**kwargs를 남용하면 코드 가독성과 디버깅이 어려워집니다.

1
2
3
4
5
6
7
8
9
10
11
# ❌ 너무 유연해서 오히려 문제
def process(*args, **kwargs):
    # 어떤 인자를 받는지 불명확
    pass

# ✅ 명시적인 매개변수 + 가변 인자 조합
def process(required_param, *optional_items, debug=False, **metadata):
    """필수 매개변수를 명시하고 가변 인자는 보조적으로 사용"""
    if len(optional_items) > 10:
        raise ValueError("선택 인자가 너무 많습니다")
    # 처리 로직

⚠️ 주의 6: 위치 인자와 키워드 인자 혼동 주의

호출 시 위치 인자는 키워드 인자보다 먼저 와야 합니다.

1
2
3
4
5
6
7
8
def func(*args, **kwargs):
    pass

# ✅ 올바른 호출
func(1, 2, 3, a=4, b=5)

# ❌ 잘못된 호출
# func(a=4, b=5, 1, 2, 3)  # SyntaxError: 위치 인자는 키워드 인자 앞에!

🧪 연습 문제

문제 1: 평균 계산기

다음 요구사항을 만족하는 calculate_average 함수를 작성하세요.

  • 개수 제한 없이 숫자를 받아 평균 계산
  • 소수점 둘째 자리까지 반올림
  • 숫자가 없으면 0 반환
💡 힌트

*args를 사용하고, len()sum()을 활용하세요.

1
2
3
4
def calculate_average(*numbers):
    if not numbers:
        return 0
    # 평균 계산 및 반올림
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
def calculate_average(*numbers):
    """개수 제한 없이 숫자를 받아 평균 계산"""
    if not numbers:
        return 0

    average = sum(numbers) / len(numbers)
    return round(average, 2)

# 테스트
print(calculate_average(10, 20, 30))           # 20.0
print(calculate_average(85, 90, 78, 92, 88))   # 86.6
print(calculate_average(100))                   # 100.0
print(calculate_average())                      # 0

문제 2: 딕셔너리 병합 함수

다음 요구사항을 만족하는 merge_dicts 함수를 작성하세요.

  • 여러 딕셔너리를 받아 하나로 병합
  • 같은 키가 있으면 나중 값으로 덮어쓰기
  • 딕셔너리가 없으면 빈 딕셔너리 반환
💡 힌트

**kwargs를 사용하지 않고, *dicts로 딕셔너리들을 받으세요.

1
2
3
4
5
def merge_dicts(*dicts):
    result = {}
    for d in dicts:
        result.update(d)
    return result
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def merge_dicts(*dicts):
    """여러 딕셔너리를 하나로 병합"""
    result = {}

    for dictionary in dicts:
        result.update(dictionary)

    return result

# 테스트
dict1 = {"a": 1, "b": 2}
dict2 = {"c": 3, "d": 4}
dict3 = {"a": 10, "e": 5}  # 'a'가 중복 (덮어쓰기됨)

merged = merge_dicts(dict1, dict2, dict3)
print(merged)
# {'a': 10, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

# 빈 경우
print(merge_dicts())  # {}

문제 3: 함수 데코레이터 시뮬레이션

다음 요구사항을 만족하는 call_function 함수를 작성하세요.

  • 함수와 그 함수의 인자들을 받음
  • 함수 호출 전후에 로그 출력
  • 함수 실행 시간 측정
  • 결과 반환
💡 힌트

func를 첫 번째 매개변수로, *args**kwargs로 나머지를 받으세요.

1
2
3
4
5
6
7
8
import time

def call_function(func, *args, **kwargs):
    start = time.time()
    result = func(*args, **kwargs)  # 함수 호출
    end = time.time()
    # 로그 출력
    return result
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import time

def call_function(func, *args, **kwargs):
    """함수를 호출하고 실행 시간을 측정"""
    print(f"=== 함수 '{func.__name__}' 호출 ===")
    print(f"위치 인자: {args}")
    print(f"키워드 인자: {kwargs}")

    start = time.time()
    result = func(*args, **kwargs)
    end = time.time()

    elapsed = (end - start) * 1000  # 밀리초로 변환
    print(f"실행 시간: {elapsed:.2f}ms")
    print(f"결과: {result}")
    print()

    return result

# 테스트용 함수들
def add(a, b):
    return a + b

def greet(name, greeting="안녕하세요"):
    return f"{greeting}, {name}님!"

def calculate_sum(*numbers):
    return sum(numbers)

# 테스트
call_function(add, 10, 20)
call_function(greet, "홍길동", greeting="Hello")
call_function(calculate_sum, 1, 2, 3, 4, 5)

출력:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
=== 함수 'add' 호출 ===
위치 인자: (10, 20)
키워드 인자: {}
실행 시간: 0.01ms
결과: 30

=== 함수 'greet' 호출 ===
위치 인자: ('홍길동',)
키워드 인자: {'greeting': 'Hello'}
실행 시간: 0.01ms
결과: Hello, 홍길동님!

=== 함수 'calculate_sum' 호출 ===
위치 인자: (1, 2, 3, 4, 5)
키워드 인자: {}
실행 시간: 0.02ms
결과: 15

📝 오늘 배운 내용 정리

  1. *args: 개수 제한 없는 위치 인자를 튜플로 받음
  2. **kwargs: 개수 제한 없는 키워드 인자를 딕셔너리로 받음
  3. 순서: 일반 → *args → 기본값 → **kwargs
  4. 언패킹: *로 리스트/튜플, **로 딕셔너리 언패킹
  5. 활용: 유연한 함수, 래퍼 함수, 설정 관리

🎯 실습 과제

과제: 만능 프린터 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def smart_print(*items, sep=" ", end="\n", prefix="", suffix="", **styles):
    """다양한 옵션으로 출력하는 함수"""
    # 스타일 적용
    styled_items = []

    for item in items:
        styled_item = str(item)

        # 스타일 옵션 적용
        if styles.get("upper"):
            styled_item = styled_item.upper()
        if styles.get("lower"):
            styled_item = styled_item.lower()
        if styles.get("bold"):
            styled_item = f"**{styled_item}**"
        if styles.get("italic"):
            styled_item = f"*{styled_item}*"

        styled_items.append(styled_item)

    # 출력 문자열 구성
    output = prefix + sep.join(styled_items) + suffix

    print(output, end=end)

# 테스트
smart_print("Hello", "World")
# Hello World

smart_print("Python", "is", "awesome", sep=" | ")
# Python | is | awesome

smart_print("Important", prefix=">>> ", suffix=" <<<")
# >>> Important <<<

smart_print("error", "warning", upper=True, bold=True)
# **ERROR** **WARNING**

smart_print("Title", "SubTitle", sep=": ", upper=True, end="\n\n")
# TITLE: SUBTITLE

smart_print("Item1", "Item2", "Item3", sep=", ", prefix="[ ", suffix=" ]", lower=True)
# [ item1, item2, item3 ]

❓ FAQ

Q1. *args**kwargs는 언제 사용하나요?

A: 함수가 받을 인자의 개수를 미리 알 수 없을 때 사용합니다.

1
2
3
4
5
6
7
8
# 예시: 로깅 함수는 메시지 개수를 모름
def log(*messages, level="INFO"):
    print(f"[{level}]", *messages)

# 예시: 설정 함수는 옵션 종류를 모름
def configure(**options):
    for key, value in options.items():
        print(f"{key} = {value}")

사용 시기:

  • 유틸리티 함수 (예: print, log)
  • 래퍼 함수 (다른 함수를 감싸는 함수)
  • 데코레이터
  • API 클라이언트 (동적 파라미터)

Q2. *args**kwargs를 항상 함께 써야 하나요?

A: 아니요, 필요에 따라 하나만 사용하거나 둘 다 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
# *args만 사용
def sum_all(*numbers):
    return sum(numbers)

# **kwargs만 사용
def create_config(**settings):
    return settings

# 둘 다 사용
def flexible_func(*args, **kwargs):
    print(f"위치: {args}, 키워드: {kwargs}")

선택 기준:

  • 위치 인자만 필요 → *args
  • 키워드 인자만 필요 → **kwargs
  • 둘 다 필요 → 함께 사용

Q3. args와 kwargs는 다른 이름으로 바꿔도 되나요?

A: 네, ***만 있으면 이름은 자유롭게 변경 가능합니다.

1
2
3
4
5
6
7
8
9
10
# 의미 있는 이름 사용 (권장)
def calculate_total(*prices):
    return sum(prices)

def create_user(name, **user_info):
    return {"name": name, **user_info}

# args/kwargs 사용 (관례)
def generic_func(*args, **kwargs):
    pass

권장 사항:

  • 의미가 명확하면 구체적인 이름 사용
  • 범용적인 경우 args/kwargs 사용
  • 일관성 유지가 중요

Q4. 매개변수 순서를 왜 지켜야 하나요?

A: Python 문법 규칙이며, 순서를 지키지 않으면 SyntaxError가 발생합니다.

1
2
3
4
5
6
7
8
# ✅ 올바른 순서
def func(a, b, *args, c=0, **kwargs):
    pass

# ❌ 잘못된 순서들
# def wrong1(*args, a):        # *args 뒤에 필수 매개변수
# def wrong2(**kwargs, a=0):   # **kwargs는 마지막이어야 함
# def wrong3(a=0, b):          # 기본값 매개변수가 필수보다 앞

올바른 순서: 필수 매개변수 → *args → 기본값 매개변수 → **kwargs

Q5. ***로 언패킹할 때 주의할 점은?

A: 언패킹 시 타입과 개수, 키 이름을 확인해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def greet(name, age, city):
    print(f"{name}, {age}세, {city}")

# ✅ 리스트 언패킹 (개수 일치)
data = ["홍길동", 25, "서울"]
greet(*data)

# ❌ 개수 불일치
# data = ["홍길동", 25]
# greet(*data)  # TypeError: 인자 1개 부족

# ✅ 딕셔너리 언패킹 (키 이름 일치)
info = {"name": "홍길동", "age": 25, "city": "서울"}
greet(**info)

# ❌ 키 이름 불일치
# info = {"username": "홍길동", "age": 25, "city": "서울"}
# greet(**info)  # TypeError: name이 없음

Q6. *args는 튜플인데 왜 수정이 안 되나요?

A: 튜플은 불변(immutable) 자료형이기 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
def modify_args(*args):
    # ❌ 직접 수정 불가
    # args[0] = 100  # TypeError

    # ✅ 리스트로 변환 후 수정
    args_list = list(args)
    args_list[0] = 100
    return args_list

result = modify_args(1, 2, 3)
print(result)  # [100, 2, 3]

이유: 튜플은 함수 내부에서 실수로 수정되는 것을 방지하기 위해 불변입니다.

Q7. 데코레이터에서 *args**kwargs가 필수인가요?

A: 범용 데코레이터를 만들려면 필수입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ✅ 범용 데코레이터 (모든 함수에 적용 가능)
def timer_decorator(func):
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = func(*args, **kwargs)  # 모든 인자 전달
        end = time.time()
        print(f"{func.__name__} 실행 시간: {end - start:.4f}")
        return result
    return wrapper

@timer_decorator
def calculate(a, b, c=10):
    return a + b + c

@timer_decorator
def greet(name):
    print(f"Hello, {name}!")

# 둘 다 정상 작동
calculate(1, 2)
greet("홍길동")

이유: 데코레이터는 어떤 함수든 감쌀 수 있어야 하므로 유연한 인자 처리가 필요합니다.

Q8. *args**kwargs를 남용하면 안 되는 이유는?

A: 코드 가독성과 유지보수성이 떨어지기 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ❌ 나쁜 예: 무엇을 받는지 불명확
def process(*args, **kwargs):
    # 어떤 인자가 필요한지 알 수 없음
    pass

# ✅ 좋은 예: 명시적 + 가변 인자 조합
def process(user_id, action, *additional_data, debug=False, **metadata):
    """
    user_id: 필수 사용자 ID
    action: 필수 작업 유형
    *additional_data: 선택적 추가 데이터
    debug: 디버그 모드 (기본값: False)
    **metadata: 선택적 메타데이터
    """
    pass

권장 사항:

  • 필수 매개변수는 명시적으로 선언
  • 가변 인자는 보조적으로 사용
  • 독스트링으로 명확히 문서화

🔗 관련 자료


📚 이전 학습

Day 26: 기본 매개변수와 키워드 인자

어제는 매개변수의 기본값을 설정하는 방법을 배웠습니다!


📚 다음 학습

Day 28: 람다 함수 (lambda) ⭐⭐⭐

내일은 한 줄로 함수를 정의하는 람다 함수를 배웁니다!


“늦었다고 생각할 때가 가장 빠른 시기입니다!” 🚀

Day 27/100 Phase 3: 제어문과 함수 #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.