HOWTO · Python

ValueError: Python에서 닫힌 파일에 대한 I/O 작업 해결

이 기사에서는 I/O 값 오류가 발생하는 방법과 이 오류를 해결할 수 있는 다양한 방법에 대한 세 가지 주요 예를 제공합니다.

이 페이지의 내용

ValueError: I/O operation on closed file을 해결하려면 파일 객체가 열려 있는 동안 읽기, 반복, 쓰기를 모두 마쳐야 합니다. 작업 전체를 with 블록 안에 넣으세요. 블록이 끝나면 Python이 파일을 닫습니다.

다음 예제를 실행해 보세요:

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as directory:
    path = Path(directory) / "notes.txt"
    with path.open("w", encoding="utf-8") as handle:
        handle.write("File is ready.\n")
    with path.open(encoding="utf-8") as handle:
        print(handle.read(), end="")
File is ready.

ValueError: I/O operation on closed file 이해하기

ValueError: I/O operation on closed file은 파일 객체가 닫힌 뒤에 그 객체에서 읽거나 쓰려고 했다는 뜻입니다. with open(...) 블록을 벗어났거나, 코드에서 close()를 호출했거나, 프로그램의 다른 부분에서 같은 객체를 닫았을 때 파일이 닫힐 수 있습니다. traceback에는 실패한 작업이 표시됩니다. 해당 파일 객체가 어디서 열리고 닫히는지 역으로 추적해 보세요.

with 문은 리소스의 수명을 관리합니다. 예외를 처리하는 블록은 아닙니다. 실행이 들여쓰기된 본문을 벗어나면 Python이 파일을 자동으로 닫습니다. 따라서 들여쓰기에 따라 파일을 사용할 수 있는 시간이 결정됩니다. 파일을 기반으로 하는 반복자가 데이터를 지연해서 읽는 경우에도 같은 원칙이 적용됩니다. 반복자를 만들었다고 해서 파일의 모든 내용이 즉시 읽히는 것은 아닙니다.

컨텍스트 관리자가 파일을 닫은 뒤 반복자를 사용하지 않기

csv.reader는 반복할 때 행을 읽습니다. 파일 컨텍스트 안에서 reader를 만들고 반복문은 컨텍스트 밖에 두면, 반복문이 시작되기 전에 컨텍스트가 끝나 reader가 읽을 열린 파일을 더 이상 사용할 수 없습니다. 잘못된 구조는 with open(path) as source: rows = csv.reader(source) 다음에 바깥 들여쓰기 수준에서 for row in rows:를 실행하는 것입니다.

모든 읽기 작업이 파일이 열린 동안 이루어지도록 반복문을 with 본문 안에 두세요. 다음 완전한 예제는 자체 CSV 입력을 만들고 각 행을 읽은 뒤, 반복이 끝나야 파일 컨텍스트를 벗어납니다.

다음 예제를 실행해 보세요:

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

import csv
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as directory:
    path = Path(directory) / "people.csv"
    path.write_text("name,role\nAda,engineer\n", encoding="utf-8")
    with path.open(newline="", encoding="utf-8") as source:
        for row in csv.reader(source):
            print(row)

핵심 해결책은 행을 메모리에 추가로 복사하는 것이 아니라 반복문의 들여쓰기를 바로잡는 것입니다. 파일이 닫힌 뒤에 처리해야 한다면 list(csv.reader(source))처럼 블록 안에서 행을 명시적으로 모두 가져올 수 있습니다. 다만 파일이 큰 경우에는 컨텍스트가 열린 동안 처리하면 모든 행을 한꺼번에 보관하지 않아도 됩니다.

반복문이 끝나기 전에 파일이 닫히는 문제 해결하기

파일을 일반 방식으로 열었더라도 너무 일찍 닫힐 수 있습니다. 예를 들어 for line in source: destination.write(line); destination.close()는 첫 번째 반복에서 대상 파일을 닫습니다. 다음 줄을 처리하면 이후의 write()가 닫힌 파일을 사용하므로 오류가 발생합니다. close()는 호출 대상 객체에만 영향을 줍니다. destination.close()는 source를 닫지 않으며, destination을 다시 닫아도 source는 닫히지 않습니다.

간단한 해결책은 반복문이 끝난 뒤 컨텍스트 관리자가 두 파일을 모두 닫게 하는 것입니다. 다음 예제는 직접 만든 작은 입력 파일을 다른 임시 파일에 복사하고, 두 컨텍스트가 모두 열린 동안 복사한 줄을 출력합니다.

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as directory:
    source_path = Path(directory) / "source.txt"
    destination_path = Path(directory) / "copy.txt"
    source_path.write_text("first\nsecond\n", encoding="utf-8")

    with source_path.open(encoding="utf-8") as source:
        with destination_path.open("w", encoding="utf-8") as destination:
            for line in source:
                destination.write(line)
                print(line.rstrip())

이 패턴에서는 close()를 직접 호출할 필요가 없습니다. 반복문에 파일을 닫는 조건이 있다면 그 조건을 반복문 뒤에 두어야 하는지, 아니면 반복마다 별도의 파일을 사용해야 하는지 확인하세요. 정리 작업은 정리 대상 작업과 같은 구조적 수준에 두세요.

닫힌 파일에 쓰려는 문제 해결하기

명시적으로 close()를 호출하면 코드가 여전히 with 본문 안에 들여쓰기되어 있더라도 파일을 사용할 수 있는 수명이 즉시 끝납니다. handle.write("first"), handle.close(), handle.write("second") 순서로 실행하면 마지막 쓰기에서 오류가 발생합니다. 컨텍스트 관리자는 본문을 벗어날 때 파일을 닫으므로, 본문 안에서 직접 닫는 것은 중복이며 이후 작업을 실패하게 만들 수 있습니다.

직접 호출한 close()를 제거하고 필요한 쓰기를 모두 컨텍스트 안에서 수행하세요. 컨텍스트 관리자를 사용하지 않는 코드라면 성공적으로 open()한 뒤 마지막 작업이 끝난 다음 한 번 close()를 호출하세요. 다른 작업에서 오류가 나더라도 정리가 이루어지도록 finally 블록에서 닫는 것이 좋습니다. 일반적인 파일 처리에서는 with가 더 간결하고 정상 종료와 예외 종료 모두에서 정리를 보장합니다.

close() 바로 다음은 확인하기 좋은 경계입니다. with 블록이 끝나기 전이어도 이 시점부터 파일을 사용할 수 없습니다. 다음 자체 완결형 예제는 발생한 ValueError를 잡아 메시지를 표시한 뒤 프로그램을 계속 실행합니다.

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as directory:
    path = Path(directory) / "notes.txt"
    with path.open("w+", encoding="utf-8") as handle:
        handle.write("ready")
        handle.close()
        try:
            handle.read()
        except ValueError as error:
            print(type(error).__name__ + ": " + str(error))
ValueError: I/O operation on closed file.

파일 수명을 관리해 닫힌 파일 오류 예방하기

traceback에서 실패한 read(), 반복, write() 또는 관련 작업을 찾으세요. 그런 다음 해당 작업에 사용된 파일 객체를 정확히 확인하고 어디서 열었는지 찾습니다. 명시적인 close() 호출, with 블록을 벗어나는 경로, 같은 객체를 전달받는 도우미 함수 등 파일을 닫을 수 있는 모든 경로를 살펴보세요. 마지막으로 들여쓰기와 반복문 위치를 비교합니다. 실패한 작업은 파일을 관리하는 컨텍스트가 끝나거나 명시적으로 파일을 닫기 전에 실행되어야 합니다.

빠르게 확인할 진단 항목은 다음과 같습니다.

  • traceback의 마지막 애플리케이션 코드 줄에는 어떤 파일 작업이 표시되나요?
  • 해당 작업에서 파일 객체로 쓰는 변수는 무엇이며, 파일은 어디서 열리나요?
  • 작업 전에 close()가 실행되거나 관련 with 블록을 벗어나나요?
  • 반복문, 콜백 또는 지연 반복자가 파일 컨텍스트 밖에서 사용되나요?
  • 읽기나 쓰기 작업 전체를 감싸도록 컨텍스트 관리자가 파일을 관리하게 할 수 있나요?

이 항목을 확인하면 흔한 세 가지 경우, 즉 원본 파일이 닫힌 뒤 지연 CSV reader를 소비하는 경우, 반복문이 끝나기 전에 출력 파일을 닫는 경우, 명시적으로 닫은 파일에 다시 쓰는 경우를 구분할 수 있습니다. 파일의 수명 안에서 작업하고, 성급한 종료를 제거한 뒤, 컨텍스트 관리자가 마지막 정리를 맡게 하세요.