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

Python 개선 제안 한국어 번역

PEP 678 – 노트로 예외 보강하기

Author:
Zac Hatfield-Dodds <zac at zhd.dev>
Sponsor:
Irit Katriel
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Requires:
654
Created:
20-Dec-2021
Python-Version:
3.11
Post-History:
27-Jan-2022
Resolution:
Discourse message

Table of Contents

번역·라이선스 안내

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

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at BaseException.add_note() and BaseException.__notes__.

×

사용자 중심 튜토리얼은 Enriching Exceptions with Notes 문서를 참조하십시오.

See PEP 1 for how to propose changes.

초록

예외 객체는 일반적으로 발생한 오류를 설명하는 메시지로 초기화됩니다. 예외가 포착되어 다시 발생하거나 ExceptionGroup에 포함될 때 추가 정보를 얻을 수 있으므로, 이 PEP는 BaseException.add_note(note) 메서드와, 이렇게 추가된 노트의 리스트를 담는 .__notes__ 속성을 추가하고, 내장 트레이스백 포매팅 코드를 수정하여 포매팅된 트레이스백에서 예외 문자열 뒤에 노트를 포함하도록 할 것을 제안합니다.

이는 PEP 654의 ExceptionGroup과 관련하여 특히 유용한데, ExceptionGroup으로 인해 기존의 우회 방법이 효과가 없어지거나 혼란스러워지기 때문입니다. 표준 라이브러리, Hypothesis와 cattrs 패키지, 그리고 재시도를 수행하는 일반적인 코드 패턴에서 사용 사례가 확인되었습니다.

동기

예외를 발생시키기 위해 생성할 때는 보통 발생한 오류를 설명하는 정보로 예외를 초기화합니다. 예외가 포착된 뒤에 정보를 추가하는 것이 유용한 경우가 있습니다. 예를 들어,

  • 테스트 라이브러리는 실패한 단언(assertion)에 관련된 값이나 실패를 재현하는 단계를 보여 주고 싶을 수 있습니다(예: pytest와 hypothesis, 아래 예제 참고).
  • 오류가 발생했을 때 작업을 재시도하는 코드는 여러 오류 각각에 반복 횟수, 타임스탬프 또는 기타 설명을 연결하고 싶을 수 있습니다. 특히 그 오류들을 ExceptionGroup으로 묶어 다시 발생시키는 경우에 그렇습니다.
  • 초보자를 위한 프로그래밍 환경은 여러 오류에 대한 더 자세한 설명과 해결 요령을 제공할 수 있습니다.

기존 방식에서는 이 추가 정보를 계속 전달하면서, 발생했고 포착되거나 연쇄되었을 수도 있는 예외의 상태와 동기화된 상태로 유지해야 합니다. 이는 이미 오류가 발생하기 쉬운 방식이며 PEP 654의 ExceptionGroup으로 인해 더욱 어려워졌으므로, 이제 내장 해결책을 마련할 때입니다. 따라서 다음을 추가할 것을 제안합니다:

  • 새 메서드 BaseException.add_note(note: str),
  • .add_note()로 추가된 노트 문자열의 리스트인 BaseException.__notes__, 그리고
  • 포매팅된 트레이스백에서 예외 문자열 뒤에 노트가 표시되도록 하는 내장 트레이스백 포매팅 코드의 지원.

사용 예

>>> try:
...     raise TypeError('bad type')
... except Exception as e:
...     e.add_note('Add some information')
...     raise
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: bad type
Add some information
>>>

예외들을 예외 그룹으로 모을 때, 개별 오류에 대한 문맥 정보를 추가하고 싶을 수 있습니다. Hypothesis에서 제안된 ExceptionGroup 지원을 사용하는 다음 예제에서는 각 예외에 최소 실패 예제에 대한 노트가 포함됩니다:

from hypothesis import given, strategies as st, target

@given(st.integers())
def test(x):
    assert x < 0
    assert x > 0


+ Exception Group Traceback (most recent call last):
|   File "test.py", line 4, in test
|     def test(x):
|
|   File "hypothesis/core.py", line 1202, in wrapped_test
|     raise the_error_hypothesis_found
|     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| ExceptionGroup: Hypothesis found 2 distinct failures.
+-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "test.py", line 6, in test
    |     assert x > 0
    |     ^^^^^^^^^^^^
    | AssertionError: assert -1 > 0
    |
    | Falsifying example: test(
    |     x=-1,
    | )
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "test.py", line 5, in test
    |     assert x < 0
    |     ^^^^^^^^^^^^
    | AssertionError: assert 0 < 0
    |
    | Falsifying example: test(
    |     x=0,
    | )
    +------------------------------------

목표가 아닌 것

노트를 추가할 때 문자열을 이어 붙이는 대신 여러 노트를 리스트로 관리하는 것은 개별 노트 간의 구분을 유지하기 위한 것입니다. 이는 friendly-traceback 같은 패키지가 노트를 번역하는 경우처럼 특수한 사용 사례에서 필요할 수 있습니다.

그러나 __notes__는 구조화된 데이터를 담기 위한 것이 아닙니다. 노트를 사람에게 보여 주는 용도가 아니라 프로그램이 사용하는 용도라면, 대신 (또는 추가로) 오류나 ExceptionGroup에 둘 속성에 대한 규약(예: err._parse_errors = ...)을 정하는 것을 권장합니다.

경험적으로, 오류를 다시 발생시키거나 개별 오류로 처리할 때는 예외 연쇄를 우선 사용하고, 예외 타입을 바꾸고 싶지 않거나 여러 예외 객체를 모아 함께 처리할 때는 .add_note()를 우선 사용할 것을 권장합니다. [1]

명세

BaseException에 새 메서드 .add_note(note: str)가 추가됩니다. note가 문자열이면 .add_note(note)는 이를 __notes__ 리스트에 추가하며, 해당 속성이 아직 없으면 새로 만듭니다. note가 문자열이 아니면 .add_note()는 TypeError를 발생시킵니다.

라이브러리는 __notes__ 리스트가 만들어져 있다면 이를 수정하거나 삭제하여 기존 노트를 지울 수 있으며, del err.__notes__로 모든 노트를 지우는 것도 여기에 포함됩니다. 이를 통해 API를 지나치게 복잡하게 만들거나 BaseException.__dict__에 여러 이름을 추가하지 않고도 첨부된 노트를 완전히 제어할 수 있습니다.

인터프리터의 내장 트레이스백 렌더링 코드가 예외를 표시할 때, 그 예외의 노트는 (있다면) 예외 메시지 바로 뒤에 추가된 순서대로 나타나며, 각 노트는 새 줄에서 시작합니다.

__notes__가 만들어져 있다면, BaseExceptionGroup.subgroup과 BaseExceptionGroup.split은 새 인스턴스마다 원래 예외 그룹의 __notes__와 같은 내용을 담은 새 리스트를 만듭니다.

사용자가 __notes__에 리스트가 아닌 값이나 문자열이 아닌 요소를 담은 리스트를 대입한 경우의 예상 동작은 명세하지 않습니다. 구현은 경고를 내보내거나, 잘못된 값을 버리거나 무시하거나, 문자열로 변환하거나, 예외를 발생시키거나, 또는 전혀 다른 일을 하도록 선택할 수 있습니다.

하위 호환성

시스템 정의 이름, 즉 “dunder” 이름(__*__ 패턴을 따르는 이름)은 언어 명세의 일부이며, 할당되지 않은 이름은 향후 사용을 위해 예약되어 있어 경고 없이 깨질 수 있습니다. 또한 __notes__를 추가해서 깨지게 될 코드도 알지 못합니다.

BaseException.add_note()를 추가해서 깨질 코드 역시 찾을 수 없었습니다: Google과 GitHub을 검색하면 .add_note() 메서드의 정의가 여러 개 발견되지만, 그중 어느 것도 BaseException의 서브클래스에 있지 않습니다.

교육 방법

add_note() 메서드와 __notes__ 속성은 언어 표준의 일부로 문서화되며, “오류와 예외” 튜토리얼의 일부로 설명될 것입니다.

참조 구현

PEP 654 [2]와 관련된 논의에 이어, 이 제안의 초기 버전이 문자열 또는 None 값을 갖는 변경 가능한 __note__ 속성과 함께 구현되었고 CPython 3.11.0a3에서 릴리스되었습니다.

CPython PR #31317이 .add_note()와 __notes__를 구현합니다.

기각된 아이디어

raise Wrapper(explanation) from err

대안이 되는 패턴은 예외 연쇄를 사용하는 것입니다: 맥락이나 설명을 담은 ‘래퍼’ 예외를 현재 예외로부터(from) 발생시키면 print()의 분리 문제를 피할 수 있습니다. 하지만 여기에는 두 가지 핵심 문제가 있습니다.

첫째, 예외의 타입이 바뀌는데, 이는 다운스트림 코드에 호환성을 깨는 변경이 되는 경우가 많습니다. 항상 Wrapper 예외를 발생시키는 것은 받아들일 수 없을 만큼 우아하지 않다고 생각합니다; 하지만 사용자 정의 예외 타입은 필수 인자를 얼마든지 가질 수 있으므로, 설명을 담은 같은 타입의 인스턴스를 항상 만들 수는 없습니다. 표준 라이브러리 http.client 코드처럼 정확한 예외 타입을 아는 경우에는 이 방법이 통할 수 있지만, 사용자 코드를 호출하는 라이브러리에서는 그렇지 않습니다.

둘째, 예외 연쇄는 여러 줄의 추가 세부 정보를 보고하는데, 이는 숙련된 사용자에게는 주의를 흩뜨리고 초보자에게는 매우 혼란스러울 수 있습니다. 예를 들어, 이 간단한 예에서 보고되는 11줄 중 6줄이 예외 연쇄와 관련되어 있으며, BaseException.add_note()를 사용하면 필요 없는 줄입니다:

class Explanation(Exception):
    def __str__(self):
        return "\n" + str(self.args[0])

try:
    raise AssertionError("Failed!")
except Exception as e:
    raise Explanation("You can reproduce this error by ...") from e
$ python example.py
Traceback (most recent call last):
File "example.py", line 6, in <module>
    raise AssertionError(why)
AssertionError: Failed!
                                                    # These lines are
The above exception was the direct cause of ...     # confusing for new
                                                    # users, and they
Traceback (most recent call last):                  # only exist due
File "example.py", line 8, in <module>              # to implementation
    raise Explanation(msg) from e                   # constraints :-(
Explanation:                                        # Hence this PEP!
You can reproduce this error by ...

이 두 문제가 해당하지 않는 경우에는 예외 연쇄(exception chaining) 사용을 권장하며, __notes__는 권장하지 않습니다.

대입 가능한 __note__ 속성

이 PEP의 첫 초안과 구현은 단일 속성 __note__를 정의했는데, 이 속성의 기본값은 None이지만 문자열을 대입할 수 있었습니다. 이 방식은 노트가 최대 하나인 경우에만, 오직 그 경우에 한해서 훨씬 더 단순합니다.

따라서 저희는 의심스러운 파싱 휴리스틱에 의존하지 않으면서 상호 운용성을 높이고 friendly-traceback 같은 라이브러리의 오류 메시지 번역을 지원하기 위해, .add_note()와 __notes__로 이루어진 API를 채택했습니다.

Exception 서브클래스를 만들어 다운스트림에서 노트 지원을 추가하는 예외 처리 방식

트레이스백 출력은 C 코드에 내장되어 있으며, traceback.py에 순수 Python으로 재구현되어 있습니다. 다운스트림 구현에서 err.__notes__를 출력하려면 사용자 정의 트레이스백 출력 코드도 함께 작성해야 합니다. 이 코드는 프로젝트 간에 공유할 수 있고 traceback.py [3]의 일부를 재사용할 수도 있지만, 저희는 업스트림에서 한 번 구현하는 편을 선호합니다.

사용자 정의 예외 타입은 저희가 제안하는 __notes__ 의미론을 포함하도록 자신의 __str__ 메서드를 구현할 수 있지만, 이는 드물게, 그리고 일관성 없이 적용될 것입니다.

Exception에 노트를 붙이지 말고, ExceptionGroup에만 저장하기

이 PEP의 최초 동기는 ExceptionGroup 안의 각 오류에 노트를 연결하는 것이었습니다. 매우 어색한 API와 앞에서 논의한 상호 참조 문제를 감수한다면, 이 사용 사례는 ExceptionGroup에 포함된 각 예외가 아니라 ExceptionGroup 인스턴스에 노트를 저장하는 방식으로 지원할 수 있습니다.

저희는 더 깔끔한 인터페이스와 앞에서 설명한 다른 사용 사례들이 이 PEP가 제안하는 더 일반적인 기능을 정당화하기에 충분하다고 믿습니다.

헬퍼 함수 contextlib.add_exc_note() 추가하기

아래와 같은 유틸리티를 표준 라이브러리에 추가하자는 제안이 있었습니다. 저희는 이 아이디어가 이 PEP 제안의 핵심이라고 보지 않으므로, 나중에 구현하거나 다운스트림에서 구현하도록 남겨 둡니다. 아마도 다음 예제 코드를 바탕으로 구현할 수 있을 것입니다:

@contextlib.contextmanager
def add_exc_note(note: str):
    try:
        yield
    except Exception as err:
        err.add_note(note)
        raise

with add_exc_note(f"While attempting to frobnicate {item=}"):
    frobnicate_or_raise(item)

raise 문 확장하기

한 논의에서는 raise Exception() with "note contents"를 제안했지만, 이는 ExceptionGroup과의 호환성이라는 원래 동기를 해결하지 못합니다.

게다가 저희는 저희가 해결하려는 문제가 새로운 언어 문법을 필요로 하거나 정당화한다고 생각하지 않습니다.

감사의 글

대화, 코드 리뷰, 설계 조언, 구현을 통해 도움을 주신 많은 분께 감사드립니다: Adam Turner, Alex Grönholm, André Roberge, Barry Warsaw, Brett Cannon, CAM Gerlach, Carol Willing, Damian, Erlend Aasland, Etienne Pot, Gregory Smith, Guido van Rossum, Irit Katriel, Jelle Zijlstra, Ken Jin, Kumar Aditya, Mark Shannon, Matti Picus, Petr Viktorin, Will McGugan, 그리고 Discord와 Reddit의 익명 댓글 작성자 여러분께 감사드립니다.

참고 문헌