본문 바로가기
Computer Science/Python

객체 참조, 가변성, 재활용

by Bloofer 2022. 11. 15.

변수는 상자가 아니다

린 안드레아 스타인 교수는 흔히 비유하는 '상자로서의 변수' 개념이 실제로는 객체지향 언어에서 참조 변수를 이해하는 데 방해가 된다고 강조했다. 파이썬 변수는 자바에서의 참조 변수와 같으므로 변수는 상자보다는 객체에 붙은 포스트잇 같은 레이블이라고 생각하는 것이 더 좋다.

 

변수는 단지 레이블일 뿐이며 객체에 여러 레이블을 붙이지 못할 이유가 없다. 여러 레이블을 붙이는 것을 별명이라고 부른다.

 

 

 

정체성, 동질성, 별명

>> charles = {'name': 'Charles L. Dogson', 'born': 1832}
>> lewis = charles
>> lewis is charles
True
>> lewis['balance'] = 950
>> charles
{'name': 'Charles L. Dogson', 'balance': 950, 'born': 1832}

 

lewis는 charles의 별명이다. is 연산자를 이용하여 이 사실을 확인할 수 있는데, is 연산자는 객체의 정체성을 비교한다.

 

== 연산자는 객체의 값을 비교하는 한편, is 연산자는 객체의 정체성을 비교한다. 여기서 객체의 정체성을 비교한다는 의미는, 객체가 가리키는 메모리 주소를 비교한다는 의미이다. is 연산자는 객체가 가리키는 id를 비교하고 위 코드에서 lewis와 charles는 동일한 id를 가진다.

 

객체 id의 실제 의미는 구현에 따라 다르다. CPython의 경우 id()는 객체의 메모리 주소를 반환하지만, 다른 파이썬 인터프리터는 메모리 주소 이외의 다른 값을 반환할 수도 있다. 다만 id는 객체마다 고유한 레이블이라는 것을 보장하며 객체가 소멸될 때까지 결코 변하지 않는다.

 

a == b는 a.__eq__(b)의 편리구문이다. object 객체에서 상속받은 __eq__() 메서드는 객체의 id를 비교하므로 is 연산자와 동일한 결과를 산출한다. 그러나 대부분의 내장 자료형은 __eq__() 메서드를 오버라이드해서 객체의 값을 비교한다.

 

 

 

기본 복사는 얕은 복사

리스트나 가변형 시퀀스의 경우 l1=l2 코드는 사본을 생성한다. 그러나 생성자나 [:]을 사용하면 얕은 사본을 생성한다. 즉, 최상위 컨테이너는 복제하지만 사본은 원래 컨테이너에 들어 있던 동일 객체에 대한 참조로 채워진다. 모든 항목이 불변형이면 이 방식은 메모리를 절약하며 아무런 문제를 일으키지 않는다. 그러나 가변 항목이 들어있을 때는 불쾌한 문제를 야기할 수도 있다.

 

>> l1 = [3, [66, 55, 44], (7, 8, 9)]
>> l2 = list(l1)
>> l1.append(100)
>> l1[1].remove(55)
>> print(l1)
[3, [66, 44], (7, 8, 9), 100]
>> print(l2)
[3, [66, 44], (7, 8, 9)]
>> l2[1] += [33, 22]
>> l2[2] += (10, 11)
>> print(l1)
[3, [66, 44, 33, 22], (7, 8, 9), 100]
>> print(l2)
[3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]

 

l2는 l1의 얕은 사본이다. l2는 l1의 값을 복사했지만 그 리스트 안에 있는 리스트는 참조된 객체이므로 그 변경이 l2에도 반영된다. l1에 100을 추가하였을 때, l2에는 반영되지 않았지만 l1[1]에서 55를 제거하였을 때 참조된 객체의 원본이 수정되어 l2가 가리키는 값에도 변경이 반영되었다.

 

아래의 경우도 동일하다. l2[1]에 += 연산자로 리스트를 연장하였을 때 참조된 객체의 원본이 수정되어 l1에도 변경이 반영되었다. 그러나, 불변 객체의 튜플의 경우 l2[2]에 += 연산자를 적용시 l1과 l2에 있는 튜플은 더 이상 동일 객체가 아니게 된다. += 연산자는 새로운 튜플을 만들어서 l2[2]에 바인딩하기 때문이다!

 

 

 

del과 가비지 컬렉션

del 명령은 이름을 제거하는 것이지, 객체를 제거하는 것이 아니다. del 명령의 결과로 객체가 가비지 컬렉트될 수 있지만, 제거된 변수가 객체를 참조하는 최후의 변수거나 객체에 도달할 수 없을 때만 가비지 컬렉트된다. 변수를 다시 바인딩해도 객체에 대한 참조 카운트를 0으로 만들어 객체가 제거될 수 있다.

 

CPython의 경우 가비지 컬렉션은 주로 참조 카운트에 기반한다. 본질적으로 각 객체는 얼마나 많은 참조가 자신을 가리키는지 참조 횟수를 계수한다. refcount가 0이 되자마자 CPython이 객체의 __del__() 메서드를 호출하고 객체에 할당되어있는 메모리를 해제함으로써 객체가 제거된다.

 

 

 

마치며

  • 모든 파이썬 객체는 정체성, 자료형, 값을 가지고 있다. 코드가 실행되는 동안 객체는 값만 바뀔 뿐이다.
  • 변수가 참조를 담고 있다는 사실은 파이썬 프로그래밍에서 객체의 바인딩과 생성에 영향을 미치고, 개발자는 이를 고려하여 코드를 작성해야 한다.

 

원문: https://www.fluentpython.com/