파이썬에서의 얕은복사와 깊은 복사 및 레퍼런스 카운트(Reference Count)

파이썬은 언어 차원으로 포인터를 다루지 않기 때문에, 얕은 복사와 깊은 복사에 대한 언어의 구조적인 이해보다는 사용법을 이해하는 것이 바람직하다 생각한다. C와 C++에 익숙해져서 그런지, 얕은 복사 깊은 복사부분은 감동도 없고 재미도 없었다. 지금은 파이썬을 C/C++ 다음의 써드(third)언어로 택했고 파이썬의 강력함에 기가 찰 때도 있지만, 배우면서 느끼는 심오함은 C/C++에 비할게 아니다. 파이썬은 마치 최신 기술 잡지는 읽는 듯한 느낌이라면 C/C++은 깊이 알면 알수록 철학적 사고를 느끼게 하는 철학공부를 하는 듯한 착각에 빠지는 건 나만일까? 아무튼 파이썬에 대한 글이니 파이썬은 세련된 언어라고 결론 내려야겠군...

 

얕은 복사와 깊은 복사에 대한 것은 다음 그림 하나로 충분하다고 생각된다.

copy 모듈을 import 했는데 copy모듈에는 copy()함수와 deepcopy()함수가 있다. copy() 함수는 얕은 복사를 하고 deepcopy()는 말 그대로 깊은 복사를 한다.

파이썬에서 일반적인 대입연산은 얕은 복사를 한다. 위 그림을 보고 얕은 복사를 할 때 어떤 문제가 생기는지 알 수 있다.

예시1) 의 경우

>>>a[1]=100

>>>b[1]

100

a와 b 동일한 id 즉, 동일한 주소를 가리킨다.

예시2)

>>>a[1]=100

>>>b[1]

2

깊은 복사를 한 경우 id값이 다르다. a와 b는 각각 독립적인 객체인 것을 알 수 있다.

 

레퍼런스 카운트(Reference Count)

또는 참조 횟수 계산 방식이라고 한다.

(참고 : C/C++은 직접 또는 스마트 포인터를 사용하여 동일한 구현을 할 수 있다. )

총 3단계의 예를 들었다 첫 번째 단계에서는 레퍼런스 카운트가 3이다. 참조카운터라고도 하는데 객체를 참조하는 변수의 수를 뜻한다. 두 번째 del키워드를 사용해 a와 b변수를 지웠다. 그럼 참조 카운트는 1이 되고 b만 남는다. 세 번째 b까지 지우게 되면 메모리는 해제된다. 세 번째 단계를 구체적으로 설명해 보자. b가 파괴될 때 메모리에 있는 객체에 대한 참조 카운터가 0이 면 객체가 바로 메모리에서 해지 된다. 이 방식은 가비지 컬렉션 방식과 좀 다른데 좀 심심하니 비교해보겠다.

 

가비지 컬렉션의 경우


참조 카운트 방식은 참조 카운트가 0인 경우(객체를 참조하는 변수가 없는 경우) 메모리에서 객체가 바로 해지되는 데 반해, 가비지 컬렉션 방식은 쓰레기 수집 알고리즘이 수행 될 해지된다. 쓰레기 수집 알고리즘은 주기를 가지고 수행되기 때문에 객체의 해지 시점은 참조카운팅 방식보다 늦을 수 있다. 또한 쓰레기 수집 알고리즘에 따라 오버헤드가 달라질 수 있다. 어떤 경우든 객체가 메모리에서 해지 되지 않는 다면 이 메모리는 계속해서 사용됨으로 인식되기 때문에 다음 작업들에서 더 이상 사용되지 않게 된다. 메모리를 해지 시켜야지 비로서 다음 작업에서 사용 가능한 영역이 된다. 이를 자바나 파이썬은 언어적인 차원에서 자동으로 해주기 때문에 자바(java)가 C보다 메모리 관리가 편하다고들 한다. C/C++에서는 이런 메모리 관리는 직접 해줘야 한다. 또는 스마트 포인터를 이용할 수도 있지만, 표준 라이브러리로 지원하는 것이지, 언어적인 차원에서 지원하는 기능은 아니다. 파이썬은 언어적인 차원에서 참조카운팅(reference count) 방식으로 메모리를 관리한다.

 

주의해야 할 사항

이 부분은 이전 포스팅에서도 언급한 부분과 연결이 되는 부분이다. 사실 이런 부분을 정확히 이해하려면, C언어에서의 포인터에 대한 개념이 있고, 파이썬에서 어떻게 객체를 다루는지 에 대한(이전에 포스팅한 내용) 사전 지식이 있어야 한다. 하드코더가 아니라면 그냥 그렇구나 하고 넘겨도 될 듯하다.

Is는 비교연산을 하는데 두 객체가 같다면 TRUE를 반환, 두 객체가 같지 않다면 FALSE를 반환한다.

위 결과는 파이썬이 내부적으로 어떻게 동작 하는지 모른다면 이해할 수 없는 부분일 것이다.(예전 포스팅에서 설명)

이렇게 파이썬은 내부적으로 최적화를 위해서 꼼수?를 부려 놨다. 256까지의 수(많이 사용하는 객체)를 싱글톤 객체로 만들어 버림으로써 오버헤드를 줄인 것이다.

C언어를 공부했다면 얕은 복사와 깊은 복사의 개념은 어려운 것이 아니다. 하지만 이 개념이 프로그램 언어에서 중요시 되는 이유는 잘 못 사용했을 시 결과만 달라지고, 특별한 에러가 없는 경우가 대부분이기 때문이다. 의도한 결과가 안 나오는 경우는 에러가 발생하는 경우보다 더욱 심각한 상황이 될 수 있다. 과연 1만 줄이 넘는 코드에서 수많은 변수들이 있고 그 변수들 가운데 특정 부분에서 얕은 복사와 깊은 복사를 잘못 사용했다면? 이런 부분은 항상 염두해 둬야 하는 부분이다.

이 댓글을 비밀 댓글로
    • 내용이
    • 2018.06.06 20:50
    너무 좋은데요.. 어설프게 알던게 정리가 되네요.. 연차가 얼마나 되시는지..
    • 한가지 질문드리겠습니다.
    • 2018.06.06 20:59
    파이썬의 속도가 느린 이유중 하나가 레퍼런스카운트 및 비교로 때문인 것으로 알고 있는데요.
    레퍼런스 카운트는 언제 감소되는지 모르겠습니다.
    블로그에서 예시로 들어준 del말고 다른 경우가 있을까요?
    혹시 레퍼런스 카운트를 확인할 수 있는 메소드(매직 메소드같은)가 있을까요?
    감사합니다.
    • >>> import sys
      >>> a=[1,2,3]
      >>> sys.getrefcount(a)
      2
      이 때 레퍼런스 카운터가
      2가 나오는 이유는 getrefcount 함수로 전달될 때 카운터가 1이 플러스 되어서 2가 되는 거에요. 참고하세요.
    • 감사합니다.
    • 2018.06.06 21:59
    https://kin.naver.com/qna/detail.nhn?d1id=1&dirId=104&docId=302929431&page=1#answer1
    네이버 지식인에 답변을 달기위해,
    애매한 부분에 대해 검색하는 중 이 블로그에 오게 되었습니다.
    배워갑니다..
    (혹시 여유가 있으시다면, 제 답변에 이상한점 코멘트 부탁드립니다.
    알려주신 sys.getrefcount로 이것저것 테스트 해봐야겠습니다.)