파이썬의 변수는 진짜,
독특하다.
진짜 독특한 나머지 변수의 특성을 제대로 알지 못하면
이후에 나오는 개념이나 예시에서
`아니 시X 이게 왜?`
혹은 `왜 이건 되고 이건 안되고 대체 무슨 기준인지...`
하는 상황을 자주 맞게 되도록 되어있다.
1. 파이썬의 모든 변수는 객체로 간주된다.
모든 변수는 어떤 메모리 공간으로의 참조, 즉 레퍼런스를 가진 것으로 간주된다.
여기서부터 독특하다.
C나 Java를 배웠다면 int형 변수와 배열 변수를 함수에 넘겨보면서
Call by value, Call by reference개념을 익혀본 적이 있을 것이다.
보통 변수를 `레퍼런스를 가지는가`(동적) 아니면 `값을 가지는가`(정적) 에 따라 두종류로 나누는데
파이썬은 그런게 없다. int고 뭐고 전부 다 객체로써 레퍼런스를 가진다.
모든 종류의 변수가 일종의 랩핑된 객체이기때문에 오버헤드를 가지기도 한다.
(C의 int형이 4바이트, 파이썬에서 32767 이하의 숫자를 가지는 변수가 차지하는 크기 14바이트)
다만 `모든게 레퍼런스`임으로 생기는 차이를 쉽게 실감하진 않는다.
메모리를 이용하는 이상 변수가 어딘가를 가리키고있다는 사실은 당연한 것이며
int형 변수에 사칙연산을 수행한다던지, print 함수로 출력한다던지, 비교를 한다던지 할때는
레퍼런스가 아닌, 값을 가진 변수처럼 느껴지도록 잘 되어있기 때문에 당장에는 이 차이를 실감하기 어렵다.
당장에는.
※ 변수의 종류를 둘로 구분하는 언어의 경우
정적 변수는 스택, 동적 변수는 힙이라는 고유 공간에 올라가는데
둘을 구분하지 않는 파이썬은 이를 어떻게 처리할까?
답 :
파이썬 가상머신님께서 놓고싶은 곳으로 놓아주신다고 한다.
CPython은 전부 힙에 올리는데 이는 CPython만의 특징이므로 다른 파이썬(ex. pypy, Jython 등)에서는 다르게 동작할 수 있다.
2. Mutable과 Immutable
개인적으로 이런저런 구글링도중 이런저런 프로그래밍 영단어들을 마주쳤지만
파이썬을 시작하기 전에 mutable이라는 단어를 마주친적은 많진 않았던 것 같다.
있었어도 기억이 안날정도로 의식할 필요가 없었을 수도 있고.
그러나 파이썬을 접한 이후 mutable, immutable은 파이썬 관련 doc을 보는데 필요한 필수 영단어중 하나이다.
`이건 되고 이건 안되고`의 구분 기준이 변수형(type)과 관련있다면
해당 변수가 mutable이냐 immutable이냐 로 완벽하게 구분되는 경우가 많은 것 같다.
mutable의 뜻은 보시다시피 변할 수 있다는 것이다. 가변적.
immutable은 당연히 안변한다는 뜻이 되겠다.
여기서 논하는 `변할지 말지의 대상`은 변수의 레퍼런스가 가리키는 값을 말한다.
고로 immutable변수가 가리키는 값은 상수(Constant)라 볼 수 있겠으나 그렇다고 해당 변수의 값을 못바꾸는건 아니다.
mutable로 간주되는 타입 : list, dictionary, set(집합), bytearray, user-defined class(사용자가 직접 정의한 클래스)
immutable로 간주되는 타입 : int, float, complex, decimal, bool, string, tuple, range, frozenset, bytes
mutable이 의미하는 바를 알아보기 위해 list의 경우를 예로 들어보자면, 다음과 같이 선언된 리스트가 있다고 치자.
a = [1, 2, 3, 4]
우리는 a[0] = 0 등의 여러 문법을 활용해서 a가 가리키는 컨테이너의 내부원소 값을 바꿀 수 있다.
레퍼런스가 가리키는 데이터의 값을 변경할 수 있기때문에 mutable이다.
immutable이 의미하는 바를 알아보기 위해 string의 경우를 예로 들어보자면, 다음과 같이 선언되어 있을 때
str = "immutable"
마치 C의 포인터에 문자열을 할당한 것 처럼, str의 문자 하나하나를 바꾸는 것은 불가능하다. 에러난다.
레퍼런스가 가리키는 데이터의 값을 변경할 수 없으므로 immutable이다.
값을 변경할 수 없다는데 str에 통째로 새로운 문자열을 입력하는 것은 에러없이 잘 수행된다.
위의 문 다음에
str = "ismutable?"
이런 문장을 작성하고 실행시키면 에러 없이 잘 돌아간다.
마찬가지로 int, float과 같은 숫자형 변수들도 값을 통째로 바꾸는게 가능하다.
a = 10; a = 20
문제없이 실행된다.
..
엄연히 값이 바뀌고있다.
string은 일부 변경이 안되기라도 하지
int나 float은 애초에 단일한 값을 가지고있는데 이게 바뀌면 안바뀌는게 뭐가 있단말인가?
는 사실 다른 언어와 비교하면 사기처럼 보이겠다 싶을 수준의 메카니즘 때문이다.
C언어에서 &변수명 문으로 해당 변수의 주소를 가져올 수 있듯, 파이썬에서는 id(객체)로 객체의 id를 가져올 수 있다.
(메모리상의 위치를 뜻하는 주소와 달리 id는 VM상에서의 위치를 의미하기 때문에 주소가 아닌 id로 칭한다.)
길게말할 것 없이 다음의 코드 수행결과로 실체가 드러난다.
a = 200
print(id(a))
a = 300
print(id(a))
a = 200
print(id(a))
a = 300
print(id(a))
수행 결과:
1617683744
54975696
1617683744
54975696
주소에 해당하는 격의 id를 내놓은 결과물이므로 수행환경에 따라 값은 다르게 나타날 수 있지만
a가 같은 값을 가질 때 같은 id를 가진다는 사실은 동일하게 나올 것이다.
그림을 곁들여서 이 상황을 설명해보자면
a = 200 부분만 실행했을때의 상태이다. 여기까진 괜찮다.
이 다음 a = 300을 실행했을때 C언어였다면 다음과 같이 되었을 것이다.
a가 가리키는 곳(1617683744번지)의 값이 200에서 300으로 바뀌었을 것이다.
But 우리 파이썬은 여기에서 다음과 같이 처리하는 것이다.
새로운 수치값이 나오면 그 값이 저장될 새로운 공간을 할당하고 해당 값(300)을 저장,
변수 a가 가리키는 곳을 새로 할당한 공간으로 재배정한다.
a = 200문을 다시 실행한다면
이미 200이 저장된 곳으로 a가 가리키는 id값만 바뀌는 것이다.
a = 301 등으로 새로운 값을 넣는다면
새로운 값을 채우기 위해 새로운 메모리 공간을 사용하고
이전의 값을 가지고 있던 공간은 후일을 도모하기 위해 그 값을 가진 채 그대로 잔류한다.
이 값은 가비지콜렉션에 정리되기 전까지 메모리에 계속 잔류한다.
메모리 사용량이 어떻게 되던간에 주제로 돌아와서 보자면
변수가 가리키는 부분이 바뀔지언정, 변수가 가리키는 부분의 값이 바뀌지 않으니까 immutable인것이다.
값이 바뀌지 않는다고해서 다른 프로그래밍 언어처럼 값을 다루는데에 기능상의 문제는 없다.(ex. 사칙연산, 문자열 이어붙이기 등..)
오히려 다 잘되니까 이런 배경사정이 잘 느껴지지 않는 것 아닐까.
다만, 위에서 설명한 모든 특징이 결합되어
함수의 인자가 Call by Value/Reference 옵션들 중 어떻게 전달되는지에 영향을 끼친다.
3. 파이썬의 Call by Value? Reference?
Call by Value와 Call by Reference의 정의를 간략하게 짚고넘어가자면
Call by Value : 함수에 들어온 매개변수가 함수에 넣었던 인자의 값에 대한 복사본
예시 코드(C) :
void adding(int addingnum) {
addingnum++;
}
void main() {
int a = 0;
printf("현재 a변수값 : %dn", a);
adding(a);
adding(a);
adding(a);
printf("adding3회의 결과 a변수값 : %dn", a);
}
결과 :
현재 a변수값 : 0
adding 3회의 결과 a변수값 : 0
해석 : 변수 a는 adding함수에 자신의 복사본을 넘겨주었기 때문에
adding함수에선 변수 a와는 별개로 존재하는(같은 값을 가진 상태로 초기화됬을 뿐인) 매개변수 addingnum을 건드리는 것임으로
addingnum을 아무리 바꾼다고 한들, 변수 a에서는 아무 일도 일어나지 않는다.
Call by Reference : 매개변수가 인자의 레퍼런스에 대한 복사본
예시코드(Java) :
public class Test {
void function(Test[] paras) {
paras = new Test[5];
}
public static void main(String[] args) {
Test a[] = new Test[3];
System.out.println("a의 크기 : " + a.length);
for(int i = 1; i <= 3; i++)
a[i - 1] = new Test();
a[0].function(a);
System.out.println("a의 function메소드 실행 후 크기 : " + a.length);
}
}
결과 :
a의 크기 : 3
a의 function메소드 실행 후 크기 : 3
해석 : 변수 a는 function메소드에 자신의 레퍼런스 복사본을 넘겨주었기 때문에
function함수에서는 변수 a와는 별개로 존재하는(같은 값을 가진 상태로 초기화됬을 뿐인) 매개변수 paras를 건드리는 것임으로
변수 paras에 새로운 레퍼런스를 준다고 한들, 변수 a는 여전히 같은 레퍼런스를 가진다.
예시코드2(Java) :
public class Test {
public int variablenum = 0;
void function(Test para) {
para.variablenum++;
}
public static void main(String[] args) {
Test a = new Test();
System.out.println("a의 variablenum값 : " + a.variablenum);
a.function(a);
a.function(a);
a.function(a);
System.out.println("function메소드 실행 후 a의 variablenum값 : " + a.variablenum);
}
}
결과 :
a의 variablenum값 : 0
function메소드 실행 후 a의 variablenum값 : 3
해석 : 변수 a는 function메소드에 자신의 레퍼런스 복사본을 넘겨주었기 때문에
function함수에서는 변수 a와 같은 레퍼런스를 가짐으로써 a와 같은 객체를 para가 가리키게 되므로
para의 변수(variablenum)을 조작하는 것은 a의 variablenum을 조작하는 것과 같다.
(쓰기전엔 예상 못했는데.. 막상 적어보니 그렇게 간략하진 않게되었다..)
다른 언어에서는 보통 이 둘의 기준으로
인자에 Primitive(원시)변수가 오냐, Object(객체)변수가 오냐 로 구분을 한다.
Primitive변수는 주로 int, bool등의 기본 자료형같은 것들을 지칭하고
Object변수는 객체, 객체로된 컬렉션같은것들을 지칭한다.
프로시저(Procedure)에 인자로 넘어갈 때 Primitive변수는 Call by Value, Object변수는 Call by Reference로 넘어간다.
(예외적으로 C는 정적 변수는 Call by Value, 동적 변수(설령 객체 타입이더라도)는 Call by Reference로 넘어간다.)
Call by Value와 Call by Reference의 구분이 필요한 가장 중요한 이유로는
어떤 프로시저에 인자로 넘겨준 내용이 원본과 단절되느냐 마느냐를 결정하기 때문일 것이다.
Call by Value는 인자를 복사하여 따로 받은 매개변수를 아무리 조작해도 원본 인자와는 단절되어있기 때문에
프로시저 바깥 영역에 (프로시저 입력값을 이용한)영향을 주는 것은 불가능하지만
Call by Reference는 인자로 받은 레퍼런스가 가리키는 영역이 프로시저 바깥에서도 조작하고 있던, 프로시저와 공유되는 영역이기에
프로시저 바깥 영역에 (프로시저 입력값을 이용한)영향을 줄 수 있다.
각각 구분되는 일종의 위험성과 편리성을 가진 중요한 특징이다.
자. 이번 글에서 맨 처음으로 시작한 내용.
파이썬의 모든 변수는 객체로 간주된다.
파이썬에서는 위의 이유로, 변수가 인자로 넘어갈 때 모든 경우에서 Call by Reference로 넘어간다.
모든 경우에서 인자의 레퍼런스를 프로시저에 복사해 가는 것이다.
고로 모든 경우에서 프로시저는 레퍼런스가 가리키는 영역을 프로시저 바깥과 공유하게 된다.
그런데 이런 특징이 immutable변수의 특징과 결합되면 뭔가 이상한 형태가 된다.
결론만 먼저 말하자면
immutable변수를 인자로 받은 프로시저에서 무슨 짓을 해도 해당 인자의 원본 값에 영향을 줄 수가 없는
마치 immutable변수가 Call by Value로 전달 된 것 처럼 작동한다는 것이다.
값을 변경하는 조작(manipulation)연산의 모든 경우를 묶어서 다음과 같이 구분해보자.
삽입, 삭제, 갱신.
삽입은 완전히 새로운 값을 입력(연산자 =와 함께하는 것들에 해당)
삭제는 기존의 값을 삭제(더이상 해당 값을 조작할 수 없고 참조도 불가능한 접근 불가 상태)
갱신은 기존의 값에 기반한 수정(수치형의 사칙연산, 배열등의 컬렉션에 내부 원소 추가 등).
이외의 조작연산은 없다.
파이썬의 immutable변수를 갱신하는 연산은 위에서 얘기했던 대로 다음과 같이 이루어진다.
당신이 수치형 변수에 사칙연산을 한 결과를 넣던, 절충되거나 연장된 문자열을 할당하던간에
파이썬은 그 모든 과정을 다음과 같이 해당 변수에게 레퍼런스를 입력시켜주고 끝낸다.
새로운 값(ex. a = 200)을 넣어줘도 해당 값의 데이터를 가진 메모리 주소(id)를 연결시켜주고
뭔가 계산된 값(ex. a = a + 100)을 넣어줘도 해당 결과값의 데이터를 가진 메모리 주소(id)를 연결시켜줄 뿐이다.
삽입연산과 갱신연산을 같은 과정으로 볼 수 있겠다.
`변수 a를 인자로써 어떤 function프로시저영역에게 para라는 매개변수로 전달해줬을때의 상황`을 그림으로 가정해보자.
※ Call by Reference에서 매개변수는 인자의 원본이 아닌 복사본임을 인지하고 둘을 구분하는 것이 중요하다.
function프로시저영역에서 변수 a의 레퍼런스를 가졌던 para가 가리키는 값을
다른 값(다른 레퍼런스)으로 바꾼 결과는 다음과 같을 것이다.
para의 값을 그 어떤 날고 기는 방법으로 function영역 내에서 수정한다고 해도
function영역 바깥의 변수 a가 값 400을 가리키는 부분을 어찌 바꿔놓을 수 없다. 단절되어있다고 볼 수 있겠다.
삭제연산은 어떨까?
function프로시저 내에서 del para문을 실행했을 때의 상황을 가정해본 것이다.
a와는 별개의 레퍼런스 변수를 날려버린 것이기에 a는 버젓이 살아서 400을 가리키고있다.
값 400에 대한 원본 변수의 참조를 날려버릴 수 없는 이 상황 또한 원본과 단절되어있다고 볼 수 있겠다.
※ mutable변수의 del을 통한 삭제의 경우에도 위의 상황은 동일하게 일어난다.
del로 복사본 변수만 날려버려서 단절된다는 의미인데, mutable변수 또한 프로시저에게 인자로 전달될 때 복사본을 전달하기 때문.
삽입연산 = 갱신연산, 삭제연산 모두 immutable변수의 프로시저 내 매개변수 처리과정에서는 원본과 단절되어있는데
이는 다른 언어의 Call by Value와 같은 특징이다.
그러나 immutable변수의 프로시저 전달과정을 Call by Value로 칭할 수는 없다.
immutable변수를 전달할때는 일단 레퍼런스를 전달하는건데,
애초에 Call by Reference와 구분되어 사용되는 Call by Value의 의미 자체가
Reference가 아닌 Value만 전달된다는 뜻이니까!
정확하게 설명한다면
immutable 변수는 Call by Reference로 전달되지만
조작 시에는 Call by Value로 전달된 것 처럼 동작한다
는게 맞을 것이다.
mutable 변수는 조작연산중에도 다른 언어의 Call by Reference와 같은 특징을 띄니까
mutable 변수는 Call by Reference처럼, immutable 변수는 Call by Value처럼 동작한다고 볼 수는 있겠지만
그렇다고 immutable변수 전달을 Call by Value로 칭할수는.. 없는... 그런 상황인 것이다.
개인적으로 구글에서 변수에 대해 뒤지고 뒤진내용을 어느정도 종합하여 정리해보았습니다.
글을 쓴 저 또한 여러가지로 뒤져보고 알아보며 파이썬을 배워가는 입장이므로 틀린 부분이 있을 수 있기에 지적 및 보충 환영합니다.
최근 덧글