본문 바로가기

진리는어디에/Python

[Python] 함수 #2 디폴트 파라미터 주의 사항

이번 포스트에서 다룰 내용은 어려운 내용은 아니지만 파이썬 프로그래밍을 하다 보면 한번은 경험하게 되는 - 특히, 다른 프로그래밍 언어 경험이 있는 경우 더 경험하기 쉬운 - 디폴트 파라미터에 대한 문제를 다뤄보도록 하겠다. 파이썬 관련 면접 질문에서도 종종 물어보는 문제이므로 이제 시작하는 프로그래머라면 잘 읽어 두도록 하자.

목차

  1. 함수 소개
  2. >> 디폴트 파라미터 주의 사항
  3. 파라미터 언패킹
  4. 파라미터 패킹
  5. 함수 객체
  6. 일급 객체(first class object)
  7. 람다(lambda)

mutable 타입을 함수의 디폴트 인자로 사용하지 말라

파이썬에서 mutable 타입이란 상태를 변경 할 수 있는 객체를 말한다. 예를들어 list, bytearray, set과 같은 시퀀스들이 좋은 예다. 반대로 immutable 이란 객체의 상태를 변경 할 수 없는 것을 말하는데, int, float, str, tuple 등이 있다. mutable과 immutable에 대한 자세한 설명은 [여기]를 참고 하도록 한다.

mutable에 대한 설명은 간략하게 이정도로 끝내고, 먼저 아래 예제를 보고 출력될 결과를 예상해보자.

만일 아래 예제의 의도를 파악하고 출력될 내용을 정확하게 맞춘다면 이 포스트를 더 이상 볼 필요 없는 사람이다. 바로 다음 장으로 넘어가자. 하지만 아래 예제를 보고 이상함을 못 느낀다면 당신은 이번 장을 꼭 봐야 하는 사람이다. 곧 재밌는 내용이 나오므로 두근거리는 마음을 가지고 이 글을 끝까지 읽도록 하자.

def add_sharp(s = []) :
    s.append('#')
    return s
    
s = [1, 2, 3]
print(add_sharp(s)) # [1, 2, 3, '#']

print(add_sharp()) # ['#']
print(add_sharp()) # ['#']
print(add_sharp()) # ['#']

위 예제의 add_sharp 함수는, 'mutable 타입'인 리스트(list) 객체를 디폴트 인자로 받아 맨 뒤에 '#'을 추가하는 간단한 함수다(mutable 타입에 따옴표를 사용하여 강조한 것을 주목하자).

6라인에서 리스트 객체 s = [1, 2, 3]을 받아 맨 뒤에 '#'을 추가했다. 이때는 함수 호출 시, 인자가 명시되어 있으므로 디폴트 인자를 사용하지 않는다.

그리고 8 부터 10라인의 add_sharp를 보자. 호출 시 인자를 넘겨 주지 않으므로, 디폴트 인자를 사용하여 add_sharp를 반복적으로 호출하고 있다.

8라인의 add_sharp함수는 디폴트 인자인 빈 리스트에 #을 추가하여 ['#']를 만든다. 9, 10라인에서도 동일한 작업을 반복하여 ['#']을 세번 출력 할 것이라 예상 할 수 있다.

하지만 결과는 다음과 같이 우리의 예상과는 다르다.

[1, 2, 3, '#']
['#']
['#', '#']
['#', '#', '#']

이유가 뭘까?

이전 포스트 [함수 객체]에서, 함수 객체는 디폴트 인자들을 __defaults__ 또는 __kwdefaults__ 어트리뷰트에 저장한다고 했다. 파이썬은 함수가 호출 될 때마다 새로운 객체를 생성하는 것이 아니라 __defaults__ 또는 __kwdefaults__에 저장되어 있는 디폴트 파라미터 객체를 참조한다.

함수가 호출 될 때마다 객체를 생성하는 것이 아니라
__defaults__ 또는 __kwdefaults__의 값을 참조한다

함수가 호출 될 때 마다 __defaults__ 어트리뷰트가 어떻게 달라지는지 확인하기 위해 위 예제를 다시 불러와 __defaults__ 출력하면서 실행 해보도록 하자.

def add_sharp(s = []) :
    s.append('#')
    return s
    
s = [1, 2, 3]
print(add_sharp(s)) # [1, 2, 3, '#']

print(add_sharp.__defaults__) # ([],)
print(add_sharp())            # 우리의 예상 ['#'], 실제 ['#']
print(add_sharp.__defaults__) # (['#'],)
print(add_sharp())            # 우리의 예상 ['#'], 실제 ['#', '#']
print(add_sharp.__defaults__) # (['#', '#'],)
print(add_sharp())            # 우리의 예상 ['#'], 실제 ['#', '#', '#']

9라인의 add_sharp 함수 호출 후 __defaults__에는 #이 하나 추가 된다. 그리고 11라인에서 add_sharp를 다시 호출하게 되면 디폴트 인자로 '빈 리스트를 새로 생성하는 것이 아닌 __defaults__의 변경된 값을 참조한다'. __defaults__에 있는 디폴트 인자의 값에는 이미 #이 하나 있으므로 두번째 호출에는 #이 두개, 세번째 호출에는 세개가 된다.

즉, 디폴트 값이 변경 가능한 mutable 타입인 리스트 형 객체이기 때문에 디폴트 값이 함수 호출 때 마다 계속 바뀌게 된다. 그래서 파이썬에서는 mutable 타입을 함수의 디폴트 인자로 사용하지 말라고 한다.

mutable 타입은 함수의 디폴트 인자로 사용해서는 안된다.

만일 아래와 같이 변경이 불가능한 immutable 타입인 정수를 디폴트 인자로 사용한다면 아무런 문제가 되지 않는다.

def add_ten(s = 10) :
    s += 10
    return s
    
s = 10
print(add_ten(s))           # 20

print(add_ten.__defaults__) # (10,)
print(add_ten())            # 우리의 예상 20, 실제 20
print(add_ten.__defaults__) # (10,)
print(add_ten())            # 우리의 예상 20, 실제 20
print(add_ten.__defaults__) # (10,)
print(add_ten())            # 우리의 예상 20, 실제 20

해결 방법

함수의 디폴트 파라미터로 mutable 타입인 리스트를 사용 할 수 없다면, 리스트 타입을 디폴트로 사용해야 하는 경우는 어떻게 해야 할까?

이때는 None을 디폴트 값으로 선언하고, 함수 내부에서 인자가 None인 경우 빈 리스트를 할당하는 방법을 사용한다.

def add_sharp(s = None) :
    if s is None :
        s = []
    s.append('#')
    return s

print(add_sharp())   # ['#']
print(add_sharp())   # ['#']
print(add_sharp())   # ['#']

마치며

기억할 것은 아래 두 가지다.

  • 정수와 같은 immutable 타입은 함수의 디폴트 인자로 사용 가능하다.
  • 리스트와 같은 mutable 타입은 함수의 디폴트 인자로 사용해서는 안된다.

어려운 문제는 아니지만 파이썬 프로그래밍을 하면서 대부분 언젠가 한번씩은 마주치는 유명한 문제이니 다음에 저런 상황을 마주하게 된다면 당황하지 말고 mutable 타입을 함수의 디폴트 리스트에서 제거 해주자.

다음글 : 파라미터 언패킹

부록 1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!