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

Python 개선 제안 한국어 번역

PEP 671 – 지연 바인딩 함수 인자 기본값을 위한 문법

Author:
Chris Angelico <rosuav at gmail.com>
Discussions-To:
Python-Ideas thread
Status:
Draft
Type:
Standards Track
Created:
24-Oct-2021
Python-Version:
3.12
Post-History:
24-Oct-2021, 01-Dec-2021

Table of Contents

번역·라이선스 안내

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

초록

함수 매개변수는 기본값을 가질 수 있으며, 이 기본값은 함수 정의 시점에 계산되어 저장됩니다. 이 제안은 함수 호출 시점에 평가되는 표현식으로 정의되는 새로운 형태의 인자 기본값을 도입합니다.

동기

선택적 함수 인자는 생략되었을 때 흔히 어떤 논리적인 기본값을 가집니다. 이 값이 다른 인자에 의존하거나 함수 호출 때마다 다시 평가되어야 하는 경우, 현재로서는 함수 헤더에 이를 깔끔하게 명시할 방법이 없습니다.

현재 이를 위해 허용되는 관용구로는 다음이 있습니다:

# Very common: Use None and replace it in the function
def bisect_right(a, x, lo=0, hi=None, *, key=None):
    if hi is None:
        hi = len(a)

# Also well known: Use a unique custom sentinel object
_USE_GLOBAL_DEFAULT = object()
def connect(timeout=_USE_GLOBAL_DEFAULT):
    if timeout is _USE_GLOBAL_DEFAULT:
        timeout = default_timeout

# Unusual: Accept star-args and then validate
def add_item(item, *optional_target):
    if not optional_target:
        target = []
    else:
        target = optional_target[0]

어느 형태에서든 help(function)은 실제 기본값을 보여 주지 못합니다. 각각에는 추가적인 문제도 있습니다. None을 사용하는 방법은 None 자체가 함수 매개변수로 전달될 법한 값이 아닐 때만 유효하고, 사용자 정의 센티널은 전역 상수를 필요로 하며, 별표 인자를 사용하면 둘 이상의 인자가 주어질 수 있다는 의미가 됩니다.

명세

함수 기본 인자는 새로운 => 표기법을 사용하여 정의할 수 있습니다:

def bisect_right(a, x, lo=0, hi=>len(a), *, key=None):
def connect(timeout=>default_timeout):
def add_item(item, target=>[]):
def format_time(fmt, time_t=>time.time()):

표현식은 검사를 위해 소스 코드 형태로 저장되며, 이를 평가하는 바이트코드가 함수 본문 앞에 추가됩니다.

특히 이 표현식은 함수가 정의된 스코프(조기 바인딩 기본값은 이 스코프에서 평가됩니다)가 아니라(NOT) 함수의 런타임 스코프에서 평가됩니다. 따라서 표현식이 다른 인자를 참조할 수 있습니다.

여러 개의 지연 바인딩 인자는 왼쪽에서 오른쪽으로 평가되며, 앞서 정의된 값을 참조할 수 있습니다. 순서는 키워드 인자가 전달되는 순서와 관계없이 함수에 의해 정해집니다.

def prevref(word=”foo”, a=>len(word), b=>a//2): # Valid def selfref(spam=>spam): # UnboundLocalError def spaminate(sausage=>eggs + 1, eggs=>sausage - 1): # Confusing, don’t do this def frob(n=>len(items), items=[]): # See below

평가 순서는 왼쪽에서 오른쪽입니다. 다만 구현은 이를 두 번의 별도 단계로 나누어, 먼저 전달된 모든 인자와 조기 바인딩 기본값을 처리하고 두 번째 단계에서 지연 바인딩 기본값을 처리하도록 선택할 수 있습니다(MAY). 그렇지 않으면 모든 인자는 엄격하게 왼쪽에서 오른쪽 순서로 할당됩니다.

기각된 표기 방식

이 문서는 name=>expression이라는 단일 문법을 명시하지만, 다른 표기 방식도 마찬가지로 타당합니다. 다음과 같은 표기 방식이 검토되었습니다:

def bisect(a, hi=>len(a)):
def bisect(a, hi:=len(a)):
def bisect(a, hi?=len(a)):
def bisect(a, @hi=len(a)):

기본 인자는 조기 바인딩이든 지연 바인딩이든 대체로 같은 방식으로 동작하므로, 선택된 문법 hi=>len(a)는 기존의 조기 바인딩 문법과 의도적으로 비슷하게 만들었습니다.

:= 문법을 기각한 이유 중 하나는 어노테이션과 함께 쓰일 때의 동작입니다. 어노테이션은 기본값 앞에 오므로, 어떤 문법을 택하든 그것이 어노테이션인지, 기본값인지, 혹은 둘 다인지가 (사람과 파서 모두에게) 모호하지 않아야 합니다. 대안 문법인 target:=expr은 어노테이션을 실수로 빠뜨린 target:int=expr으로 잘못 해석될 위험이 있으며, 그로 인해 버그를 가릴 수 있습니다. 선택된 문법인 target=>expr에는 이러한 문제가 없습니다.

교육 방법

조기 바인딩 기본 인자는 인자를 평가하는 더 단순하고 효율적인 방법이므로 항상 먼저 가르쳐야 합니다. 이를 바탕으로 설명하면, 지연 바인딩 인자는 대체로 함수 맨 위에 있는 코드와 동등합니다:

def add_item(item, target=>[]):

# Equivalent pseudocode:
def add_item(item, target=<OPTIONAL>):
    if target was omitted: target = []

간단한 경험칙은 다음과 같습니다. “target=expression”은 함수가 정의될 때 평가되고, “target=>expression”은 함수가 호출될 때 평가됩니다. 어느 쪽이든 호출 시점에 인자가 제공되면 기본값은 무시됩니다. 이것이 모든 미묘한 차이를 완전히 설명하지는 못하지만, 여기서 중요한 구분(그리고 둘이 비슷하다는 사실)을 다루기에는 충분합니다.

다른 제안과의 상호 작용

PEP 661은 이 제안이 해결하려는 문제 중 하나를 똑같이 해결하려고 합니다. 그 제안은 기본 인자에 쓰이는 센티널 값의 문서화를 개선하려는 것이고, 이 제안은 흔히 볼 수 있는 여러 경우에서 센티널이 필요 없도록 하려는 것입니다. PEP 661은 임의로 복잡한 함수에서도 문서화를 개선할 수 있습니다(그 제안은 주된 동기로 traceback.print_exception을 드는데, 이 함수에는 둘 다 지정하거나 둘 다 지정하지 않아야 하는 인자가 두 개 있습니다). 반면에 함수가 실제 기본값을 정의할 수 있다면 흔한 경우의 상당수에서는 더 이상 센티널이 필요하지 않습니다. 또한 전용 센티널 객체는 딕셔너리 조회 키로 사용할 수 있는데, 이 경우에는 PEP 671이 적용되지 않습니다.

지연 평가를 위한 범용 시스템이 때때로 제안되어 왔습니다(어노테이션에 한정된 PEP 563 및 PEP 649와 혼동하지 마십시오). 겉보기에는 늦게 바인딩되는 인자 기본값이 비슷한 성격을 띠는 것처럼 보일 수 있지만, 실제로는 서로 관련이 없는 직교적인 아이디어이며, 둘 다 언어에 가치가 있을 수 있습니다. 이 제안의 채택 여부는 지연 평가 제안의 실현 가능성에 영향을 주지 않으며, 그 반대도 마찬가지입니다. (일반화된 지연 평가와 인자 기본값의 핵심적인 차이는, 인자 기본값은 언제나 함수가 실행을 시작할 때, 그리고 오직 그때에만 평가되는 반면 지연된 표현식은 참조될 때에만 실제로 평가된다는 점입니다.)

구현 세부 사항

다음 내용은 참조 구현에 관한 것이며, 반드시 명세의 일부인 것은 아닙니다.

인자 기본값(위치 인자 또는 키워드 인자)은 기존처럼 보존되는 값과 함께 추가 정보 하나를 가집니다. 위치 인자의 경우 추가 정보는 __defaults_extra__에 튜플로 저장되고, 키워드 전용 인자의 경우 __kwdefaults_extra__에 딕셔너리로 저장됩니다. 이 속성이 None이면, 모든 인자 기본값에 대해 None을 가지는 것과 동등합니다.

늦게 바인딩되는 기본값을 가진 각 매개변수에 대해서는 특수 값 Ellipsis가 값의 자리 표시자로 저장되며, 대응하는 추가 정보를 조회해야 합니다. 그 정보가 None이면 기본값은 실제로 Ellipsis 값입니다. 그렇지 않으면 그 정보는 설명 문자열이며, 실제 값은 함수가 시작될 때 계산됩니다.

늦게 바인딩되는 기본값을 가진 매개변수가 생략되면, 함수는 해당 매개변수가 바인딩되지 않은 상태로 시작합니다. 함수는 시작할 때 새로운 옵코드 QUERY_FAST/QUERY_DEREF를 사용해 늦게 바인딩되는 기본값을 가진 각 매개변수를 검사하고, 바인딩되지 않았다면 원래 표현식을 평가합니다. 이 옵코드(fast 지역 변수와 클로저 변수에만 사용 가능)는 주어진 지역 변수에 값이 있으면 스택에 True를, 없으면 False를 푸시합니다. 즉, LOAD_FAST나 LOAD_DEREF가 UnboundLocalError를 일으킬 상황이면 False를, 성공할 상황이면 True를 푸시합니다.

참조 대상이 인자나 일찍 바인딩되는 기본값으로부터 값을 가지고 있기만 하면, 순서에 맞지 않는 변수 참조도 허용됩니다.

비용

늦게 바인딩되는 인자 기본값을 사용하지 않을 때 발생하는 비용은 다음이 전부여야 합니다.

  • 함수 객체에 추가 포인터 두 개가 필요하며, 이 포인터들은 NULL이 됩니다
  • 코드 컴파일과 함수 생성 시 추가 플래그 검사가 이루어집니다
  • Ellipsis를 기본값으로 사용하면 늦게 바인딩되는 기본값이 있는지 확인하기 위한 실행 시간 검증이 필요합니다.

이러한 비용은 최소한일 것으로 예상되며(64비트 Linux에서 모든 함수 객체의 크기가 152바이트에서 168바이트로 증가합니다), 늦은 바인딩 기본값을 사용하지 않을 때는 런타임 비용이 사실상 없습니다.

하위 호환성 문제

늦게 바인딩되는 기본값을 사용하지 않는 경우 동작은 동일해야 합니다. Ellipsis가 발견되면 그것이 자기 자신을 나타내지 않을 수도 있으므로 주의해야 하지만, 그 외에는 도구에 기존 코드가 변경 없이 그대로 보여야 합니다.

참고 문헌

https://github.com/rosuav/cpython/tree/pep-671