요약

리스트는 번호가 붙은 칸이 한 줄로 늘어선 사물함이다. 번호만 알면 바로 꺼낼 수 있고, 맨 끝에 넣고 빼는 것도 빠르다. 대신 맨 앞이나 가운데에 넣고 빼면 뒤의 칸을 모두 한 칸씩 밀어야 해서 느리다. 무엇이 들어 있는지 찾을 때도 처음부터 다 봐야 한다. 문자열은 글자로 된 줄인데, 한 번 만들면 고칠 수 없어서 바꿀 때마다 새로 만든다.

예시로 보기

a = [10, 20, 30, 40, 50]을 칸으로 그리면 이렇다.

값        10   20   30   40   50
앞 번호    0    1    2    3    4
뒤 번호   -5   -4   -3   -2   -1
  • 칸 번호(인덱스)는 0부터 센다. a[0]은 10, a[4]는 50이다.
  • 음수 번호는 뒤에서 센다. a[-1]은 맨 끝의 50이다.
  • 칸 수는 len(a)로 알며 5다. 쓸 수 있는 번호는 0~4, 또는 −5~−1이다. a[5]를 꺼내면 IndexError가 난다.

자르기 (슬라이싱)

a[i:j]는 i번부터 j번 바로 앞까지 잘라 새 리스트로 만든다. j번 칸은 들어가지 않는다.

코드 결과 풀어 말하면
a[1:4] [20, 30, 40] 1, 2, 3번 칸
a[:2] [10, 20] 처음부터 2번 앞까지
a[3:] [40, 50] 3번부터 끝까지
a[::2] [10, 30, 50] 한 칸씩 건너뛰며
a[::-1] [50, 40, 30, 20, 10] 거꾸로

잘라 낸 것은 복사본이다. b = a[1:3]로 만든 b를 고쳐도 a는 그대로다. 대신 복사하는 데 자른 길이만큼 시간이 든다.

넣고 빼기와 비용

코드 하는 일 비용1
a[i], a[i] = x i번 칸 읽기, 바꾸기 \(O(1)\)
a.append(x) 맨 끝에 넣기 \(O(1)\) (평균)
a.pop() 맨 끝을 빼서 돌려주기 \(O(1)\)
a.pop(0), a.insert(0, x) 맨 앞에서 빼기, 넣기 \(O(n)\). 뒤 칸을 모두 민다
x in a, a.index(x), a.count(x), a.remove(x) 찾기, 세기, 지우기 \(O(n)\). 처음부터 본다
a[i:j] 잘라 복사하기 \(O(j - i)\)
a.sort(), sorted(a) 정렬 \(O(n \log n)\)

비용 표기는 시간 복잡도로 방법 고르기를 따른다. 맨 앞에서 자주 빼야 하면 덱(deque)을, 들어 있는지 자주 물어야 하면 집합을 쓴다.

한 줄로 만들기 (컴프리헨션)

squares = [x * x for x in range(5)]          # [0, 1, 4, 9, 16]
evens = [x for x in a if x % 20 == 0]        # [20, 40]

[만들 값 for 이름 in 모음 if 조건]은 아래 반복문을 한 줄로 쓴 것이다.

evens = []
for x in a:
    if x % 20 == 0:
        evens.append(x)

2차원 리스트 (표)

격자나 표는 리스트 안에 리스트를 넣어 만든다. grid[r][c]는 r번째 줄의 c번째 칸이다.

n, m = 3, 4
grid = [[0] * m for _ in range(n)]   # 3줄 4칸, 줄마다 따로 만든다
grid[1][2] = 7

[[0] * m] * n으로 만들면 안 된다. 이렇게 하면 같은 줄 하나를 n번 가리키는 표가 된다. 그래서 한 칸을 바꾸면 모든 줄의 같은 칸이 함께 바뀐다.

문자열

문자열도 번호로 꺼내고 자를 수 있다. s = "hello"이면 s[0]은 "h", s[1:3]은 "el"이다. 하지만 s[0] = "H"처럼 칸을 바꿀 수는 없다. 바꾸려면 새 문자열을 만든다: "H" + s[1:].

코드 결과
"a b c".split() ['a', 'b', 'c'] (빈칸 여러 개도 하나로 본다)
"2021.05.02".split(".") ['2021', '05', '02']
"-".join(["a", "b", "c"]) 'a-b-c'
"banana".replace("an", "_") 'b__a'
"banana".find("na"), "banana".count("a") 2, 3
"abc123".isdigit(), "123".isdigit() False, True
"Hi".lower(), "Hi".upper() 'hi', 'HI'
ord("a"), chr(98) 97, 'b'
"na" in "banana" True (이어진 부분이 있는지)

글자를 여러 번 붙여 긴 문자열을 만들 때는 조각을 리스트에 모았다가 마지막에 "".join(조각들)로 한 번에 붙인다. 문자열은 고칠 수 없어서 붙일 때마다 새로 만들어지기 때문이다.

검증: 예시와 표의 모든 결과, 슬라이스가 복사본이라는 것, [[0]*m]*n에서 줄이 함께 바뀌는 것, 컴프리헨션과 반복문의 결과가 같다는 것을 실행해 확인했다. 원소 10만 개에서 pop(0)이 pop()보다 몇십 배 이상 느리다는 것도 실험으로 확인했다 — 03_list-string_verify.py

활용

  • 거의 모든 문제의 입력이 리스트나 문자열로 온다.
  • 자주 하는 실수:
    • 번호를 1부터 세는 문제(“i번째부터 j번째까지”)를 0부터 세는 파이썬으로 옮길 때 하나씩 어긋난다. 1부터 센 i~j번째는 a[i - 1:j]다.
    • 반복문 안에서 a.pop(0)이나 x in a를 쓰면 전체가 \(O(n^2)\)이 된다.
    • 순회하는 리스트를 도는 중에 지우면 원소를 건너뛴다. 새 리스트를 만든다.

연결

  • 선수: 파이썬 기본 문법
  • 다음 개념: 딕셔너리와 집합(빠른 찾기), 문자열을 규칙대로 자르는 방법은 문자열 파싱 문서에서 다룬다
  • append가 평균 \(O(1)\)인 까닭은 꽉 차면 칸 수를 일정한 비율로 늘리기 때문이다. 그러면 복사 횟수의 합이 등비급수가 되어 넣은 개수의 상수배를 넘지 않는다.
  • a[i:j]는 j − i칸이다(0 ≤ i ≤ j ≤ len(a)일 때). 양 끝을 모두 넣는 “i번째부터 j번째까지”는 j − i + 1개로, 수열과 합의 기호의 항 개수 공식 n − m + 1과 같은 셈이다.
  • 컴프리헨션 [x * x for x in a if 조건]은 집합의 조건제시법 \(\{x^2 : x \in A,\ \text{조건}\}\)(\(\in\)은 “~에 속한다”)과 모양이 같다. 다른 점은 리스트가 순서와 중복을 남긴다는 것이다. 중괄호로 쓴 {x * x for x in a if 조건}은 결과가 집합이라 중복이 사라진다.
  • 연습: 평균 구하기, 숫자 문자열과 영단어

확인 문제

C1 다음 코드가 끝난 뒤 a와 b는?
a = [3, 1, 4, 1, 5]
a.append(9)
a.pop(0)
b = a[1:3]
b[0] = 7

답: a = [1, 4, 1, 5, 9], b = [7, 1]. append로 9가 끝에 붙고, pop(0)으로 맨 앞의 3이 빠진다. a[1:3]은 1, 2번 칸 [4, 1]의 복사본이라, b[0]을 바꿔도 a는 그대로다.

흔한 오답: b = [4, 1, 5]처럼 3번 칸까지 넣는 것. 슬라이스의 끝 번호는 들어가지 않는다.

C2 grid = [[0] * 3] * 2 다음에 grid[0][0] = 1을 하면 grid는 [[1, 0, 0], [1, 0, 0]]이 된다. 왜 두 줄이 다 바뀌는가?

답: 바깥의 * 2는 줄을 새로 만드는 게 아니라, 같은 줄 하나를 두 번 가리킨다. 그래서 grid[0]과 grid[1]이 같은 줄이다. 줄마다 따로 만들려면 [[0] * 3 for _ in range(2)]처럼 반복해서 만든다.

C3 result = [s.upper() for s in words if len(s) >= 3]을 컴프리헨션 없이 반복문으로 바꿔 써라.

답:

result = []
for s in words:
    if len(s) >= 3:
        result.append(s.upper())

대괄호 맨 앞의 식이 append할 값, for 부분이 반복, if 부분이 거르는 조건이 된다.

  1. Python Wiki, “TimeComplexity” (wiki.python.org/moin/TimeComplexity)의 list 항목. 문법과 메서드는 Python 3 공식 튜토리얼 3.1.2 “Text”, 3.1.3 “Lists”, 5.1 “More on Lists”와 표준 라이브러리 문서의 “Built-in Types” 중 Sequence Types, Text Sequence Type (str). ↩