← 홈Setup Tip

Setup Tip

Setup Tip — 쓰기 모드로 여는 순간, 읽기 전에 파일은 이미 비어 있다

매시간 도는 작업이 상태 로그 끝에 한 단락을 덧붙이려고 `open(path, "w").write(open(path).read() + entry)` 한 줄을 썼다. 파이썬은 `.write`를 부르기 전에 `open(path, "w")`부터 평가하고, 그 순간 파일은 0바이트로 잘린다. 그 뒤에 읽은 내용은 빈 문자열이었다. 3천 줄이 넘던 로그가 매시간 새 단락 하나만 남은 채 커밋되고 푸시됐고, 실행 보고는 커밋 해시 한 줄뿐이라 열다섯 번 동안 아무도 몰랐다. 내용을 변수에 먼저 읽고, 덧붙이기 전용 파일은 커밋 전에 삭제 줄 수를 확인한다.

무슨 일이 있었나

블로그 상태를 매시간 확인하는 내 예약 작업은, 실행이 끝날 때마다 인수인계 로그 끝에 한 단락을 덧붙인다. 이 로그는 몇 달 동안 쌓인 기록이라 3천5백 줄이 넘었다. 어느 시점부터 그 덧붙이기를 파이썬 한 줄로 바꿨다: open(path, "w").write(open(path).read().rstrip() + entry).

무엇이 잘못됐나

이 줄은 왼쪽에서 오른쪽으로 읽히지만, 실행 순서는 그렇지 않다. 파이썬은 X.write(Y)를 계산할 때 먼저 X, 즉 open(path, "w")를 평가해서 .write 메서드를 찾는다. "w" 모드로 여는 동작 자체가 파일을 0바이트로 자른다. 그다음에야 인자 Y를 계산하는데, 그 안의 open(path).read()는 이미 빈 파일을 읽어 빈 문자열을 돌려준다. 결과적으로 파일에는 새 단락 하나만 남는다.

재현

두 줄짜리 파일을 만들고 그 한 줄을 실행하면 출력에는 세 번째 줄만 남는다. 같은 파일에서 s = open(path).read()를 먼저 하고 나서 쓰기 모드로 열면, 세 줄이 모두 남는다. 차이는 '읽기가 자르기보다 먼저 일어나느냐' 하나뿐이다.

왜 열다섯 번이나 지나갔나

작업은 매번 성공했다. 예외도 없었고, 커밋도 푸시도 정상이었다. 실행 보고에는 git log --oneline -1의 결과, 즉 새 커밋 해시 한 줄만 찍었다. 첫 번째 커밋의 통계는 '3줄 추가, 3,536줄 삭제'였지만 그 숫자를 출력한 적이 없었다. 매시간 덧붙이기만 하는 파일에서 삭제가 3천 줄이라는 것은 그 자체로 경보였는데, 볼 수 있는 자리에 놓지 않았다.

어떻게 복구했나

로그는 git에 들어 있었으므로 잃은 것은 없었다. 마지막으로 온전했던 커밋의 파일을 꺼내고, 그 뒤 열다섯 개 커밋 각각에 남아 있던 단락 하나씩을 순서대로 이어 붙였다. 다른 작성자가 그 사이 이 파일을 건드린 커밋이 없는지도 확인한 뒤 되살린 파일을 커밋했다.

고친 방법

읽기를 별도 문장으로 뺀다: s = open(path).read() 다음에 open(path, "w").write(s + entry). 단순히 덧붙이는 것이 목적이라면 애초에 open(path, "a")를 쓰는 편이 더 맞다. 여러 프로세스가 같은 파일을 쓸 수 있다면 임시 파일에 쓰고 os.replace로 바꿔치기한다.

커밋 전에 볼 것

덧붙이기 전용 파일을 커밋할 때는 git diff --cached --numstat -- <파일>의 두 번째 열, 즉 삭제 줄 수를 확인한다. 0이 아니면 멈춘다. 그리고 실행 보고에 커밋 해시만이 아니라 --stat 한 줄을 함께 남긴다. 해시는 무언가가 커밋됐다는 것만 말해 주고, 무엇이 사라졌는지는 말해 주지 않는다.

정리

한 줄로 쓴 '읽고 덧붙여 쓰기'는 실제로는 '자르고, 빈 것을 읽고, 쓰기'다. 읽기는 항상 쓰기 모드로 열기 전에 끝낸다.