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 654ExceptionGroup에서 특히 유용합니다. 표준 라이브러리, Hypothesis 및 cattrs 패키지, 그리고 재시도와 관련된 일반적인 코드 패턴에서 사용 사례가 확인되었습니다.

동기

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

  • 테스트 라이브러리는 실패한 어설션에 관련된 값이나 실패를 재현하는 단계를 표시하려 할 수 있습니다(예: pytesthypothesis; 아래 예제 참조).
  • 오류 발생 시 작업을 재시도하는 코드는 여러 오류 각각에 반복 횟수, 타임스탬프 또는 기타 설명을 연결하려 할 수 있으며, 특히 해당 오류들을 ExceptionGroup에서 다시 발생시키는 경우에 그러합니다.
  • 초보자를 위한 프로그래밍 환경은 다양한 오류에 대해 더 자세한 설명과 해결 방법을 제공할 수 있습니다.

기존 접근 방식은 발생한 예외와 잠재적으로 포착되거나 연결된 예외의 상태와 동기화된 상태를 유지하면서 이 추가 정보를 전달해야 합니다. 이는 이미 오류가 발생하기 쉬운 방식이며 PEP 654ExceptionGroups로 인해 더욱 어려워졌으므로, 이제는 내장 솔루션을 마련할 시점입니다. 따라서 다음을 추가할 것을 제안합니다.

  • 새로운 메서드 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’ proposed support for 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 = ...를 사용하는 것과 같이 속성에 대한 규약을 선택할 것을 we recommend.

일반적인 원칙으로, 오류를 다시 발생시키거나 개별 오류로 처리할 경우에는 exception chaining을 선호하고, 예외 유형을 변경하지 않거나 여러 예외 객체를 함께 처리하기 위해 수집하는 경우에는 .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.subgroupBaseExceptionGroup.split은 새 인스턴스마다 새 목록을 생성하며, 이 목록에는 원래 예외 그룹의 __notes__와 동일한 내용이 포함됩니다.

사용자가 __notes__에 목록이 아닌 값을 할당했거나 문자열이 아닌 요소가 포함된 목록을 할당한 경우의 예상 동작은 지정하지 않습니다. 구현에서는 잘못된 값에 대해 경고를 내보내거나, 폐기하거나 무시하거나, 문자열로 변환하거나, 예외를 발생시키거나, 완전히 다른 동작을 선택할 수 있습니다.

하위 호환성

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

BaseException.add_note()를 추가하여 손상될 코드도 찾을 수 없었습니다. Google 검색과 GitHub 검색에서 .add_note() 메서드의 정의를 여럿 찾을 수 있었지만, 그중 어느 것도 BaseException의 서브클래스에 있지 않습니다.

이것을 가르치는 방법

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

참조 구현

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

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

거부된 아이디어

raise Wrapper(explanation) from err

또 다른 패턴은 예외 연결을 사용하는 것입니다. 현재 예외 from에 컨텍스트나 설명이 포함된 ‘래퍼’ 예외를 발생시키면 print()에서 발생하는 분리 문제를 피할 수 있습니다. 그러나 여기에는 두 가지 중요한 문제가 있습니다.

첫째, 예외의 유형이 변경되며, 이는 다운스트림 코드에서 하위 호환성을 깨뜨리는 변경인 경우가 많습니다. Wrapper예외를 항상 발생시키는 것은 용납할 수 없을 만큼 우아하지 않다고 생각합니다. 그러나 사용자 정의 예외 유형에는 필요한 인자의 수가 얼마든지 있을 수 있으므로, 설명을 포함한 동일한 유형의 인스턴스를 항상 생성할 수는 없습니다. 정확한 예외 유형을 알고 있는 경우에는 표준 라이브러리 http.clientcode와 같이 이 방법이 작동할 수 있지만, 사용자 코드를 호출하는 라이브러리에는 적용되지 않습니다.

둘째, 예외 연결은 추가 세부 정보가 담긴 여러 줄을 출력하므로 숙련된 사용자에게는 주의를 분산시키고 초보자에게는 매우 혼란스러울 수 있습니다. 예를 들어 이 간단한 예제에서 보고되는 열한 줄 중 여섯 줄은 예외 연결과 관련되어 있으며, 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 코드에 내장되어 있으며, 순수 Python으로 traceback.py에 재구현되어 있습니다. 하위 구현에서 err.__notes__를 출력하려면 사용자 지정 트레이스백 출력 코드도 작성해야 하며, 이를 프로젝트 간에 공유하고 traceback.py [3]의 일부를 재사용할 수 있지만, 이를 업스트림에서 한 번만 구현하는 편을 선호합니다.

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

Exception에 메모를 첨부하지 말고 ExceptionGroup에 저장하십시오

이 PEP의 최초 동기는 ExceptionGroup의 각 오류에 메모를 연결하는 것이었습니다. 이는 상당히 불편한 API와 앞서 논의한 상호 참조 문제를 감수하면, 각 예외에 메모를 저장하는 대신 해당 예외를 포함하는 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에서 활동한 익명의 댓글 작성자 여러분께 감사드립니다.

참고 문헌