본문 바로가기

프로그래밍언어/Python

[Python] Default Argument Value - mutable object

반응형

파이썬 함수를 호출하다가 이상한 점을 발견하였다. 분명히 인자로 아무 값도 넘겨주지 않았는데 함수 내부에서는 파라미터에 값이 들어가 있는 것이다. 디버깅을 한 결과 파이썬의 default argument value 를 잘못 사용하고 있었다는 것을 알게 되었다. 이 문서에서는 문제를 발견하고 이를 디버깅한 과정과 이후 알게 된 내용을 정리한다.

1. 이슈 발견 및 디버깅

예시에서는 Context 클래스의 func 함수를 호출하고 있다. func 함수는 v 라는 파라미터와 l 이라는 파라미터를 가지고 있다. 여기서 v int 1 을 기본값으로 l list [] 를 기본값으로 가지고 있다.

 

func 함수는 리스트 l 에다가 v 의 값을 추가한 다음에 이를 화면에 출력하는 함수이다. 그렇기 때문에 함수를 호출할 때 아무런 값도 넘겨주지 않는다면 빈 리스트에 값 1 만 추가되어서 [1] 로 출력될 것이고, 만약 v 에 값을 넘겨준다면 [v] 를 화면에 출력될 것이다.

 

첫번째로 Context().func() 를 호출했을때는 예상한대로 v의 입력값인 1을 포함한  [1] 이 출력되었다. 하지만 두번째로 호출했을 때는 v 의 값인 2만 포함한 리스트  [2] 가 아니라 [1, 2] 가 출력되었다.

 

이 상황이 발생했을때 처음에는 나도 모르는 사이에 인자값을 넣어준 곳이 있는지 확인해보았다. 하지만 로직상 함수를 호출하는 곳과 인자로 넘겨주는 값 모두 디버깅 모드로 확인했을때 문제가 없었다. 여러가지 확인을 하다가 func() 함수 안에서 l 객체의 id 를 확인해보기로 하였다. 무언가 함수 내부의 변수가 초기화되지 않고 계속 유지가 되는 예감이 들었기 때문이었다.

 

확인한 결과 함수를 여러번 호출하여도 따로 l 파라미터에 별도의 인자를 입력하지 않으면 매번 객체의 id 가 같다는 것을 확인할 수 있었다. 이를 통해서 함수의 default argument 가 매번 새로 초기화되어서 파라미터에 할당되는 것이 아니라 특정 메모리에서 계속 존재한다는 것을 유추할 수 있었다.

 

계속해서 디버깅을 하던중, 파이썬 클래스의 함수 객체에 __defaults__ 라는 속성이 있는 것을 확인하였다. 이 값은 해당 함수 파라미터들의 기본값을 의미하고 있다.

 

 

위와 같이 __defaults__ 를 출력했을때, 튜플형식으로 fun() 함수의 파라미터인 v 와 l 의 기본값이 출력이 된다. 그런데 이 기본값 자체가 함수를 호출함으로 변경했던 l 의 값과 동일하다는 것을 확인하였다. 이 부분을 통해서 함수의 기본값과 관련된 문제라는 것을 의심하게 되었다.

 

Default Argument 와 관련된 문제임을 확인하고나서 구글링을 통해서 문제를 찾아보니 mutual object 를 default argument 로 사용했을때의 문제라는 것을 금방 확인할 수 있었다. 이에 대한 내용은 다음 항목에서 정리한다.

2. Default Argument Values - mutual object

파이썬에서 함수를 정의할 때는 default argument value 를 파라미터 정의시에 지정해줄 수 있다. 이를 통해서 함수를 호출할 때 불필요한 인자들까지 모두 입력하지 않아도 된다. 대신 입력되지 않은 값은 기본값으로 지정해준 값들이 자동으로 들어가게된다.

 

그런데 여기서 중요한 점은 인자로 들어가는 기본값이 매번 새로 생성되는 것이 아니라 기본갑에 들어간 값이 한번만 실행된다는 것이다. 이때문에 기본값이 int 나 float 와 같은 타입이 아닌 리스트, 딕셔너리, 또는 클래스의 객체와 같이 mutable object, 가변객체인 경우에 차이점이 발생한다.

 

이들은 초기에 한번만 생성되어서 class 에 저장되는데, 이때문에 위의 상황과 같이 이전 입력값에 의해 기본값이 변경되는 상황이 발생하게 되는 것이다.

 

공식문서에서는 이러한 문제때문에 인자에 mutable object 를 입력하기 보다는 아래와 같이 None 을 입력하고 함수 내부에서 초기화하도록 하는 것을 추천한다. 아래의 예제는 인자에 mutable object 인 리스트를 입력한 func_a() 함수와 이를 개선한 func_b() 함수이다.

 

def func_a(a, L=[]):
	L.append(a)
	return L

def func_b(a, L=None):
    if L is None:
	    L = []
    L.append(a)
    return a

3. 결론

파이썬의 default argument 를 사용할 때 list 나 dict 와 강튼 mutual object 를 사용해야 한다면, 이들의 객체를 사용하는 것이 아니라 None 을 넘겨주고 함수 안에서 초기화하도록 수정해야한다.

 

이러한 과정을 겪으면서 무엇보다 간단해 보이는 기술이어도 공식문서를 읽어보고 관련한 레퍼런스들을 체크한 다음에 기술을 사용해야 한다는 것을 다시 깨닫게 되었다. 공식문서에도 기술되어 있는 부분을 모른채 사용했다는 것이 개인적으로 많이 부끄러웠고, 이후로는 기술을 사용할때 문서를 확인하여서 이번과 같은 실수를 반복하지 않도록 할 것이다.

[Reference]

- https://docs.python.org/3/tutorial/controlflow.html#default-argument-values

 

4. More Control Flow Tools

As well as the while statement just introduced, Python uses a few more that we will encounter in this chapter. if Statements: Perhaps the most well-known statement type is the if statement. For exa...

docs.python.org

 

반응형

'프로그래밍언어 > Python' 카테고리의 다른 글

[Python] GIL (Global Interpreter Lock)  (0) 2022.07.24
[Python] Awaitable  (0) 2022.04.19
[Python] Decorator  (0) 2022.04.01
[Python] module, package, library  (0) 2022.02.11
[Python] copy  (0) 2022.02.08