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

Python 개선 제안 한국어 번역

PEP 553 – 내장 breakpoint()

Author:
Barry Warsaw <barry at python.org>
Status:
Final
Type:
Standards Track
Created:
05-Sep-2017
Python-Version:
3.7
Post-History:
05-Sep-2017, 07-Sep-2017, 13-Sep-2017
Resolution:
Python-Dev message

Table of Contents

번역·라이선스 안내

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

초록

이 PEP는 호출 지점에서 Python 디버거로 진입하는 breakpoint()라는 새로운 내장 함수를 추가할 것을 제안합니다. 또한 어떤 디버거로 진입할지 설정할 수 있도록 sys 모듈에 두 개의 새 이름을 추가합니다.

근거

Python은 오래전부터 표준 라이브러리에 pdb라는 훌륭한 디버거를 갖추고 있었습니다. 중단점 설정은 흔히 다음과 같이 작성합니다:

foo()
import pdb; pdb.set_trace()
bar()

따라서 foo()를 실행한 후 bar()를 실행하기 전에 Python은 디버거로 진입합니다. 하지만 이 관용구에는 몇 가지 단점이 있습니다.

  • 입력할 것이 많습니다(27자).
  • 오타를 내기 쉽습니다. PEP 작성자도 이 줄을 자주 잘못 입력하는데, 예를 들어 세미콜론을 빠뜨리거나 밑줄 대신 점을 입력합니다.
  • 디버깅을 pdb라는 선택에 직접 묶어 버립니다. IDE나 다른 개발 환경을 사용하는 경우처럼 다른 디버깅 수단이 있을 수도 있습니다.
  • Python 린터(예: flake8 [linters])는 이 줄이 두 개의 문을 포함하고 있기 때문에 경고합니다. 이 관용구를 두 줄로 나누면 정리할 때 실수할 여지가 더 많아지므로 사용하기가 번거로워집니다. 즉, 더 이상 코드를 디버깅할 필요가 없을 때 두 줄 중 하나를 지우는 것을 잊어버릴 수 있습니다.

Python 개발자는 선택할 수 있는 다른 디버거도 많이 있지만, 그것들을 호출하는 방법을 기억하는 것이 문제가 될 수 있습니다. 예를 들어 IDE에 중단점을 설정하는 사용자 인터페이스가 있더라도, 그냥 코드를 편집하는 편이 여전히 더 편리할 수 있습니다. 프로그램에서 디버거로 진입하는 API가 제각각이어서 정확히 무엇을 입력해야 하는지 기억하기 어려울 수 있습니다.

이 PEP에서 제안하는 것처럼 디버거로 진입하는 범용 API를 제공하면 이 모든 문제를 해결할 수 있습니다.

제안

JavaScript 언어는 문이 나타나는 지점에서 디버거로 진입하는 debugger 문 [js-debugger]을 제공합니다.

이 PEP는 호출 위치에서 Python 디버거로 진입하는 breakpoint()라는 새로운 내장 함수를 제안합니다. 따라서 위의 예제는 다음과 같이 작성하게 됩니다:

foo()
breakpoint()
bar()

나아가 이 PEP는 sys 모듈에 sys.breakpointhook()과 sys.__breakpointhook__이라는 두 개의 새 이름 바인딩을 추가할 것을 제안합니다. 기본적으로 sys.breakpointhook()은 실제 임포트와 pdb.set_trace() 진입을 구현하며, 다른 함수로 설정하여 breakpoint()가 진입하는 디버거를 바꿀 수 있습니다.

sys.__breakpointhook__은 sys.breakpointhook()과 같은 함수로 초기화되므로, 언제든지 sys.breakpointhook()을 기본값으로 쉽게 재설정할 수 있습니다(예: sys.breakpointhook = sys.__breakpointhook__을 실행하여). 이는 기존 훅인 sys.displayhook() / sys.__displayhook__와 sys.excepthook() / sys.__excepthook__가 동작하는 방식과 정확히 같습니다 [hooks].

이 내장 함수의 시그니처는 breakpoint(*args, **kws)입니다. 위치 인자와 키워드 인자는 sys.breakpointhook()에 그대로 전달되므로 시그니처가 일치해야 하며, 그렇지 않으면 TypeError가 발생합니다. sys.breakpointhook()의 반환값은 breakpoint()로 다시 전달되어 breakpoint()에서 반환됩니다.

이에 대한 근거는 기반이 되는 디버거가 추가적인 선택적 인자를 받을 수 있다는 관찰에 바탕을 두고 있습니다. 예를 들어, IPython에서는 중단점에 진입할 때 출력되는 문자열을 지정할 수 있습니다 [ipython-embed]. Python 3.7부터는 pdb 모듈도 선택적인 헤더 인자인 header를 지원합니다 [pdb-header].

환경 변수

sys.breakpointhook()의 기본 구현은 PYTHONBREAKPOINT라는 새 환경 변수를 참조합니다. 이 환경 변수는 다양한 값을 가질 수 있습니다:

  • PYTHONBREAKPOINT=0은 디버깅을 비활성화합니다. 구체적으로, 이 값이 설정되면 sys.breakpointhook()은 즉시 None을 반환합니다.
  • PYTHONBREAKPOINT= (즉, 빈 문자열). 이는 환경 변수를 전혀 설정하지 않은 것과 같으며, 이 경우 pdb.set_trace()가 평소처럼 실행됩니다.
  • PYTHONBREAKPOINT=some.importable.callable. 이 경우 sys.breakpointhook()은 some.importable 모듈을 임포트하고, 그 결과 모듈에서 callable 객체를 가져온 다음 이를 호출합니다. 값은 점이 없는 문자열일 수도 있으며, 이 경우 내장 호출 가능 객체의 이름을 나타냅니다. 예: PYTHONBREAKPOINT=int. (Guido는 setuptools 스타일의 진입점 문법이 아닌 일반적인 Python 점 경로를 선호한다는 의견을 밝혔습니다 [syntax].)

이 환경 변수를 사용하면 외부 프로세스가 중단점 처리 방식을 제어할 수 있습니다. 몇 가지 사용 사례는 다음과 같습니다:

  • 실수로 프로덕션에 반영된 모든 breakpoint() 호출을 완전히 비활성화하는 경우. 이는 실행 환경에서 PYTHONBREAKPOINT=0을 설정하여 달성할 수 있습니다. PEP 검토자들이 제안한 또 다른 방법은 이 경우 PYTHONBREAKPOINT=sys.exit를 설정하는 것이었습니다.
  • 임베디드 실행을 위한 특수 디버거와 IDE를 통합하는 경우. IDE는 PYTHONBREAKPOINT를 자체 내부 디버깅 훅으로 설정한 디버깅 환경에서 프로그램을 실행할 것입니다.

PYTHONBREAKPOINT는 sys.breakpointhook()에 도달할 때마다 다시 해석됩니다. 이를 통해 프로세스는 프로그램 실행 중에 그 값을 변경하고, breakpoint()가 그 변경에 반응하도록 할 수 있습니다. 디버거에 진입하면 정의상 실행이 멈추므로, 이 부분은 성능이 중요한 구간으로 간주되지 않습니다. 따라서 프로그램은 다음과 같이 할 수 있습니다:

os.environ['PYTHONBREAKPOINT'] = 'foo.bar.baz'
breakpoint()    # Imports foo.bar and calls foo.bar.baz()

sys.breakpointhook을 재정의하면 PYTHONBREAKPOINT를 참조하는 기본 동작이 무력화됩니다. 원한다면 PYTHONBREAKPOINT를 참조하는 것은 재정의하는 코드의 몫입니다.

PYTHONBREAKPOINT 호출 가능 객체에 대한 접근이 어떤 식으로든 실패하면(예: 임포트가 실패하거나, 결과 모듈에 해당 호출 가능 객체가 없는 경우) RuntimeWarning이 발생하며, 어떤 중단점 함수도 호출되지 않습니다.

다른 모든 PYTHON* 환경 변수와 마찬가지로, 인터프리터가 -E로 시작되면 PYTHONBREAKPOINT는 무시된다는 점에 유의하십시오. 이는 기본 동작이 일어난다는 뜻입니다(즉, pdb.set_trace()가 실행됩니다). 대안으로 -E일 때 PYTHONBREAKPOINT=0이 적용된 것처럼 취급하자는 논의가 있었지만, 의견이 결론에 이르지 못했기 때문에 이것은 특수한 경우로 다룰 만큼 특별하지 않다고 결정되었습니다.

구현

제안된 구현을 담은 풀 리퀘스트가 있습니다 [impl].

실제 구현은 C로 되어 있지만, 이 기능에 대한 Python 의사 코드는 대략 다음과 같습니다:

# In builtins.
def breakpoint(*args, **kws):
    import sys
    missing = object()
    hook = getattr(sys, 'breakpointhook', missing)
    if hook is missing:
        raise RuntimeError('lost sys.breakpointhook')
    return hook(*args, **kws)

# In sys.
def breakpointhook(*args, **kws):
    import importlib, os, warnings
    hookname = os.getenv('PYTHONBREAKPOINT')
    if hookname is None or len(hookname) == 0:
        hookname = 'pdb.set_trace'
    elif hookname == '0':
        return None
    modname, dot, funcname = hookname.rpartition('.')
    if dot == '':
        modname = 'builtins'
    try:
        module = importlib.import_module(modname)
        hook = getattr(module, funcname)
    except:
        warnings.warn(
            'Ignoring unimportable $PYTHONBREAKPOINT: {}'.format(
                hookname),
            RuntimeWarning)
        return None
    return hook(*args, **kws)

__breakpointhook__ = breakpointhook

기각된 대안

새 키워드

원래 저자는 새 키워드나 break here와 같은 기존 키워드의 확장을 고려했습니다. 이 방안은 여러 측면에서 기각되었습니다.

  • 거의 모든 새 키워드는 기존 코드와 충돌할 수 있으므로, 완전히 새로운 키워드를 활성화하려면 __future__가 필요합니다. 이렇게 되면 디버거에 쉽게 진입할 수 있다는 장점이 사라집니다.
  • break here와 같은 확장 키워드는 더 읽기 쉽고 __future__도 필요하지 않지만, 키워드 확장을 이 새 기능에 묶어 버리므로 PEP 548에서 제안된 것과 같은 더 유용한 확장을 가로막게 됩니다.
  • 새 키워드를 도입하려면 문법을 수정해야 하며 새 바이트코드도 필요할 가능성이 높습니다. 이들 각각이 구현을 더 복잡하게 만듭니다. 새 내장 함수는 기존 코드를 전혀 깨뜨리지 않으며(같은 이름의 모듈 전역 이름이 이미 있다면 그것이 내장 함수를 가릴 뿐이기 때문입니다) 구현하기도 매우 쉽습니다.

sys.breakpoint()

왜 sys.breakpoint()가 아닙니까? 모든 모듈이 sys를 임포트하는 것은 아니므로, 디버거를 호출하기 위해 임포트를 요구하는 방식은 명시적으로 기각되었습니다. 이는 타이핑만 더 늘릴 뿐이며 다음과 같은 코드로 이어질 것입니다:

import sys; sys.breakpoint()

이 방식은 이 PEP가 해결하려는 여러 문제를 그대로 물려받습니다.

버전 이력

  • 2019-10-13
    • 의사 코드의 except 절에 누락된 return None을 추가했습니다.
  • 2017-09-13
    • PYTHONBREAKPOINT 환경 변수를 일급 기능으로 격상했습니다.
  • 2017-09-07
    • debug()의 이름을 breakpoint()로 변경했습니다.
    • 시그니처를 breakpoint(*args, **kws)로 변경했으며, 인자는 sys.breakpointhook()에 그대로 전달됩니다.

참고 문헌