본문 바로가기
C.W.K.
Stream
Lesson 03 of 06 · published

문자열, 옷만 갈아입은 배열

~11 min · strings, arrays, immutability

Level 0호기심 많은 입문자
0 XP0/85 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete
"문자열은 배열의 기술을 그대로 빌려 쓰는 친척이야. 다만 Python str은 고쳐 쓸 수 없는 유니코드 시퀀스고, 그 불변성이 저 악명 높은 += 함정을 만들어. 그냥 문자 배열이겠거니 하고 덤비면 데여."

속을 보면 배열이야

문자열은 순서가 있고 인덱스로 짚을 수 있는 시퀀스라 배열 기법이 거의 그대로 넘어와. Python str은 유니코드 코드 포인트의 불변 시퀀스인데, 여기서 한 발 더 나가서 내부가 고정 바이트 문자 배열일 거라고 단정하면 안 돼. 그건 보장된 게 아니거든. 대신 인덱싱, 선형 탐색, 슬라이싱의 비용 모델은 그대로 쓸 수 있어서 투 포인터 같은 기법을 바로 얹을 수 있어.

유일한 반전: 불변성

문자열이 리스트와 갈라지는 지점이 여기야. Python 문자열은 불변(immutable)이라 글자 하나를 제자리에서 못 바꿔. s[0] = 'x'는 그냥 에러가 나. "수정"처럼 보이는 동작은 전부 새 문자열을 통째로 짓는 거야. 말로만 들으면 별것 아닌 것 같은데, 이게 반복문 안으로 들어가면 입문 Python에서 제일 유명한 성능 사고가 돼.

+= 함정

반복문 안의 result += piece는 불변 문자열을 매번 다시 만들 수 있어. 일반적인 비용 모델로 보면 복사가 쌓여서 O(n²)까지 갈 수 있다는 뜻이야. CPython이 참조 상태에 따라 일부 연결을 최적화해 주기도 하지만, 그건 언어가 보장하는 성능이 아니야. 구현이 어쩌다 잘해주는 것에 기대지 말고, 조각이 많아질 것 같으면 처음부터 한 번에 합치는 쪽으로 짜.

안전한 기본값은 조각을 리스트에 모아뒀다가 "".join(pieces)로 한 번에 붙이는 거야. 작업량이 결과 길이에 비례하고, +=가 최적화되든 말든 의도가 선명하게 드러나. 짧은 연결 몇 번까지 금지할 건 없지만, 큰 문자열을 반복해서 쌓는 자리라면 join으로 가.

Python 문자열은 불변 유니코드 시퀀스야. 배열 기법은 거의 그대로 쓰되, 내부 표현까지 단순한 문자 배열이라고 가정하진 마. 조각을 많이 이어 붙일 땐 리스트에 모았다가 join으로 한 번에 합치는 게 비용이 예측되는 기본값이야.

대체 왜 불변으로 만들어?

불변성은 벌칙이 아니라 값을 치르고 산 기능이야. 절대 안 바뀐다는 보장이 있으니까 문자열이 hashable할 수 있어. dict 키나 set 원소가 될 수 있다는 뜻이고, 다음 트랙 전체가 여기에 얹혀 있어. 누가 뒤에서 내용을 바꿔놓을 걱정 없이 프로그램 여기저기서 같은 문자열을 공유해도 안전하고. 똑같은 문자열은 인터닝해서 한 번만 저장하고 계속 재사용할 수도 있어. += 함정은 그 대가로 치른 값이야.

피파의 고백

처음 만든 텍스트 조립 함수는 수천 행짜리 반복문에서 report += line으로 리포트를 쌓았어. 테스트 파일에서는 눈 깜짝할 새에 끝났는데, 실제로 뽑아낸 데이터에서는 꼬박 1분을 기어갔지. 아빠가 한 번 보더니 그러더라. "매 줄마다 리포트 전체를 다시 짓고 있잖아." 리스트에 모아뒀다가 마지막에 join 한 번 하도록 바꾸니까 1분이 밀리초가 됐어. 어떤 설명보다 확실하게 박히더라. 불변성은 제곱이 되기 전까지는 눈에 안 보여.

Code

+= 함정, 그리고 join으로 고치기·python
# 문자열은 읽기엔 배열처럼 행동해...
s = "pippa"
print(s[0], s[-1], s[1:4])   # 'p' 'a' 'ipp' — 인덱스 & 슬라이스, 배열처럼
print("p" in s)              # True — 근데 이건 O(n) 훑기

# ...근데 불변이라, '편집' 은 전체를 다시 지어.
# s[0] = "P"   # TypeError: 'str' object does not support item assignment

# 일반 비용 모델: 반복문 안 += 는 누적 복사로 O(n^2)까지 커질 수 있음.
def build_bad(n):
    out = ""
    for i in range(n):
        out += str(i)          # 매번 새 문자열 할당 -> 총 O(n^2)
    return out

# 고침: 리스트에 모으고 (O(1) append), 한 번 join (O(n)).
def build_good(n):
    parts = []
    for i in range(n):
        parts.append(str(i))   # 리스트 append 는 분할 상환 O(1)
    return "".join(parts)      # O(n) 한 번 녹이기

assert build_bad(1000) == build_good(1000)   # 같은 답
# 같은 출력. build_bad 는 O(n^2). build_good 은 O(n). 큰 n 에선 격차가 잔혹해.

External links

Exercise

행 n개를 돌면서 csv += row + "\n"로 CSV 문자열을 만드는 함수가 있어. 실제 복잡도가 얼마고 왜 그런지 말한 다음, O(n)으로 다시 써 봐. 그리고 하나 더. 문자열은 dict 키가 되는데 리스트는 왜 안 될까?
Hint
+= 버전은 O(n²)야. 붙일 때마다 그때까지 커진 문자열 전체를 복사하거든. 행을 리스트에 모아뒀다가 '\n'.join으로 합쳐. 문자열은 불변이라 hashable해서 dict 키가 되고, 리스트는 가변이라 hashable하지 않아.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.