C나 C++를 공부한 적이 있다면 Call by reference와 Call by value에 대해 들어본 적이 있을 것이다.
두 방식은 함수의 인자 전달 방식인데, 각각 어떤 타입을 전달하느냐에 차이를 둔다.
1. Call by value : 변수를 복사한 값을 전달하는 방식
함수의 인자(argument)를 받을 때, 변수에 담긴 값 자체를 stack에다가 복사하여 넘겨준다.
예를 들어 변수 a가 있고 함수 def1가 있을 때,
def1(a)에서 전달받은 a는 a 자체(주소 값)가 아니라 a의 복사 값, 레플리카이다.
따라서 함수 내에서 해당 인자를 조작하여 바꾸었다고 해도 원본 변수 a는 변하지 않는다.
원본을 건드리지 않아 안전하지만, 함수의 사용으로 해당 전역 변수를 바꾸고 싶을 때는
리턴 값을 다시 전역 변수로 집어넣어야 하는 번거로움과 시간 사용의 단점이 있다.
2. Call by reference : 인자로 받은 변수의 주소 값을 전달하는 것
함수의 인자를 받을 때, 변수가 가리키는 주소 값을 전달한다.
예를 들어 변수 a가 있고 함수 def2가 있을 때,
def2(a)에서 전달받은 a는 원본 전역 변수 a의 주소 값이다.
(여기서 말하는 변수는 특정 저장장소-메모리-를 할당받았으며, 그 장소는 주소 값이 존재한다)
따라서 함수 내에서 해당 인자를 조작하면 원본 변수의 주소 값으로 타고 들어가 해당 값 자체를 바꿔버린다.
이를 통해 전역 변수의 즉각적 변경이 가능하지만, 부주의하면 변수를 잘못 조작해 프로그램에 문제를 야기할 수 있다.
조악하게 비유하자면,
시험 범위를 정리해놓은 노트가 있다고 하자.
그 노트의 내용에 필기를 추가로 해가며 공부하려고 하는데,
Call by value의 경우, 그 노트를 가져와 복사하여 복사본 위에 필기를 추가로 하는 것이다. 당연히 원래 노트에 무언가 덧쓰이지는 않는다.
Call by reference의 경우, 원본 노트가 있는 사물함 위치를 가르쳐주는 것이다(주소 값 참조).
그 사물함에 가서 노트를 가져와 그 위에 필기를 덧쓰는 것이니, 당연히 원본 노트는 내 필기가 추가되고 더러워질 수 있다.
이 과정에서 원본 노트의 틀린 부분이나 바꿀 부분을 변경할 수는 있지만, 자칫하면 중요한 부분을 건드려 노트를 망칠 위험이 있다.
그렇다면 Python에서의 함수 인자 전달 방식은 무엇일까?
정답은
3. Call by assignment이다.(또는 Call by object-reference)
python의 경우는 위의 경우처럼 주소 값 참조나, 값 복사와 조금 다르다.
이유는 python은 모든 것을 "객체"로 판단한다는 데에 있다.
객체는 현실세계로 비유하자면 '물건' 혹은 'thing'으로 생각하면 된다.
python에서 변수를 선언할 때,
a="alpaca"라는 선언문이 있다고 생각해보자.
선언문이 실행되면 "alpaca"라는 문자열 객체가 생기고, 그 객체에 대하여 a라는 이름표를 붙이게 된다.
즉, python에서 변수는 위에서처럼 특정 메모리 공간을 할당받은 컨테이너 개념이 아니라, 어떤 객체에 붙여진 이름표일 뿐이다.
물건에는 위치가 존재할 수 있다. 예를 들어 알파카 인형을 합정역 31번 사물함에 숨겨뒀다면
"알파카 인형"은 객체, "합정역 31번 사물함"은 알파카 인형의 위치, 즉 주소 값이 된다.
그러나 알파카 인형에 붙여진 이름표, 즉 변수에 대해서도 위치(주소값)를 지정해 두지는 않는다.
"알파카 인형"에는 여러 가지 이름표(변수)가 붙어있을 수도 있다.
예를 들어 해당 인형에 "귀여운 동물", "복학생 아싸"등의 여러 이름표가 붙어있다고 하자.
이때 변수"귀여운 동물"의 주소 값(위치)과 변수"복학생 아싸"의 주소 값(위치)은 객체"알파카 인형"의 주소 값(위치)으로 같다.
다시 Call by object-reference이야기로 돌아가자면,
python에서는 global인지 local인지 영역에 따라 변수들의 정보를 저장하는 namespace가 따로 있다.
즉, 전역 변수를 함수에서 인자로 받아오더라도 함수 내에서는 지역변수(이름표)에 불과하다.
함수 내에서 이름표를 떼서 다른 객체에 붙인다고 하더라도,
그 이름표는 함수 내에서만 사용하는 이름표일 뿐이다.
결국 함수 호출이 끝나면 전역 변수(이름표)가 여전히 그 객체에 붙어있다.
예를 들어 list 1 = [1,2,3,4] 일 때,
함수 내에서 list 1을 [5,6,7,8]이라는 새로운 객체랑 binding 한다고 해도
함수 호출이 끝나면 list 1은 그대로 [1,2,3,4]이다.
주의할 점은
이름표(변수)만 떼고 붙이는 것이 아니라, 이름표가 붙여진 물건(객체)의 구성품을 직접 조작하는 경우이다.
예를 들어 위의 예시와 같이 list 1 = [1,2,3,4] 일 때,
함수 내에서 list 1이라는 이름표가 붙여진 객체 [1,2,3,4]에 대하여
list 1 [0] = 5
이와 같이 객체 내의 요소(element)를 조작할 수 있다.
이 경우에 함수의 호출이 끝나서 지역 이름표가 전역 이름표로 바뀐다고 하더라도
객체가 변한 상태이므로 list 1은 [5,2,3,4]가 된다.
"복학생 아싸" 이름표가 붙은 알파카 인형의 털을 초록색으로 염색했다면
"귀여운 동물" 이름표가 붙은 알파카 인형의 털도 초록색인 게 당연하지 않겠는가?
(하나의 알파카 인형에 두 개의 다른 이름표가 붙어있으므로)
이때, 객체 자체를 바꾸려면 당연히 객체가 mutable, 즉 가변적인 포맷이어야 한다.
따라서 Call by object-reference 방식은
immutable 한 포맷의 객체(tuple 등)는 변경할 수 없지만,
mutable한 포맷의 객체(list, dictionary, 직접 만든 클래스 등)는 변경할 수 있다는 특성을 갖는다.
-------------------------------------------------------------
파이썬과 objective-reference, scope, mutables
(https://devdoggo.netlify.com/post/python/python_objective_reference/)
글에서 많이 참조했습니다.
미흡한 점이 많을 수 있습니다. 지적 부탁드립니다.
----------------------------------------------------------------
2021-02-23 용어를 수정했습니다.
Call by Objective reference(X) -> Call by object-reference
Call by Object(X) -> Call by assignment
댓글