본문 바로가기
개발 (언어)/Python

[Python] 함수, (Im)mutable, 얕은 복사

by 진현개발일기 2023. 4. 25.

■ 함수 

 함수는 def로 정의가 가능하다. 아래는 재귀 함수를 만들어 활용해봤다.

재귀 함수 예시

■ Mutable / Immutable

Python의 객체[변수]는 크게 immutable, mutable로 나뉜다.

mutable은 이름 그대로 변경 가능한 객체를 의미한다. 예로는 리스트, 딕셔너리, 집합(set) 등이 있다.

immutable은 이름 그대로 변경이 불가능한 객체를 의미한다.  예로는 숫자, 문자열, 튜플 등이 여기에 속한다.

 

(1) immutable

x = 4

y = x를 할당할 경우. 아래와 같이 동일한 주소값을 갖게된다.

주소값을 확인하는 함수는 id()이다.

하지만 같은 주소값임에도 불구하고 y = 7을 대입했을 때 y 값만 바뀌는 것을 확인할 수 있다.

 

또한 y값이 바뀌면서 y의 주소값이 달라진 것을 확인할 수 있다.

 

(설명)

x = 4

y = x 에서

 

x는 4의 값을 변수에 직접 저장하는게 아니라 객체에 대한 참조를 저장하는 것이다. 객체는 메모리에 저장되고 변수는 해당 객체의 주소값을 가리키고 있다. 

 y는 x가 4의 값을 가진 동일한 메모리에 대한 동일한 참조값을 바라보고 있다는 뜻이다.

 

 여기서 Immutable의 특징을 이해할 수 있다. y = 7를 대입했을 때 y의 값은 '변할 수 없기' 때문에 x의 참조값을 변경하는 것이아니라 '7'이라는 값을 가진 새로운 메모리를 생성하고 그 메모리의 주소값을 바라보는 참조값을 y에 대입한 것이다.

그렇기 때문에  y=x와 y=7의 주소값이 달라진 것이다.

 

즉, 참조값을 변경하지 못하고 새로운 메모리를 생성하는 것이 immutable의 특징이다.

문자열을 예시로 추가하자면

 

(추가 예시)

firstStr = "hello"인 상태에서

firstStr = "World"라는 새로운 값을 할당한다면, "hello"라는 데이터가 존재하는 주소에 들어가 값을 변경하는 것이 아니라

World라는 개체를 가진 메모리를 새로 할당하여 그 주소를 가리키는 참조값을 firstStr에다 대입해주는 것이다.

 

 * 그러면 hello에 대한 메모리는??

파이썬에는 C#과 같이 가비지 컬렉터가 존재한다. 그래서 World라는 개체를 위해 메모리를 새로 할당하여 참조값을 대입해준다면, "hello"를 참조하는 변수가 존재하지 않게된다. 이때 가비지 컬렉터는 객체를 참조 하고있는 횟수 (reference count)를 확인해 참조 카운트가 0이 되는 객체는 더 이상 사용되지 않는 것으로 간주하여 메모리를 정리하고 회수한다.

 

(추가 의문점)

a = 1

b = 1도 같은 주소값을 갖고있다. 따로 할당을 했음에도 불구하고 주소값이 같은 이유가 궁금해서 찾아봤는데

 

아까 위에서 x = 4는 4의 값을 변수에 저장하는게 아니라 '4'라는 데이터를 갖고있는 개체의 주소를 참조하는 값을 대입한 것. 이라고 정의했다. 그렇다면 다시,  a = 1, b = 1 예시로, 돌아와서 보면 a는 1를 갖고있는 개체를 참조하는 값을 대입한 것이기 때문에 b 같은 경우에도 동일한 값을 참조하면 또 다른 메모리를 만들어 공간을 차지할 필요가 없다. 

 

 (★) 즉, 파이썬은 메모리 효율성을 위해 내부적으로 작은 정수 값들 (일반적으로 -5부터 256까지) 미리 생성해놓고 공유한다고 한다. 이를 정수 객체 풀(Integer Object Pool)이라고 한다.

그래서 같은 값을 가진 작은 정수 객체들은 메모리에서 같은 위치를 가리키고있고 위 예시를 기준으로 'a is b'를 하면 True를 반환한다.

 

 

 

(2) mutable

 

아래 a라는 리스트를 만들어봤고  List의 주소값과 원소들의 각 주소값들을 확인했다.

 

a[0] 즉 0번째 인덱스에다가 '5'를 할당했다.

immutable인 0번째 인덱스의 참조값은 '0' 개체의 주소값에서  '5' 개체의 주소값으로 바뀐 것을 확인했다.

mutable인 a 리스트의 주소값은 동일한 것을 또 확인했다.

 

 

■ 얕은 복사

C#에서의 List를 생각하고 이것저것 따라해봤는데 다른 점들이 있었다.

 

C#에서

List a = new List() { 0, 1, 2};

List b = a; 

를 한 뒤에 b의 원소 값을 변경하면 a의 동일한 인덱스의 값이 변경된다. 그래야한다 얕은 복사이기 때문에

그런데 Python에서는 b = a[:]로 얕은 복사를 할 경우 동일 인덱스의 원소가 안바뀐다...;

아니 그러면 깊은 복사 아니야? 라고 생각을 했는데 차이점을 다소 이해하게 되었다.

 

Python에서 말하는 얕은 복사는 '원본 객체와 복사된 객체가 같은 메모리에 저장된 객체들을 참조하는 것'을 의미한다.

얕은 복사 방식에는 (아직까지 공부한 거로는) 크게  두 방식이 있다. 

 

[종류]

1. 'b = a'  와 같은 방식

이런 경우에는 b와 a는 동일한 리스트 객체를 참조한다. 이 경우 a와 b는 완전히 같은 객체이므로 하나를 변경하면 다른 하나도 변경이 된다.

Python
a = [4, 1, 2, 5]
b = a[:]
a[0] = 99

print(a)  # [99, 1, 2, 5]
print(b)  # [4, 1, 2, 5] - a와 다른 객체를 참조하기 때문에 변경되지 않음

(예시)

 

 

2. 'b = a[:]' 혹은 import copy후 copy.copy() 와 같은 방식

이런 경우에는 b는 a의 원소를 가진 새로운 리스트 객체를 참조한다. a와 b는 서로 다른 객체를 가리키므로, a의 원소를 변경해도 b에 영향을 주지 않는다. 이것도 얕은 복사의 한 형태이지만, 외부 리스트의 참조는 공유하지 않는다.

 

 이 방식의 얕은 복사는 내부 리스트나 참조 타입의 원소가 있는 경우에도 같은 원리로 작동한다. 내부 객체에 대한 참조는 공유되기 때문에, 내부 객체를 변경하면 두 리스트 모두 영향을 준다.

 

결론적으로 'b=a'와 'b = a[:]'는 둘다 얕은 복사의 한 형태이지만, 외부 리스트의 참조를 공유하는지 여부에 차이가 있다.

 

'b = a'완전히 동일한 객체를 참조하며,

'b = a[:]'외부 리스트의 참조를 공유하지 않는 독립적인 객체를 생성한다.

그러나 얕은 복사의 특성상, 공통점으로, 내부 객체에 대한 참조는 여전히 공유된다.

* 용어 정리
1. 외부 객체 (Outer Object)
 컬렉션 객체에서 원소들을 직접 저장하고 있는 객체를 말한다. 예를 들어, 아래 리스트 객체 자제가 외부 객체가 된다.




2. 내부 객체 (Inner Object)

컬렉션 객체 내에 들어있는 원소들이 다른 각체를 참조하고 있을 때, 참조된 객체를 내부 객체라고 한다.


위의 예에서 'inner_list'는 외부 객체인 'outer_list'의 원소로 포함되어 있으므로, 내부 객체이다.

 

결론은 

(A) 예시
a = [4, 1, 2, 5]
b = [4, 1, 2, 5]
(B) 예시
a = [4, 1, 2, 5]
b = a[:]

은 큰 차이가 없고. a[:]를 했을 때 a의 원소 중에 mutable(대표적으로 리스트, 딕셔너리 등등) 객체가 있을 때

차이점을 확인할 수 있다.

(A) 예시
(B) 예시

 

 

[Import Copy]

 위 종류 2의 Import Copy의 예시를 기록해봤다.

b는 a[:]와 같이 독립적인 객체를 생성하는 것을 확인할 수 있다. 그로 인해 주소값이 다르고 아래와 같이 

b의 외부 객체를 변경했을 때 a의 원소에 영향이 안가는 것을 확인했다.

 

▼ 반면에 내부 객체를 수정했을 때 a 리스트에도 영향을 주는 것 또한 확인해봤다.

728x90