>백엔드 개발 >파이썬 튜토리얼 >Python 바이트코드에 중첩 함수에 대한 지원을 추가하는 방법

Python 바이트코드에 중첩 함수에 대한 지원을 추가하는 방법

Susan Sarandon
Susan Sarandon원래의
2024-12-31 18:58:18899검색

How I added support for nested functions in Python bytecode

정말 멋진 것들을 여러분과 공유하고 싶었습니다. 저는 Python 바이트코드에 대해 배우고 중첩에 대한 지원을 추가한 방법을 포함합니다. 기능이 있는데 인쇄소 직원이 500단어 미만으로 유지해야 한다고 하더군요.

연휴 주간입니다라고 그는 어깨를 으쓱했습니다. 내가 무엇을 하기를 바라나요?

코드조각 제외, 흥정했어요

괜찮습니다, 양보했습니다.

애초에 왜 바이트코드를 사용하는지 아시나요?

저는 인쇄기만 운영하고 있지만 당신을 믿습니다.

그렇습니다. 시작해 보겠습니다.

애초에 바이트코드를 사용하는 이유

Rust로 작성된 Python 인터프리터인 Memphis에는 두 개의 실행 엔진이 있습니다. 둘 다 모든 코드를 실행할 수는 없지만 둘 다 일부 코드를 실행할 수는 있습니다.

treewalk 인터프리터는 자신이 무엇을 하고 있는지 모른다면 만들 수 있는 것입니다. ?‍♂️ 입력 Python 코드를 토큰화하고 추상 구문 트리(AST)를 생성한 다음 트리를 살펴보고 각 노드를 평가합니다. 표현식은 값을 반환하고 명령문은 Python 범위 지정 규칙을 준수하는 일련의 범위로 구현되는 기호 테이블을 수정합니다. 쉬운 뉴모닉 LEGB(로컬, 엔클로징, 글로벌, 내장)를 기억하세요.

바이트코드 VM은 자신이 무엇을 하고 있는지 모르지만 자신이 했던 것처럼 행동하고 싶을 때 구축할 것입니다. 또한 ?‍♂️. 이 엔진의 경우 토큰과 AST는 동일하게 작동하지만 걷기보다는 질주를 시작합니다. AST를 이하 바이트코드로 알려진 중간 표현(IR)으로 컴파일합니다. 그런 다음 개념적으로 CPU처럼 작동하여 바이트코드 명령을 순서대로 실행하지만 완전히 소프트웨어로 구현되는 스택 기반 가상 머신(VM)을 만듭니다.

(두 가지 접근 방식을 모두 혼란 없이 완벽하게 안내하려면 Crafting Interpreters가 훌륭합니다.)

애초에 우리는 왜 이런 일을 하는 걸까요? 이식성과 성능이라는 두 가지 P를 기억하세요. 2000년대 초반에는 Java 바이트코드의 이식성에 대해 아무도 입을 다물지 않았던 것을 기억하십니까? JVM만 있으면 모든 시스템에서 컴파일된 Java 프로그램을 실행할 수 있습니다! Python은 기술적인 이유와 마케팅적인 이유로 이 접근 방식을 선택하지 않았지만 이론적으로는 동일한 원칙이 적용됩니다. (실제로는 컴파일 단계가 달라서 이 웜캔을 열어본게 후회스럽습니다.)

그래도 성능이 가장 중요합니다. 프로그램 수명 동안 AST를 여러 번 탐색하는 것보다 컴파일된 IR이 더 효율적인 표현입니다. AST를 반복적으로 탐색하는 오버헤드를 피함으로써 성능이 향상되었으며, 플랫 구조로 인해 런타임 시 분기 예측 및 캐시 지역성이 향상되는 경우가 많습니다.

(컴퓨터 아키텍처에 대한 배경 지식이 없다면 캐싱에 대해 생각하지 않는다고 비난하지는 않습니다. 저는 해당 업계에서 경력을 시작했으며 캐싱을 피하는 방법에 대해 생각하는 것보다 훨씬 적게 생각합니다. 동일한 코드 줄을 두 번 작성합니다. 그러니 성능 부분에서는 저를 믿으세요. 그게 제 리더십 스타일입니다: 맹목적인 신뢰.)

안녕 친구, 500 단어입니다. 프레임을 로드하고 찢어버려야 합니다.

벌써?! 코드 조각을 제외하셨나요?

코드 조각은 없습니다.

알았어 알았어. 500개만 더요. 약속합니다.

Python 변수의 컨텍스트 문제

약 1년 전에 바이트코드 VM 구현을 표로 작성하기 전에는 꽤 많은 시간이 걸렸습니다. Python 함수와 클래스를 정의하고 해당 함수를 호출하고 해당 클래스를 인스턴스화할 수 있었습니다. 나는 몇 가지 테스트를 통해 이 동작을 단속했습니다. 하지만 내 구현이 지저분하고 더 재미있는 것을 추가하기 전에 기본 사항을 다시 살펴봐야 한다는 것을 알고 있었습니다. 이제 크리스마스 주간인데 재미있는 내용을 추가하고 싶습니다.

TODO를 주시하면서 함수를 호출하려면 이 스니펫을 고려하세요.

fn compile_function_call(
    &mut self,
    name: &str,
    args: &ParsedArguments)
) -> Result<Bytecode, CompileError> {
    let mut opcodes = vec![];

    // We push the args onto the stack in reverse call order so that we will pop
    // them off in call order.
    for arg in args.args.iter().rev() {
        opcodes.extend(self.compile_expr(arg)?);
    }

    let (_, index) = self.get_local_index(name);

    // TODO how does this know if it is a global or local index? this may not be the right
    // approach for calling a function
    opcodes.push(Opcode::Call(index));

    Ok(opcodes)
}

고려가 끝났나요? 함수 인수를 스택에 로드하고 "함수를 호출"합니다. 바이트코드에서는 모든 이름이 인덱스로 변환되지만(VM 런타임 동안 인덱스 액세스가 더 빠르기 때문에) 여기서는 로컬 인덱스를 다루고 있는지, 글로벌 인덱스를 다루고 있는지 알 수 있는 방법이 없습니다.

이제 개선된 버전을 살펴보세요.

fn compile_function_call(
    &mut self,
    name: &str,
    args: &ParsedArguments)
) -> Result<Bytecode, CompileError> {
    let mut opcodes = vec![self.compile_load(name)];

    // We push the args onto the stack in reverse call order so that we will pop
    // them off in call order.
    for arg in args.args.iter().rev() {
        opcodes.extend(self.compile_expr(arg)?);
    }

    let argc = opcodes.len() - 1;
    opcodes.push(Opcode::Call(argc));

    Ok(opcodes)
}

해당 코드를 고려해 주셔서 감사합니다.

이제 중첩된 함수 호출을 지원합니다! 무엇이 바뀌었나요?

  1. 이제 Call opcode는 함수에 대한 인덱스가 아닌 여러 위치 인수를 사용합니다. 이는 함수를 호출하기 전에 스택에서 꺼낼 인수 수를 VM에 지시합니다.
  2. 스택에서 인수를 제거한 후 함수 자체는 스택에 남게 되며 compile_load는 이미 로컬 범위와 전역 범위를 처리했습니다.

LOAD_GLOBAL 대 LOAD_FAST

compile_load가 무엇을 하는지 살펴보겠습니다.

fn compile_load(&mut self, name: &str) -> Opcode {
    match self.ensure_context() {
        Context::Global => Opcode::LoadGlobal(self.get_or_set_nonlocal_index(name)),
        Context::Local => {
            // Check locals first
            if let Some(index) = self.get_local_index(name) {
                return Opcode::LoadFast(index);
            }

            // If not found locally, fall back to globals
            Opcode::LoadGlobal(self.get_or_set_nonlocal_index(name))
        }
    }
}

여기에는 몇 가지 핵심 원칙이 적용됩니다.

  1. 현재 상황에 맞춰 매칭합니다. Python 의미 체계를 준수하면 Context::Global이 모든 모듈(스크립트의 진입점뿐만 아니라)의 최상위 수준에 있고 Context::Local이 모든 블록(예: 함수 정의 또는 클래스 정의) 내부에 있는 것으로 간주할 수 있습니다.
  2. 이제 로컬 인덱스와 비로컬 인덱스를 구분합니다. (다른 위치에서 인덱스 0이 무엇을 참조하는지 해독하려고 미친 듯이 노력했기 때문에 유형 정수를 도입했습니다. LocalIndex와 NonlocalIndex는 유형이 지정되지 않은 부호 없는 정수에 대해 유형 안전성을 제공합니다. 이에 대해 나중에 쓸 수도 있습니다!)
  3. 바이트코드 컴파일 시 특정 이름을 가진 지역 변수가 존재하는지 여부를 알 수 있으며, 존재하지 않는 경우 런타임에 전역 변수를 검색합니다. 이는 Python에 내장된 역동성을 나타냅니다. 함수가 실행될 때 변수가 해당 모듈의 전역 범위에 존재하는 한 해당 값은 런타임에 해결될 수 있습니다. 그러나 이러한 동적 해상도는 성능 저하를 수반합니다. 지역 변수 조회는 스택 인덱스를 사용하도록 최적화되어 있지만 전역 조회에서는 전역 네임스페이스 사전을 검색해야 하므로 속도가 더 느립니다. 이 사전은 힙에 존재할 수 있는 객체에 대한 이름의 매핑입니다. “생각은 글로벌하게, 행동은 지역적으로”라는 말을 누가 알았겠습니까? 실제로 Python 범위를 언급하고 있었나요?

이름에는 무엇이 있나요?

오늘 마지막으로 알려드릴 내용은 이러한 변수 이름이 어떻게 매핑되는지 살펴보는 것입니다. 아래 코드 조각에서 로컬 인덱스는 code.varnames에 있고 로컬이 아닌 인덱스는 code.names에 있음을 알 수 있습니다. 둘 다 변수 및 이름 매핑을 포함하여 Python 바이트코드 블록에 대한 메타데이터가 포함된 CodeObject에 있습니다.

fn compile_function_call(
    &mut self,
    name: &str,
    args: &ParsedArguments)
) -> Result<Bytecode, CompileError> {
    let mut opcodes = vec![];

    // We push the args onto the stack in reverse call order so that we will pop
    // them off in call order.
    for arg in args.args.iter().rev() {
        opcodes.extend(self.compile_expr(arg)?);
    }

    let (_, index) = self.get_local_index(name);

    // TODO how does this know if it is a global or local index? this may not be the right
    // approach for calling a function
    opcodes.push(Opcode::Call(index));

    Ok(opcodes)
}

varname과 이름의 차이 때문에 몇 주 동안 괴로웠지만(CPython에서는 이를 co_varname과 co_name이라고 부릅니다) 실제로는 매우 간단합니다. varnames는 특정 범위의 모든 지역 변수에 대한 변수 이름을 보유하고 이름은 모든 비지역 변수에 대해 동일하게 수행됩니다.

이를 제대로 추적하면 다른 모든 것은 잘 작동합니다. 런타임 시 VM은 LOAD_GLOBAL 또는 LOAD_FAST를 확인하고 각각 전역 네임스페이스 사전 또는 로컬 스택을 찾는 방법을 알고 있습니다.

친구! 구텐베르그 씨가 전화를 해서 더 이상 인쇄기를 보류할 수 없다고 말했습니다.

알겠습니다! 괜찮은! 알겠습니다! 배송해드려요. ?

멤피스의 다음 단계는 무엇입니까?

쉿! 인쇄기사님은 제가 결론을 쓰고 있는 줄 모르셔서 짧게 말씀드리겠습니다.

변수 범위 지정과 함수 호출을 탄탄하게 활용하면서 점차 스택 추적 및 비동기 지원과 같은 기능에 관심을 기울이고 있습니다. 바이트코드에 대한 이 다이빙이 즐거웠거나 자신만의 인터프리터 구축에 대해 질문이 있는 경우, 귀하의 의견을 듣고 싶습니다. 댓글을 남겨주세요!


구독 및 할인 [아무것도 없음]

이런 게시물을 받은편지함으로 직접 받아보려면 여기에서 구독하세요!

저와 함께 일하세요

저는 때때로 어리석은 지원 환경에서 기술적인 문제를 해결하고 경력 성장을 이룰 수 있도록 소프트웨어 엔지니어를 멘토링합니다. 관심이 있으시면 여기에서 세션을 예약하실 수 있습니다.

다른 곳

멘토링 외에도 자영업과 자폐증을 늦게 진단받은 경험에 대해서도 글을 씁니다. 코드는 적고 농담 수는 동일합니다.

  • 호수 효과 커피, 2장 - Scratch dot org에서

위 내용은 Python 바이트코드에 중첩 함수에 대한 지원을 추가하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.