Python 리스트 완전 정리: 생성, 슬라이싱, 메서드, 시간 복잡도

2026. 6. 12. 16:57Developer/Python

반응형

리스트는 Python에서 가장 많이 쓰는 자료구조입니다. 여러 값을 순서대로 담아두고, 추가하고, 꺼내고, 정렬하는 모든 작업이 리스트로 시작합니다.

핵심 요약

  • 리스트는 여러 값을 순서대로 담는 자료구조입니다. [1, 2, 3]처럼 대괄호로 만듭니다.
  • 인덱스는 0부터 시작하고, 음수 인덱스(-1)로 뒤에서부터 접근할 수 있습니다.
  • 슬라이싱([start:end:step])으로 리스트의 일부분을 잘라낼 수 있습니다.
  • append(), insert(), remove(), sort() 등 메서드로 리스트를 조작합니다.
  • 리스트 컴프리헨션([x for x in ...])은 반복문을 한 줄로 줄여주는 Python 고유 문법입니다.
  • append()는 빠르지만(O(1)), insert(0, x)는 느립니다(O(n)). 차이를 알면 성능 문제를 피할 수 있습니다.

1. 리스트 만들기

대괄호로 생성

numbers = [1, 2, 3, 4, 5]
fruits = ["사과", "바나나", "딸기"]
empty = []  # 빈 리스트

서로 다른 타입도 섞을 수 있다

mixed = [1, "hello", 3.14, True, None]

Python 리스트는 타입 제한이 없습니다. 다만 실무에서는 같은 타입의 값을 모아두는 경우가 대부분입니다.

리스트 안에 리스트 (중첩)

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

print(matrix[0])     # [1, 2, 3] — 첫 번째 행
print(matrix[1][2])  # 6 — 두 번째 행의 세 번째 값

다른 방법으로 만들기

# list()로 변환
chars = list("hello")       # ['h', 'e', 'l', 'l', 'o']
nums = list(range(5))       # [0, 1, 2, 3, 4]

# 반복으로 초기화
zeros = [0] * 10            # [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

2. 인덱싱 — 값 꺼내기

리스트에서 하나의 값을 꺼낼 때 인덱스를 사용합니다. 인덱스는 0부터 시작합니다.

fruits = ["사과", "바나나", "딸기", "포도", "수박"]
#          0       1        2       3       4
#         -5      -4       -3      -2      -1
print(fruits[0])    # "사과" — 첫 번째
print(fruits[2])    # "딸기" — 세 번째
print(fruits[-1])   # "수박" — 마지막
print(fruits[-2])   # "포도" — 뒤에서 두 번째

값 변경하기

리스트는 변경 가능(mutable)합니다. 인덱스로 특정 위치의 값을 바꿀 수 있습니다.

fruits[1] = "망고"
print(fruits)  # ["사과", "망고", "딸기", "포도", "수박"]

범위를 벗어나면 에러

fruits = ["사과", "바나나", "딸기"]
# fruits[5]  # IndexError: list index out of range

3. 슬라이싱 — 범위로 잘라내기

슬라이싱은 리스트의 일부분을 새 리스트로 가져오는 문법입니다.

리스트[start:end]       # start부터 end-1까지
리스트[start:end:step]  # step 간격으로

기본 슬라이싱

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

print(nums[2:5])    # [2, 3, 4] — 2번째~4번째 (5번째 미포함)
print(nums[:3])     # [0, 1, 2] — 처음부터 2번째까지
print(nums[7:])     # [7, 8, 9] — 7번째부터 끝까지
print(nums[:])      # [0, 1, 2, ..., 9] — 전체 복사

step 사용

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

print(nums[::2])    # [0, 2, 4, 6, 8] — 2칸씩 건너뛰기
print(nums[1::2])   # [1, 3, 5, 7, 9] — 홀수 인덱스만
print(nums[::-1])   # [9, 8, 7, ..., 0] — 뒤집기

슬라이싱으로 값 변경

nums = [0, 1, 2, 3, 4]
nums[1:3] = [10, 20]  # 1~2번째를 교체
print(nums)  # [0, 10, 20, 3, 4]

nums[1:3] = [10, 20, 30, 40]  # 길이가 달라도 됨
print(nums)  # [0, 10, 20, 30, 40, 3, 4]

슬라이싱은 새 리스트를 만든다

original = [1, 2, 3, 4, 5]
sliced = original[1:4]  # [2, 3, 4] — 새로운 리스트

sliced[0] = 99
print(original)  # [1, 2, 3, 4, 5] — 원본은 변하지 않음

4. 주요 메서드

추가하기

fruits = ["사과", "바나나"]

# 맨 뒤에 추가
fruits.append("딸기")
print(fruits)  # ["사과", "바나나", "딸기"]

# 특정 위치에 삽입
fruits.insert(1, "망고")
print(fruits)  # ["사과", "망고", "바나나", "딸기"]

# 여러 개를 한번에 추가
fruits.extend(["포도", "수박"])
print(fruits)  # ["사과", "망고", "바나나", "딸기", "포도", "수박"]

삭제하기

fruits = ["사과", "바나나", "딸기", "바나나"]

# 값으로 삭제 (첫 번째 일치 항목만)
fruits.remove("바나나")
print(fruits)  # ["사과", "딸기", "바나나"]

# 인덱스로 삭제 (삭제된 값을 반환)
removed = fruits.pop(1)
print(removed)  # "딸기"
print(fruits)   # ["사과", "바나나"]

# 마지막 값 삭제
last = fruits.pop()
print(last)    # "바나나"
print(fruits)  # ["사과"]

# 전체 삭제
fruits.clear()
print(fruits)  # []

정렬하기

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

# 오름차순 정렬 (원본을 변경)
numbers.sort()
print(numbers)  # [1, 1, 2, 3, 4, 5, 6, 9]

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

# 원본을 유지하고 새 리스트를 만들고 싶으면 sorted() 사용
original = [3, 1, 4, 1, 5]
new_list = sorted(original)
print(original)  # [3, 1, 4, 1, 5] — 원본 유지
print(new_list)  # [1, 1, 3, 4, 5]

찾기와 세기

fruits = ["사과", "바나나", "딸기", "바나나"]

# 위치 찾기
print(fruits.index("딸기"))   # 2
# fruits.index("포도")        # ValueError — 없으면 에러

# 개수 세기
print(fruits.count("바나나"))  # 2

# 존재 여부 확인 (in 연산자)
print("딸기" in fruits)       # True
print("포도" in fruits)       # False

뒤집기

nums = [1, 2, 3, 4, 5]
nums.reverse()
print(nums)  # [5, 4, 3, 2, 1]

메서드 요약표

메서드 동작 반환값
append(x) 맨 뒤에 x 추가 None (원본 변경)
insert(i, x) i번째에 x 삽입 None (원본 변경)
extend(iterable) 여러 값을 뒤에 추가 None (원본 변경)
remove(x) 첫 번째 x 삭제 None (원본 변경)
pop(i) i번째 삭제 후 반환 삭제된 값
pop() 마지막 삭제 후 반환 삭제된 값
clear() 전체 삭제 None
sort() 오름차순 정렬 None (원본 변경)
reverse() 순서 뒤집기 None (원본 변경)
index(x) x의 위치 반환 인덱스 (없으면 에러)
count(x) x의 개수 반환 정수
copy() 얕은 복사본 반환 새 리스트

5. 리스트 컴프리헨션

반복문으로 리스트를 만드는 작업을 한 줄로 줄여주는 Python 문법입니다.

기본 형태

# 반복문 방식
squares = []
for x in range(5):
    squares.append(x ** 2)
# [0, 1, 4, 9, 16]

# 컴프리헨션 (같은 결과, 한 줄)
squares = [x ** 2 for x in range(5)]
# [0, 1, 4, 9, 16]

조건 필터링

# 짝수만 골라내기
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = [n for n in numbers if n % 2 == 0]
# [2, 4, 6, 8, 10]

# 양수만 제곱
values = [-3, -1, 0, 2, 5]
positive_squares = [x ** 2 for x in values if x > 0]
# [4, 25]

변환에 활용

# 문자열 리스트를 대문자로
names = ["alice", "bob", "charlie"]
upper_names = [name.upper() for name in names]
# ["ALICE", "BOB", "CHARLIE"]

# 문자열 → 정수 변환
str_nums = ["1", "2", "3", "4"]
int_nums = [int(s) for s in str_nums]
# [1, 2, 3, 4]

언제 컴프리헨션을 쓰고, 언제 for문을 쓰는가

상황 권장
단순 변환/필터링 컴프리헨션
조건이 복잡하거나 여러 줄이 필요 for문
결과 리스트가 필요 없고 부수 효과만 원할 때 for문
# 컴프리헨션이 적합
doubled = [x * 2 for x in range(10)]

# for문이 적합 (복잡한 로직)
results = []
for item in data:
    if item.is_valid():
        processed = item.transform()
        if processed.score > 0.5:
            results.append(processed)

한 줄이 너무 길어지면(80자 이상) for문이 읽기 쉽습니다.


6. 리스트 복사

얕은 복사 (shallow copy)

original = [1, 2, 3]

# 방법 1: 슬라이싱
copy1 = original[:]

# 방법 2: copy() 메서드
copy2 = original.copy()

# 방법 3: list() 생성자
copy3 = list(original)

# 복사본을 변경해도 원본은 안전
copy1[0] = 99
print(original)  # [1, 2, 3]

주의: 중첩 리스트는 얕은 복사로 부족

matrix = [[1, 2], [3, 4]]
shallow = matrix[:]

shallow[0][0] = 99
print(matrix)  # [[99, 2], [3, 4]] — 원본도 변경됨!

얕은 복사는 바깥 리스트만 복사하고, 안쪽 리스트는 같은 객체를 공유합니다. 중첩된 구조까지 완전히 복사하려면 깊은 복사가 필요합니다.

import copy

matrix = [[1, 2], [3, 4]]
deep = copy.deepcopy(matrix)

deep[0][0] = 99
print(matrix)  # [[1, 2], [3, 4]] — 원본 안전

7. 유용한 내장 함수들

리스트와 함께 자주 쓰이는 내장 함수입니다.

nums = [3, 1, 4, 1, 5, 9]

len(nums)    # 6 — 길이
sum(nums)    # 23 — 합계
min(nums)    # 1 — 최솟값
max(nums)    # 9 — 최댓값
sorted(nums) # [1, 1, 3, 4, 5, 9] — 정렬된 새 리스트 (원본 유지)

enumerate — 인덱스와 값을 함께

fruits = ["사과", "바나나", "딸기"]

for i, fruit in enumerate(fruits):
    print(f"{i}번: {fruit}")
# 0번: 사과
# 1번: 바나나
# 2번: 딸기

enumerate()를 쓰면 인덱스 변수를 따로 관리할 필요가 없습니다.

zip — 여러 리스트를 묶기

names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]

for name, score in zip(names, scores):
    print(f"{name}: {score}점")
# Alice: 85점
# Bob: 92점
# Charlie: 78점

8. 시간 복잡도 — 어떤 작업이 빠르고 느린가

리스트의 각 연산이 얼마나 빠른지 알아두면, 데이터가 많아졌을 때 느려지는 코드를 피할 수 있습니다.

연산 시간 복잡도 의미
lst[i] O(1) 인덱스 접근은 항상 빠름
lst.append(x) O(1) 맨 뒤에 추가는 빠름
lst.pop() O(1) 맨 뒤에서 삭제는 빠름
lst.pop(0) O(n) 맨 앞에서 삭제는 느림 (뒤 요소를 모두 이동)
lst.insert(0, x) O(n) 맨 앞에 삽입도 느림
x in lst O(n) 처음부터 끝까지 찾아야 할 수 있음
lst.sort() O(n log n) 정렬은 중간 정도
lst[i:j] O(j-i) 잘라내는 크기만큼
len(lst) O(1) 길이 확인은 항상 빠름

O(1)과 O(n)의 차이

  • O(1): 리스트에 100만 개가 있어도 같은 시간이 걸림
  • O(n): 리스트에 100만 개가 있으면 최대 100만 번 작업해야 함

실무에서 주의할 패턴

# 느린 패턴 — 맨 앞에 계속 삽입 (매번 전체를 이동)
lst = []
for i in range(10000):
    lst.insert(0, i)  # O(n) × n번 = O(n²)

# 빠른 패턴 — 뒤에 추가하고 나중에 뒤집기
lst = []
for i in range(10000):
    lst.append(i)     # O(1) × n번 = O(n)
lst.reverse()

맨 앞에서 자주 추가/삭제해야 한다면 collections.deque를 사용하는 것이 더 적합합니다.


9. 자주 하는 실수

실수 1: sort()와 sorted() 혼동

numbers = [3, 1, 4, 1, 5]

# sort()는 None을 반환 — 이렇게 쓰면 None이 됨!
result = numbers.sort()
print(result)   # None

# sorted()는 새 리스트를 반환
result = sorted(numbers)
print(result)   # [1, 1, 3, 4, 5]

sort()는 원본을 변경하고 None을 반환합니다. 새 리스트가 필요하면 sorted()를 사용합니다.

실수 2: 반복 중 리스트 수정

# 위험! 반복 중 삭제하면 인덱스가 밀림
nums = [1, 2, 3, 4, 5]
for n in nums:
    if n % 2 == 0:
        nums.remove(n)
print(nums)  # [1, 3, 5]가 아닐 수 있음!

# 안전한 방법: 컴프리헨션으로 새 리스트 생성
nums = [1, 2, 3, 4, 5]
odds = [n for n in nums if n % 2 != 0]
print(odds)  # [1, 3, 5]

실수 3: 리스트 곱셈으로 중첩 리스트 만들기

# 위험! 같은 리스트 객체가 3번 반복됨
matrix = [[0] * 3] * 3
matrix[0][0] = 1
print(matrix)  # [[1, 0, 0], [1, 0, 0], [1, 0, 0]] — 전부 바뀜!

# 안전한 방법
matrix = [[0] * 3 for _ in range(3)]
matrix[0][0] = 1
print(matrix)  # [[1, 0, 0], [0, 0, 0], [0, 0, 0]]

실수 4: 빈 리스트 체크를 길게 쓰기

# 불필요하게 긴 표현
if len(items) == 0:
    print("비어 있음")

# Python다운 표현
if not items:
    print("비어 있음")

10. 정리

항목 핵심
생성 [1, 2, 3], list(), [0] * n
인덱싱 0부터 시작, 음수는 뒤에서부터
슬라이싱 [start:end:step], 새 리스트 반환
추가 append() 빠름, insert(0, x) 느림
삭제 pop() 빠름, pop(0) 느림
정렬 sort() 원본 변경, sorted() 새 리스트
컴프리헨션 [x for x in ... if ...] — 간결한 리스트 생성
복사 얕은 복사 [:], 깊은 복사 copy.deepcopy()

다음 글에서는 Python 딕셔너리를 다룹니다. 키-값 쌍으로 데이터를 관리하는 방법과, 언제 리스트 대신 딕셔너리를 쓰는지 정리합니다.


관련 글

반응형