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는 줄 이벤트만 다룹니다.

추적이 켜져 있을 때 줄 이벤트는 다음과 같은 경우에 생성됩니다.

  • 새로운 소스 코드 줄에 도달한 경우
  • 리스트 컴프리헨션에서 일어날 수 있는 것처럼, 같은 줄로 점프하더라도 역방향 점프가 발생한 경우

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

추적 목적상 코드로 간주되는 것

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

일반적으로 모든 문은 실행 가능한 코드로도 간주됩니다. 하지만 하나의 문이 여러 줄에 걸쳐 있을 때는 문의 어느 부분을 실행 가능한 코드로 간주할지 고려해야 합니다.

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

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

  • del – 삭제할 표현식이 실행 가능한 코드로 취급됩니다.
  • else – 런타임 효과 없음
  • finally – 런타임 효과 없음
  • global – 순수하게 선언적임
  • nonlocal – 순수하게 선언적임

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

이벤트 시퀀스 예시

다음 예시에서 이벤트는 “이름”, 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입니다.

생성되는 시퀀스는 다음과 같은 속성을 가집니다:

  • 시퀀스의 첫 번째 범위는 start가 0입니다
  • (start, end) 범위는 감소하지 않으며 연속적입니다. 즉, 임의의 튜플 쌍에서 두 번째 튜플의 start는 첫 번째 튜플의 end와 같습니다.
  • 역방향 범위는 없습니다. 즉, 모든 트리플에 대해 end >= start입니다.
  • 시퀀스의 마지막 범위는 end가 바이트코드의 크기와 같습니다.
  • line은 양의 정수이거나 None입니다.

너비가 0인 범위

너비가 0인 범위, 즉 start == end인 범위도 허용됩니다. 너비가 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

a가 True이면 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_lineno는 PyFrame_GetLineNumber로 읽을 수 있습니다. f_lineno는 PyObject_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의 데이터는 불변이지만, 그 수명은 해당 코드 객체에 따라 달라집니다. 안정적으로 동작하게 하려면 PyLineTable_InitAddressRange를 호출하기 전에 linetable을 로컬 버퍼에 복사해야 합니다.

이 함수들은 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