파이썬으로 알고리즘을 풀다보면 파이썬의 주소값 체계 때문에 배열이 꼬이는 현상이 자주 발생한다.
이를 해결하기 위해 이번기회에 파이썬의 동작에 대해 완벽하게 이해해보자
1. 파이썬의 변수 할당의 의미
파이썬의 변수는 모두 포인터인가? 혹은 c++의 참조자인가? 에 대한 얘기가 많다.
찾아보니 파이썬의 variable은 해당 객체에 name을 binding 한다는 얘기가 있다.
무슨뜻인가?
바로 이런거다
a=1
1이라는 객체의 이름을 a 라고 하겠다! 라는 의미이다
2. C++ 포인터, 참조자와의 비교
한번 직접 실험해보자
python
>>> a = 1
>>> b = a
>>> a = 2
>>> b
1
C++ 포인터
#include <iostream>
#include <string>
int main()
{
int num = 1;
int* a = #
int* b = a;
*a=2;
std::cout<<*b<<std::endl;
}
결과 : 2
C++ 참조자
// Example program
#include <iostream>
#include <string>
int main()
{
int num = 1;
int& a = num;
int& b = a;
a=2;
std::cout<<b<<std::endl;
}
결과 : 2
둘이 상반된 결과를 보인다.
3. 어떻게 쉽게 이해할까?
우리가 보통 다른언어에서 변수를 만들면 상자를 만드는것에 비유한다
파이썬은 반대로 변수를 만드는 것은 상자에다가 이름표를 붙이는것으로 비유할 수 있다
즉 변수할당의 형태는 다음과 같다
이름표 = 상자
그럼 아래 상황은 쉽게 이해된다.
>>> a = 1
>>> b = a
>>> a = 2
>>> b
1
- 1이라는 상자에 a라는 이름표를 붙인다
- b라는 이름표를 a라는 이름표가 붙은 1이라는 상자에 붙인다
- a라는 이름표를 1에서 떼가지고 2라는 상자에 붙인다
- b는 여전히 1이라는 상자에 붙어있다
4. 리스트와의 조합
위 내용을 기반으로 하면 아래와 같은 상황이 쉽게 이해가 된다
>>> a = []
>>> b = a
>>> a[0] = 9
>>> b
[9]
- a라는 이름표를 하나의 리스트 상자에 붙인다
- b라는 이름표도 a라는 이름표가 붙은 상자에 붙인다
- a라는 이름표가 붙은 상자의 값을 바꾼다
- b라는 이름표도 같은 상자에 붙어있으니 출력해보면 값이 바뀌어 있다
5. Mutable과 Immutable 객체
재밌는 내용이 하나 있다
>>> a = []
>>> b = []
>>> id(a)
1773155443712
>>> id(b)
1773155425664
#둘이 다르다!
>>> a = 1
>>> b = 1
>>> id(a)
140708013200168
>>> id(b)
140708013200168
#둘이 같다!
위코드에서 앞부분은 똑같이 텅빈 리스트를 만들었는데 가르키는 주소값이 다르고, 아래의 경우 똑같은 1을 가리켰는데 주소값이 같다.
이는 mutable과 immutable 객체의 차이이다
Mutable 객체
한번 값이 할당되면 바뀔 수 없다. 그리고 중복되지도 않는다
Immutable 객체
값이 바뀔 수 있다. 중복될 수 있다
즉 1이라는 객체는 한번 생성되면 변하지도 않고 중복되지도 않으니 위 코드에서 후자부분은 같은 id값을 가지는 것이다
Mutable 데이터 타입
- 사용자 정의 객체
- list
- dictionary
- set
Immutable 데이터 타입
- int
- float
- decimal
- bool
- string
- tuple
- range
6. 배열이 꼬이는 현상에 대한 이해
먼저 아래의 코드를 보자
>>> a = [0] * 3
>>> a
[0, 0, 0]
>>> a[0] = 9
>>> a
[9, 0, 0]
그리고 아래 코드를 보자. 배열이 꼬이는 예시이다
>>> a = [[0] * 3] * 3
>>> a
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
>>> a[0][0] = 9
>>> a
[[9, 0, 0], [9, 0, 0], [9, 0, 0]]
왜 이런현상이 발생했을 까?
먼저 [0] * 3 케이스부터 살펴보자
- [ 이름표 ] * 숫자 -> 리스트 안에있는 이름표를 숫자만큼 복사하라는 뜻이다.
- [0] * 3 의 연산으로 a[0], a[1], a[2] 라는 이름표가 0 을 가리키게 된다
- a[0] = 9 는 a[0]라는 이름표를 9 라는 상자에 붙인 것이다
- a[1], a[2]는 이름표는 여전히 0이라는 상자에 붙어있다
다음으로 [ [0] * 3] * 3 ] 을 살펴보자
- [ [0] * 3 ] * 3 의 안에있는 식부터 해결해보자
- [ [0,0,0] ] 여기서 a[0] 는 [ 0, 0, 0] 이라는 상자를 가리키는 이름표이다
- a[0]를 가리키던 상자에 a[1], a[2] 라는 새로운 이름표가 생겼다
- a[0][0] = 9 는 a[0]가 가리키던 상자에 0번째 이름표를 9라는 상자에 붙이는 작업이다
- a[1], a[2]의 이름표들은 a[0]와 같은 상자를 가리키므로 값이 같이 바뀌어 버린다
이를 해결하려면? 이름표를 복사하는게 아닌 상자를 새로 만드는 작업을 하면 된다!
>>> a = [[ 0 for _ in range(3)] for _ in range(3)]
>>> a
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
>>> a[0][0] = 9
>>> a
[[9, 0, 0], [0, 0, 0], [0, 0, 0]]
7. 얕은복사와 깊은복사의 정확한 비유
위 예시에서 우리는 한가지 사실을 알 수 있다.
- 얕은복사는 이름표를 복제하는 작업니다.
- 깊은복사는 상자를 새로 만드는 작업이다
얕은복사의 예시
b = a[:]
b = a.copy()
는 모두 이름표를 복사하는 얕은 복사이다
>>> box = [1,2,3]
>>> a = [box]
>>> b = a[:] # 혹은 a.copy()
>>> box[0] = 9
>>> b
[[9, 2, 3]]
>>> id(a[0])
1773155424000
>>> id(b[0])
1773155424000
깊은복사의 예시
copy 모듈의 deepcopy를 사용하면 상자를 새로 만들어서 복사한다!
>>> import copy
>>> box = [1,2,3]
>>> a = [box]
>>> b = copy.deepcopy(a)
>>> b[0]
[9, 2, 3]
>>> id(b[0])
1773155425664
>>> id(a[0])
1773155424000
이제 파이썬을 다룰 시 배열이 꼬이는 현상, 혹은 변수가 꼬이는 현상에 대해서 완벽히 이해 할 수 있다
'Python > 알고리즘팁' 카테고리의 다른 글
파이썬의 iterator 사용법 (0) | 2023.01.06 |
---|---|
파이썬 다차원 배열 복사 시 주의사항 (0) | 2022.12.15 |
파이썬 약수 구하기 (0) | 2022.12.13 |
파이썬 Queue vs Deque 어느것을 사용할 까? (0) | 2022.12.09 |
[파이썬] global과 nonlocal 이해하기 (0) | 2022.01.21 |
댓글