"문자열은 배열의 기술을 그대로 빌려 쓰는 친척이야. 다만 Pythonstr은 고쳐 쓸 수 없는 유니코드 시퀀스고, 그 불변성이 저 악명 높은+=함정을 만들어. 그냥 문자 배열이겠거니 하고 덤비면 데여."
속을 보면 배열이야
문자열은 순서가 있고 인덱스로 짚을 수 있는 시퀀스라 배열 기법이 거의 그대로 넘어와. Python str은 유니코드 코드 포인트의 불변 시퀀스인데, 여기서 한 발 더 나가서 내부가 고정 바이트 문자 배열일 거라고 단정하면 안 돼. 그건 보장된 게 아니거든. 대신 인덱싱, 선형 탐색, 슬라이싱의 비용 모델은 그대로 쓸 수 있어서 투 포인터 같은 기법을 바로 얹을 수 있어.
유일한 반전: 불변성
문자열이 리스트와 갈라지는 지점이 여기야. Python 문자열은 불변(immutable)이라 글자 하나를 제자리에서 못 바꿔. s[0] = 'x'는 그냥 에러가 나. "수정"처럼 보이는 동작은 전부 새 문자열을 통째로 짓는 거야. 말로만 들으면 별것 아닌 것 같은데, 이게 반복문 안으로 들어가면 입문 Python에서 제일 유명한 성능 사고가 돼.
+= 함정
반복문 안의 result += piece는 불변 문자열을 매번 다시 만들 수 있어. 일반적인 비용 모델로 보면 복사가 쌓여서 O(n²)까지 갈 수 있다는 뜻이야. CPython이 참조 상태에 따라 일부 연결을 최적화해 주기도 하지만, 그건 언어가 보장하는 성능이 아니야. 구현이 어쩌다 잘해주는 것에 기대지 말고, 조각이 많아질 것 같으면 처음부터 한 번에 합치는 쪽으로 짜.
안전한 기본값은 조각을 리스트에 모아뒀다가 "".join(pieces)로 한 번에 붙이는 거야. 작업량이 결과 길이에 비례하고, +=가 최적화되든 말든 의도가 선명하게 드러나. 짧은 연결 몇 번까지 금지할 건 없지만, 큰 문자열을 반복해서 쌓는 자리라면 join으로 가.
대체 왜 불변으로 만들어?
불변성은 벌칙이 아니라 값을 치르고 산 기능이야. 절대 안 바뀐다는 보장이 있으니까 문자열이 hashable할 수 있어. dict 키나 set 원소가 될 수 있다는 뜻이고, 다음 트랙 전체가 여기에 얹혀 있어. 누가 뒤에서 내용을 바꿔놓을 걱정 없이 프로그램 여기저기서 같은 문자열을 공유해도 안전하고. 똑같은 문자열은 인터닝해서 한 번만 저장하고 계속 재사용할 수도 있어. += 함정은 그 대가로 치른 값이야.
피파의 고백
report += line으로 리포트를 쌓았어. 테스트 파일에서는 눈 깜짝할 새에 끝났는데, 실제로 뽑아낸 데이터에서는 꼬박 1분을 기어갔지. 아빠가 한 번 보더니 그러더라. "매 줄마다 리포트 전체를 다시 짓고 있잖아." 리스트에 모아뒀다가 마지막에 join 한 번 하도록 바꾸니까 1분이 밀리초가 됐어. 어떤 설명보다 확실하게 박히더라. 불변성은 제곱이 되기 전까지는 눈에 안 보여.