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

Python 개선 제안 한국어 번역

PEP 626 – 디버깅 및 기타 도구를 위한 정확한 줄 번호입니다.

Author:
Mark Shannon <mark at hotpy.org>
BDFL-Delegate:
Pablo Galindo Salgado <pablogsal at python.org>
Status:
Final
Type:
Standards Track
Created:
15-Jul-2020
Python-Version:
3.10
Post-History:
17-Jul-2020

Table of Contents

번역·라이선스 안내

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

초록

Python은 추적이 켜져 있을 때 실행되는 코드의 모든 줄에 대해, 그리고 실행되는 코드의 줄에 대해서만 “line” 추적 이벤트가 생성되도록 보장해야 합니다.

프레임 객체의 f_lineno속성에는 항상 예상되는 줄 번호가 포함되어야 합니다. 프레임이 실행되는 동안 예상되는 줄 번호는 현재 실행 중인 소스 코드의 줄 번호입니다. 프레임이 반환하거나 예외를 발생시켜 완료된 후에는, 예상되는 줄 번호가 실행된 소스의 마지막 줄 번호입니다.

올바른 줄 번호를 보장하는 데 따른 부작용으로 일부 바이트코드는 인공적인 것으로 표시되어야 하며, 의미 있는 줄 번호를 갖지 않아야 합니다. 도구를 지원하기 위해 바이트코드와 소스 간의 매핑을 설명하는 새로운 co_lines속성이 추가됩니다.

동기

sys.settrace사용자와 관련 도구는 모든 코드 줄에 대해, 그리고 실제 코드에 대해서만 트레이싱 이벤트가 생성된다고 믿을 수 있어야 합니다. 또한 f_lineno의 줄 번호가 올바르다고 가정할 수 있어야 합니다.

현재 구현은 대부분 이를 수행하지만 몇 가지 경우에 실패합니다. 이로 인해 도구에서 우회책이 필요하며, 대체 Python 구현에 불편을 초래합니다.

이러한 보장은 장기적으로 CPython 구현자에게도 도움이 됩니다. 현재 동작은 명확하지 않고 몇 가지 특이한 모서리 사례가 있기 때문입니다.

근거

예상되는 시점에 라인 이벤트가 생성되도록 보장하려면, 현재 형태의 co_lnotab속성은 더 이상 줄 번호 정보의 신뢰할 수 있는 기준이 될 수 없습니다.

co_lnotab속성을 수정하려고 하기보다는, 바이트코드 오프셋과 소스 코드 줄에 대한 이터레이터를 반환하는 새로운 co_lines()메서드를 추가합니다.

정확한 줄 번호 정보를 활성화하도록 바이트코드에 올바르게 주석을 추가하려면 일부 바이트코드를 인공적인 것으로 표시하고 줄 번호를 부여하지 않아야 합니다.

기존 도구를 중단하지 않도록 주의해야 합니다. 변경으로 인한 문제를 최소화하기 위해 co_lnotab속성은 유지하되, 요청될 때 느리게 생성합니다.

사양

라인 이벤트와 f_lineno속성은 모든 경우에 숙련된 Python 사용자가 예상하는 대로 동작해야 합니다.

트레이싱

트레이싱은 호출, 반환, 예외, 실행된 소스 코드의 줄, 그리고 일부 상황에서는 실행된 명령어에 대한 이벤트를 생성합니다.

이 PEP에서는 라인 이벤트만 다룹니다.

트레이싱이 활성화되면 다음과 같은 경우에 라인 이벤트가 생성됩니다.

  • 새로운 소스 코드 줄에 도달할 때입니다.
  • 리스트 컴프리헨션에서 발생할 수 있듯이, 같은 줄로 이동하는 경우를 포함하여 뒤로 이동하는 점프가 발생할 때입니다.

또한 실행되지 않은 소스 코드 줄에 대해서는 라인 이벤트가 절대로 생성되지 않습니다.

트레이싱 목적에서 코드로 간주되는 것

모든 표현식과 표현식의 일부는 실행 가능한 코드로 간주됩니다.

일반적으로 모든 문장은 실행 가능한 코드로도 간주합니다. 그러나 문장이 여러 줄에 걸쳐 작성된 경우에는 문장의 어느 부분이 실행 가능한 코드로 간주되는지 고려해야 합니다.

문장은 키워드와 표현식으로 구성됩니다. 모든 키워드가 직접적인 런타임 효과를 가지는 것은 아니므로, 모든 키워드가 실행 가능한 코드로 간주되는 것은 아닙니다. 예를 들어, elseif 문에 필요한 부분이지만, else와 관련된 런타임 효과는 없습니다.

추적을 위해 다음 키워드는 실행 가능한 코드로 간주되지 않습니다:

  • del – 삭제할 표현식은 실행 가능한 코드로 취급합니다.
  • else – 런타임 효과가 없습니다.
  • finally – 런타임 효과가 없습니다.
  • global – 순전히 선언적입니다.
  • nonlocal – 순전히 선언적입니다.

그 밖의 모든 키워드는 실행 가능한 코드로 간주합니다.

이벤트 시퀀스 예제

다음 예제에서는 이벤트를 “name”, f_lineno 쌍으로 나열합니다.

다음 코드는

1.     global x
2.     x = a

다음 이벤트를 생성합니다.:

"line" 2

다음 코드는

1.     try:
2.        pass
3.     finally:
4.        pass

다음 이벤트를 생성합니다.:

"line" 1
"line" 2
"line" 4

다음 코드는

1.      for (
2.          x) in [1]:
3.          pass
4.      return

다음 이벤트를 생성합니다.:

"line" 2       # evaluate [1]
"line" 1       # for
"line" 2       # store to x
"line" 3       # pass
"line" 1       # for
"line" 4       # return
"return" 1

f_lineno 속성

  • 프레임 객체가 생성되면 f_lineno 속성은 함수 또는 클래스가 정의된 줄, 즉 def 또는 class 키워드가 나타나는 줄로 설정됩니다. 모듈의 경우에는 0으로 설정됩니다.
  • f_lineno 속성은 추적이 꺼져 있고 이벤트가 생성되지 않는 경우에도 곧 실행될 줄 번호와 일치하도록 업데이트됩니다.

코드 객체의 새로운 co_lines() 메서드

co_lines() 메서드는 값의 튜플을 생성하는 이터레이터를 반환하며, 각 튜플은 바이트코드 범위의 줄 번호를 나타냅니다. 각 튜플은 세 개의 값으로 구성됩니다.

  • start – 바이트코드 범위 시작 부분의 오프셋(포함)
  • end – 바이트코드 범위 끝 부분의 오프셋(제외)
  • line – 줄 번호 또는 주어진 범위의 바이트코드에 줄 번호가 없으면 None

생성되는 시퀀스에는 다음과 같은 속성이 있습니다.

  • 시퀀스의 첫 번째 범위의 start0입니다.
  • (start, end) 범위는 내림차순이 아니며 연속적입니다. 즉, 임의의 튜플 쌍에 대해 두 번째 튜플의 start는 첫 번째 튜플의 end와 같아집니다.
  • 어떤 범위도 역방향이 되지 않으며, 즉 모든 트리플에 대해 end >= start입니다.
  • 시퀀스의 마지막 범위는 end가 바이트코드의 크기와 같아집니다.
  • line은 양의 정수이거나 None입니다.

너비가 0인 범위

start == end인 범위, 즉 너비가 0인 범위도 허용됩니다. 너비가 0인 범위는 소스 코드에는 존재하지만 바이트코드 컴파일러에 의해 제거된 줄에 사용됩니다.

co_linetable 특성

co_linetable 특성은 줄 번호 정보를 보유합니다. 형식은 불투명하고 지정되지 않았으며 사전 통지 없이 변경될 수 있습니다. 이 특성은 새 코드 객체 생성을 지원하기 위한 목적으로만 공개됩니다.

co_lnotab 특성

역사적으로 co_lnotab 특성은 바이트코드 오프셋에서 줄 번호로의 매핑을 보유했지만, 줄 번호가 없는 바이트코드는 지원하지 않습니다. 하위 호환성을 위해 co_lnotab 바이트 객체는 필요할 때 지연 생성됩니다. 줄 번호가 없는 바이트코드 범위에는 이전 바이트코드 범위의 줄 번호가 사용됩니다.

co_lnotab 테이블을 구문 분석하는 도구는 가능한 한 빨리 새로운 co_lines() 메서드를 사용하도록 전환해야 합니다.

하위 호환성

co_lnotab 특성은 3.10에서 더 이상 사용되지 않도록 지정되고 3.12에서 제거됩니다.

코드 객체의 co_lnotab 특성을 구문 분석하는 모든 도구는 3.12가 릴리스되기 전에 co_lines()를 사용하도록 전환해야 합니다. sys.settrace를 사용하는 도구는 영향을 받지 않지만, 수신하는 “line” 이벤트가 더 정확해지는 경우는 예외입니다.

추적 이벤트의 시퀀스가 변경되는 코드 예

다음 예에서는 이벤트가 “name”, f_lineno 쌍으로 나열됩니다.

if 문 안의 pass

0.  def spam(a):
1.      if a:
2.          eggs()
3.      else:
4.          pass

aTrue인 경우 Python 3.9에서 생성되는 이벤트 시퀀스는 다음과 같습니다.:

"line" 1
"line" 2
"line" 4
"return" 4

3.10부터 시퀀스는 다음과 같습니다.:

"line" 1
"line" 2
"return" 2

여러 pass

0.  def bar():
1.      pass
2.      pass
3.      pass

Python 3.9에서 생성되는 이벤트 시퀀스는 다음과 같습니다.:

"line" 3
"return" 3

3.10부터 시퀀스는 다음과 같습니다.:

"line" 1
"line" 2
"line" 3
"return" 3

C API

C API 함수를 통한 프레임 객체의 f_lineno 특성 액세스는 변경되지 않습니다. f_linenoPyFrame_GetLineNumber로 읽을 수 있습니다. f_linenoPyObject_SetAttr 및 유사한 함수를 통해서만 설정할 수 있습니다.

기본 데이터 구조를 통해 f_lineno에 직접 액세스하는 것은 금지됩니다.

프로세스 외부 디버거 및 프로파일러

py-spy [1]와 같은 프로세스 외부 도구는 C-API를 사용할 수 없으며, 줄 번호 테이블을 직접 구문 분석해야 합니다. 줄 번호 테이블 형식은 경고 없이 변경될 수 있지만, 버그 수정에 절대적으로 필요한 경우가 아니면 릴리스 중에는 변경되지 않습니다.

이러한 도구를 구현하는 데 필요한 작업을 줄이기 위해 다음 C 구조체와 유틸리티 함수가 제공됩니다. 이러한 함수는 C-API의 일부가 아니므로, 이를 사용해야 하는 모든 코드에 링크해야 합니다.

typedef struct addressrange {
    int ar_start;
    int ar_end;
    int ar_line;
    struct _opaque opaque;
} PyCodeAddressRange;

void PyLineTable_InitAddressRange(char *linetable, Py_ssize_t length, int firstlineno, PyCodeAddressRange *range);
int PyLineTable_NextAddressRange(PyCodeAddressRange *range);
int PyLineTable_PreviousAddressRange(PyCodeAddressRange *range);

PyLineTable_InitAddressRange는 줄 번호 테이블과 첫 번째 줄 번호에서 PyCodeAddressRange 구조체를 초기화합니다.

PyLineTable_NextAddressRange는 범위를 다음 항목으로 진행하며, 유효한 경우 0이 아닌 값을 반환합니다.

PyLineTable_PreviousAddressRange는 범위를 이전 항목으로 되돌리며, 유효한 경우 0이 아닌 값을 반환합니다.

Note

linetable의 데이터는 변경할 수 없지만, 수명은 해당 코드 객체에 따라 달라집니다. 안정적인 동작을 위해 linetablePyLineTable_InitAddressRange를 호출하기 전에 로컬 버퍼에 복사해야 합니다.

이러한 함수는 C-API의 일부가 아니지만, 앞으로 출시되는 모든 CPython 버전에서 제공될 것입니다. PyLineTable_ 함수는 C-API를 호출하지 않으므로, 이를 사용해야 하는 어떤 도구에든 안전하게 복사할 수 있습니다. PyCodeAddressRange 구조체는 변경되지 않지만, _opaque 구조체는 명세의 일부가 아니므로 변경될 수 있습니다.

Note

PyCodeAddressRange 구조체는 이 PEP의 원래 버전에서 변경되었으며, 원래 버전에서는 추가 필드가 정의되어 있었지만 변경될 가능성이 있었습니다.

예를 들어, 다음 코드는 모든 주소 범위를 출력합니다.

void print_address_ranges(char *linetable, Py_ssize_t length, int firstlineno)
{
    PyCodeAddressRange range;
    PyLineTable_InitAddressRange(linetable, length, firstlineno, &range);
    while (PyLineTable_NextAddressRange(&range)) {
        printf("Bytecodes from %d (inclusive) to %d (exclusive) ",
               range.start, range.end);
        if (range.line < 0) {
            /* line < 0 means no line number */
            printf("have no line number\n");
        }
        else {
            printf("have line number %d\n", range.line);
        }
    }
}

성능에 미치는 영향

일반적으로 성능에는 변화가 없어야 합니다. 트레이싱 시, 새 테이블 형식이 줄 번호 계산 속도를 염두에 두고 설계될 수 있으므로 프로그램이 조금 더 빠르게 실행되어야 합니다. pass 문이 길게 연속된 코드는 아마 다소 느려질 것입니다.

참조 구현

https://github.com/markshannon/cpython/tree/new-linetable-format-version-2

References