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

Python 개선 제안 한국어 번역

PEP 637 – 키워드 인자를 사용한 인덱싱 지원

Author:
Stefano Borini
Sponsor:
Steven D’Aprano
Discussions-To:
Python-Ideas list
Status:
Rejected
Type:
Standards Track
Created:
24-Aug-2020
Python-Version:
3.10
Post-History:
23-Sep-2020
Resolution:
Python-Dev thread

Table of Contents

번역·라이선스 안내

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

Note

이 PEP는 거부되었습니다. 일반적으로 새로운 구문을 도입하는 비용은 인식된 이점으로 상쇄되지 못했습니다. 자세한 내용은 Resolution 헤더 필드의 링크를 참조하십시오.

초록

현재 키워드 인자는 함수 호출에서는 허용되지만 항목 접근에서는 허용되지 않습니다. 이 PEP는 항목 접근에서 키워드 인자를 허용하도록 Python을 확장할 것을 제안합니다.

다음 예는 일반 함수 호출에서 키워드 인자를 사용하는 방법을 보여 줍니다.:

>>> val = f(1, 2, a=3, b=4)

이 제안은 인덱싱 연산과 유사한 구문을 허용하도록 구문을 확장합니다.:

>>> val = x[1, 2, a=3, b=4]  # getitem
>>> x[1, 2, a=3, b=4] = val  # setitem
>>> del x[1, 2, a=3, b=4]    # delitem

또한 적절한 의미론을 제공합니다. 인자의 단일 별표 및 이중 별표 언패킹도 제공합니다.:

>>> val = x[*(1, 2), **{a=3, b=4}]  # Equivalent to above.

이 PEP는 2019년에 관심 부족으로 거부된 PEP 472의 후속입니다. 그 이후 이 기능에 대한 관심이 다시 높아졌습니다.

개요

배경

PEP 472는 2014년에 제안되었습니다. 이 PEP는 다양한 사용 사례를 자세히 설명했으며, python-ideas 메일링 리스트에서 이루어진 광범위한 논의로부터 구현 전략을 추출하여 작성되었지만, 어떤 전략을 사용해야 하는지에 대한 명확한 합의에는 이르지 못했습니다. 많은 경계 사례를 더 면밀히 검토한 결과, 어색하거나 하위 호환성이 없거나 둘 다인 것으로 느껴졌습니다.

이 PEP는 5년 동안 존재했음에도 불구하고 기능에 대한 관심이 부족했던 것을 주된 이유로 2019년에 결국 거부되었습니다 [1].

그러나 PEP 484에서 타입 힌트가 도입된 이후, 대괄호 표기법은 타입 어노테이션을 풍부하게 하는 데 일관되게 사용되어 왔으며, 예를 들어 정수 목록을 Sequence[int]로 지정하는 데 사용됩니다. 또한 pandas 및 xarray와 같은 데이터 분석 패키지가 크게 증가했으며, 이러한 패키지는 이름을 사용하여 표의 열(pandas)이나 nd-array의 축(xarray)을 설명합니다. 이러한 패키지를 사용하면 이름으로 특정 데이터에 접근할 수 있지만, 현재는 이 기능에 인덱스 표기법([])을 사용할 수 없습니다.

그 결과, 이름이 지정된 정보를 허용하는 더 유연한 구문에 대한 관심이 여러 python-ideas 논의에서 간헐적으로 다시 제기되었으며, 최근에는 2019년 Caleb Donovick [2]와 2020년 Andras Tantos [3]가 제기했습니다. 이러한 요청으로 python-ideas 메일링 리스트에서 활발한 논의가 이루어졌고, 다양한 선택지가 다시 논의되었으며, 이제 구현 전략에 대한 전반적인 합의에 도달했습니다.

사용 사례

다음의 실용적인 사용 사례는 키워드 지정이 표기법을 개선하고 추가적인 가치를 제공하는 다양한 경우를 보여 줍니다.

  1. 인덱스에 더 전달력 있는 의미를 부여하여 인덱스의 우연한 반전을 방지합니다.:
    >>> grid_position[x=3, y=5, z=8]
    >>> rain_amount[time=0:12, location=location]
    >>> matrix[row=20, col=40]
    
  2. 특히 제네릭을 사용할 때 키워드로 타입 표기법을 풍부하게 합니다.:
    def function(value: MyType[T=int]):
    
  3. 계산 물리학 및 화학과 같은 일부 분야에서는 Basis[Z=5]와 같은 표기법이 정확도 수준을 나타내는 도메인 특정 언어 표기법입니다.:
    >>> low_accuracy_energy = computeEnergy(molecule, BasisSet[Z=3])
    
  4. Pandas는 현재 다음과 같은 표기법을 사용합니다.:
    >>> df[df['x'] == 1]
    

    이는 df[x=1]로 대체할 수 있습니다.

  5. xarray에는 이름이 지정된 차원이 있습니다. 현재 이들은 .isel 함수로 처리합니다.:
    >>> data.isel(row=10)  # Returns the tenth row
    

    이는 data[row=10]로 대체할 수도 있습니다. 더 복잡한 예입니다.:

    >>> # old syntax
    >>> da.isel(space=0, time=slice(None, 2))[...] = spam
    >>> # new syntax
    >>> da[space=0, time=:2] = spam
    

    또 다른 예입니다.:

    >>> # old syntax
    >>> ds["empty"].loc[dict(lon=5, lat=6)] = 10
    >>> # new syntax
    >>> ds["empty"][lon=5, lat=6] = 10
    
    >>> # old syntax
    >>> ds["empty"].loc[dict(lon=slice(1, 5), lat=slice(3, None))] = 10
    >>> # new syntax
    >>> ds["empty"][lon=1:5, lat=6:] = 10
    
  6. 인수가 다른 함수(및 그 인수)인 함수/메서드는 어떤 인수가 대상 함수에 전달되는 것이고 어떤 인수가 대상 함수를 실행하는 방식을 구성하는 데 사용되는지 결정할 방법이 필요합니다. 위치 매개변수의 경우에는 간단하지만(확장할 수 없다는 점을 제외하면), 키워드의 경우에는 이를 구별할 방법이 필요합니다. [4]

    인덱싱 표기법은 호출자의 코드를 복잡하게 만들지 않고 이러한 함수에 키워드 인자를 전달하는 파이썬다운 방법을 제공할 수 있습니다.

    >>> # Let's start this example with basic syntax without keywords.
    >>> # the positional values are arguments to `func` while
    >>> # `name=` is processed by `trio.run`.
    >>> trio.run(func, value1, value2, name="func")
    >>> # `trio.run` ends up calling `func(value1, value2)`.
    
    >>> # If we want/need to pass value2 by keyword (keyword-only argument,
    >>> # additional arguments that won't break backwards compatibility ...),
    >>> # currently we need to resort to functools.partial:
    >>> trio.run(functools.partial(func, param2=value2), value1, name="func")
    >>> trio.run(functools.partial(func, value1, param2=value2), name="func")
    
    >>> # One possible workaround is to convert `trio.run` to an object
    >>> # with a `__call__` method, and use an "option" helper,
    >>> trio.run.option(name="func")(func, value1, param2=value2)
    >>> # However, foo(bar)(baz) is uncommon and thus disruptive to the reader.
    >>> # Also, you need to remember the name of the `option` method.
    
    >>> # This PEP allows us to replace `option` with `__getitem__`.
    >>> # The call is now shorter, more mnemonic, and looks+works like typing
    >>> trio.run[name="func"](func, value1, param2=value2)
    
  7. 별표 인자를 사용할 수 있으면 PEP 646 가변 제네릭에 도움이 되며, 특히 a[*x]a[*x, *y, p, q, *z] 형태에서 유용합니다. 이 PEP는 “Unpacking: Star Operator” 섹션에서 바로 이 표기법을 자세히 설명합니다.

표기법이 어떻게 해석되는지는 구현에 달려 있다는 점을 유의해야 합니다. 이 PEP는 전달된 키워드 인자와 관련된 Python의 동작만 정의하고 규정할 뿐, 이러한 인자를 구현하는 클래스가 어떻게 해석하고 사용하는지는 정의하거나 규정하지 않습니다.

인덱싱 연산의 현재 상태입니다.

인덱싱 표기법의 새로운 구문과 의미 체계를 자세히 설명하기 전에, 오늘날 인덱싱 표기법이 어떻게 작동하는지, 어떤 컨텍스트에서 작동하는지, 함수 호출과 어떻게 다른지를 분석하는 것이 중요합니다.

서브스크립팅 obj[x]은 사실상 obj(x)와 비교할 때 여러 차이와 제약이 있는 함수 호출 구문의 대체적이고 특수화된 형태입니다. 현재 Python 구문은 인덱스를 표현할 때 위치에만 전적으로 초점을 맞추며, 비점형 선택(슬라이스)을 나타내기 위한 구문 설탕도 포함합니다. 몇 가지 일반적인 예입니다.:

>>> a[3]       # returns the fourth element of 'a'
>>> a[1:10:2]  # slice notation (extract a non-trivial data subset)
>>> a[3, 2]    # multiple indexes (for multidimensional arrays)

이는 인덱스를 포함하는 단일 매개변수(__getitem____delitem__의 경우) 또는 인덱스와 값을 포함하는 두 매개변수(__setitem__의 경우)가 전달되는 __(get|set|del)item__듀더 호출로 변환됩니다.

인덱싱 호출의 동작은 여러 측면에서 함수 호출과 근본적으로 다릅니다.

첫 번째 차이는 독자에게 전달되는 의미입니다. 함수 호출은 “부작용이 발생할 수 있는 임의의 함수 호출”이라고 말합니다. 인덱싱 연산은 “조회”를 의미하며, 일반적으로 엔터티의 하위 집합이나 특정 하위 측면(타입 표기법의 경우와 같이)을 가리킵니다. 이러한 근본적인 차이로 인해 남용을 막을 수는 없지만, 구현자는 조회의 동작을 변경하기 위해 키워드 인자를 도입하면 이 본질적인 의미를 위반할 수 있다는 점을 인식해야 합니다.

함수와 비교한 인덱싱 표기법의 두 번째 차이는 인덱싱을 가져오기 및 설정 작업 모두에 사용할 수 있다는 것입니다. Python에서 함수는 할당의 왼쪽에 올 수 없습니다. 다시 말해, 다음 두 가지는 모두 유효합니다.:

>>> x = a[1, 2]
>>> a[1, 2] = 5

하지만 이 중 첫 번째만 유효합니다.:

>>> x = f(1, 2)
>>> f(1, 2) = 5  # invalid

이 비대칭은 중요하며, 두 형식 사이에 자연스러운 불균형이 있음을 이해하게 합니다. 따라서 두 형식이 투명하고 대칭적으로 동작해야 한다고 당연히 전제할 수는 없습니다.

세 번째 차이점은 전달된 매개변수가 *args로 캡처되는 경우를 제외하면 함수의 인자에는 이름이 할당되고, 이 경우에는 해당 인자들이 args 튜플의 항목으로 들어간다는 점입니다. 다시 말해, 함수는 인덱싱 연산과 정확히 마찬가지로 이미 익명 인자 의미론을 가집니다. 그러나 __(get|set|del)item__*args와 동작을 통일하기 위해 항상 index인자로 튜플을 받는 것은 아닙니다. 실제로, 간단한 클래스가 주어지면:

class X:
    def __getitem__(self, index):
        print(index)

인덱스 연산은 기본적으로 대괄호 안의 내용을 있는 그대로 index인자로 전달합니다.:

>>> x=X()
>>> x[0]
0
>>> x[0, 1]
(0, 1)
>>> x[(0, 1)]
(0, 1)
>>>
>>> x[()]
()
>>> x[{1, 2, 3}]
{1, 2, 3}
>>> x["hello"]
hello
>>> x["hello", "hi"]
('hello', 'hi')

네 번째 차이점은 파서의 지원 덕분에 인덱싱 연산이 콜론 표기법을 슬라이스로 변환하는 방법을 알고 있다는 점입니다. 이것은 유효합니다.:

a[1:3]

이것은 유효하지 않습니다.:

f(1:3)

다섯 번째 차이점은 인자가 0개인 형식이 없다는 점입니다. 이것은 유효합니다.:

f()

이것은 유효하지 않습니다.:

a[]

명세

사양을 설명하기 전에 위치 인덱스, 최종 인덱스키워드 인자 사이의 명명법 차이를 강조하는 것이 중요합니다. 이는 작동하는 근본적인 비대칭성을 이해하는 데 중요하기 때문입니다. __(get|set|del)item__은 본질적으로 인덱싱 연산이며, 요소를 검색하거나 설정하거나 삭제하는 방식은 인덱스, 즉 final index 를 통하는 것입니다.

현재의 관행은 대괄호 사이에 전달된 것, 즉 positional index 로부터 final index 를 직접 구성하는 것입니다. 다시 말해, 대괄호 안에 전달된 값은 __getitem__의 코드가 이후 인덱싱 연산에 사용하는 값을 생성하는 데 그대로 사용됩니다. 딕셔너리에서 이미 살펴본 것처럼 d[1]1이라는 위치 인덱스와 1이라는 최종 인덱스를 모두 가집니다. 이는 해당 요소가 이후 딕셔너리에 추가되기 때문입니다. 또한 d[1, 2](1, 2)라는 위치 인덱스와 (1, 2)라는 최종 인덱스를 가집니다. 이 역시 딕셔너리에 추가되는 요소이기 때문입니다. 그러나 딕셔너리는 위치 인덱스 d[1,2:3]을 허용하지 않습니다. 슬라이스 객체는 해시할 수 없으므로 위치 인덱스를 최종 인덱스로 변환할 방법이 없기 때문입니다. 위치 인덱스는 현재 __getitem__index매개변수로 알려진 것입니다. 그럼에도 불구하고 위치 인덱스를 문자열로 변환하는 등의 방식으로 최종 인덱스를 생성하는 딕셔너리와 유사한 클래스를 만드는 데에는 아무런 제약이 없습니다.

이 PEP는 현재의 관행을 확장하여, 전달된 경우 위치 인덱스와 키워드 인자를 결합하는 향상된 문법을 통해 최종 인덱스를 생성할 수 있는 더 큰 유연성을 제공합니다.

위 내용은 중요한 점을 분명히 보여 줍니다. 인덱스 연산의 맥락에서 키워드 인자는 최종 인덱스를 얻기 위한 인덱싱 결정을 내리는 데 사용될 수 있으므로, 함수에서 일반적이지 않은 값도 받아들여야 합니다. 예를 들어 슬라이스가 허용되는 사용 사례 1을 보십시오.

이 PEP가 성공적으로 구현되면 다음과 같은 동작이 이루어집니다.

  1. 컨텍스트와 관계없이 빈 서브스크립트는 여전히 허용되지 않습니다(거부된 아이디어 참조).:
    obj[]  # SyntaxError
    
  2. 단일 인덱스 값은 전달될 때에도 단일 인덱스 값으로 유지됩니다.:
    obj[index]
    # calls type(obj).__getitem__(obj, index)
    
    obj[index] = value
    # calls type(obj).__setitem__(obj, index, value)
    
    del obj[index]
    # calls type(obj).__delitem__(obj, index)
    

    인덱스 뒤에 키워드가 오는 경우에도 이는 동일하게 유지됩니다. 아래의 5번 항목을 참조하십시오.

  3. 쉼표로 구분된 인자는 계속 튜플로 파싱되어 단일 위치 인자로 전달됩니다.:
    obj[spam, eggs]
    # calls type(obj).__getitem__(obj, (spam, eggs))
    
    obj[spam, eggs] = value
    # calls type(obj).__setitem__(obj, (spam, eggs), value)
    
    del obj[spam, eggs]
    # calls type(obj).__delitem__(obj, (spam, eggs))
    

    위의 내용은 서브스크립트에서 키워드 인자를 지원하지 않으려는 클래스는 아무것도 할 필요가 없음을 의미하므로, 이 기능은 완전한 하위 호환성을 가집니다.

  4. 키워드 인자가 있는 경우에는 위치 인자 뒤에 와야 합니다.:
    obj[1, 2, spam=None, 3]  # SyntaxError
    

    이는 위치 인자와 키워드 인자를 섞어 사용하면 SyntaxError가 발생하는 함수 호출과 같습니다.

  5. 키워드 서브스크립트가 있는 경우 함수 호출에서와 같이 처리합니다. 예를 들어 다음과 같습니다.:
    # Single index with keywords:
    
    obj[index, spam=1, eggs=2]
    # calls type(obj).__getitem__(obj, index, spam=1, eggs=2)
    
    obj[index, spam=1, eggs=2] = value
    # calls type(obj).__setitem__(obj, index, value, spam=1, eggs=2)
    
    del obj[index, spam=1, eggs=2]
    # calls type(obj).__delitem__(obj, index, spam=1, eggs=2)
    
    # Comma-separated indices with keywords:
    
    obj[foo, bar, spam=1, eggs=2]
    # calls type(obj).__getitem__(obj, (foo, bar), spam=1, eggs=2)
    
    obj[foo, bar, spam=1, eggs=2] = value
    # calls type(obj).__setitem__(obj, (foo, bar), value, spam=1, eggs=2)
    
    del obj[foo, bar, spam=1, eggs=2]
    # calls type(obj).__detitem__(obj, (foo, bar), spam=1, eggs=2)
    

    다음에 유의하십시오:

    • 키워드 값을 하나 추가한다고 해서 단일 위치 인덱스가 튜플로 변환되지는 않습니다.
    • __setitem__에서는 인덱스와 값에 대해 동일한 순서를 유지합니다. 키워드 인자는 함수 정의에서 일반적인 방식과 같이 마지막에 배치합니다.
  6. 키워드 서브스크립트에도 함수 호출의 키워드와 동일한 규칙을 적용합니다:
    • 인터프리터는 각 키워드 서브스크립트를 해당 메서드의 명명된 매개변수에 대응시킵니다.
    • 명명된 매개변수가 두 번 사용되면 오류입니다.
    • 모든 키워드를 사용했을 때 값이 없는 명명된 매개변수가 남아 있으면 해당 매개변수에 기본값이 할당됩니다(있는 경우).
    • 이러한 매개변수에 기본값이 없으면 오류입니다.
    • 모든 명명된 매개변수에 값이 할당된 후에도 키워드 서브스크립트가 남아 있고 메서드에 **kwargs 매개변수가 있으면, 해당 서브스크립트를 딕셔너리로 **kwargs 매개변수에 바인딩합니다.
    • 그러나 **kwargs 매개변수가 정의되어 있지 않으면 오류입니다.
  7. 서브스크립트 내부에서는 시퀀스 언패킹을 허용합니다.:
    obj[*items]
    

    따라서 [:, *args, :]와 같은 표기를 사용할 수 있으며, 이는 [(slice(None), *args, slice(None))]로 처리할 수 있습니다. 여러 별표 언패킹을 허용합니다.:

    obj[1, *(2, 3), *(4, 5), 6, foo=5]
    # Equivalent to obj[(1, 2, 3, 4, 5, 6), foo=3)
    

    다음 표기 동치를 준수해야 합니다.:

    obj[*()]
    # Equivalent to obj[()]
    
    obj[*(), foo=3]
    # Equivalent to obj[(), foo=3]
    
    obj[*(x,)]
    # Equivalent to obj[(x,)]
    
    obj[*(x,),]
    # Equivalent to obj[(x,)]
    

    특히 사례 3에 유의하십시오. 단일 요소의 시퀀스 언패킹은 단일 인자 하나만 전달한 것처럼 동작하지 않습니다. 관련된 경우는 다음 예입니다.:

    obj[1, *(), foo=5]
    # Equivalent to obj[(1,), foo=5]
    # calls type(obj).__getitem__(obj, (1,), foo=5)
    

    그러나 앞서 살펴본 것처럼 하위 호환성을 위해 단일 인덱스는 있는 그대로 전달합니다.:

    obj[1, foo=5]
    # calls type(obj).__getitem__(obj, 1, foo=5)
    

    즉, 단일 위치 인덱스는 시퀀스 언패킹이 없을 때에만 “있는 그대로” 전달합니다. 시퀀스 언패킹이 있으면 언패킹 후 인덱스의 요소 수와 관계없이 인덱스는 튜플이 됩니다.

  8. 딕셔너리 언패킹을 허용합니다.:
    items = {'spam': 1, 'eggs': 2}
    obj[index, **items]
    # equivalent to obj[index, spam=1, eggs=2]
    

    다음 표기 동치를 준수해야 합니다.:

    obj[**{}]
    # Equivalent to obj[()]
    
    obj[3, **{}]
    # Equivalent to obj[3]
    
  9. 키워드 전용 서브스크립트를 허용합니다. 위치 인덱스는 빈 튜플이 됩니다.:
    obj[spam=1, eggs=2]
    # calls type(obj).__getitem__(obj, (), spam=1, eggs=2)
    
    obj[spam=1, eggs=2] = 5
    # calls type(obj).__setitem__(obj, (), 5, spam=1, eggs=2)
    
    del obj[spam=1, eggs=2]
    # calls type(obj).__delitem__(obj, (), spam=1, eggs=2)
    

    빈 튜플을 센티널로 선택하는 방안에 대해서는 논의가 있었습니다. 자세한 내용은 거부된 아이디어 섹션에서 제공합니다.

  10. 키워드 인자는 슬라이스 구문을 허용해야 합니다.:
    obj[3:4, spam=1:4, eggs=2]
    # calls type(obj).__getitem__(obj, slice(3, 4, None), spam=slice(1, 4, None), eggs=2)
    

    이를 통해 일반 함수 호출에도 동일한 구문을 사용할 가능성이 열릴 수 있지만, 이는 이 권고의 일부가 아닙니다.

  11. 키워드 인자는 기본값을 허용합니다.:
    # Given type(obj).__getitem__(obj, index, spam=True, eggs=2)
    obj[3]               # Valid. index = 3, spam = True, eggs = 2
    obj[3, spam=False]   # Valid. index = 3, spam = False, eggs = 2
    obj[spam=False]      # Valid. index = (), spam = False, eggs = 2
    obj[]                # Invalid.
    
  12. 위에서 제시한 동일한 의미 체계는 __class__getitem__에도 확장해야 합니다. PEP 560 이후 타입 힌트는 x[y]에서 __getitem__ 메서드를 찾지 못하고, x가 타입(클래스) 객체이며, x에 클래스 메서드 __class_getitem__가 있으면 해당 메서드를 호출하도록 디스패치됩니다. 이 메서드에도 동일한 변경 사항을 적용하여 list[T=int]와 같은 표기를 허용할 수 있도록 해야 합니다.

표준 클래스(dict, list 등)의 인덱싱 동작

이 PEP에서 제안하는 어떤 내용도 인덱싱을 사용하는 현재 핵심 클래스의 동작을 변경하지 않습니다. 사용자 정의 클래스의 인덱스 연산에 키워드를 추가하는 것은, 예를 들어 표준 dict 타입을 수정하여 키워드 인자를 처리하게 만드는 것과는 다릅니다. 실제로 dict뿐 아니라 인덱싱 의미 체계를 가진 list 및 기타 표준 라이브러리 클래스도 기존과 동일하게 유지되며, 계속해서 키워드 인자를 허용하지 않습니다. 즉, ddict라면 d[1, a=2] 문은 TypeError를 발생시킵니다. 해당 구현이 키워드 인자 사용을 지원하지 않기 때문입니다. 다른 모든 클래스(list, dict 등)에도 동일하게 적용됩니다.

예외 사례 및 주의 사항

새로운 표기법이 도입되면 몇 가지 예외 사례를 분석해야 합니다.

  1. 기술적으로 클래스가 다음과 같이 getter를 정의하면:
    def __getitem__(self, index):
    

    호출자는 다음 두 사례처럼 키워드 구문을 사용하여 이를 호출할 수 있습니다.:

    obj[3, index=4]
    obj[index=1]
    

    결과 동작은 자동으로 오류가 됩니다. 이는 index 인자에 두 값을 전달하여 메서드를 호출하는 것과 같기 때문이며, TypeError가 발생합니다. 첫 번째 경우 index3이 되고, 두 번째 경우에는 빈 튜플 ()이 됩니다.

    이 동작은 현재 인덱싱에 의존하는 모든 기존 클래스에 적용되므로, 이와 관련하여 새로운 동작이 하위 호환성 문제를 일으킬 방법은 없습니다.

    이 동작을 명시적으로 강조하려는 클래스는 매개변수를 위치 전용으로 정의할 수 있습니다.:

    def __getitem__(self, index, /):
    
  2. setter 표기에서도 유사한 사례가 발생합니다.:
    # Given type(obj).__setitem__(obj, index, value):
    obj[1, value=3] = 5
    

    값은 자동으로 전달되므로 문제가 되지 않으며, Python 인터프리터는 TypeError: got multiple values for keyword argument 'value'를 발생시킵니다.

  3. 서브스크립트 특수 메서드가 위치 또는 키워드 매개변수를 사용하도록 선언되어 있으면, 인자가 메서드에 전달될 때 예상하기 어려운 사례가 발생할 수 있습니다. 다음 시그니처가 주어졌을 때:
    def __getitem__(self, index, direction='north')
    

    호출자가 다음과 같이 사용하면:

    obj[0, 'south']
    

    메서드 호출 결과에 아마 놀라게 될 것입니다.:

    # expected type(obj).__getitem__(obj, 0, direction='south')
    # but actually get:
    type(obj).__getitem__(obj, (0, 'south'), direction='north')
    

    해결책: 모범 사례에 따르면 가능한 경우 키워드 서브스크립트를 키워드 전용으로 표시해야 합니다.:

    def __getitem__(self, index, *, direction='north')
    

    인터프리터가 이 규칙을 강제할 필요는 없습니다. 원하는 동작일 수 있는 상황도 있기 때문입니다. 그러나 린터는 키워드 전용 플래그를 사용하지 않는 서브스크립트 메서드에 대해 경고하도록 선택할 수 있습니다.

  4. 살펴본 것처럼 키워드 인자가 뒤따르는 단일 값은 튜플로 변경되지 않습니다. 즉, d[1, a=3]__getitem__(d, 1, a=3)으로 처리되며, __getitem__(d, (1,), a=3)으로 처리되는 것이 아닙니다. 키워드 인자를 추가했을 때 전달되는 인덱스의 타입이 변경된다면 매우 혼란스러울 것입니다. 다시 말해, 단일 값 서브스크립트에 키워드를 추가해도 해당 값이 튜플로 변경되지는 않습니다. 실제 튜플을 전달해야 하는 경우에는 적절한 구문을 사용해야 합니다.:
    obj[(1,), a=3]
    # calls type(obj).__getitem__(obj, (1,), a=3)
    

    이 경우 호출은 단일 요소를 전달합니다. 위 규칙에 따라 해당 요소는 있는 그대로 전달되며, 단지 그 단일 요소가 튜플일 뿐입니다.

    이 동작은 obj[1,]표기가 obj[(1,)]의 축약형이라는 사실을 보여줄 뿐입니다. 또한 예상되는 동작에 따라 obj[1]obj[(1)]의 축약형입니다. 키워드가 있으면 가장 바깥쪽 괄호 쌍을 생략할 수 있다는 규칙은 더 이상 성립하지 않습니다.:

    obj[1]
    # calls type(obj).__getitem__(obj, 1)
    
    obj[1, a=3]
    # calls type(obj).__getitem__(obj, 1, a=3)
    
    obj[1,]
    # calls type(obj).__getitem__(obj, (1,))
    
    obj[(1,), a=3]
    # calls type(obj).__getitem__(obj, (1,), a=3)
    

    이 점은 두 항목이 전달되는 경우에 특히 관련이 있습니다.:

    obj[1, 2]
    # calls type(obj).__getitem__(obj, (1, 2))
    
    obj[(1, 2)]
    # same as above
    
    obj[1, 2, a=3]
    # calls type(obj).__getitem__(obj, (1, 2), a=3)
    
    obj[(1, 2), a=3]
    # calls type(obj).__getitem__(obj, (1, 2), a=3)
    

    그리고 특히 튜플이 변수로 추출되는 경우에 그렇습니다.:

    t = (1, 2)
    obj[t]
    # calls type(obj).__getitem__(obj, (1, 2))
    
    obj[t, a=3]
    # calls type(obj).__getitem__(obj, (1, 2), a=3)
    

    왜냐하면 obj[1, 2, a=3]의 경우 두 요소를 전달하며, 이 요소들은 튜플로 묶인 다음 인덱스로 전달되기 때문입니다. obj[(1, 2), a=3]의 경우에는 단일 요소를 전달하며, 이 요소는 그대로 전달되고 우연히 튜플입니다. 최종 결과는 서로 동일하다는 것입니다.

C 인터페이스

인덱싱 연산의 해석은 다음 함수 호출을 통해 수행됩니다.

  • PyObject_GetItem(PyObject *o, PyObject *key)는 가져오기 연산에 사용됩니다.
  • PyObject_SetItem(PyObject *o, PyObject *key, PyObject *value)는 설정 연산에 사용됩니다.
  • PyObject_DelItem(PyObject *o, PyObject *key)는 삭제 연산에 사용됩니다.

이러한 함수는 Python 실행 파일 내부에서 광범위하게 사용되며, Include/abstract.h가 내보내는 공개 C API의 일부이기도 합니다. 이 함수의 시그니처는 변경할 수 없으며, 확장된 호출을 지원하려면 C 수준의 다른 함수들을 구현해야 한다는 점은 분명합니다. 다음을 제안합니다.

  • PyObject_GetItemWithKeywords(PyObject *o, PyObject *key, PyObject *kwargs)
  • PyObject_SetItemWithKeywords(PyObject *o, PyObject *key, PyObject *value, PyObject *kwargs)
  • PyObject_GetItemWithKeywords(PyObject *o, PyObject *key, PyObject *kwargs)

확장 호출을 위해서는 새로운 오피코드가 필요합니다. 현재 구현은 기존 함수를 호출하기 위해 BINARY_SUBSCR, STORE_SUBSCRDELETE_SUBSCR를 사용합니다. 새로운 연산에는 BINARY_SUBSCR_KW, STORE_SUBSCR_KWDELETE_SUBSCR_KW를 제안합니다. 컴파일러는 이러한 새로운 오피코드를 생성해야 합니다. 기존 C 구현은 kwargs로 NULL을 전달하여 확장 메서드를 호출합니다.

마지막으로 다음과 같은 새로운 슬롯을 PyMappingMethods구조체에 추가해야 합니다.

  • mp_subscript_kw
  • mp_ass_subscript_kw

이러한 슬롯은 키워드를 포함하는 딕셔너리 객체를 처리하는 데 적합한 시그니처를 갖습니다.

“가르치는 방법” 권장 사항

피드백 세션에서 제기된 한 가지 요청은 학생, 데이터 과학자 및 이와 유사한 대상에게 이 기능을 가르치기 위한 가능한 설명 방식을 자세히 기술해 달라는 것이었습니다. 이 절에서는 그러한 요구를 다룹니다.

앞서 언급한 대상이 접하게 될 가능성이 높은 측면이 사용 측면이므로, 구현 측면이 아닌 사용 측면에서만 인덱싱을 설명합니다. 사용자의 일부만이 자신만의 듀더 함수를 구현해야 하며, 이는 고급 사용으로 간주할 수 있습니다. 적절한 설명은 다음과 같을 수 있습니다:

인덱싱 연산은 일반적으로 인덱스를 사용하여 더 큰 데이터셋의 부분 집합을 참조하는 데 사용합니다. 흔히 볼 수 있는 경우 인덱스는 하나 이상의 숫자, 문자열, 슬라이스 등으로 구성됩니다.

일부 타입에서는 인덱스뿐만 아니라 이름이 지정된 값을 사용하여 인덱싱할 수도 있습니다. 이러한 이름이 지정된 값은 함수 호출 키워드 인자에 사용되는 것과 동일한 구문을 사용하여 대괄호 안에 지정합니다. 이름의 의미와 사용 방법은 타입마다 다르므로 해당 타입의 문서에서 확인할 수 있습니다.

이제 강사는 제시된 라이브러리에서 이 기능의 의미를 설명하면서 실제 세계의 실용적인 예를 몇 가지 보여 줍니다. 물론 이 글을 작성하는 시점에는 이러한 예가 존재하지 않지만, 이 기능을 구현할 가능성이 가장 높은 라이브러리는 pandas와 numpy이며, 이름으로 열을 참조하는 방법으로 사용될 수도 있습니다.

참조 구현

참조 구현은 현재 여기에서 개발되고 있습니다 [6].

우회책

Python 언어를 변경하는 모든 PEP는 “기존 언어 사양으로는 PEP가 해결하는 문제를 해결하기에 왜 충분하지 않은지 명확히 설명해야 합니다”.

제안된 확장 기능의 대략적인 대응 방법인 이른바 우회책은 이미 사용할 수 있습니다. 우회책은 새로운 구문을 활성화하는 대신 사용할 수 있는 대안을 제공하며, 의미는 다른 곳에서 정의하도록 둡니다.

이러한 우회책은 다음과 같습니다. 여기서 도우미 HP는 범용으로 사용할 수 있도록 의도된 것이 아닙니다. 예를 들어 모듈이나 패키지에서 자체 도우미를 사용하도록 요구할 수 있습니다.

  1. 사용자 정의 클래스에는 컨테이너에 저장된 값을 각각 가져오고 삭제하는 getitemdelitem 메서드를 제공할 수 있습니다.:
    >>> val = x.getitem(1, 2, a=3, b=4)
    >>> x.delitem(1, 2, a=3, b=4)
    

    setitem에 대해서는 동일한 작업을 수행할 수 없습니다. 유효한 구문이 아니기 때문입니다.:

    >>> x.setitem(1, 2, a=3, b=4) = val
    SyntaxError: can't assign to function call
    
  2. 여기서 H라고 부르는 도우미 클래스를 사용하여 컨테이너와 매개변수의 역할을 서로 바꿀 수 있습니다. 다시 말해, 다음을 사용합니다.:
    H(1, 2, a=3, b=4)[x]
    

    다음을 대신 사용합니다.:

    x[1, 2, a=3, b=4]
    

    이 방법은 getitem, delitemsetitem에도 적용됩니다. 이는 다음과 같은 이유 때문입니다.:

    >>> H(1, 2, a=3, b=4)[x] = val
    

    다음은 유효한 구문이며, 적절한 의미를 부여할 수 있습니다.

  3. 여기서 P라고 부르는 도우미 함수를 사용하여 인자들을 하나의 객체에 저장할 수 있습니다. 예를 들어 다음과 같습니다.:
    >>> x[P(1, 2, a=3, b=4)] = val
    

    다음은 유효한 구문이며, 적절한 의미를 부여할 수 있습니다.

  4. 슬라이스의 lo:hi:step 구문은 때때로 매우 유용합니다. 이 구문은 우회책에서 직접 사용할 수 없습니다. 그러나 다음과 같습니다.:
    s[lo:hi:step]
    

    다음은 어디에서나 사용할 수 있는 우회 방법을 제공하며, 여기서는 다음과 같습니다.:

    class S:
        def __getitem__(self, key): return key
    
    s = S()
    

    도우미 객체 s를 정의합니다.

거부된 아이디어

이전 PEP 472 해결책

PEP 472는 이제 모두 거부된 것으로 간주해야 할 상당히 많은 아이디어를 제시합니다. D’Aprano가 저자에게 보낸 개인 이메일에는 구체적으로 다음과 같이 적혀 있었습니다.

이제 PEP 472를 처음부터 끝까지 주의 깊게 읽어 보았으며, 유감스럽게도 PEP에 현재 제시된 전략 중 어느 것도 지지할 수 없습니다.

어떤 이유에서든 해당 옵션들이 현재 제시된 방안보다 열등하다는 데 동의합니다.

이 문서를 간결하게 유지하기 위해 PEP 472에 제시된 모든 옵션에 대한 반론을 여기에서 제시하지는 않겠습니다. 해당 내용들이 논의되었으며, 제안된 각 대안에는 하나 또는 몇 가지의 결정적인 문제가 있었다고만 말해 두겠습니다.

새로운 던더 추가

__(get|set|del)item_ex__라는 새로운 던더를 도입하여, 해당 던더가 존재할 경우 __(get|set|del)item__ 삼중 구조 대신 호출하도록 하자는 제안이 있었습니다.

이 선택의 근거는 대괄호에 키워드 인자 지원을 추가하는 방법에 대한 직관을 더 명확하게 만들고 함수의 동작 방식과 일치시키기 위한 것입니다. 다음과 같이 하면:

def __getitem_ex__(self, x, y): ...

이 모든 방식은 문제없이 작동하며 동일한 결과를 손쉽게 생성합니다.:

obj[1, 2]
obj[1, y=2]
obj[y=2, x=1]

다시 말해 이 해결책은 __getitem__의 동작을 전통적인 함수 시그니처와 통일하지만, __getitem__을 변경하여 하위 호환성을 깨뜨릴 수는 없으므로 우선적으로 사용되는 확장 버전을 두게 됩니다.

이 접근 방식의 문제점은 다음과 같은 것으로 밝혀졌습니다.

  • 첨자 사용이 느려집니다. 첨자에 접근할 때마다 클래스에서 이 새로운 던더 속성을 조사하고, 해당 속성이 없으면 기본 키 변환 함수를 실행합니다. 이를 처리하기 위해 메서드를 클래스 인스턴스화 시점에만 래핑하는 방법부터 이러한 메서드의 사용 가능 여부를 나타내는 비트 플래그를 추가하는 방법까지 여러 아이디어가 제안되었습니다. 해결책이 무엇이든 새로운 던더는 나중에 추가되는 경우가 아니라 클래스 생성 시점에 추가되는 경우에만 유효합니다. 이는 일반적이지 않으며, 필요할 수 있는 어떤 이유로든 메서드를 몽키 패치하는 것을 허용하지 않고 예상치 못한 방식으로 동작하게 만듭니다.
  • 메커니즘의 복잡성이 증가합니다.
  • 이 기간에는 라이브러리가 어떻게든 두 호출 규약을 모두 지원해야 하므로 길고 고통스러운 전환 기간이 필요합니다. 인자의 조건이 올바르게 충족되면 확장 메서드가 기존 메서드에 위임할 가능성이 높거나, 일부 클래스는 기존 던더를 지원하고 다른 클래스는 확장 던더를 지원할 것이기 때문입니다. 이는 호출 코드에는 영향을 주지 않지만 개발에는 영향을 줍니다.
  • 잠재적으로 getter에는 확장 버전이 정의되어 있지만 setter에는 정의되어 있지 않은 혼합 상황이 발생할 수 있습니다.
  • __setitem_ex__시그니처에서는 지정된 인덱스에 따라 인덱스의 길이가 임의로 결정되므로 value를 첫 번째 요소로 만들어야 합니다. 시각적 표기와 시그니처가 일치하지 않으므로 어색해 보일 것입니다.:
    obj[1, 2] = 3
    # calls type(obj).__setitem_ex__(obj, 3, 1, 2)
    
  • 이 해결책은 모든 키워드 인덱스가 반드시 위치 인덱스에 매핑되거나 이름을 가져야 한다는 가정에 의존합니다. 이 가정은 틀릴 수 있습니다. 레이블이 지정된 차원을 사용하는 NumPy 배열을 위한 주요 Python 패키지인 xarray는 기본 NumPy 배열의 차원에 직접 대응하지 않는 추가 차원(소위 “비차원 좌표”)을 통한 인덱싱을 지원하며, 이러한 차원에는 대응시킬 위치가 없습니다. 다시 말해 익명 인덱스는 이 해결책이 제거하게 될 가능성 있는 사용 사례이지만, *args를 사용하면 이 문제를 해결할 수 있다고 주장할 수도 있습니다.

어댑터 함수 추가

전처리 함수가 호출되어 “새 스타일” 인덱싱을 전달할 “이전 스타일” 인덱싱으로 변환한다는 의미에서 위와 유사합니다. 위와 유사한 문제가 있습니다.

새로운 “kwslice” 객체를 생성합니다.

이 제안은 PEP 472의 “New arguments contents” P4에서 이미 검토되었습니다.:

obj[a, b:c, x=1]
# calls type(obj).__getitem__(obj, a, slice(b, c), key(x=1))

이 해결책에서는 키워드 인자가 필요한 모든 사람이 튜플 및/또는 키 객체를 직접 구문 분석하여 해당 인자를 추출해야 합니다. 이는 번거로울 뿐 아니라 get/set/del 함수가 의미가 있는지 여부와 관계없이 항상 임의의 키워드 인자를 받아들이도록 만들 여지가 있습니다. 개발자가 어떤 인자가 의미가 있고 어떤 인자가 그렇지 않은지를 지정할 수 있어야 합니다.

단일 비트를 사용하여 동작을 변경합니다.

특수한 클래스 더던더 플래그입니다.:

__keyfn__ = True

__get|set|delitem__의 시그니처를 “함수와 유사한” 디스패치로 변경하며, 이는 다음을 의미합니다.:

>>> d[1, 2, z=3]

다음 호출이 이루어집니다.:

>>> type(obj).__getitem__(obj, 1, 2, z=3)
# instead of type(obj).__getitem__(obj, (1, 2), z=3)

이 선택지는 메서드의 시그니처가 다른 더던더의 특정 값에 의존한다는 점이 이상하게 느껴지기 때문에 거부되었습니다. 이는 정적 타입 검사기와 사람 모두에게 혼란을 줄 수 있습니다. 정적 타입 검사기는 이를 위해 특수한 경우를 하드코딩해야 하기 때문입니다. 실제로 Python에는 더던더의 시그니처가 다른 더던더의 값에 의존하는 다른 사례가 전혀 없습니다. __getitem__더던더를 구현해야 하는 사람은 더던더를 작성하기 전에 클래스 또는 그 서브클래스 중 어느 것에든 __keyfn__이 있는지 확인해야 합니다. 게다가 __keyfn__ 플래그가 설정된 기본 클래스를 추가하면 현재 메서드의 시그니처가 깨집니다. 런타임에 플래그가 변경되거나, 무작위로 True 또는 다른 값을 반환하는 함수를 호출하여 플래그가 생성되는 경우에는 더욱 문제가 됩니다.

빈 인덱스 표기법 obj[] 허용

현재 제안에서는 obj[]가 유효한 표기가 되는 것을 방지합니다. 그러나 한 논평자는 다음과 같이 말했습니다.

Tuple[int, int]는 두 정수의 튜플입니다. 그리고 Tuple[int]는 한 정수의 튜플입니다. 또한 때때로 no 값의 튜플을 표기해야 합니다. 이는 ()의 타입이기 때문입니다. 그러나 현재는 이를 Tuple[()]로 작성해야만 합니다. Tuple[]를 허용하면 이 특이한 예외 사례가 사라집니다.

따라서 딕셔너리 타입이 이를 거부하도록 만들 수만 있다면 구문적으로 obj[]를 허용하는 데 아마도 동의할 수 있습니다.

이 제안에서는 위치 인덱스가 주어지지 않은 경우 전달되는 값이 빈 튜플이어야 한다고 이미 정립했습니다. 빈 인덱스 표기법을 허용하면 딕셔너리 타입이 이를 자동으로 받아들여 빈 튜플을 키로 하는 값을 삽입하거나 참조하게 됩니다. 게다가 Tuple[]와 같은 타입 표기는 인덱싱 표기법 없이 Tuple로 쉽게 작성할 수 있습니다.

그러나 구현 중 Brandt Bucher와 후속 논의를 진행하면서 obj[]라는 경우가 가변 제너릭의 자연스러운 발전에 부합한다는 사실이 밝혀졌으며, 이는 위 논평에 더 큰 힘을 실어 줍니다. 결국 D’Aprano, Bucher 및 저자가 논의한 후, 당장은 obj[]표기를 구문 오류로 남겨 두고, 추후 추가 PEP를 통해 obj[]obj[()]의 동등성을 추가할 가능성을 열어 두기로 결정했습니다.

주어진 위치 인덱스가 없을 때 사용할 센티널 값

다음 경우에 인덱스로 전달할 값에 관한 주제입니다.:

obj[k=3]

상당한 논의가 이루어졌습니다.

겉보기에는 합리적인 한 가지 선택은 키워드 전용 인자 기능을 활용하여 값을 전혀 전달하지 않는 것이지만, 안타깝게도 __setitem__던더에서는 제대로 작동하지 않습니다. 값에 대한 위치 요소가 항상 전달되므로, 첫 번째 인자가 인덱스가 지정된 경우에는 인덱스를 가리키고 인덱스가 지정되지 않은 경우에는 값을 가리키도록 하는 매우 이상한 동작을 도입하지 않는 한 인덱스를 “건너뛸” 수 없기 때문입니다. 이는 매우 기만적이며 오류가 발생하기 쉽습니다.

위의 고려 사항으로 인해 키워드 전용 던더를 둘 수 없으며, 인덱스가 전달되지 않을 때 인덱스 위치에 어떤 엔터티를 전달해야 하는지에 대한 의문이 제기됩니다.:

obj[k=3] = 5
# would call type(obj).__setitem__(obj, ???, 5, k=3)

제안된 한 가지 편법은 인덱스가 지정되지 않았을 때 사용할 엔터티를 사용자가 지정할 수 있도록 index에 기본값을 지정하는 것이지만, 이렇게 하면 (값은 설계상 항상 전달되므로 절대 사용되지 않을) value에도 반드시 기본값을 지정해야 합니다. 기본값이 지정된 인자 뒤에는 기본값이 없는 인자를 둘 수 없기 때문입니다.:

def __setitem__(self, index=SENTINEL, value=NEVERUSED, *, k)

이는 추하고, 중복되며, 혼란스러워 보입니다. 따라서 obj[k=3] 표기법이 사용될 때 Python 구현이 어떤 형태로든 센티널 인덱스를 전달해야 한다는 점을 받아들여야 합니다. 이는 또한 해당 매개변수의 기본 인자가 단순히 절대 사용되지 않음을 의미합니다. (하지만 현재 구현에서도 이미 그러하므로 여기에는 변경 사항이 없습니다.)

또한 일부 클래스는 키워드 전용 인자 대신 **kwargs를 사용하려 할 수 있으므로, 다음과 같은 정의를 갖는 것은:

def __setitem__(self, index, value, **kwargs):

키워드 value를 전달하려는 사용자가:

x[value=1] = 0

다음과 같은 호출을 기대하면:

type(obj).__setitem__(obj, SENTINEL, 0, **{"value": 1})

대신 명명된 value에 의해 우연히 포착되어 duplicate value error가 발생합니다. 두 인자가 실질적으로 위치 전용이라면 사용자는 해당 인자들의 실제 로컬 이름을 걱정하지 않아야 합니다. 안타깝게도 위치 전용 값을 사용하면 이러한 일이 발생하지 않도록 할 수 있지만, 인덱스가 제공되지 않은 경우에도 indexvalue를 모두 전달해야 한다는 필요까지 해결하지는 못합니다. 요점은 클래스 구현자가 매개변수 목록에서 우연히 그러한 이름을 사용한다는 이유만으로 사용자가 열 index, value(또는 self)를 가리키는 키워드 인자를 사용하지 못하게 해서는 안 된다는 것입니다.

더욱이 세 던더가 동일한 방식으로 동작하도록 해야 합니다. __setitem__만 이 센티널을 받고 __get|delitem__은 받지 않는다면 매우 불편할 것입니다. 후자의 경우에는 인덱스 지정을 허용하지 않는 시그니처로 처리할 수 있어 사용자가 기본 인덱스를 지정할 수 있기 때문입니다.

센티널을 무엇으로 선택하든 다음 사례들을 퇴화시켜 던더에서 서로 구별할 수 없게 만듭니다.:

obj[k=3]
obj[SENTINEL, k=3]

이제 문제는 어떤 엔터티가 센티널을 나타내야 하는지로 옮겨갑니다. 선택지는 다음과 같았습니다.

  1. 빈 튜플
  2. None
  3. NotImplemented
  4. 새로운 센티널 객체(예: NoIndex)

선택지 1의 경우 호출은 다음과 같이 됩니다.:

type(obj).__getitem__(obj, (), k=3)

따라서 obj[k=3]obj[(), k=3]이 동일한 퇴화 사례가 되어 서로 구별할 수 없게 됩니다.

이 선택지가 매력적으로 들리는 이유는 다음과 같습니다.

  1. NumPy 커뮤니티에 문의가 이루어졌으며 [5], 응답의 전반적인 합의는 빈 튜플이 적절해 보인다는 것이었습니다.
  2. 이는 위치 인자가 전달되지 않은 함수에서 *args가 동작하는 방식과 유사합니다.:
    >>> def foo(*args, **kwargs):
    ...     print(args, kwargs)
    ...
    >>> foo(k=3)
    () {'k': 3}
    

    함수와 비교할 때 단일 값이 전달되는 경우의 다음과 같은 동작 비대칭은 받아들이지만, 이미 돌이킬 수 없는 일이 되었습니다.:

    >>> foo(5, k=3)
    (5,) {'k': 3}   # for indexing, a plain 5, not a 1-tuple is passed
    

선택지 2인 None을 사용하는 것에 대해서는 NumPy가 이를 새로운 축/차원을 삽입한다는 의미로 사용한다는 반론이 제기되었습니다(np.newaxis라는 별칭도 있습니다).:

arr = np.array(5)
arr.ndim == 0
arr[None].ndim == arr[None,].ndim == 1

이는 극복할 수 없는 문제는 아니지만, 분명 NumPy에 파급 효과를 일으킬 것입니다.

위 두 가지의 유일한 문제는 빈 튜플과 None이 모두 잠재적으로 유효한 인덱스이며, 두 퇴화 사례를 구별할 수 있다면 가치가 있을 수 있다는 점입니다.

따라서 대안 전략(옵션 3)은 유효한 인덱스로 사용될 가능성이 낮은 기존 엔티티를 사용하는 것입니다. 한 가지 옵션은 현재 존재하는 내장 상수 NotImplemented를 사용하는 것으로, 이는 현재 연산자 메서드가 특정 연산을 구현하지 않으며 다른 전략을 시도해야 한다(예: 다른 객체에 물어보기)는 것을 알리기 위해 반환됩니다. 안타깝게도 그 이름과 전통적인 용법은 사용자가 무언가를 전달하지 않았다는 사실보다는, 사용할 수 없는 기능을 떠올리게 합니다.

이렇게 되면 옵션 4, 즉 새로운 내장 상수가 남습니다. 이 상수는 해시 불가능해야 하며(따라서 절대 유효한 키가 될 수 없습니다), 그 맥락을 명확히 알 수 있는 이름을 가져야 합니다: NoIndex. 이는 위의 모든 문제를 해결할 수 있겠지만, 문제는 그럴 가치가 있는가 하는 것입니다.

간단히 조사해 본 결과, python-ideas의 대다수 사람들은 그것이 필수적이지 않으며 빈 튜플이 받아들일 만한 옵션이라고 생각하는 것으로 보입니다. 따라서 결과로 나오는 계열은 다음과 같습니다:

obj[k=3]
# type(obj).__getitem__(obj, (), k=3). Empty tuple

obj[1, k=3]
# type(obj).__getitem__(obj, 1, k=3). Integer

obj[1, 2, k=3]
# type(obj).__getitem__(obj, (1, 2), k=3). Tuple

그리고 다음 두 표기법은 퇴화한 형태가 됩니다:

obj[(), k=3]
# type(obj).__getitem__(obj, (), k=3)

obj[k=3]
# type(obj).__getitem__(obj, (), k=3)

흔히 제기되는 반론

  1. 그냥 메서드 호출을 사용하십시오.

    사용 사례 중 하나는 타이핑으로, 여기서는 인덱싱만 사용되며 함수 호출은 논외입니다. 게다가 함수 호출은 배열에 일부 경우 흔히 사용되는 슬라이스 표기법을 처리하지 못합니다.

    한 가지 문제는 Python 3.9에서 타입 힌트 생성이 내장 타입까지 확장되어, 더 이상 Dict, List 등을 임포트할 필요가 없어졌다는 것입니다.

    [] 내부에 키워드 인자가 없다면, 다음과 같은 것을 할 수 없을 것입니다:

    Vector = dict[i=float, j=float]
    

    하지만 명백한 이유로, 내장 타입을 사용하여 사용자 정의 타입 힌트를 만드는 호출 구문은 옵션이 될 수 없습니다:

    dict(i=float, j=float)
    # would create a dictionary, not a type
    

    마지막으로, 함수 호출은 개요에서 보였듯이 setitem과 같은 표기법을 허용하지 않습니다. f(1, x=3) = 5와 같은 연산은 허용되지 않으며, 대신 인덱싱 연산에서는 허용됩니다.

참고 자료