Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 3151 – OS 및 IO 예외 계층 구조 재작업

Author:
Antoine Pitrou <solipsis at pitrou.net>
BDFL-Delegate:
Barry Warsaw
Status:
Final
Type:
Standards Track
Created:
21-Jul-2010
Python-Version:
3.3
Post-History:

Resolution:
Python-Dev message

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

표준 예외 계층 구조는 Python 언어의 중요한 부분입니다. 표준 예외 계층 구조에는 두 가지 핵심 특성이 있습니다. 범용적이면서 동시에 선택적이라는 것입니다. 범용적이라는 것은 문맥과 관계없이 같은 예외 타입이 발생하고 처리될 수 있다는 의미입니다(예를 들어, 정수에 무언가를 더하려 하든, 문자열 메서드를 호출하려 하든, 소켓에 객체를 쓰려 하든, 잘못된 인자 타입에 대해서는 TypeError가 발생합니다). 선택적이라는 것은 사용자가 특정 종류의 오류 상황을 쉽게 처리(무시, 검사, 가공, 저장 또는 캡슐화 등)하면서 다른 오류는 더 상위의 호출 문맥으로 전파되도록 할 수 있다는 의미입니다. 예를 들어, 다른 ArithmeticError(OverflowError 등)의 기본 처리에 영향을 주지 않으면서 ZeroDivisionError만 잡도록 선택할 수 있습니다.

이 PEP는 위에서 언급한 특성을 더 잘 구현하기 위해 예외 계층 구조의 일부, 즉 운영 체제 시스템 호출과 관련된 오류(OSError, IOError, mmap.error, select.error 및 이들의 모든 서브클래스)에 대한 변경을 제안합니다.

근거

혼란스러운 OS 관련 예외 집합

OS 관련(또는 시스템 호출 관련) 예외는 현재 여러 클래스로 나뉘어 있으며, 다음과 같은 하위 계층 구조로 배치되어 있습니다:

+-- EnvironmentError
    +-- IOError
        +-- io.BlockingIOError
        +-- io.UnsupportedOperation (also inherits from ValueError)
        +-- socket.error
            +-- socket.gaierror
            +-- socket.herror
            +-- socket.timeout
    +-- OSError
        +-- VMSError
        +-- WindowsError
    +-- mmap.error
+-- select.error

이러한 구분 중 일부는 구현상의 고려 사항으로 설명할 수 있지만, 더 높은 수준에서 보면 그다지 논리적이지 않은 경우가 많습니다. 예를 들어, OSError와 IOError를 가르는 경계는 모호한 경우가 많습니다. 다음을 살펴보십시오:

>>> os.remove("fff")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: [Errno 2] No such file or directory: 'fff'
>>> open("fff")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: [Errno 2] No such file or directory: 'fff'

같은 오류 상황(존재하지 않는 파일)이 어떤 라이브러리 함수를 호출했는지에 따라 서로 다른 두 예외로 나타납니다. 그 이유는 os 모듈은 오직 OSError(또는 그 서브클래스인 WindowsError)만 발생시키는 반면, io 모듈은 대부분 IOError를 발생시키기 때문입니다. 그러나 사용자가 관심을 두는 것은 오류의 성격이지, 오류가 인터프리터의 어느 부분에서 왔는지가 아닙니다(후자는 트레이스백 메시지나 애플리케이션 소스 코드를 읽으면 분명히 알 수 있기 때문입니다).

사실 OSError는 잡아야 하지만 IOError는 잡지 말아야 하는 상황, 또는 그 반대의 상황은 떠올리기 어렵습니다.

이러한 구분이 모호하다는 또 다른 증거는 표준 라이브러리조차 때때로 어느 쪽을 택할지 결정하는 데 어려움을 겪는다는 점입니다. 예를 들어, select 모듈에서는 select(), poll 객체, kqueue 객체, epoll 객체 중 무엇을 사용하느냐에 따라 비슷한 실패가 select.error, OSError 또는 IOError를 발생시킵니다. 이 때문에 사용자 코드는 쓸데없이 복잡해집니다. 하나의 기본 연산에 대해 런타임에 정확히 어떤 구현을 사용하기로 선택하느냐에 따라 다양한 예외 타입을 잡을 준비를 해야 하기 때문입니다.

WindowsError의 경우, 무의미한 구분으로 보입니다. 첫째, WindowsError는 Windows 시스템에만 존재하므로 크로스 플랫폼 애플리케이션에서 번거로운 호환성 코드가 필요합니다(그러한 코드는 Lib/shutil.py에서 찾아볼 수 있습니다). 둘째, WindowsError는 OSError를 상속하며, 다른 시스템에서 OSError가 발생하는 것과 비슷한 오류에 대해 발생합니다. 셋째, 저수준 예외 세부 정보에 접근하려는 사용자는 어차피 errno 또는 winerror 속성을 검사해야 합니다.

Note

Appendix B에서는 인터프리터와 표준 라이브러리 전반에 걸친 다양한 예외 타입의 사용 현황을 조사합니다.

세분화된 예외의 부재

현재의 다양한 OS 관련 예외로는 사용자가 원하는 종류의 실패를 쉽게 걸러낼 수 없습니다. 예를 들어, 파일이 존재하면 삭제하는 작업을 생각해 보십시오. LBYL(Look Before You Leap) 관용구에는 명백한 경쟁 상태 문제가 있습니다:

if os.path.exists(filename):
    os.remove(filename)

os.path.exists와 os.remove 호출 사이에 다른 스레드나 프로세스가 filename이라는 이름의 파일을 생성하면, 그 파일은 삭제되지 않습니다. 이는 애플리케이션에 버그를 일으킬 수 있으며, 심지어 보안 문제로 이어질 수도 있습니다.

따라서 해결책은 파일 삭제를 시도하고, 파일이 존재하지 않으면 오류를 무시하는 것입니다(이는 허락을 구하기보다 용서를 구하는 편이 더 쉽다(Easier to Ask Forgiveness than to get Permission, EAFP)는 관용구로 알려져 있습니다). 신중한 코드는 다음과 같은 모습이 됩니다(POSIX와 Windows 시스템 모두에서 동작합니다).:

try:
    os.remove(filename)
except OSError as e:
    if e.errno != errno.ENOENT:
        raise

또는 다음과 같이 작성할 수도 있습니다.:

try:
    os.remove(filename)
except EnvironmentError as e:
    if e.errno != errno.ENOENT:
        raise

이렇게 하면 입력할 내용이 훨씬 많아지고, 사용자가 errno 모듈의 여러 난해한 니모닉을 기억해야 합니다. 이는 추가적인 인지 부담을 주며 금세 번거로워집니다. 그 결과, 많은 프로그래머는 대신 예외를 지나치게 광범위하게 묵살하는 다음과 같은 코드를 작성합니다.:

try:
    os.remove(filename)
except OSError:
    pass

os.remove는 파일이 존재하지 않을 때뿐만 아니라 다른 여러 상황(예를 들어 파일 이름이 디렉터리를 가리키거나, 현재 프로세스에 파일을 삭제할 권한이 없는 경우)에서도 OSError를 발생시킬 수 있으며, 이러한 상황은 모두 애플리케이션 로직의 버그를 나타내므로 묵살해서는 안 됩니다. 프로그래머가 대신 작성하고 싶은 것은 다음과 같은 코드입니다.:

try:
    os.remove(filename)
except FileNotFoundError:
    pass

호환성 전략

예외 계층 구조를 재구성하면 당연히 적어도 일부 기존 코드의 정확한 의미가 바뀝니다. 정확한 의미를 바꾸지 않고서는 현재 상황을 개선할 수 없지만, 더 좁은 유형의 호환성을 정의할 수는 있으며, 이를 유용한 호환성이라고 부르겠습니다.

이를 위해 먼저 신중한 예외 처리와 부주의한 예외 처리라고 부를 것을 설명해야 합니다. 부주의한 (또는 “순진한”) 코드는 errno 속성을 확인하지 않고 OSError, IOError, socket.error, mmap.error, WindowsError, select.error 중 어느 것이든 맹목적으로 잡는 코드로 정의합니다. 이러한 예외 타입은 너무 광범위해서 어떤 의미도 나타내지 못하기 때문입니다. 이들 중 어느 것이든 다음과 같이 매우 다양한 오류 상황에서 발생할 수 있습니다: 잘못된 파일 디스크립터(보통 프로그래밍 오류를 나타냅니다), 연결되지 않은 소켓(마찬가지입니다), 소켓 타임아웃, 파일 타입 불일치, 잘못된 인자, 전송 실패, 권한 부족, 존재하지 않는 디렉터리, 가득 찬 파일 시스템 등입니다.

(게다가 이러한 예외 중 일부는 일관성 없이 사용됩니다. Appendix B는 구현에 따라 서로 다른 예외를 발생시키는 select 모듈의 사례를 보여 줍니다.)

신중한 코드는 위의 예외 중 어느 것을 잡을 때 errno 속성을 검사하여 실제 오류 상황을 판단하고 그에 따라 조치하는 코드로 정의합니다.

그러면 유용한 호환성을 다음과 같이 정의할 수 있습니다:

  • 유용한 호환성은 예외를 잡는 범위를 더 좁히지는 않지만, 부주의한 예외 처리 코드에 대해서는 더 넓힐 수 있습니다. 다음과 같은 종류의 코드 조각에서, 이 PEP 이전에 잡히던 모든 예외는 이 PEP 이후에도 잡히지만, 그 반대는 성립하지 않을 수 있습니다(OSError, IOError 등이 통합되므로 except 절이 조금 더 넓은 그물을 던지게 되기 때문입니다).:
    try:
        ...
        os.remove(filename)
        ...
    except OSError:
        pass
    
  • 유용한 호환성은 신중한 예외 처리 코드의 동작을 바꾸지 않습니다. 다음과 같은 종류의 코드 조각에서는, 이 PEP의 구현 여부와 관계없이 같은 오류가 묵살되거나 다시 발생해야 합니다.:
    try:
        os.remove(filename)
    except OSError as e:
        if e.errno != errno.ENOENT:
            raise
    

이러한 절충의 근거는, 부주의한 코드는 사실상 도울 방법이 없지만 적어도 “동작하는” 코드가 갑자기 오류를 발생시키며 중단되지는 않는다는 것입니다. 이러한 코드는 cron 작업이나 자동화된 시스템 관리 프로그램으로 사용되는 스크립트에 있을 가능성이 높으므로 이 점이 중요합니다.

반면에 신중한 코드는 불이익을 받아서는 안 됩니다. 실제로 이 PEP의 목적 중 하나는 신중한 코드를 더 쉽게 작성할 수 있게 하는 것입니다.

1단계: 예외 타입 통합

해결의 첫 단계는 기존 예외 타입을 통합하는 것입니다. 다음과 같은 변경을 제안합니다:

  • socket.error와 select.error를 모두 OSError의 별칭으로 만듭니다.
  • mmap.error를 OSError의 별칭으로 만듭니다.
  • WindowsError와 VMSError를 모두 OSError의 별칭으로 만듭니다.
  • IOError를 OSError의 별칭으로 만듭니다.
  • EnvironmentError를 OSError로 통합

이 변경 사항들은 각각 정확한 호환성을 유지하지는 않지만, 유용한 호환성은 유지합니다(위의 “호환성” 절 참조).

이 변경 사항들은 각각 개별적으로 수락하거나 거부할 수 있지만, 물론 이 첫 번째 단계를 전부 수락할 때 가장 큰 효과를 얻을 수 있다고 봅니다. 이 경우 IO 예외 하위 계층 구조는 다음과 같이 됩니다:

+-- OSError   (replacing IOError, WindowsError, EnvironmentError, etc.)
    +-- io.BlockingIOError
    +-- io.UnsupportedOperation (also inherits from ValueError)
    +-- socket.gaierror
    +-- socket.herror
    +-- socket.timeout

근거

이 첫 번째 단계는 Rationale 절에서 설명한 것처럼 사용자에게 더 단순한 구조를 제시할 뿐만 아니라, Step 2를 더 낫고 더 완전하게 해결할 수 있게 해 줍니다(Prerequisite 참조).

일반적인 OS 관련 예외의 공식 이름으로 OSError를 유지하는 근거는, 바로 그것이 IOError보다 더 일반적이라는 점입니다. EnvironmentError는 입력하기가 더 번거롭고 훨씬 덜 알려져 있기도 합니다.

Appendix B의 조사에 따르면 오늘날 표준 라이브러리에서 가장 많이 쓰이는 오류는 IOError입니다. 서드파티 Python 코드의 경우, Google Code Search에 따르면 사용자 코드에서 IOError는 EnvironmentError보다 10배, OSError보다 3배 더 많이 쓰입니다 [3]. 그러나 중기적으로 IOError를 폐지할 의도는 없으므로, OSError가 덜 쓰인다는 점은 문제가 되지 않습니다.

예외 속성

WindowsError가 OSError로 통합되므로, OSError는 Windows에서 winerror 속성을 갖게 됩니다. 이 속성은 의미가 없는 상황에서는 None으로 설정되며, 이는 errno, filename, strerror 속성에서 이미 그러한 것과 같습니다(예를 들어 Python 코드에서 OSError를 직접 발생시키는 경우).

이름의 폐지

다음 단락들에서는 기존 예외 이름을 폐지하는 데 쓸 수 있는 전략을 간략히 설명합니다. 그러나 당분간은 이 이름들을 별칭으로 유지하기로 결정되었습니다. 이 결정은 Python 4.0 즈음에 재검토될 수 있습니다.

내장 예외

builtins 네임스페이스에서 일어나는 조회는 성능에 결정적인 영향을 미치므로, 이 조회를 모두 가로채는 간단한 방식으로 기존 내장 예외를 폐지할 수는 없습니다. 또한 폐지된 이름은 폐지되지 않은 객체의 별칭이 되므로, 객체 수준에서 작업할 수도 없습니다.

한 가지 해결책은 컴파일 시점에 이 이름들을 인식한 다음, 일반적인 LOAD_GLOBAL 대신 별도의 LOAD_OLD_GLOBAL 옵코드를 내보내는 것입니다. 이 특수화된 옵코드는 이름이 전역 네임스페이스에는 존재하지 않고 내장 네임스페이스에만 존재할 때 DeprecationWarning(또는 결정된 정책에 따라 PendingDeprecationWarning)의 출력을 처리합니다. 이렇게 하면 오탐(예를 들어 누군가 모듈 안에서 자체 OSError를 정의하는 경우)을 충분히 피할 수 있으며, 미탐(예를 들어 누군가 OSError에 직접 접근하지 않고 builtins 모듈을 통해 접근하는 경우)은 드물 것입니다.

모듈 수준 예외

위의 방식은 코드 객체를 컴파일할 때 일부 모듈을 특별히 처리해야 하므로 쉽게 사용할 수 없습니다. 그러나 이 이름들은 구조상 훨씬 덜 눈에 띄며(내장 네임스페이스에 나타나지 않습니다) 덜 알려져 있기도 하므로, 각자의 네임스페이스에 그대로 두기로 결정할 수도 있습니다.

2단계: 추가 서브클래스 정의

해결의 두 번째 단계는 특정 errno 값에 대해 부모 클래스 대신 발생시킬 서브클래스를 정의하여 계층 구조를 확장하는 것입니다. 어떤 errno 값을 대상으로 할지는 논의가 필요하지만, 기존의 예외 매칭 관행에 대한 조사(Appendix A 참조)를 바탕으로 전체 값 중 합리적인 부분 집합을 제안할 수 있습니다. 실제로 모든 errno 니모닉을 매핑하려는 시도는 어리석고 무의미해 보이며, 루트 네임스페이스를 어지럽힐 것입니다.

또한 몇몇 경우에는 서로 다른 errno 값이 같은 예외 서브클래스를 발생시킬 수 있습니다. 예를 들어 EAGAIN, EALREADY, EWOULDBLOCK, EINPROGRESS는 모두 논블로킹 소켓에 대한 연산이 블록될 것임(따라서 나중에 다시 시도해야 함)을 알리는 데 사용됩니다. 따라서 이들이 모두 동일한 서브클래스를 발생시키고, 사용자가 원한다면 errno 속성을 살펴보도록 할 수 있습니다(아래의 “예외 속성”을 참조하십시오).

선행 조건

Step 1은 이를 위한 느슨한 선행 조건입니다.

선행 조건인 이유는 현재 일부 errno가 서로 다른 예외 클래스에 연결될 수 있기 때문입니다. 예를 들어 ENOENT는 맥락에 따라 OSError와 IOError 모두에 연결될 수 있습니다. 유용한 호환성을 깨뜨리지 않으려면, 오늘날 일치에 성공하는 예외에 대해 except OSError (또는 IOError)가 일치에 실패하도록 만들 수는 없습니다.

느슨한 이유는 기존 예외 클래스들이 통합되지 않더라도 2단계를 부분적으로 해결하는 쪽으로 결정할 수 있기 때문입니다. 예를 들어 ENOENT는 이전에 IOError가 발생하던 곳에서는 가상의 FileNotFoundError를 발생시키고, 그 외의 경우에는 계속 OSError를 발생시킬 수 있습니다.

새 서브클래스들이 다중 상속을 사용하여 기존의 모든 슈퍼클래스(또는 적어도 가장 널리 쓰인다고 할 수 있는 OSError와 IOError)와 일치하도록 한다면 1단계에 대한 의존성을 완전히 제거할 수 있습니다. 그러나 그렇게 하면 계층 구조가 더 복잡해지고, 따라서 사용자가 이해하기 더 어려워집니다.

새 예외 클래스

다음의 잠정적인 서브클래스 목록을 각 서브클래스에 대한 설명 및 매핑되는 errno 목록과 함께 논의에 부칩니다:

  • FileExistsError: 이미 존재하는 파일이나 디렉터리를 생성하려고 시도하는 경우 (EEXIST)
  • FileNotFoundError: 파일이나 디렉터리를 요청했지만 존재하지 않는 모든 상황 (ENOENT)
  • IsADirectoryError: 파일 수준의 연산(open(), os.remove()…)을 디렉터리에 대해 요청한 경우 (EISDIR)
  • NotADirectoryError: 디렉터리 수준의 연산을 디렉터리가 아닌 대상에 대해 요청한 경우 (ENOTDIR)
  • PermissionError: 적절한 접근 권한(예를 들어 파일 시스템 권한) 없이 연산을 실행하려고 시도하는 경우 (EACCES, EPERM)
  • BlockingIOError: 논블로킹 연산으로 설정된 객체(예: 소켓)에서 연산이 블록될 경우 (EAGAIN, EALREADY, EWOULDBLOCK, EINPROGRESS). 이는 기존의 io.BlockingIOError의 역할을 확장한 것입니다
  • BrokenPipeError: 반대쪽 끝이 닫힌 파이프에 쓰려고 시도하거나, 쓰기가 종료(shutdown)된 소켓에 쓰려고 시도하는 경우 (EPIPE, ESHUTDOWN)
  • InterruptedError: 들어온 시그널에 의해 시스템 호출이 인터럽트된 경우 (EINTR)
  • ConnectionAbortedError: 상대방이 연결 시도를 중단한 경우 (ECONNABORTED)
  • ConnectionRefusedError: 상대방이 연결을 재설정한 경우 (ECONNREFUSED)
  • ConnectionResetError: 상대방이 연결을 재설정한 경우 (ECONNRESET)
  • TimeoutError: 연결 시간 초과(ETIMEDOUT). 이는 socket.timeout을 대체하는 일반적인 타임아웃 예외로 재정의할 수 있으며, 다른 종류의 타임아웃(예를 들어 Lock.acquire())에도 유용합니다.
  • ChildProcessError: 자식 프로세스에 대한 연산이 실패한 경우 (ECHILD). 이 예외는 주로 wait() 계열 함수에서 발생합니다.
  • ProcessLookupError: 주어진 프로세스(예를 들어 프로세스 ID로 식별되는 프로세스)가 존재하지 않는 경우 (ESRCH).

또한 다음 예외 클래스를 추가할 것을 제안합니다:

  • ConnectionError: ConnectionAbortedError, ConnectionRefusedError, ConnectionResetError의 베이스 클래스

다음 그림은 제안된 추가 사항을 해당 errno 값(해당하는 경우)과 함께 요약한 것입니다. 하위 계층의 루트(Step 1이 전부 수용된다고 가정하면 OSError)는 표시하지 않았습니다:

+-- BlockingIOError        EAGAIN, EALREADY, EWOULDBLOCK, EINPROGRESS
+-- ChildProcessError                                          ECHILD
+-- ConnectionError
    +-- BrokenPipeError                              EPIPE, ESHUTDOWN
    +-- ConnectionAbortedError                           ECONNABORTED
    +-- ConnectionRefusedError                           ECONNREFUSED
    +-- ConnectionResetError                               ECONNRESET
+-- FileExistsError                                            EEXIST
+-- FileNotFoundError                                          ENOENT
+-- InterruptedError                                            EINTR
+-- IsADirectoryError                                          EISDIR
+-- NotADirectoryError                                        ENOTDIR
+-- PermissionError                                     EACCES, EPERM
+-- ProcessLookupError                                          ESRCH
+-- TimeoutError                                            ETIMEDOUT

명명

다양한 명명 논쟁이 생길 수 있습니다. 그중 하나는 모든 예외 클래스 이름이 “Error”로 끝나야 하는지에 관한 문제입니다. 찬성 측 근거는 나머지 예외 계층 구조와의 일관성이고, 반대 측 근거는 간결성입니다(특히 ConnectionAbortedError 같은 긴 이름의 경우).

예외 속성

유용한 호환성을 유지하기 위해, 이 서브클래스들은 여전히 슈퍼클래스에 정의된 여러 예외 속성(예를 들어 errno, filename, 그리고 선택적으로 winerror)에 적절한 값을 설정해야 합니다.

구현

서브클래스를 순전히 errno의 값에 따라 발생시키도록 제안하므로, 확장 모듈(표준 모듈이든 서드파티 모듈이든)에는 변경이 거의 또는 전혀 필요하지 않을 것입니다.

첫 번째 방법은 PyErr_SetFromErrno() 계열 함수(Windows에서는 PyErr_SetFromWindowsErr())가 적절한 OSError 서브클래스를 발생시키도록 수정하는 것입니다. 그러나 이 방법은 다음과 같은 관용구(Lib/tempfile.py에서 볼 수 있음)를 사용하여 OSError를 직접 발생시키는 Python 코드는 다루지 못합니다.:

raise IOError(_errno.EEXIST, "No usable temporary file name found")

Marc-Andre Lemburg가 제안한 두 번째 방법은 OSError.__new__가 적절한 서브클래스의 인스턴스를 생성하도록 수정하는 것입니다. 이 방법은 위와 같은 Python 코드까지 다룰 수 있다는 이점이 있습니다.

예상되는 반대 의견

네임스페이스 오염

예외 계층 구조를 더 세분화하면 루트(또는 내장) 네임스페이스가 커집니다. 그러나 다음과 같은 이유로 이 문제는 크지 않습니다.

  • 추가로 제안되는 클래스는 몇 개에 불과합니다.
  • 표준 예외 타입은 루트 네임스페이스에 있지만, 다른 거의 모든 내장 이름은 소문자 명명을 사용하는 반면(True, False, None, Ellipsis, NotImplemented 제외) 예외 타입은 CamelCase 규칙을 사용한다는 점에서 시각적으로 구별됩니다.

대안으로 세분화된 예외를 담은 별도의 모듈을 제공할 수도 있지만, 그렇게 하면 사용자가 이미 접근 가능한 이름을 쓰는 대신 새 모듈을 먼저 임포트해야 하므로, 부주의한 코드보다 신중한 코드를 장려한다는 목적이 무색해집니다.

이전 논의

이러한 공식 제안이 이루어진 것은 이번이 처음이지만, 이 아이디어는 과거에 비공식적인 지지를 받은 바 있습니다 [1]; 세분화된 예외 클래스의 도입과 OSError 및 IOError의 통합 모두에 대해서입니다.

WindowsError만 제거하는 방안은 다른 PEP의 일부로 논의되었다가 기각되었지만, OSError와의 구별이 의미가 없다는 데에는 합의가 있었던 것으로 보입니다. 이는 적어도 WindowsError를 OSError의 별칭으로 만드는 것을 뒷받침합니다.

구현

참조 구현은 Python 3.3에 통합되었습니다. 이전에는 http://hg.python.org/features/pep-3151/ 저장소의 pep-3151 브랜치에서 개발되었으며, 버그 트래커 http://bugs.python.org/issue12555 에서도 추적되었습니다. Linux, Windows, OpenIndiana, FreeBSD 빌드봇 등 다양한 시스템에서 성공적으로 테스트되었습니다.

문제의 원인 중 하나는 OSError와 WindowsError의 각 생성자가 서로 호환되지 않았다는 점이었습니다. 이 문제는 OSError 시그니처를 유지하면서, Windows 오류 코드(POSIX errno와는 다름)를 전달할 수 있도록 선택적인 네 번째 인자를 추가하는 방식으로 해결했습니다. 네 번째 인자는 winerror로 저장되고, 이를 POSIX로 변환한 값은 errno로 저장됩니다. PyErr_SetFromWindowsErr* 함수들은 올바른 생성자 호출을 사용하도록 수정되었습니다.

약간 복잡한 경우는 PyErr_SetExcFromWindowsErr* 함수가 WindowsError가 아닌 OSError로 호출될 때입니다. 이때 예외 객체의 errno 속성에는 POSIX로 변환한 값(예: EPIPE의 경우 32)이 아니라 Windows 오류 코드(예: ERROR_BROKEN_PIPE의 경우 109)가 저장되며, 현재도 그렇게 동작합니다. 소켓이 아닌 오류 코드의 경우, 이는 비공개 _multiprocessing 모듈에서만 발생하며 이 모듈에 대해서는 호환성 문제가 없습니다.

Note

소켓 오류의 경우, errno 모듈에 반영된 “POSIX errno”는 WSAGetLastError 시스템 호출이 반환하는 Windows 소켓 오류 코드와 수치상 같습니다:

>>> errno.EWOULDBLOCK
10035
>>> errno.WSAEWOULDBLOCK
10035

가능한 대안

패턴 매칭

또 다른 가능성은 예외를 잡을 때 쓸 수 있는 고급 패턴 매칭 문법을 도입하는 것입니다. 예를 들면 다음과 같습니다:

try:
    os.remove(filename)
except OSError as e if e.errno == errno.ENOENT:
    pass

이 제안에는 몇 가지 문제가 있습니다:

  • 새로운 문법을 도입합니다. 저자는 이것이 예외 계층 구조를 재작업하는 것보다 더 큰 변경이라고 봅니다
  • 입력해야 하는 수고를 크게 줄여 주지 않습니다
  • 프로그래머가 errno 니모닉을 기억해야 하는 부담을 덜어 주지 않습니다

이 PEP에서 다루지 않는 예외

이 PEP는 EOFError를 다루지 않습니다. EOFError는 다양한 프로토콜 및 파일 형식 구현(예를 들어 GzipFile)에서 입력 스트림이 잘렸음을 알리는 예외입니다. EOFError는 OS나 IO와 관련된 오류가 아니라, 더 높은 수준에서 발생하는 논리적 오류입니다.

이 PEP는 SSLError도 다루지 않습니다. SSLError는 OpenSSL 라이브러리가 알린 오류를 전파하기 위해 ssl 모듈이 발생시키는 예외입니다. 이상적으로는 SSLError도 비슷하지만 별도의 처리를 받는 것이 좋을 것입니다. SSLError는 오류 유형에 대한 자체 상수(ssl.SSL_ERROR_WANT_READ 등)를 정의하기 때문입니다. Python 3.2에서 SSLError는 소켓 타임아웃을 알리는 경우 이미 socket.timeout으로 대체됩니다(issue 10272 참고).

마지막으로, socket.gaierror와 socket.herror를 어떻게 할지는 아직 정해지지 않았습니다. 이 예외들은 덜 난해한 이름을 가질 만하지만, 이 문제는 예외 계층 구조 재편 작업과 별도로 다룰 수 있습니다.

부록 A: 흔한 errno 조사

다음은 표준 라이브러리와 그 테스트에서 except 절의 일부로 검사하는 다양한 errno 니모닉을 간단히 정리한 목록입니다.

OSError와 함께 쓰이는 흔한 errno

  • EBADF: 잘못된 파일 디스크립터(보통 파일 디스크립터가 닫혔다는 뜻입니다)
  • EEXIST: 파일 또는 디렉터리가 이미 존재합니다
  • EINTR: 함수 호출이 인터럽트됨
  • EISDIR: 디렉터리입니다
  • ENOTDIR: 디렉터리가 아닙니다
  • ENOENT: 해당 파일 또는 디렉터리가 없습니다
  • EOPNOTSUPP: 소켓에서 지원되지 않는 연산입니다(기존 io.UnsupportedOperation과 혼동될 수 있습니다)
  • EPERM: 허용되지 않는 연산입니다(예를 들어 os.setuid()를 사용할 때)

IOError와 함께 쓰이는 흔한 errno

  • EACCES: 권한이 거부되었습니다(파일 시스템 연산의 경우)
  • EBADF: 잘못된 파일 디스크립터(select.epoll 사용 시)입니다. 쓰기 전용 GzipFile에 대한 읽기 연산이나 그 반대의 경우에도 발생합니다
  • EBUSY: 장치 또는 자원이 사용 중입니다
  • EISDIR: 디렉터리입니다(open()을 시도할 때)
  • ENODEV: 해당 장치가 없음
  • ENOENT: 해당 파일이나 디렉터리가 없음 (open()을 시도할 때)
  • ETIMEDOUT: 연결 시간 초과

socket.error에서 흔히 발생하는 errno

이 모든 오류는 일반 IOError와 함께 발생할 수도 있습니다. 소켓의 파일 디스크립터에 대해 read()를 호출하는 경우가 그 예입니다.

  • EAGAIN: 리소스를 일시적으로 사용할 수 없음 (connect()를 제외한 논블로킹 소켓 호출 중)
  • EALREADY: 이미 연결이 진행 중임 (논블로킹 connect() 중)
  • EINPROGRESS: 작업이 진행 중임 (논블로킹 connect() 중)
  • EINTR: 함수 호출이 인터럽트됨
  • EISCONN: 소켓이 이미 연결되어 있음
  • ECONNABORTED: 상대방이 연결을 중단함 (accept() 호출 중)
  • ECONNREFUSED: 상대방이 연결을 거부함
  • ECONNRESET: 상대방이 연결을 재설정함
  • ENOTCONN: 소켓이 연결되어 있지 않음
  • ESHUTDOWN: 전송 엔드포인트가 종료된 후에는 보낼 수 없음
  • EWOULDBLOCK: EAGAIN과 같은 이유

select.error에서 흔히 발생하는 errno

  • EINTR: 함수 호출이 인터럽트됨

부록 B: 발생하는 OS 및 IO 오류 조사

VMSError에 관하여

VMSError는 인터프리터 코어와 표준 라이브러리에서 전혀 사용되지 않습니다. 이 예외는 2002년에 Jean-François Piéronne이 제출한 OpenVMS 패치의 일부로 추가되었습니다 [4]; VMSError를 포함한 이유는 서드파티 패키지가 이 예외를 발생시킬 수 있다는 것이었습니다.

인터프리터 코어

PYTHONSTARTUP 처리 과정에서 IOError가 발생합니다 (하지만 이 오류는 버려집니다):

$ PYTHONSTARTUP=foox ./python
Python 3.2a0 (py3k:82920M, Jul 16 2010, 22:53:23)
[GCC 4.4.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
Could not open PYTHONSTARTUP
IOError: [Errno 2] No such file or directory: 'foox'

PyObject_Print()는 FILE * 매개변수에 대해 ferror()가 오류를 알리면 IOError를 발생시킵니다 (소스 트리에서 이 매개변수는 항상 stdout 또는 stderr입니다).

mbcs 인코딩을 사용하는 유니코드 인코딩 및 디코딩은 일부 오류 상황에서 WindowsError를 발생시킬 수 있습니다.

표준 라이브러리

bz2

모든 곳에서 IOError를 발생시킵니다 (OSError는 사용되지 않습니다):

>>> bz2.BZ2File("foox", "rb")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: [Errno 2] No such file or directory
>>> bz2.BZ2File("LICENSE", "rb").read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: invalid data stream
>>> bz2.BZ2File("/tmp/zzz.bz2", "wb").read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: file is not ready for reading

curses

검토하지 않았습니다.

dbm.gnu, dbm.ndbm

_dbm.error와 _gdbm.error는 IOError를 상속합니다:

>>> dbm.gnu.open("foox")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
_gdbm.error: [Errno 2] No such file or directory

fcntl

모든 곳에서 IOError를 발생시킵니다(OSError는 사용하지 않습니다).

imp 모듈

잘못된 파일 디스크립터에 대해 IOError를 발생시킵니다:

>>> imp.load_source("foo", "foo", 123)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: [Errno 9] Bad file descriptor

io 모듈

Unix에서 디렉터리를 열려고 하면 IOError를 발생시킵니다:

>>> open("Python/", "r")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: [Errno 21] Is a directory: 'Python/'

지원하지 않는 연산에 대해 IOError 또는 io.UnsupportedOperation(IOError를 상속합니다)을 발생시킵니다:

>>> open("LICENSE").write("bar")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: not writable
>>> io.StringIO().fileno()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
io.UnsupportedOperation: fileno
>>> open("LICENSE").seek(1, 1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: can't do nonzero cur-relative seeks

하위 I/O 계층이 잘못 동작하면(즉, 구현해야 하는 API를 위반하면) IOError 또는 TypeError를 발생시킵니다.

기반 OS 자원이 유효하지 않게 되면 IOError를 발생시킵니다:

>>> f = open("LICENSE")
>>> os.close(f.fileno())
>>> f.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: [Errno 9] Bad file descriptor

…또는 구현별 최적화를 위해 발생시킵니다:

>>> f = open("LICENSE")
>>> next(f)
'A. HISTORY OF THE SOFTWARE\n'
>>> f.tell()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: telling position disabled by next() call

논블로킹 객체에 대한 호출이 블록될 상황이면 BlockingIOError(IOError를 상속합니다)를 발생시킵니다.

mmap

Unix에서는 모든 곳에서 자체 예외인 mmap.error(EnvironmentError를 상속합니다)를 발생시킵니다:

>>> mmap.mmap(123, 10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
mmap.error: [Errno 9] Bad file descriptor
>>> mmap.mmap(os.open("/tmp", os.O_RDONLY), 10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
mmap.error: [Errno 13] Permission denied

그러나 Windows에서는 주로 WindowsError를 발생시킵니다(소스 코드에는 mmap.error를 사용하는 곳도 몇 군데 있습니다):

>>> fd = os.open("LICENSE", os.O_RDONLY)
>>> m = mmap.mmap(fd, 16384)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
WindowsError: [Error 5] Accès refusé
>>> sys.last_value.errno
13
>>> errno.errorcode[13]
'EACCES'

>>> m = mmap.mmap(-1, 4096)
>>> m.resize(16384)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
WindowsError: [Error 87] Paramètre incorrect
>>> sys.last_value.errno
22
>>> errno.errorcode[22]
'EINVAL'

multiprocessing

검토하지 않았습니다.

os / posix

os(또는 posix) 모듈은 모든 곳에서 OSError를 발생시킵니다. 다만 Windows에서는 대신 WindowsError를 발생시킬 수 있습니다.

ossaudiodev

모든 곳에서 IOError를 발생시킵니다(OSError는 사용하지 않습니다):

>>> ossaudiodev.open("foo", "r")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: [Errno 2] No such file or directory: 'foo'

readline

여러 파일 처리 함수에서 IOError를 발생시킵니다:

>>> readline.read_history_file("foo")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: [Errno 2] No such file or directory
>>> readline.read_init_file("foo")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: [Errno 2] No such file or directory
>>> readline.write_history_file("/dev/nonexistent")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: [Errno 13] Permission denied

select

  • select()와 poll 객체는 select.error를 발생시키며, 이 예외는 아무것도 상속하지 않습니다(단, poll.modify()는 IOError를 발생시킵니다);
  • epoll 객체는 IOError를 발생시킵니다;
  • kqueue 객체는 OSError와 IOError를 모두 발생시킵니다.

덧붙이자면, EnvironmentError를 상속하지 않기 때문에 select.error에는 유용한 errno 속성이 없습니다. 대신 사용자 코드에서 args[0]을 확인해야 합니다:

>>> signal.alarm(1); select.select([], [], [])
0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
select.error: (4, 'Interrupted system call')
>>> e = sys.last_value
>>> e
error(4, 'Interrupted system call')
>>> e.errno == errno.EINTR
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'error' object has no attribute 'errno'
>>> e.args[0] == errno.EINTR
True

signal

signal.ItimerError는 IOError를 상속합니다.

소켓

socket.error는 IOError를 상속합니다.

sys

GetVersionEx() 호출이 실패하면 sys.getwindowsversion()은 잘못된 오류 번호와 함께 WindowsError를 발생시킵니다.

time

time.time()과 time.sleep()에서 내부 오류가 발생하면 IOError를 발생시킵니다.

zipimport

zipimporter.get_data()는 IOError를 발생시킬 수 있습니다.

감사의 글

Alyssa Coghlan에게서 중요한 의견을 받았습니다.

참고 문헌