Study/Python

[Python] 리스트(List)

the.Dev.Cat 2026. 3. 7. 23:15

 

동빈나 채널의 파이썬 문법 부수기 유튜브 강의를 참고하여 정리한 내용이다.

 

파이썬에서 가장 많이 쓰는 자료구조라고 할 수 있다.

배열처럼 사용하고, 크기를 동적으로 늘릴 수 있고, 다양한 내장 메서드를 제공한다.

 


 

리스트 개념

리스트는 순서가 있는 데이터의 묶음이다.

배열처럼 인덱스로 접근하고, 다른 언어와 달리 파이썬 리스트는 여러 타입의 데이터를 함께 저장할 수 있다.

# 기본 생성
a = [1, 2, 3, 4, 5]
b = []  # 빈 리스트
c = list()  # 빈 리스트 - b와 동일

# 혼합 타입도 가능 (코딩테스트에선 잘 안 씀)
mixed = [1, "hello", 3.14, True]

print(a)       # [1, 2, 3, 4, 5]
print(len(a))  # 5 - 길이

 


 

리스트 초기화

직접 초기화

# 값을 미리 알 때
scores = [90, 85, 92, 78, 95]
names = ["alice", "bob", "charlie"]

 

반복으로 초기화

# n개의 0으로 초기화
n = 5
a = [0] * n
print(a)  # [0, 0, 0, 0, 0]

# n개의 특정 값으로
b = [1] * 10
print(b)  # [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

[0] * n 패턴은 코딩테스트에서 배열을 초기화할 때 매우 자주 쓴다. DP 테이블이나 방문 체크 배열을 초기화할 때 기본이다.

 


 

인덱싱

양수 인덱스

a = [1, 2, 3, 4, 5]
#    0  1  2  3  4  (양수 인덱스)

print(a[0])  # 1 - 첫 번째
print(a[2])  # 3 - 세 번째
print(a[4])  # 5 - 마지막

 

음수 인덱스

파이썬은 뒤에서부터 접근할 때 음수 인덱스를 사용한다. 이게 편하다.

a = [1, 2, 3, 4, 5]
#   -5 -4 -3 -2 -1  (음수 인덱스)

print(a[-1])  # 5 - 마지막
print(a[-2])  # 4 - 뒤에서 두 번째
print(a[-5])  # 1 - 첫 번째 (a[0]과 동일)

마지막 원소를 가져올 때 a[len(a)-1] 대신 a[-1]을 쓴다. 훨씬 깔끔하다.

 

인덱스 범위 오류

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

print(a[5])
# IndexError: list index out of range

print(a[-6])
# IndexError: list index out of range

인덱스는 0부터 len(a)-1까지, 음수는 -len(a)부터 -1까지만 유효하다. 이 범위를 넘으면 IndexError가 발생한다.

 

값 변경

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

a[-1] = 99
print(a)  # [1, 2, 10, 4, 99]

 


 

슬라이싱

리스트의 일부를 잘라내는 기능이다. a[시작:끝] 형태이며, 끝 인덱스는 포함하지 않는다.

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

print(a[1:3])   # [2, 3] - 인덱스 1, 2 (3은 미포함)
print(a[0:2])   # [1, 2]
print(a[2:])    # [3, 4, 5] - 2부터 끝까지
print(a[:3])    # [1, 2, 3] - 처음부터 2까지
print(a[:])     # [1, 2, 3, 4, 5] - 전체 복사

 

스텝 사용

a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

print(a[::2])    # [0, 2, 4, 6, 8] - 2칸씩 건너뜀
print(a[1::2])   # [1, 3, 5, 7, 9] - 홀수 인덱스
print(a[::-1])   # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] - 역순
print(a[5:1:-1]) # [5, 4, 3, 2] - 5부터 2까지 역순

a[::-1]은 리스트를 뒤집는 관용 표현이다. a.reverse()와 달리 원본은 그대로고 새 리스트를 반환한다.

 

슬라이싱으로 값 수정

a = [1, 2, 3, 4, 5]
a[1:3] = [20, 30]
print(a)  # [1, 20, 30, 4, 5]

# 다른 길이로 교체도 가능
a[1:3] = [200, 300, 400]
print(a)  # [1, 200, 300, 400, 4, 5]

 


 

리스트 컴프리헨션

반복문으로 리스트를 만드는 파이썬스러운 방법이다. 간결하고 일반적으로 for 루프보다 빠르다.

기본 형태

# 기본 형태
result = [표현식 for 변수 in 이터러블]

# 0~4의 제곱 리스트
squares = [i ** 2 for i in range(5)]
print(squares)  # [0, 1, 4, 9, 16]

# 동일한 for 루프
squares = []
for i in range(5):
    squares.append(i ** 2)

 

조건부 컴프리헨션

# 조건이 있는 경우
evens = [i for i in range(10) if i % 2 == 0]
print(evens)  # [0, 2, 4, 6, 8]

# 1~30 중 3의 배수
multiples_of_3 = [i for i in range(1, 31) if i % 3 == 0]
print(multiples_of_3)  # [3, 6, 9, 12, 15, 18, 21, 24, 27, 30]

# if-else는 표현식 앞에
abs_vals = [i if i >= 0 else -i for i in [-3, -1, 2, 4, -5]]
print(abs_vals)  # [3, 1, 2, 4, 5]

 

2차원 리스트 초기화

코딩테스트에서 2차원 배열을 초기화할 때 반드시 컴프리헨션 방식을 써야 한다.

n, m = 3, 4

# 올바른 방법 - 컴프리헨션
matrix = [[0] * m for _ in range(n)]
print(matrix)
# [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]

matrix[0][0] = 1
print(matrix)
# [[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] - 의도한 대로
# 잘못된 방법 - 같은 리스트 객체가 공유됨
matrix_wrong = [[0] * m] * n
print(matrix_wrong)
# [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]

matrix_wrong[0][0] = 1
print(matrix_wrong)
# [[1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0]] - 모든 행이 같이 바뀜!

[[0] * m] * n은 내부에서 동일한 리스트 객체를 n번 참조하는 구조다.

하나를 바꾸면 전부 바뀐다. 코딩테스트에서 이 함정에 빠지면 디버깅하는 데 시간을 많이 쓰게 된다.

[[0] * m for _ in range(n)] 이 방식만 사용한다. [[0] * m] * n은 쓰지 않는다.

 

언더바(_) 활용

반복 변수를 실제로 사용하지 않을 때 _를 쓴다. 관례적으로 “이 변수는 필요 없음”을 표현한다.

# _ 는 값을 버리는 관례
for _ in range(5):
    print("hello")

# 컴프리헨션에서도 동일
zeros = [0 for _ in range(10)]
print(zeros)  # [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

# 다중 언패킹에서 필요 없는 값 버리기
a, _, c = (1, 2, 3)
print(a, c)  # 1 3

 


 

리스트 메서드

주요 메서드와 시간복잡도

메서드 기능 시간복잡도
append(x) 끝에 원소 추가 O(1)
insert(i, x) i번 인덱스에 x 삽입 O(N)
remove(x) 첫 번째로 나오는 x 제거 O(N)
pop() 마지막 원소 제거 후 반환 O(1)
pop(i) i번 인덱스 원소 제거 후 반환 O(N)
sort() 정렬 (오름차순) O(N log N)
reverse() 역순 정렬 O(N)
count(x) x의 개수 반환 O(N)
index(x) x의 첫 번째 인덱스 반환 O(N)
clear() 모든 원소 삭제 O(N)
copy() 얕은 복사 반환 O(N)

 

append와 insert

a = [1, 2, 3]

# append: 끝에 추가
a.append(4)
print(a)  # [1, 2, 3, 4]

# insert: 특정 위치에 삽입
a.insert(1, 10)
print(a)  # [1, 10, 2, 3, 4]

# insert는 O(N) 이므로 끝에 추가할 때는 append 사용

 

sort와 sorted

a = [3, 1, 4, 1, 5, 9, 2, 6]

# sort(): 원본을 정렬 (반환값 없음)
a.sort()
print(a)  # [1, 1, 2, 3, 4, 5, 6, 9]

# 내림차순
a.sort(reverse=True)
print(a)  # [9, 6, 5, 4, 3, 2, 1, 1]

# sorted(): 원본 유지, 새 리스트 반환
b = [3, 1, 4, 1, 5]
c = sorted(b)
print(b)  # [3, 1, 4, 1, 5] - 원본 유지
print(c)  # [1, 1, 3, 4, 5]

# key 함수 사용
words = ["banana", "apple", "cherry", "date"]
words.sort(key=len)  # 길이 기준 정렬
print(words)  # ['date', 'apple', 'banana', 'cherry']

# 절댓값 기준 정렬
nums = [-5, 3, -1, 4, -2]
nums.sort(key=abs)
print(nums)  # [-1, 3, -2, 4, -5]
# 자주 하는 실수: sort()의 반환값을 변수에 저장
a = [3, 1, 2]
b = a.sort()  # sort()는 None을 반환
print(b)  # None

# 올바른 방법
a.sort()
print(a)  # [1, 2, 3]

# 또는 sorted() 사용
a = [3, 1, 2]
b = sorted(a)
print(b)  # [1, 2, 3]

 

remove와 pop

a = [1, 2, 3, 2, 4]

# remove(): 첫 번째로 나오는 값 제거
a.remove(2)
print(a)  # [1, 3, 2, 4] - 첫 번째 2만 제거

# pop(): 마지막 원소 제거 후 반환
last = a.pop()
print(last)  # 4
print(a)     # [1, 3, 2]

# pop(i): i번 인덱스 제거 후 반환
item = a.pop(1)
print(item)  # 3
print(a)     # [1, 2]
# 없는 값을 remove하면 에러
a = [1, 2, 3]
a.remove(5)
# ValueError: list.remove(x): x not in list

# 제거 전에 확인
if 5 in a:
    a.remove(5)

 

count와 index

a = [1, 2, 3, 2, 2, 4]

print(a.count(2))   # 3 - 2가 3개
print(a.count(5))   # 0 - 없으면 0

print(a.index(3))   # 2 - 3은 인덱스 2에 있음
print(a.index(2))   # 1 - 첫 번째 2의 인덱스

# 없는 값의 index는 에러
a.index(5)
# ValueError: 5 is not in list

 


 

특정 값 원소 모두 제거

remove()는 한 번에 하나만 지운다. 특정 값을 모두 제거하려면 컴프리헨션을 쓴다.

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

# 2를 모두 제거
result = [x for x in a if x != 2]
print(result)  # [1, 3, 4, 5, 3]

# 원본 변경
a = [x for x in a if x != 2]
print(a)  # [1, 3, 4, 5, 3]

집합을 이용한 방법도 있지만, 순서가 유지되지 않으니 주의한다.

a = [1, 2, 3, 4, 5, 2, 3, 2]
remove_set = {2, 3}

# 여러 값을 한 번에 제거
result = [x for x in a if x not in remove_set]
print(result)  # [1, 4, 5]

x not in remove_set에서 remove_set이 집합이면 in 연산이 O(1)이고,

리스트면 O(N)이다. 제거할 값이 많을 때는 집합으로 만들어 두는 게 효율적이다.

 


 

리스트 연산

a = [1, 2, 3]
b = [4, 5, 6]

# 합치기
c = a + b
print(c)  # [1, 2, 3, 4, 5, 6]

# 반복
d = a * 3
print(d)  # [1, 2, 3, 1, 2, 3, 1, 2, 3]

# in 연산자
print(2 in a)   # True
print(5 in a)   # False
print(5 not in a)  # True

 


 

리스트 복사 주의사항

# 얕은 복사 - 같은 객체를 참조
a = [1, 2, 3]
b = a        # 같은 리스트를 가리킴
b[0] = 100
print(a)     # [100, 2, 3] - a도 바뀜!

# 진짜 복사
c = a.copy()     # 방법 1
d = a[:]         # 방법 2
e = list(a)      # 방법 3

c[0] = 999
print(a)  # [100, 2, 3] - 원본 유지
print(c)  # [999, 2, 3]
# 2차원 리스트는 얕은 복사로 안 됨
import copy

matrix = [[1, 2], [3, 4]]
shallow = matrix.copy()
shallow[0][0] = 999
print(matrix)  # [[999, 2], [3, 4]] - 내부 리스트는 공유됨!

deep = copy.deepcopy(matrix)
deep[0][0] = 100
print(matrix)  # [[999, 2], [3, 4]] - 영향 없음

2차원 이상의 리스트를 복사할 때는 copy.deepcopy()를 사용한다.

 


 

정리

  • 2차원 리스트 초기화는 반드시 [[0] * m for _ in range(n)]
  • a[::-1]로 역순 리스트 생성
  • sort()는 원본 변경, sorted()는 새 리스트 반환
  • remove()는 첫 번째 값만 제거, 없으면 ValueError
  • insert()pop(i)는 O(N), append()pop()은 O(1)
  • 리스트 복사는 =가 아닌 a[:] 또는 a.copy() 사용