파이썬

변수는 자료의 주소를 담는 박스임을 알면 Call-by-value, Call-by-reference, 얕은 복사, 깊은 복사 이런 거 껌이다

supersumin 2024. 8. 13. 22:28

1. 변수가 저장되는 과정

C언어에서 'a = 5;'와 같은 코드를 작성할 때, 'a'는 5라는 값을 저장하는 것이 아닌 메모리의 특정 위치에 5라는 값이 저장되는 정수형 변수이다.

 

'a'는 5를 담고있는 그릇, 박스라고 생각할 수 있다. 변수 'a'는 RAM의 스택 영역에 위치하며(전역 변수의 경우 Data Segment) 그 메모리 위치에 5라는 정수 값이 저장된다.

 

* 변수 'a'에 5가 아닌 5가 저장된 주소가 저장되어 있다면 왜 주소로 출력되지 않을까? *

프로그래밍 언어는 일반적으로 변수의 값을 읽고 쓸 수 있도록 설계되어 있다. 컴퓨터의 메모리는 데이터를 저장하는 장소로, 메모리 주소를 통해 접근된다.

 

메모리는 기본적으로 주소를 가지며, 이 주소를 통해 CPU는 실제 메모리에서 데이터를 읽거나 쓸 수 있다.

 

컴퓨터는 주소를 받으면 주소에 있는 값을 표현하지 주소를 표현하지는 않는다. 하지만 C언어에서는 &연산자를 통해 주소를 직접 다룰 수 있다. 

 

2. 주소값 넘김의 이용 사례

- 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)

immutable 객체는 얕은 복사든 깊은 복사든 원본 객체와 동일한 주소를 가지기 때문에 객체가 참조하는 내부 객체까지 모두 복사하는 깊은 복사는 불필요할 수 있다. 일반적으로는 복사의 필요성이 매우 적다.

 

a = 5, b = 'Hello World'

 

와 같이 특정 문자를 받기 위해서 쓰이는 느낌이지 이를 수정하고자 하면 새로운 객체를 생성해야 하기 때문이다.

복사를 통한 변경 가능성 방지나 데이터 구조 일관성 확보, 프로그래밍 안전성 등의 이유로 복사하는 경우가 있긴 하다.

 

mutable 객체를 얕은 복사하게 되는 경우는 참조(주소)만 복사하게 된다. 그러면 복사된 변수는 참조를 공유하게 되므로 복사본을 수정하면 원본도 바뀌게 된다.

 

mutable 객체를 깊은 복사하게 되는 경우는 참조뿐만 아니라 내부에 포함된 모든 객체들까지 복사하기 때문에 복사본은 원본과 달리 독자적이게 된다. 복사본을 수정해도 원본은 바뀌지 않는다는 뜻이다.

 

또한 복사된 객체가 원본 객체와 다른 메모리 주소를 가지도록 만들기 때문에, 원본 객체와 복사된 객체는 서로 독립적인 객체가 됩니다. 따라서 한 쪽의 변경이 다른 쪽에 영향을 미치지 않습니다.

*요약*

immutable => 복사를 하는 경우는 별로 없다. 얕은 복사와 깊은 복사 결과가 같다.

mutable => 얕은 복사: 참조만 공유되기 때문에 내부 객체가 수정되면 원본 데이터도 수정된다.

                   깊은 복사: 주소가 다른 새로운 객체를 생성, 내부 객체까지 모두 복사하기 때문에 원본과 독립적인 객체 생성

 

*용어정리*

  • Mutable: "변경 가능한"이라는 뜻이다. 라틴어 'mutare'는 "변경하다는"는 어원에서 나왔다. 객체가 변경될 수 있음을 나타내며, 객체의 상태나 내용을 수정할 수 있는 속성을 가진 객체이다. 예를 들면 파이썬의 리스트(list), 딕셔너리(dict), 집합(set) 등이 mutable 객체이다.
  • Immutable: "변경 불가능한"이라는 뜻이다. 객체가 생성된 후에는 그 상태를 변경할 수 없다. 객체의 내용이나 상태를 수정할 수 없으며, 새로운 값을 만들려면 새로운 객체를 생성해야 한다. 예를 들어, 파이썬의 튜플(tuple), 문자열(str), 정수(int), 부동 소수점(float) 등이 immutable 객체이다.
  • 얕은 복사(Shallow Copy): 최상위 레벨의 객체만 복사되며, 그 객체가 참조하는 내부 객체들은 복사되지 않고, 원본과 같은 참조를 공유한다. 따라서 새로운 객체가 생성되지만, 그 내부의 참조된 객체들은 원본과 동일한 객체를 참조하게 된다. 얕은 복사는 복사본을 통해 원본도 같이 변할 수 있다는 장점이 있다.
  • 깊은 복사(Deep Copy): 객체와 그 객체가 참조하는 모든 내부 객체들까지 재귀적으로 새롭게 복사한다. 그래서 원본과 완전히 독립된 새로운 객체가 만들어집니다. 원본 데이터를 유지하고 싶을 때 유용하게 사용할 수 있다.

 

*예시*

  • `deep_copied_list[0] = 'a'`는 `deep_copied_list`의 첫 번째 요소를 'a'로 변경한다. 이 변경은 최상위 객체를 새로운 값으로 변경하는 것이기 때문에 원본데이터의 참조는 변하지 않게 된다.
  • `deep_copied_list[3][0] = 'b'`는 `deep_copied_list`의 내부 리스트 `[4, 5, 6]`의 첫 번째 요소를 'b'로 변경한다. 이때, 이 내부 리스트는 `original_list`와 `deep_copied_list` 모두가 참조하는 동일한 객체인 얕은 복사가 이루어진 상황이므로, 내부 객체는 변하여도 최상위 객체는 공유하므로 변화가 공유된다.

  • 깊은 복사가 이루어진 경우 복사된 객체는 원본 객체와 독립적인 객체이다. 서로 다른 참조를 하기 때문이다. 그렇기에 수정이 원본에 반영되지 않는다.

=> 최상위 객체가 바뀌는 경우는 원본 데이터에 영향을 주지 않지만 최상위 객체가 안 바뀌고 하위 원소만 바뀌는 경우는 원본 데이터가 같은 주소를 참조하기 때문에 원본에 영향을 준다.

 
*얕은 복사와 깊은 복사를 하는 방법*
얕은 복사
  • 슬라이싱: 슬라이싱 연산자를 사용하여 얕은 복사 수행

  • copy.copy() 함수: 'copy' 모듈의 'copy()' 함수를 사용

 

깊은 복사

  • copy.deepcopy() 함수: 'copy' 모듈의 'deepcopy()' 함수를 사용하여 깊은 복사를 수행

 

 

- 함수를 통해 값을 변경하고 싶다면 주소를 참조해야 한다.

위의 예시와 마찬가지로 함수를 호출할 때 변수를 전달하는 과정에서 copy가 일어나게 되는데 이 때 의도와는 다른 결과가 일어날 수 있다. 예시를 보며 확인해보자.

 

1) Python의 인수 전달 방식: Call-by-Object Reference

파이썬에서는 함수 호출 시 인수의 객체 참조를 전달한다. 함수는 인수의 객체를 가리키는 주소를 받게 된다는 뜻이다. 객체의 mutable 여부에 따라 함수에서 인수를 어떻게 수정할 수 있는지가 달라지게 된다.

 

  • Immutable 객체: 파이썬에서 함수 호출 시 전달된 객체의 값은 변경되지 않고, 함수 내에서 변경이 필요한 경우 새로운 객체를 선언한다.

함수를 호출하며 x는 a를 얕은 복사 => x는 함수 내에 존재하는 변수로 x=10을 참조하게 됨가 동시에 x는 a와 다른 값을 참조 => x는 함수 호출이 끝나며 메모리에서 사라지는 변수(스택 프레임) => a의 값은 변하지 않는다.

 

  • Mutable 객체: 파이썬에서 함수 호출 시 전달된 객체는 직접 수정할 수 있으며 함수 내에서 변경된 내용은 원본 객체에도 영향을 준다.

list를 전달할 때 참조를 전달하는 얕은 복사이며 list를 함수에서 수정한다면 그 결과가 원본에도 적용되는 모습이다.

 

이 때도 함수 내에서 객체를 다시 생성하면 원본과 독립적인 객체로 생성되니 원본까지 수정되지는 않는다.

 

 

2) C언어의 인수 전달 방식: Call-by-value와 Call-by-reference

함수에 인수를 전달할 때 사용하는 방법이다. C 언어에서는 직접적으로 Call-by-Reference를 지원하지 않지만, 포인터를 사용하여 비슷한 효과를 낸다.

  • Call-by-Value(값에 의한 호출)

함수에 인수를 전달할 때 인수의 값을 복사하여 전달하는 방식이다.

a는 5의 주소값이 저장

함수 호출을 통해 x = a하는 얕은 복사 진행

x = 10을 통해 10이라는 새로운 값의 주소를 할당

x는 함수 내의 변수이므로 함수가 종료하면 메모리에서 삭제된다.

=> a는 변함이 없다.

 

  • Call-by-Reference(주소에 의한 호출)

함수 호출 시 변수의 메모리 주소가 전달되며 이 경우, 함수는 원본 변수의 주소를 통해 직접 접근할 수 있다.

&연산자는 주소 그 자체를 다룰 수 있게 해주는 연산자이며 *는 메모리 주소에 저장된 실제 값을 참조하거나 변경하는 데 사용된다.

 

포인터 'x'가 특정 메모리 주소를 가리키고 있을 때, '*x = 10;'이라는 코드는 그 메모리 주소에 저장된 값으 10으로 변경하겠다는 뜻이다.

 

같은 주소를 사용하지만 주소에 저장된 값이 바뀌었기 때문에 a의 값은 10으로 바뀐다.

 

*: 선언 시 해당 변수가 포인터임을 나타내고, 포인터가 가리키는 메모리 주소에 실제 저장된 값을 변경할 때 사용한다.