파이썬에서 함수를 작성하다 보면 다음과 같은 코드를 작성할 수 있다.
def add_item(item, items=[]):
items.append(item)
return items
처음 보면 add_item()을 호출할 때마다 items=[]가 실행되어 새로운 빈 리스트가 만들어질 것처럼 보인다.
하지만 실제로는 그렇지 않다.
print(add_item(1))
print(add_item(2))
print(add_item(3))
결과는 다음과 같다.
[1]
[1, 2]
[1, 2, 3]
왜 이런 일이 발생할까?
mutable은 값을 변경할 수 있는 객체라는 뜻이다.
대표적으로 다음과 같은 자료형들이 mutable이다.
list
dict
set
예를 들어 리스트는 생성된 이후에도 내용을 변경할 수 있다.
a = [1, 2]
a.append(3)
print(a)
결과:
[1, 2, 3]
반대로 다음과 같은 자료형은 대표적인 immutable 객체다.
int
float
str
tuple
즉, Mutable Default Argument를 직역하면
변경 가능한 객체를 함수의 기본 인자로 사용하는 것
이라고 볼 수 있다.
함수에서 인자를 전달하지 않았을 때 자동으로 사용되는 값을 기본 인자(Default Argument)라고 한다.
예를 들어:
def hello(name="Kim"):
print(name)
다음과 같이 호출하면:
hello()
name을 직접 전달하지 않았기 때문에 기본값인 "Kim"이 사용된다.
Kim
물론 직접 전달하면 해당 값이 사용된다.
hello("Woojin")
Woojin
다음 코드를 다시 보자.
def add_item(item, items=[]):
items.append(item)
return items
처음에는 보통 이렇게 생각하기 쉽다.
add_item(1)
→ items = []
→ [1]
add_item(2)
→ items = []
→ [2]
하지만 파이썬은 이렇게 동작하지 않는다.
파이썬에서 기본 인자는 함수가 호출될 때가 아니라 함수가 정의될 때 평가된다.
즉,
def add_item(item, items=[]):
이 코드가 실행되는 순간 []가 만들어진다.
개념적으로 보면 다음과 비슷하다.
default_items = []
def add_item(item, items=default_items):
items.append(item)
return items
따라서 함수 호출 과정을 보면:
함수 정의
↓
[] 생성
↓
함수의 기본값으로 저장
그 이후:
add_item(1)
↓
저장되어 있던 리스트 사용
↓
[1]
다시 호출하면:
add_item(2)
↓
아까 사용했던 같은 리스트 사용
↓
[1, 2]
또 호출하면:
add_item(3)
↓
같은 리스트 사용
↓
[1, 2, 3]
즉 호출할 때마다 새로운 리스트가 만들어지는 것이 아니다.
이것은 리스트만의 특별한 규칙이 아니다.
파이썬의 기본 인자 자체가 함수 정의 시점에 한 번 평가되도록 설계되어 있기 때문이다.
예를 들어:
def calculate(x=10 + 20):
return x
함수가 정의될 때:
10 + 20
이 계산이 이루어지고 30이 기본값으로 저장된다.
조금 더 확실하게 확인해보자.
def get_value():
print("get_value 실행")
return 10
def test(x=get_value()):
return x
이 코드를 실행하면 test()를 호출하지 않아도
get_value 실행
이 출력된다.
왜냐하면
def test(x=get_value()):
를 정의하는 순간 get_value()가 실행되기 때문이다.
전체 흐름은 다음과 같다.
함수 정의
↓
기본 인자 표현식 평가
↓
결과를 함수 객체에 저장
↓
나중에 함수 호출
↓
인자가 없으면 저장된 기본값 사용
호출 시점에는 새로운 기본값을 만드는 것이 아니다.
파이썬은 대략 다음과 같이 판단한다고 생각하면 된다.
함수를 호출했을 때
인자를 직접 전달했는가?
↓
┌───┴───┐
YES NO
↓ ↓
전달한 값 저장된 기본값
사용 사용
예를 들어:
def add_item(item, items=[]):
items.append(item)
return items
다음과 같이 호출하면:
print(add_item(1))
print(add_item(2, [100]))
print(add_item(3))
결과:
[1]
[100, 2]
[1, 3]
두 번째 호출에서는 [100]을 직접 전달했으므로 기본 리스트를 사용하지 않는다.
따라서 기본 리스트는 여전히 [1] 상태다.
세 번째 호출에서 다시 기본값을 사용하기 때문에:
[1, 3]
이 된다.
다음과 같이 작성하는 것은 피하는 것이 좋다.
def add_item(item, items=[]):
items.append(item)
return items
보통 None을 기본값으로 사용한다.
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
이제:
print(add_item(1))
print(add_item(2))
print(add_item(3))
결과는:
[1]
[2]
[3]
가 된다.
왜냐하면:
items = []
가 함수 본문 안에 있기 때문이다.
함수 본문은 함수가 호출될 때마다 실행된다.
따라서:
add_item(1)
→ 새로운 [] 생성
add_item(2)
→ 새로운 [] 생성
add_item(3)
→ 새로운 [] 생성
이 된다.
이 차이가 가장 중요하다.
def func(items=[]):
pass
[] 생성 시점:
함수 정의 시 한 번
def func(items=None):
if items is None:
items = []
[] 생성 시점:
함수를 호출할 때마다
따라서 다음 두 코드는 생김새는 비슷하지만 완전히 다르게 동작한다.
# 같은 리스트를 계속 사용할 수 있음
def func(a=[]):
...
# 호출할 때마다 새로운 리스트 생성
def func(a=None):
if a is None:
a = []
다음 코드는 일반적으로 문제가 없다.
def func(x=10):
return x
또는:
def hello(name="hello"):
return name
10이나 "hello" 역시 함수 정의 시점에 기본값으로 저장된다.
그런데 int, str은 immutable이다.
즉 기존 객체의 내부 값을 변경할 수 없다.
반면 리스트는:
items.append(1)
처럼 같은 객체의 내용을 직접 변경할 수 있다.
따라서 Mutable Default Argument 문제가 발생한다.
핵심은 다음 두 가지 규칙의 조합이다.
파이썬의 기본 인자는 함수 정의 시 한 번 평가된다.
리스트와 딕셔너리 같은 mutable 객체는 내용을 변경할 수 있다.
이 두 가지가 합쳐지면서 예상하지 못한 결과가 발생하는 것이다.
다음과 같은 코드는 주의해야 한다.
def func(a=[]):
...
def func(a={}):
...
def func(a=set()):
...
보통 다음과 같이 작성하는 것이 안전하다.
def func(a=None):
if a is None:
a = []
Dictionary라면:
def func(a=None):
if a is None:
a = {}
Set이라면:
def func(a=None):
if a is None:
a = set()
다음 함수에서 []는 func()를 호출할 때마다 새롭게 생성된다.
def func(a=[]):
return a
정답: X
[]는 함수가 호출될 때가 아니라 함수가 정의될 때 한 번 생성된다.
다음 코드의 결과는 [1], [2]이다.
def func(a=[]):
a.append(1)
return a
print(func())
print(func())
정답: X
결과:
[1]
[1, 1]
두 호출 모두 같은 기본 리스트를 사용한다.
리스트를 기본 인자로 사용하면 무조건 문법 오류가 발생한다.
def func(a=[]):
pass
정답: X
문법적으로는 완전히 정상이다.
다만 기본 리스트를 수정하면 예상하지 못한 문제가 발생할 수 있기 때문에 일반적으로 피하는 것이다.
다음 방식은 Mutable Default Argument 문제를 피하기 위해 흔히 사용하는 방법이다.
def func(a=None):
if a is None:
a = []
정답: O
a = []가 함수 본문에서 실행되므로 함수를 호출할 때마다 새로운 리스트가 생성된다.
다음 코드에서 두 번째 func()는 첫 번째 호출에서 수정된 리스트를 다시 사용한다.
def func(a=[]):
a.append(10)
return a
func()
func()
정답: O
기본 리스트는 함수 정의 시 한 번 만들어지고 계속 재사용된다.
int, str 같은 값을 기본 인자로 사용하는 것도 리스트와 똑같은 문제가 발생한다.
def func(x=10):
...
정답: X
int, str은 immutable 객체이기 때문에 리스트의 append()처럼 기존 객체 자체의 내용을 수정할 수 없다.
따라서 일반적인 기본값으로 안전하게 사용할 수 있다.
다음 코드에서 [100]은 함수의 기본 리스트에 영향을 준다.
def func(x, arr=[]):
arr.append(x)
return arr
func(1)
func(2, [100])
func(3)
정답: X
두 번째 호출에서는 [100]이라는 별도의 리스트를 직접 전달했다.
따라서 기본 리스트와 별개의 객체다.
결과는:
func(1) # [1]
func(2, [100]) # [100, 2]
func(3) # [1, 3]
이다.
파이썬의 기본 인자에 들어가는 표현식은 함수 정의 시점에 평가된다.
정답: O
예를 들어:
def get_value():
print("실행")
return 10
def func(x=get_value()):
pass
func()를 호출하지 않아도 def func(...)가 실행되는 시점에 get_value()가 호출된다.
Mutable Default Argument를 이해하기 위해서는 다음 한 문장을 기억하면 된다.
파이썬의 기본 인자는 호출할 때마다 만들어지는 것이 아니라 함수 정의 시점에 한 번 평가되어 저장된다.
따라서:
def func(a=[]):
에서 []는 호출할 때마다 초기화되지 않는다.
같은 리스트가 계속 사용된다.
반면:
def func(a=None):
if a is None:
a = []
에서는 a = []가 함수 본문에 있기 때문에 호출할 때마다 실행된다.
최종적으로 다음처럼 기억하면 된다.
기본 인자에 [] 사용
→ 정의할 때 한 번 생성
→ 같은 객체 재사용
함수 내부에서 [] 생성
→ 호출할 때 실행
→ 매번 새로운 객체 생성
def func(a=[])의 []는 호출 시점이 아니라 정의 시점에 만들어진다.