파이썬에서는 immutable 객체인지 mutable 객체인지에 따라 참조하는 방식이 다르다.
- immutable 객체: 한 번 생성되면 값을 변경할 수 없는 객체. int, float, str 같은 자료형의 객체는 한 번 객체가 생성되면 그 객체의 값을 바꿀 수 없다.
- mutable 객체: 값을 변경할 수 있는 객체. list, set, dictionary 같은 자료형의 객체는 생성된 이후에 그 객체의 값을 바꿀 수 있다.
지금부터 immutable 객체와 mutable 객체를 참조하는 방식에 대해 알아보도록 하자.
파이썬의 변수 (C언어와의 차이점)
우선 파이썬의 변수가 값을 저장하는 것이 아닌 값을 참조한다는 사실을 알아야 한다.
이해를 돕기 위해 C언어와 파이썬을 예시로 들어 이해해 보자.
먼저 C언어에서 변수를 만들어 값을 저장하고 주소값을 출력하면, 아래와 같이 고정된 메모리 공간에 값이 저장되는 것을 알 수 있다. 값이 바뀐다고 하여도 그 값이 저장되는 주소값은 바뀌지 않는다. 서로 같은 값을 갖는 변수라고 하여도 서로 다른 메모리 공간이 확보되어 있기 때문에, 각각의 메모리 공간에 값이 따로따로 저장된다.
C언어에서 변수가 선언되면 값이 저장될 메모리 공간이 마련되고, 값은 그 메모리 공간에 저장된다. 따라서 int a;와 같이 메모리 공간만 마련하라는 명령도 당연히 유효하다.
#include <stdio.h>
int main(){
int a = 10;
int b = 10;
printf("a의 메모리 주소 = %p\nb의 메모리 주소 = %p\n", &a, &b);
a = 20;
b = 30;
printf("a의 메모리 주소 = %p\nb의 메모리 주소 = %p\n", &a, &b);
return 0;
}
반면 파이썬의 경우에는 변수가 값을 저장하지 않고 객체를 가르킨다. C언어로 비유하자면 변수가 포인터와 비슷한 역할을 하는 것이다. 파이썬에서 변수에 값을 할당한다는 의미는, 그 값을 갖는 객체를 변수에게 참조시킨다는 뜻이다. 파이썬에서는 C언어의 'int a;' 처럼 'a' 와 같이 변수만 선언하는 명령은 유효하지 않다. 이 이유도 파이썬의 변수는 객체를 참조하기 때문에 아무 객체도 참조하지 않는 변수는 의미가 없기 때문이다.
이러한 점을 알고 아래 내용을 읽어보면 이해가 비교적 쉬울 것이다.
immutable 객체
immutable 객체가 무엇인지 말로만 들어서는 무슨 말인지 이해가 잘 가지 않는다. 예시를 통해 확인해가면서 알아가보자.
int, float, str과 같은 자료형의 객체는 immutable하다. 즉, 참조하고 있는 객체의 값을 변경하는 것이 불가능하다는 뜻이다. 그래서 파이썬에서는 (immutable 객체를 참조하는) 변수의 값을 바꾸라는 명령을 받으면 새로운 객체를 생성하고, 기존의 참조를 끊고 새로 생성된 객체를 가르킨다. 아래 예시를 보면 변수의 값을 변경했을 때에, 참조하는 메모리 주소가 달라지는 것을 알 수 있다.
파이썬의 immutable 객체를 다루는 방식의 특별한 점은, 같은 값을 가르키는 변수의 경우에는 같은 객체를 가르키도록 한다는 것이다. 아래와 같은 예시를 보면 a와 b에 같은 값을 할당했을 때에, a와 b가 같은 객체를 참조하고 있는 것을 알 수 있다. 이는 immutable 객체 생성으로 인한 성능 저하를 막는 파이썬의 최적화 방법 중 하나이다. 밑에서 immutable 객체 생성으로 인한 성능 저하를 막는 방법에 대해 자세히 알아보자.
a = 1
b = 1
print("a가 참조하는 객체의 메모리 주소 = ", id(a))
print("b가 참조하는 객체의 메모리 주소 = ", id(b))
a = 2
b = 3
print("a가 참조하는 객체의 메모리 주소 = ", id(a))
print("b가 참조하는 객체의 메모리 주소 = ", id(b))
mutable 객체
반면 list, set, dictionary와 같은 자료형의 객체는 mutable 객체이기 때문에 값을 변경할 수 있다. 변경이 잦은 리스트나 딕셔너리의 특징을 보면 당연해 보인다. 특히 immutable 객체들과 달리 크기가 커지는 경우가 많다는 점을 보면, mutable 객체들을 immutable 객체로 만들면 요소를 추가할 때마다 mutable 객체의 크기 만큼의 메모리가 낭비될 것이므로 정말 비효율적일 것이다.
아래 코드는 mutable 객체 중 하나인 리스트를 참조하는 예시 코드이다. 결과는 예상대로 리스트에 값을 추가하는 변경에도 리스트 객체의 메모리 주소는 변하지 않았다. 또한 같은 요소들을 갖는 리스트이더라도, 서로 다른 메모리 공간에 저장되어, 한 리스트의 변경에도 다른 리스트는 변경되지 않음을 알 수 있다.
한편, mutable 객체인 리스트 안에 요소로서 존재하는 immutable 객체인 int형 객체는 여전히 immutable한 특성을 유지하고 있음을 알 수 있다. 아래 예시에서 보면 알 수 있듯이 리스트 안의 모든 3이 같은 메모리 공간에 저장되어 있음을 알 수 있다.
arr1 = [1,2,3]
arr2 = [1,2,3]
print("arr1이 참조하는 객체의 메모리 주소 = ", id(arr1))
print("arr2가 참조하는 객체의 메모리 주소 = ", id(arr2))
arr1.append(3)
arr2.append(3)
print("arr1이 참조하는 객체의 메모리 주소 = ", id(arr1))
print("arr2가 참조하는 객체의 메모리 주소 = ", id(arr2))
print("arr1[2]가 참조하는 객체의 메모리 주소 = ", id(arr1[2]))
print("arr2[2]가 참조하는 객체의 메모리 주소 = ", id(arr2[2]))
print("arr1[3]가 참조하는 객체의 메모리 주소 = ", id(arr1[3]))
print("arr2[3]가 참조하는 객체의 메모리 주소 = ", id(arr2[3]))
immutable 객체 생성의 비효율성과 최적화 방법
immutable 객체와 mutable 객체에 대해 공부하고 나면, 당연하게도 한 가지 궁금증이 떠오른다. 변수가 immutable 객체를 참조하고 있을 때, 값을 바꾸려고 하면 새로운 객체가 생성된다. for i in range(10000) 이라는 코드에 10000개의 int형 객체가 생성되는 것이다. 이렇게 많은 객체 생성은 메모리를 낭비시키고, 객체 생성 시간으로 인해 성능을 저하시킬 수 있다.
다행히도 파이썬은 immutable 객체의 생성으로 인한 성능 저하를 최대한 피하기 위해 Object Interning이라는 최적화 방법을 사용한다. Object Interning이란 동일한 값을 가지는 객체들을 동일한 메모리 주소를 공유하도록 하는 것을 말한다. 이를 통해 같은 값을 갖는 immutable 객체가 반복되어 생성되는 비효율적인 상황을 피할 수 있다. 심지어 정말 자주 사용되는 정수인 -5 부터 256 에 대해서는, 파이썬이 아예 미리 객체를 생성해두고 필요할 때 참조할 수 있도록 한다.
파이썬 내부적인 최적화 말고도 우리가 직접 사용할 수 있는 최적화 방법도 있다. 바로 mutable 객체를 이용하는 것이다. 문자열은 어떻게 보면 리스트와 비슷하게 동작한다. 이를 이용해 문자열을 리스트형으로 저장하는 것이다. 이미 존재하는 문자열에 다른 문자를 + 연산자를 이용해 더하면, 아예 새로운 문자열 객체가 생성된다. 반면에 각 문자를 요소로 하는 리스트에 문자열을 저장하고 append() 메서드를 통해 문자를 붙인다면, (이미 리스트에 있던 문자라면) 새로운 문자열 객체가 생성되지 않는다. 그렇게 리스트에 문자열을 저장해두었다가, 마지막에 "".join(list)를 이용해 문자열을 생성해준다면, 단 한 번의 객체 생성으로 최종적인 문자열을 얻어낼 수 있다.
'프로그래밍 언어 > Python' 카테고리의 다른 글
[Python] 파이썬의 슬라이싱은 깊은 복사일까, 얕은 복사일까? (0) | 2023.08.17 |
---|