Python & AI Tutorials Logo
Python 프로그래밍

20. 함수 매개변수와 인자

19장에서는 기본 매개변수로 함수를 정의하고 호출하는 방법을 배웠습니다. 이제 Python의 유연한 매개변수와 인자 시스템을 깊이 있게 살펴보겠습니다. 이러한 메커니즘을 이해하면 강력하면서도 사용하기 쉬운 함수를 작성할 수 있습니다.

20.1) 위치 인자와 키워드 인자

함수를 호출할 때 인자(argument)를 전달하는 기본 방식은 두 가지입니다. 위치로 전달하거나 이름(키워드)으로 전달하는 것입니다.

20.1.1) 위치 인자

위치 인자(positional arguments)는 순서에 따라 매개변수(parameter)에 매칭됩니다. 첫 번째 인자는 첫 번째 매개변수로, 두 번째 인자는 두 번째 매개변수로 전달되는 식입니다.

python
def calculate_discount(price, discount_percent):
    """Calculate the final price after applying a discount."""
    discount_amount = price * (discount_percent / 100)
    final_price = price - discount_amount
    return final_price
 
# 위치로 인자 전달하기
result = calculate_discount(100, 20)
print(result)

Output:

80.0

이 예시에서는 함수 호출에서의 위치만을 기준으로 100price에, 20discount_percent에 할당됩니다.

위치 인자에서는 순서가 매우 중요합니다:

python
# 상품 가격은 $100, 할인율은 20% 가정
 
# 올바른 순서: 먼저 price, 그다음 discount
print(calculate_discount(100, 20))
 
# 잘못된 순서: 먼저 discount, 그다음 price
print(calculate_discount(20, 100))

Output:

80.0
-16.0

인자 순서를 바꾸면 Python은 실수를 했는지 알지 못하고, 단지 순서대로 할당할 뿐입니다. 그 결과 수학적으로는 유효하지만 논리적으로는 잘못된 결과(음수 가격!)가 나옵니다.

20.1.2) 키워드 인자

키워드 인자(keyword arguments)는 매개변수 이름 뒤에 등호와 값을 붙여, 어떤 매개변수에 어떤 값이 들어가는지 명시적으로 지정합니다. 이렇게 하면 코드 가독성이 좋아지고 순서 실수를 방지할 수 있습니다.

python
def create_user_profile(username, email, age):
    """Create a user profile with the given information."""
    profile = f"User: {username}\nEmail: {email}\nAge: {age}"
    return profile
 
# 키워드 인자 사용하기
profile = create_user_profile(username="alice_smith", email="alice@example.com", age=28)
print(profile)

Output:

User: alice_smith
Email: alice@example.com
Age: 28

키워드 인자를 쓰면 순서는 중요하지 않습니다:

python
# 같은 결과, 다른 순서
profile1 = create_user_profile(username="bob", email="bob@example.com", age=35)
profile2 = create_user_profile(age=35, username="bob", email="bob@example.com")
profile3 = create_user_profile(email="bob@example.com", age=35, username="bob")
 
# 세 개 모두 동일한 결과를 생성합니다
print(profile1 == profile2 == profile3)

Output:

True

이런 유연성은 매개변수가 많은 함수에서 특히 유용하며, 어떤 값이 어떤 매개변수에 대응하는지 쉽게 확인할 수 있습니다.

20.1.3) 위치 인자와 키워드 인자 섞어 쓰기

하나의 함수 호출에서 두 스타일을 함께 사용할 수 있지만, 중요한 규칙이 있습니다. 위치 인자는 키워드 인자보다 앞에 와야 합니다.

python
def format_address(street, city, state, zip_code):
    """Format a mailing address."""
    return f"{street}\n{city}, {state} {zip_code}"
 
# 유효: 위치 인자를 먼저, 그다음 키워드 인자
address = format_address("123 Main St", "Springfield", state="IL", zip_code="62701")
print(address)

Output:

123 Main St
Springfield, IL 62701

여기서 "123 Main St""Springfield"는 위치 인자(각각 street, city에 할당)이고, statezip_code는 이름으로 지정합니다.

키워드 인자 뒤에 위치 인자를 놓으려고 하면 오류가 발생합니다:

python
# 유효하지 않음: 키워드 인자 뒤의 위치 인자
# address = format_address(street="123 Main St", "Springfield", state="IL", zip_code="62701")
# SyntaxError: positional argument follows keyword argument

Python이 이 규칙을 강제하는 이유는, 키워드 인자를 사용하기 시작하면 그 뒤에 나오는 이름 없는 인자가 어떤 위치 매개변수를 채워야 하는지 모호해지기 때문입니다.

20.1.4) 각 스타일을 언제 사용할까

위치 인자는 다음과 같은 경우에 사용하세요:

  • 함수의 매개변수가 적을 때(보통 1~3개)
  • 매개변수 순서가 명확하고 직관적일 때
  • 함수가 흔히 사용되며 순서가 잘 알려져 있을 때
python
# 명확하고 간결함
print(len("hello"))
result = max(10, 20, 5)

키워드 인자는 다음과 같은 경우에 사용하세요:

  • 함수의 매개변수가 많을 때
  • 매개변수 의미가 즉시 명확하지 않을 때
  • 기본값이 있는 일부 매개변수를 건너뛰고 싶을 때(다음에서 다룹니다)
  • 코드를 자체 문서화(self-documenting)하고 싶을 때
python
# 명확하고 명시적임
user = create_user_profile(username="charlie", email="charlie@example.com", age=42)

20.2) 기본 매개변수 값

함수는 매개변수에 기본값(default values)을 지정할 수 있습니다. 호출자가 기본값이 있는 매개변수에 대해 인자를 제공하지 않으면, Python은 대신 기본값을 사용합니다.

20.2.1) 기본값이 있는 매개변수 정의하기

기본값은 함수 정의에서 대입 연산자를 사용해 지정합니다:

python
def greet_user(name, greeting="Hello"):
    """Greet a user with a customizable greeting."""
    return f"{greeting}, {name}!"
 
# 기본 인사말 사용하기
print(greet_user("Alice"))
 
# 사용자 정의 인사말 제공하기
print(greet_user("Bob", "Good morning"))
print(greet_user("Carol", greeting="Hi"))

Output:

Hello, Alice!
Good morning, Bob!
Hi, Carol!

매개변수 greeting의 기본값은 "Hello"입니다. greet_user("Alice")를 호출하면 Python이 이 기본값을 사용합니다. 두 번째 인자를 제공하면 기본값을 덮어씁니다.

20.2.2) 기본값이 있는 매개변수는 필수 매개변수 뒤에 와야 합니다

Python은 기본값이 있는 매개변수가 기본값이 없는 모든 매개변수 뒤에 오도록 요구합니다. 이 규칙은 어떤 인자가 어떤 매개변수에 대응하는지에 대한 모호함을 방지합니다.

python
# 올바름: 필수 매개변수 먼저, 그다음 기본값
def create_product(name, price, category="General", in_stock=True):
    """Create a product record."""
    return {
        "name": name,
        "price": price,
        "category": category,
        "in_stock": in_stock
    }
 
product = create_product("Laptop", 999.99)
print(product)

Output:

{'name': 'Laptop', 'price': 999.99, 'category': 'General', 'in_stock': True}

기본값이 있는 매개변수 뒤에 필수 매개변수를 두려고 하면 구문 오류가 발생합니다:

python
# 유효하지 않음: 기본값 매개변수 뒤의 필수 매개변수
# def invalid_function(name="Unknown", age):
#     return f"{name} is {age} years old"
# SyntaxError: non-default argument follows default argument

이는 타당합니다. name에는 기본값이 있지만 age에는 없다면, Python은 invalid_function(25)name=25이고 age가 빠진 것인지, 아니면 age=25이고 name이 기본값을 사용하는 것인지 알 수 없습니다. 이 규칙은 이런 모호함을 제거합니다.

20.2.3) 기본 매개변수의 실용적 사용

기본 매개변수는 특정 인자가 거의 바뀌지 않는 함수에 매우 적합합니다:

python
def calculate_shipping(weight, distance, express=False):
    """Calculate shipping cost based on weight and distance."""
    base_rate = 0.50 * weight + 0.10 * distance
    
    if express:
        base_rate *= 2  # 특급 배송은 비용이 두 배입니다
    
    return round(base_rate, 2)
 
# 대부분의 배송은 일반 배송입니다
standard_cost = calculate_shipping(5, 100)
print(f"Standard: ${standard_cost}")
 
# 가끔 누군가는 특급이 필요합니다
express_cost = calculate_shipping(5, 100, express=True)
print(f"Express: ${express_cost}")

Output:

Standard: $12.5
Express: $25.0

이 설계는 흔한 경우(일반 배송)는 호출을 간단하게 만들면서도, 필요한 경우에는 덜 흔한 경우(특급 배송)도 지원합니다.

20.2.4) 여러 기본값과 선택적 덮어쓰기

함수에 기본값이 있는 매개변수가 여러 개 있으면, 키워드 인자를 사용해 그중 어떤 조합이든 덮어쓸 수 있습니다:

python
def format_currency(amount, currency="USD", show_symbol=True, decimal_places=2):
    """Format a number as currency."""
    symbols = {"USD": "$", "EUR": "€", "GBP": "£", "JPY": "¥"}
    
    formatted = f"{amount:.{decimal_places}f}"
    
    if show_symbol and currency in symbols:
        formatted = f"{symbols[currency]}{formatted}"
    
    return formatted
 
# 모든 기본값 사용하기
print(format_currency(42.5))
 
# currency만 덮어쓰기
print(format_currency(42.5, currency="EUR"))
 
# 여러 기본값 덮어쓰기
print(format_currency(42.5, currency="JPY", decimal_places=0))

Output:

$42.50
€42.50
¥42

이 유연성 덕분에 호출자는 필요한 부분만 정확히 커스터마이즈할 수 있고, 함수 호출은 간결하게 유지됩니다.

20.3) *args로 가변 길이 인자 목록 받기

때로는 함수가 인자를 몇 개 받을지 미리 알 수 없는 상태에서, 얼마든지 많은 개수의 인자를 받도록 만들고 싶을 때가 있습니다. Python은 이를 위해 *args를 제공합니다.

20.3.1) *args 이해하기

매개변수 목록에서 *args 문법은 추가 위치 인자를 모두 튜플(tuple)로 모읍니다. args라는 이름은 관례(“arguments”의 약자)일 뿐이며, 별표 뒤에는 어떤 유효한 매개변수 이름이든 사용할 수 있습니다.

python
def calculate_total(*numbers):
    """Calculate the sum of any number of values."""
    total = 0
    for num in numbers:
        total += num
    return total
 
# 어떤 개수의 인자에도 동작합니다
print(calculate_total(10))
print(calculate_total(10, 20))
print(calculate_total(10, 20, 30, 40))
print(calculate_total())

Output:

10
30
100
0

함수 내부에서 numbers는 함수에 전달된 모든 위치 인자를 담고 있는 튜플입니다. 인자가 제공되지 않으면 빈 튜플입니다.

20.3.2) 일반 매개변수와 *args 결합하기

*args 앞에 일반 매개변수를 둘 수 있습니다. 일반 매개변수는 앞쪽 인자를 소비하고, *args는 나머지를 모읍니다:

python
def create_team(team_name, *members):
    """Create a team with a name and any number of members."""
    member_list = ", ".join(members)
    return f"Team {team_name}: {member_list}"
 
# 첫 번째 인자는 team_name으로, 나머지는 members로 들어갑니다
print(create_team("Alpha", "Alice", "Bob"))
print(create_team("Beta", "Carol"))
print(create_team("Gamma", "Dave", "Eve", "Frank", "Grace"))

Output:

Team Alpha: Alice, Bob
Team Beta: Carol
Team Gamma: Dave, Eve, Frank, Grace

첫 번째 인자("Alpha", "Beta", "Gamma")는 team_name에 할당되고, 나머지 모든 인자는 members 튜플로 수집됩니다.

20.4) 키워드 전용 매개변수와 **kwargs 매개변수

Python은 인자를 처리하기 위한 두 가지 추가 메커니즘을 제공합니다. 키워드 전용 매개변수(keyword-only parameters)와 임의의 키워드 인자를 모으는 **kwargs입니다.

20.4.1) 키워드 전용 매개변수

키워드 전용 매개변수(keyword-only parameters)는 키워드 인자로만 지정해야 하며, 위치로는 전달할 수 없습니다. 매개변수 목록에서 * 뒤에 두거나 *args 뒤에 두면 만들 수 있습니다.

python
def create_account(username, *, email, age):
    """Create an account. Email and age must be specified by name."""
    return {
        "username": username,
        "email": email,
        "age": age
    }
 
# 올바름: email과 age를 키워드로 지정
account = create_account("alice", email="alice@example.com", age=28)
print(account)
 
# 유효하지 않음: email과 age를 위치로 전달하려고 함
# account = create_account("bob", "bob@example.com", 30)
# TypeError: create_account() takes 1 positional argument but 3 were given

Output:

{'username': 'alice', 'email': 'alice@example.com', 'age': 28}

매개변수 목록의 *는 구분자 역할을 합니다. 그 뒤에 있는 모든 것은 키워드 인자로 전달되어야 합니다. 이는 호출자가 특정 매개변수를 명확히 지정하도록 강제하여 코드 가독성을 높이고 오류 가능성을 줄이고 싶을 때 유용합니다.

일반 매개변수, *args, 키워드 전용 매개변수를 함께 조합할 수도 있습니다:

python
def log_event(event_type, *details, severity="INFO", timestamp=None):
    """Log an event with optional details and metadata."""
    # datetime 모듈은 39장에서 자세히 배우겠지만,
    # 지금은 이 줄들이 현재 시간을 가져오고
    # 타임스탬프 문자열로 포맷한다는 것만 알아두세요
    from datetime import datetime
    
    if timestamp is None:
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
    details_str = " | ".join(details)
    return f"[{timestamp}] {severity}: {event_type} - {details_str}"
 
# event_type은 위치 인자이며, details는 *details로 수집되고,
# severity와 timestamp는 키워드 전용입니다
print(log_event("Login", "User: alice", "IP: 192.168.1.1"))
print(log_event("Error", "Database connection failed", severity="ERROR"))

Output (timestamp will vary based on when you run the code):

[2025-12-18 19:28:13] INFO: Login - User: alice | IP: 192.168.1.1
[2025-12-18 19:28:13] ERROR: Error - Database connection failed

20.4.2) **kwargs 이해하기

**kwargs 문법은 추가 키워드 인자를 모두 딕셔너리(dictionary)로 모읍니다. args와 마찬가지로 kwargs라는 이름은 관례(“keyword arguments”의 약자)일 뿐이며, 이중 별표 뒤에는 어떤 유효한 이름이든 사용할 수 있습니다.

python
def create_product(**attributes):
    """Create a product with any number of attributes."""
    product = {}
    for key, value in attributes.items():
        product[key] = value
    return product
 
# 원하는 키워드 인자를 얼마든지 전달합니다
laptop = create_product(name="Laptop", price=999.99, brand="TechCorp", in_stock=True)
print(laptop)
 
phone = create_product(name="Phone", price=699.99, color="Black")
print(phone)

Output:

{'name': 'Laptop', 'price': 999.99, 'brand': 'TechCorp', 'in_stock': True}
{'name': 'Phone', 'price': 699.99, 'color': 'Black'}

함수 내부에서 attributes는 키가 매개변수 이름이고 값이 전달된 인자인 딕셔너리입니다.

20.4.3) 일반 매개변수, *args, **kwargs 함께 사용하기

이 모든 메커니즘을 함께 사용할 수 있지만, 반드시 특정 순서로 나와야 합니다:

  1. 일반 위치 매개변수
  2. *args(있다면)
  3. 키워드 전용 매개변수(있다면)
  4. **kwargs(있다면)
python
def complex_function(required, *args, keyword_only, **kwargs):
    """Demonstrate all parameter types together."""
    print(f"Required: {required}")
    print(f"Args: {args}")
    print(f"Keyword-only: {keyword_only}")
    print(f"Kwargs: {kwargs}")
 
complex_function(
    "value1",           # required
    "value2", "value3", # args
    keyword_only="kw",  # keyword_only
    extra1="e1",        # kwargs
    extra2="e2"         # kwargs
)

Output:

Required: value1
Args: ('value2', 'value3')
Keyword-only: kw
Kwargs: {'extra1': 'e1', 'extra2': 'e2'}

이 유연성은 강력하지만 절제해서 사용해야 합니다. 대부분의 함수는 이 모든 메커니즘을 필요로 하지 않습니다.

20.4.4) 실용 사례: 설정 함수

**kwargs의 흔한 용도는 설정 옵션을 받아들이는 함수를 만드는 것입니다:

python
def connect_to_database(host, port, **options):
    """Connect to a database with flexible configuration options."""
    connection_string = f"Connecting to {host}:{port}"
    
    # 추가 옵션 처리하기
    if options.get("ssl"):
        connection_string += " with SSL"
    
    if options.get("timeout"):
        connection_string += f" (timeout: {options['timeout']}s)"
    
    if options.get("pool_size"):
        connection_string += f" (pool size: {options['pool_size']})"
    
    return connection_string
 
# 기본 연결
print(connect_to_database("localhost", 5432))
 
# SSL 사용
print(connect_to_database("db.example.com", 5432, ssl=True))
 
# 여러 옵션 사용
print(connect_to_database("db.example.com", 5432, ssl=True, timeout=30, pool_size=10))

Output:

Connecting to localhost:5432
Connecting to db.example.com:5432 with SSL
Connecting to db.example.com:5432 with SSL (timeout: 30s) (pool size: 10)

이 패턴은 매개변수 목록에 모든 옵션을 명시적으로 정의하지 않고도, 함수가 원하는 만큼의 선택적 설정 매개변수를 받아들일 수 있게 해줍니다.

위치

추가 위치

키워드 전용

추가 키워드

함수 호출

매개변수 타입?

일반 매개변수

*args 튜플

키워드 전용 매개변수

**kwargs 딕셔너리

위치로 할당

튜플로 수집

이름 사용 필수

딕셔너리로 수집

20.5) 함수 호출 시 인자 언패킹

*args**kwargs가 함수를 정의할 때 인자를 수집하는 것처럼, 함수를 호출할 때도 ***를 사용해 컬렉션을 언패킹(unpack)할 수 있습니다.

20.5.1) *로 시퀀스 언패킹하기

* 연산자는 시퀀스(리스트, 튜플 등)를 개별 위치 인자로 언패킹합니다:

python
def calculate_rectangle_area(width, height):
    """Calculate the area of a rectangle."""
    return width * height
 
# 인자를 하나씩 전달하는 대신
dimensions = [5, 10]
area = calculate_rectangle_area(dimensions[0], dimensions[1])
print(area)
 
# 리스트를 직접 언패킹
area = calculate_rectangle_area(*dimensions)
print(area)

Output:

50
50

*dimensions라고 쓰면 Python은 리스트 [5, 10]을 두 개의 개별 인자로 언패킹하여, calculate_rectangle_area(5, 10)이라고 쓴 것과 같게 동작합니다.

이는 어떤 이터러블(iterable)에도 동작합니다:

python
def format_name(first, middle, last):
    """Format a full name."""
    return f"{first} {middle} {last}"
 
# 튜플 언패킹
name_tuple = ("John", "Q", "Public")
print(format_name(*name_tuple))
 
# 리스트 언패킹
name_list = ["Jane", "M", "Doe"]
print(format_name(*name_list))
 
# 문자열도 언패킹 가능(각 문자가 인자가 됩니다)
# 이 방식은 함수가 올바른 개수의 인자를 기대할 때만 동작합니다
def show_first_three(a, b, c):
    return f"{a}, {b}, {c}"
 
print(show_first_three(*"ABC"))

Output:

John Q Public
Jane M Doe
A, B, C

20.5.2) **로 딕셔너리 언패킹하기

** 연산자는 딕셔너리를 키워드 인자로 언패킹합니다:

python
def create_user(username, email, age):
    """Create a user profile."""
    return f"User: {username}, Email: {email}, Age: {age}"
 
# 키가 매개변수 이름과 일치하는 딕셔너리
user_data = {
    "username": "alice",
    "email": "alice@example.com",
    "age": 28
}
 
# 딕셔너리 언패킹
profile = create_user(**user_data)
print(profile)

Output:

User: alice, Email: alice@example.com, Age: 28

**user_data라고 쓰면 Python은 딕셔너리를 키워드 인자로 언패킹하며, 다음과 동일합니다:

python
create_user(username="alice", email="alice@example.com", age=28)

딕셔너리 키는 함수의 매개변수 이름과 일치해야 하며, 그렇지 않으면 오류가 발생합니다:

python
# 유효하지 않음: 딕셔너리 키가 매개변수 이름과 일치하지 않음
invalid_data = {"name": "bob", "email": "bob@example.com", "age": 30}
# profile = create_user(**invalid_data)
# TypeError: create_user() got an unexpected keyword argument 'name'

20.5.3) 언패킹과 일반 인자 결합하기

언패킹한 인자와 일반 인자를 섞어 쓸 수 있습니다:

python
def calculate_total(base_price, tax_rate, discount):
    """Calculate total price after tax and discount."""
    subtotal = base_price * (1 + tax_rate)
    total = subtotal * (1 - discount)
    return round(total, 2)
 
# 일부는 일반 인자, 일부는 언패킹
pricing = [0.08, 0.10]  # tax_rate와 discount
total = calculate_total(100, *pricing)
print(total)

Output:

97.2

하나의 호출에서 여러 컬렉션을 언패킹할 수도 있습니다:

python
def create_full_address(street, city, state, zip_code, country):
    """Create a complete address."""
    return f"{street}, {city}, {state} {zip_code}, {country}"
 
street_address = ["123 Main St", "Springfield"]
location_details = ["IL", "62701", "USA"]
 
address = create_full_address(*street_address, *location_details)
print(address)

Output:

123 Main St, Springfield, IL 62701, USA

20.5.4) 실용 예시: 유연한 함수 호출

언패킹은 외부 소스에서 온 데이터를 다룰 때 특히 유용합니다:

python
def send_email(recipient, subject, body, cc=None, bcc=None):
    """Send an email with optional CC and BCC."""
    message = f"To: {recipient}\nSubject: {subject}\n\n{body}"
    
    if cc:
        message += f"\nCC: {cc}"
    if bcc:
        message += f"\nBCC: {bcc}"
    
    return message
 
# 설정 파일이나 데이터베이스에서 온 이메일 데이터
email_config = {
    "recipient": "user@example.com",
    "subject": "Welcome",
    "body": "Thank you for signing up!",
    "cc": "manager@example.com"
}
 
# 설정을 바로 언패킹
result = send_email(**email_config)
print(result)

Output:

To: user@example.com
Subject: Welcome
 
Thank you for signing up!
CC: manager@example.com

이 패턴은 함수 인자를 데이터 구조로 다루기 쉽게 해주며, API를 만들거나 설정 파일을 처리할 때 흔히 사용됩니다.

* 연산자

** 연산자

컬렉션

언패킹 타입

시퀀스 언패킹

딕셔너리 언패킹

리스트/튜플 → 위치 인자

딕셔너리 → 키워드 인자

함수 호출

20.6) 가변 기본 인자의 함정(리스트 기본값이 유지되는 이유)

Python에서 가장 악명 높은 함정 중 하나는 리스트나 딕셔너리 같은 가변 객체(mutable objects)를 기본 매개변수 값으로 사용하는 것입니다. 이 문제를 이해하는 것은 올바른 함수를 작성하는 데 매우 중요합니다.

20.6.1) 문제: 공유되는 가변 기본값

다음의 겉보기엔 무해한 함수를 생각해 봅시다:

python
def add_student(name, grades=[]):
    """Add a student with their grades."""
    grades.append(name)
    return grades
 
# 첫 번째 호출
students1 = add_student("Alice")
print(students1)
 
# 두 번째 호출 - 새로운 리스트를 기대함
students2 = add_student("Bob")
print(students2)
 
# 세 번째 호출
students3 = add_student("Carol")
print(students3)

Output:

['Alice']
['Alice', 'Bob']
['Alice', 'Bob', 'Carol']

이 동작은 많은 프로그래머를 놀라게 합니다. grades 인자를 제공하지 않고 add_student()를 호출할 때마다 새 리스트가 사용되는 것이 아니라, 같은 리스트 객체가 사용됩니다. 이 리스트는 함수 호출 사이에서도 유지되며 값이 누적됩니다.

20.6.2) 왜 이런 일이 생길까: 기본값은 한 번만 생성됩니다

이 동작을 이해하는 핵심은 기본값이 언제 생성되는지 아는 것입니다. Python은 기본 매개변수 값을 함수가 호출될 때마다 평가하는 것이 아니라, 함수가 정의될 때 한 번만 평가합니다.

python
def demonstrate_default_creation():
    """Show when defaults are created."""
    print("Function defined!")
 
def use_default(value=demonstrate_default_creation()):
    """Use a default that calls a function."""
    return value
 
# 메시지는 호출될 때가 아니라 함수가 정의될 때 출력됩니다

Output:

Function defined!

Python이 def use_default 줄을 만나면, 기본 매개변수 value=demonstrate_default_creation()를 평가합니다. 이때 demonstrate_default_creation()이 호출되어 즉시 "Function defined!"를 출력합니다. 이후 use_default()를 호출할 때는 기본값을 다시 평가하지 않으므로 추가 출력이 없습니다.

Python이 def add_student(name, grades=[]):를 만나면 빈 리스트 객체를 만들고 이를 grades의 기본값으로 저장합니다. 이후 grades 인자를 제공하지 않는 모든 호출은 그 동일한 리스트 객체를 사용합니다.

객체 동일성(object identity)을 사용하면 더 명확하게 확인할 수 있습니다:

python
def show_list_identity(items=[]):
    """Show that the same list object is reused."""
    print(f"List ID: {id(items)}")
    items.append("item")
    return items
 
# 각 호출은 같은 리스트 객체(같은 ID)를 사용합니다
show_list_identity()
show_list_identity()
show_list_identity()

Output:

List ID: 140234567890123
List ID: 140234567890123
List ID: 140234567890123

정확한 ID 값은 시스템에 따라 달라지지만, 세 번의 호출이 모두 같은 ID를 보여준다는 점에 주목하세요. 이는 같은 리스트 객체를 사용하고 있다는 증거입니다. id() 함수는 메모리에서 각 객체의 고유 식별자를 반환하며, ID가 같으면 같은 객체입니다.

20.6.3) 올바른 패턴: 기본값으로 None 사용하기

표준 해결책은 기본값으로 None을 사용하고 함수 내부에서 새로운 가변 객체를 생성하는 것입니다:

python
def add_student_correct(name, grades=None):
    """Add a student with their grades (correct version)."""
    if grades is None:
        grades = []  # 매번 NEW 리스트를 생성
    
    grades.append(name)
    return grades
 
# 이제 각 호출은 자기만의 리스트를 가집니다
students1 = add_student_correct("Alice")
print(students1)
 
students2 = add_student_correct("Bob")
print(students2)
 
students3 = add_student_correct("Carol")
print(students3)

Output:

['Alice']
['Bob']
['Carol']

이 패턴이 동작하는 이유는 None이 불변(immutable)이며, gradesNone일 때마다 함수 본문에서 새 리스트가 생성되기 때문입니다.

20.6.4) 딕셔너리에서도 같은 문제

이 문제는 리스트뿐 아니라 모든 가변 타입에 영향을 줍니다:

python
# WRONG: 딕셔너리 기본값
def create_config_wrong(key, value, config={}):
    """Create a configuration (BUGGY VERSION)."""
    config[key] = value
    return config
 
config1 = create_config_wrong("theme", "dark")
print(config1)
 
config2 = create_config_wrong("language", "en")
print(config2)
 
print("---")
 
# CORRECT: 기본값으로 None 사용
def create_config_correct(key, value, config=None):
    """Create a configuration (CORRECT VERSION)."""
    if config is None:
        config = {}
    
    config[key] = value
    return config
 
config1 = create_config_correct("theme", "dark")
print(config1)
 
config2 = create_config_correct("language", "en")
print(config2)

Output:

{'theme': 'dark'}
{'theme': 'dark', 'language': 'en'}
---
{'theme': 'dark'}
{'language': 'en'}

20.6.5) 요약: 황금 규칙

가변 객체(리스트, 딕셔너리, 세트)를 기본 매개변수 값으로 절대 사용하지 마세요. 항상 None을 사용하고 함수 내부에서 가변 객체를 생성하세요:

python
# ❌ WRONG
def function(items=[]):
    pass
 
# ✅ CORRECT
def function(items=None):
    if items is None:
        items = []
    # 이제 items를 안전하게 사용합니다

이 패턴은 각 함수 호출이 독립적인 가변 객체를 갖도록 보장하여, 호출 간 데이터가 새어나가 생기는 알 수 없는 버그를 방지합니다.


이 장에서는 Python의 유연한 매개변수와 인자 시스템을 깊이 있게 살펴보았습니다. 위치 인자와 키워드 인자를 사용하는 방법, 기본값을 제공하는 방법, *args**kwargs로 가변 개수의 인자를 처리하는 방법, 함수를 호출할 때 컬렉션을 언패킹하는 방법, 그리고 가변 기본 인자 함정을 피하는 방법을 배웠습니다.

이 메커니즘들은 유연하면서도 사용하기 쉬운 함수 인터페이스를 설계할 수 있게 해주는 강력한 도구입니다. 더 많은 함수를 작성하다 보면, 어떤 상황에서 어떤 매개변수 패턴이 가장 적합한지에 대한 감각이 생길 것입니다. 핵심은 유연성과 명확성 사이의 균형입니다. 함수를 올바르게 호출하기는 쉽고, 잘못 호출하기는 어렵게 만드세요.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai