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

Python 개선 제안 한국어 번역

PEP 3151 – 운영 체제 및 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 및 이들의 모든 서브클래스)입니다.

근거

혼란스러운 운영 체제 관련 예외 집합

운영 체제 관련(또는 시스템 호출 관련) 예외는 현재 다양한 클래스의 집합이며, 다음 하위 계층으로 구성되어 있습니다.:

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

Note

Appendix B에서는 인터프리터와 표준 라이브러리 전반에서 다양한 예외 유형이 사용되는 방식을 조사합니다.

세분화된 예외의 부족

현재의 다양한 운영 체제 관련 예외로는 사용자가 원하는 종류의 실패를 쉽게 필터링할 수 없습니다. 예를 들어, 파일이 존재할 경우 해당 파일을 삭제하는 작업을 생각해 보십시오. LBYL(Look Before You Leap) 관용구에는 명백한 경쟁 조건이 있습니다.:

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

os.path.exists호출과 os.remove호출 사이에 다른 스레드나 프로세스가 filename이라는 이름의 파일을 만들면, 해당 파일은 삭제되지 않습니다. 이로 인해 애플리케이션에 버그가 발생하거나 심지어 보안 문제가 발생할 수 있습니다.

따라서 해결책은 파일 삭제를 시도하고, 파일이 존재하지 않는 경우에는 오류를 무시하는 것입니다(이는 “허가를 요청하기보다 용서를 구하는 것이 더 쉽다”라는 관용구, 즉 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로 통합합니다.

이러한 변경 사항은 정확한 호환성을 보존하지는 않지만, 유용한 호환성은 보존합니다(“compatibility” 섹션 참조).

이러한 변경 사항은 각각 개별적으로 수용하거나 거부할 수 있지만, 물론 이 첫 단계가 전체적으로 수용될 때 가장 큰 영향을 달성할 수 있다고 여겨집니다. 이 경우 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보다 열 배 더 많이 사용되며, OSError보다도 세 배 더 많이 사용됩니다 [3]. 그러나 중기적으로 IOError를 사용 중단할 의도는 없으므로 OSError의 낮은 사용 빈도는 문제가 되지 않습니다.

예외 속성

WindowsError가 OSError로 통합되므로, 후자는 Windows에서 winerror속성을 얻습니다. errno, filenamestrerror 속성의 경우와 마찬가지로 의미가 없는 상황에서는 None으로 설정됩니다(예를 들어 Python 코드가 OSError를 직접 발생시키는 경우).

이름의 사용 중단

다음 문단에서는 이전 예외 이름에 대한 가능한 사용 중단 전략의 개요를 설명합니다. 그러나 당분간은 이 이름들을 별칭으로 유지하기로 결정되었습니다. 이 결정은 Python 4.0에 맞춰 재검토될 수 있습니다.

내장 예외

이전 내장 예외의 사용 중단은 builtins 네임스페이스의 모든 조회를 가로채는 간단한 방식으로 수행할 수 없습니다. 이러한 조회는 성능상 매우 중요하기 때문입니다. 또한 객체 수준에서 처리할 수도 없습니다. 사용 중단될 이름이 사용 중단되지 않는 객체의 별칭이 될 것이기 때문입니다.

한 가지 해결책은 컴파일 시점에 이러한 이름을 인식한 다음, 일반적인 LOAD_GLOBAL 대신 별도의 LOAD_OLD_GLOBAL 연산 코드를 내보내는 것입니다. 이 특수 연산 코드는 이름이 globals 네임스페이스에는 없고 builtins 네임스페이스에만 있을 때 DeprecationWarning(또는 결정된 정책에 따라 PendingDeprecationWarning)을 출력합니다. 이렇게 하면 잘못된 양성(예를 들어 누군가 모듈에서 자체 OSError를 정의하는 경우)을 방지할 수 있으며, 잘못된 음성은 드물 것입니다(예를 들어 누군가 builtins 모듈을 통해 직접 접근하지 않고 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: 반대쪽 끝이 닫힌 동안 파이프에 쓰려고 하거나, 쓰기가 종료된 소켓에 쓰려고 할 때(EPIPE, ESHUTDOWN)
  • InterruptedError: 시스템 호출이 수신 신호에 의해 중단된 경우(EINTR)
  • ConnectionAbortedError: 피어에 의해 연결 시도가 중단된 경우(ECONNABORTED)
  • ConnectionRefusedError: 피어에 의해 연결이 재설정된 경우(ECONNREFUSED)
  • ConnectionResetError: 피어에 의해 연결이 재설정된 경우(ECONNRESET)
  • TimeoutError: 연결 시간이 초과된 경우(ETIMEDOUT); 이는 일반적인 시간 초과 예외로 다시 정의하여 socket.timeout을 대체할 수 있으며, 다른 유형의 시간 초과(예: Lock.acquire())에도 유용합니다.
  • ChildProcessError: 자식 프로세스에 대한 작업이 실패한 경우(ECHILD); 이는 주로 wait() 계열 함수에서 발생합니다.
  • ProcessLookupError: 지정된 프로세스(예: 프로세스 ID로 식별되는 프로세스)가 존재하지 않는 경우(ESRCH)입니다.

또한 다음 예외 클래스를 포함할 것을 제안합니다.

  • ConnectionError: ConnectionAbortedError, ConnectionRefusedErrorConnectionResetError의 베이스 클래스

다음 그림은 제안된 추가 사항을 해당 errno 값(해당하는 경우)과 함께 요약하려고 합니다. 하위 계층 구조의 루트(OSError, Step 1 이 완전히 수락된다고 가정)는 표시하지 않습니다.:

+-- 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 서브클래스를 발생시키도록 하는 것입니다. 그러나 이렇게 하면 다음 관용구를 사용하여 OSError를 직접 발생시키는 Python 코드는 처리하지 못합니다(Lib/tempfile.py에서 볼 수 있습니다).:

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

두 번째 가능성은 Marc-Andre Lemburg가 제안한 것으로, OSError.__new__를 조정하여 적절한 서브클래스를 인스턴스화하는 것입니다. 이는 위와 같은 Python 코드도 처리할 수 있다는 이점이 있습니다.

가능한 반론

네임스페이스 오염

예외 계층을 더 세분화하면 루트(또는 builtins) 네임스페이스가 더 커집니다. 그러나 다음과 같은 이유로 이는 완화됩니다.

  • 제안된 추가 클래스가 극소수이기 때문입니다.
  • 표준 예외 타입은 루트 네임스페이스에 존재하지만, CamelCase 규칙을 사용한다는 점에서 시각적으로 구별되며, 거의 모든 다른 내장 객체는 소문자 명명 방식을 사용합니다(True, False, None, Ellipsis 및 NotImplemented는 제외).

다른 방법으로는 더 세분화된 예외를 포함하는 별도 모듈을 제공할 수 있지만, 그렇게 하면 부주의한 코드보다 신중한 코드를 장려하려는 목적에 어긋납니다. 사용자가 이미 접근할 수 있는 이름을 사용하는 대신 먼저 새 모듈을 가져와야 하기 때문입니다.

이전 논의

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

WindowsError의 제거만으로도 another PEP의 일부로 논의되었으며 거부되었지만, OSError와의 구분이 의미 있지 않다는 데에는 합의가 있었던 것으로 보입니다. 이는 적어도 OSError의 별칭으로 만드는 것을 뒷받침합니다.

구현

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

문제의 한 원인은 서로 호환되지 않았던 OSErrorWindowsError의 각 생성자였습니다. 해결 방법은 OSError의 시그니처를 유지하고, POSIX errno와는 다른 Windows 오류 코드를 전달할 수 있도록 네 번째 선택적 인자를 추가하는 것입니다. 네 번째 인자는 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 Socket error code와 수치적으로 동일합니다.:

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

가능한 대안

패턴 매칭

또 다른 가능성은 예외를 포착할 때 고급 패턴 매칭 구문을 도입하는 것입니다. 예를 들어:

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

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

  • 새로운 구문을 도입하므로, 저자가 보기에 예외 계층 구조를 재구성하는 것보다 더 큰 변경으로 인식됩니다.
  • 입력해야 하는 수고를 크게 줄이지 못합니다.
  • 프로그래머가 errno 니모닉을 기억해야 하는 부담을 덜어 주지 못합니다.

이 PEP에서 무시하는 예외

이 PEP에서는 다양한 프로토콜 및 파일 형식 구현에서 잘린 입력 스트림을 나타내는 EOFError를 무시합니다(예를 들어 GzipFile). EOFError는 OS 또는 IO와 관련된 오류가 아니라 더 높은 수준에서 발생하는 논리적 오류입니다.

이 PEP에서는 ssl 모듈이 OpenSSL 라이브러리에서 신호한 오류를 전달하기 위해 발생시키는 SSLError도 무시합니다. 이상적으로는 SSLError가 오류 유형에 대한 자체 상수(ssl.SSL_ERROR_WANT_READ, 등)를 정의하므로 유사하지만 별도의 처리를 통해 이점을 얻을 수 있습니다. Python 3.2에서는 소켓 타임아웃을 나타낼 때 SSLError가 이미 socket.timeout으로 대체됩니다(issue 10272 참조).

마지막으로 socket.gaierrorsocket.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는 인터프리터 코어와 표준 라이브러리에서 완전히 사용되지 않습니다. 이는 Jean-François Piéronne이 2002년에 제출한 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()는 ferror()가 FILE * 매개변수에서 오류를 알릴 때 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(앞의 예외를 상속함)을 발생시킵니다.:

>>> 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

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

sys

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

time

time.time() 및 time.sleep()의 내부 오류에 대해 IOError를 발생시킵니다.

zipimport

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

감사의 말

Alyssa Coghlan으로부터 상당한 입력을 받았습니다.

참고 문헌