[PS] 배열 (리스트, 튜플)

방법이있지·2025년 5월 19일

훈련소에선 한 사람만 잘못해도 다함께 얼차려를 받죠. 리스트도 마찬가지입니다. 두 변수가 동일 리스트를 가리키면, 한 쪽만 바꿔도 나란히 얻어맞죠.

배열

  • 묶음 단위로 값을 저장하는 자료구조
  • 원소: 배열에 저장된 객체로, 0, 1, 2... 순으로 인덱스를 부여받음
  • 파이썬에선 배열을 리스트, 튜플로 구현할 수 있음

뮤터블 vs 이뮤터블

  • 리스트는 뮤터블: 원소를 변경할 수 있음
    • 뮤터블 자료형: 리스트, 딕셔너리, 집합
a = [1, 2, 3]
a[2] = 4
print(a)    # [1, 2, 4]
  • 튜플은 이뮤터블: 원소를 변경할 수 없음
    • 이뮤터블 자료형: 튜플, 문자열, 정수, 실수
b = (1, 2, 3)
b[2] = 4    # TypeError 발생

파이썬 변수의 참조 방식

  • Python의 변수는 값을 직접 저장하지 않음
  • 변수는 객체가 저장된 위치를 가리키는 '이름표'일 뿐
    • 😊 숫자, 문자, 리스트, 튜플, 함수, 클래스 등 파이썬의 모든 값은 객체이며, 변수는 그 객체를 가리키는 역할을 합니다.
a = 3           # 변수 a는 객체 3을 참조
print(id(3))    # 140722435000824
print(id(a))    # 140722435000824

b = 3
print(a is b)   # True
  • 변수에 객체를 대입하면, 변수는 해당 객체의 저장 위치를 가리키게 됨
    • 이를 변수가 객체를 참조한다라고 표현함
  • 모든 객체는 식별 번호를 가짐 -> id() 함수로 확인 가능
    • 실제로 객체 3과, 이를 참조하는 변수 a의 식별 번호가 동일함을 확인할 수 있음
  • 두 변수가 동일 객체를 참조하는지는 is로 확인 가능
    • 내부적으로 두 변수의 식별 번호를 비교

리스트의 경우

  • 리스트를 변수에 대입하고, 해당 변수를 다른 변수에 대입하면, 두 변수는 같은 리스트 객체를 참조함
  • 따라서 한쪽에서 값을 변경하면, 다른 한쪽도 바뀜
list1 = [1, 2, 3, 4, 5]

# list2와 list1이 참조하는 배열은 동일함
list2 = list1
print(list1 is list2)   # True

# list2의 원소값을 바꾸면, 공통으로 참조하는 리스트 객체가 수정되어 list2에서도 원소값이 바뀜
list1[2] = 5
print(list1)        # [1, 2, 5, 4, 5]
print(list2)        # [1, 2, 5, 4, 5]

# list2의 원소값을 바꾸면, 공통으로 참조하는 리스트 객체가 수정되어 list1에서도 원소값이 바뀜
list2[4] = -1
print(list1)        # [1, 2, 5, 4, -1]
print(list2)        # [1, 2, 5, 4, -1]

  • 😊 "어?? 정수로 똑같이 해 봤는데, 이러면 한쪽만 바뀌고 다른 쪽은 그대론데요?"라고 생각하시는 분들도 있을 겁니다
num1 = 3
num2 = num1
num2 += 2
print(num1, num2)   # 3, 5

  • 😊 하지만 num2 += 2는 실제로 num2 = num2 + 2와 같습니다. 이 연산은 객체 3의 값을 변경하지 않습니다. 애초에 int형은 이뮤터블이라서 값을 변경할 수 없어요.
  • 😊 대신 새로운 객체 5를 만들고, num25를 참조하도록 대입하게 됩니다.
num1 = 3
num2 = num1
print(num1 is num2) # True
num2 += 2
print(num1 is num2) # False
print(num1, num2)   # 3, 5
  • 😊 실제로 대입한 뒤 id를 비교해 보면, num1num2가 참조하는 객체가 달라졌음을 확인할 수 있죠

복사

  • 실제로 지금 리스트를 옮겨오고 싶은데, 원래 리스트의 값은 놔두고 싶을 때가 있을 것임
  • 이럴 땐 원래 리스트를 복사한 뒤 변수에 대입해야 함

얕은 복사

list1 = [1, 2, 3, 4, 5]

# list1의 값을 복사하여 다른 객체를 만들고, list2에 대입
list2 = list1.copy()
print(list1 is list2)   # False

# list2의 원소값을 바꾸어도 list1의 원소값은 바뀌지 않음
# 두 변수가 다른 객체를 참조하고 있기 때문
list1[2] = 5
print(list1)        # [1, 2, 3, 4, 5]
print(list2)        # [1, 2, 5, 4, 5]

  • list.copy() 메서드 사용 시, 원래 리스트를 복사해서 다른 객체를 만듦
  • 따라서 한쪽의 원소값을 바꾸어도, 반대쪽은 변하지 않음
  • 단, 위와 같은 얕은 복사는 2차원 이상 배열에선 정상 작동하지 않음
list1 = [[1, 2], [3, 4]]
list2 = list1.copy()

list2[1][1] = 5
print(list1)        # [[1, 2], [3, 5]]
print(list2)        # [[1, 2], [3, 5]]
print(list1[0] is list2[0]) # True

  • 얕은 복사: 객체(예: 바깥 리스트)가 갖는 멤버(예: 안쪽 리스트)의 경우, 멤버의 참조만 복사
    • list1[0][1, 2], list[1][3, 4]를 참조한다는 사실만 복사됨
  • list2의 바깥 리스트는 새로 생성되지만, 안쪽 리스트 ([1, 2], [3, 4])들은 원본과 같은 참조를 공유
    • list1[0]list2[0]은 동일 객체를 가리킴
    • list1[1]list2[1]은 동일 객체를 가리킴

깊은 복사

import copy
list1 = [[1, 2], [3, 4]]
list2 = copy.deepcopy(list1)

list2[1][1] = 5
print(list1)        # [[1, 2], [3, 4]]
print(list2)        # [[1, 2], [3, 5]]
print(list1[0] is list2[0]) # False

  • 깊은 복사: 객체(예: 바깥 리스트)가 갖는 멤버(예: 안쪽 리스트)의 경우, 멤버가 참조하는 객체 자체를 복사
  • list2의 안쪽 리스트 ([1, 2], [3, 4]) 역시 리스트 객체 자체가 복사됨
    • list1[0]list2[0]은 다른 객체를 가리킴
    • list1[1]list2[1]은 다른 객체를 가리킴
  • copy.deepcopy()로 깊은 복사를 사용해야 함

함수 전달 시

  • 함수에 인수를 전달하면, 매개변수는 인수와 동일한 객체를 참조함
  • 단 인수가 뮤터블인지, 이뮤터블인지에 따라 동작이 달라짐

인수가 이뮤터블일 때 (튜플 등)

  • 이뮤터블 객체는 내부 값을 변경할 수 없음
  • 함수 안에서 매개변수의 내용을 수정하면, 새로운 객체를 생성함
  • 원본 객체의 값은 변하지 않음
def add(t, value):
    t = t + (value,) # 새 튜플 생성
    return t

tup = (1, 2, 3)
print(add(tup, 4))  # (1, 2, 3, 4)
print(tup)          # (1, 2, 3) -> 원본은 그대로

인수가 뮤터블일 때 (리스트 등)

  • 뮤터블 객체는 내부 값을 변경할 수 있음
  • 함수 안에서 매개변수의 내용을 수정하면, 원본 객체 자체를 변경함
def change(a, idx, value):
    a[idx] = value  # 원본 리스트 직접 수정
    return a

array = [1, 2, 3, 4, 5]
print(change(array, 2, 10))     # [1, 2, 10, 4, 5]
print(array)                    # [1, 2, 10, 4, 5] -> 원본도 변경됨
  • 원본은 그대로 놔두고 싶다면, 함수 내에서 .copy()copy.deepcopy()를 사용해야 함
def change(a, idx, value):
    a = a.copy()
    a[idx] = value  # 원본 리스트의 복사본 수정
    return a

array = [1, 2, 3, 4, 5]
print(change(array, 2, 10))     # [1, 2, 10, 4, 5]
print(array)                    # [1, 2, 3, 4, 5]

이차원 배열 만들기

  • Python에서 2차원 배열을 만들 시, 아래와 같이 만들면 안 됨
a = [[0] * 3] * 2
print(a)            # [[0, 0, 0], [0, 0, 0]]
a[1][1] = 3
print(a)            # [[0, 3, 0], [0, 3, 0]]
print(a[0] is a[1]) # True
  • a[1][1]a[0][1]도 바뀐 것을 볼 수 있음
    • a[0], a[1]가 동일 객체를 참조하기 때문
a = [[0] * 3 for _ in range(2)]
print(a)            # [[0, 0, 0], [0, 0, 0]]
a[1][1] = 3
print(a)            # [[0, 0, 0], [0, 3, 0]]
print(a[0] is a[1]) # False
  • 위의 경우 a[0]a[1]이 다른 객체를 참조
  • 리스트를 반복해서 생성할 시 *가 아니라 for문을 사용해야 함!!

정리

특징뮤터블이뮤터블
예시리스트, 딕셔너리, 집합튜플, 문자열, 정수, 실수
값 변경가능 여부OX
함수 내부에서 변경 시기존 객체 변경 -> 원본 객체도 변경새로운 객체 생성 -> 원본 객체는 그대로

리스트 연산의 시간 복잡도

  • 리스트의 원소 수를 NN으로 둘 때,

O(1)인 경우

  • 인덱스로 리스트의 원소 접근하기: O(1)O(1)
a = [1, 2, 3, 4, 5]
print(a[2]) # 3
  • 리스트 맨 뒤에 값을 삽입할 때: O(1)O(1)
    • 기존 원소의 위치가 바뀌지 않아, 추가 연산 없음
a = [1, 2, 3]
a.append(4)
print(a)    # [1, 2, 3, 4]
  • 리스트 맨 뒤의 값을 삭제할 때: O(1)O(1)
    • 기존 원소의 위치가 바뀌지 않아, 추가 연산 없음
a = [1, 2, 3]
print(a.pop())  # 3
print(a)        # [1, 2]
  • 리스트의 길이를 확인할 때: O(1)O(1)
    • 일일이 원소 수를 세진 않고, 객체 내부에 길이가 저장되어 있음
a = [1, 2, 3]
print(len(a))   # 3

O(N)인 경우

  • 리스트 맨 앞 / 중간에 값을 삽입할 때: O(N)O(N)
  • 이후 원소들을 한 칸씩 뒤로 미는 연산을 수행
  • 밀어야 하는 원소 수는 최대 NN개이므로 O(N)O(N)
a = [2, 3, 4]
a.insert(0, 1)  # 0 인덱스에 1 삽입
print(a)    # [1, 2, 3, 4]
  • 리스트 맨 앞 / 중간의 값을 삭제할 때: O(N)O(N)
    • 이후 원소들을 한 칸씩 앞으로 미는 연산을 수행
    • 밀어야 하는 원소 수는 최대 N1N-1개이므로 O(N)O(N)
  • 맨 앞의 값 삭제의 경우, O(1)O(1)deque.popleft()를 쓰는 것이 빠름
a = [1, 2, 3]
print(a.pop(0)) # 1
print(a)        # [2, 3]
  • 리스트에 특정 값의 여부를 확인할 때: O(N)O(N)
    • 리스트의 모든 값을 순회하며 일치하는지 확인
    • 최대 NN개이므로 O(N)O(N)
  • O(1)O(1)인 집합 자료형을 쓰는 것이 빠름
a = [1, 2, 3]
print(2 in a)   # True
print(4 in a)   # False
profile
뭔가 만드는 걸 좋아하는 개발자 지망생입니다. 프로야구단 LG 트윈스를 응원하고 있습니다.

0개의 댓글