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
번역·라이선스 안내
이 비공식 한국어 번역은 원문 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입니다. 그 이후로 이 기능에 대한 관심이 다시 높아졌습니다.
개요
배경
PEP 472는 2014년에 작성되었습니다. 이 PEP는 다양한 사용 사례를 자세히 설명했으며, python-ideas 메일링 리스트에서 이루어진 폭넓은 논의로부터 구현 전략을 추려 내어 작성되었지만, 어떤 전략을 사용해야 하는지에 대해서는 명확한 합의에 이르지 못했습니다. 많은 코너 케이스를 더 면밀히 검토한 결과, 어색하거나 하위 호환성을 깨뜨리거나 둘 다에 해당한다고 판단되었습니다.
이 PEP는 5년 동안 존재했음에도 이 기능에 대한 관심이 부족했던 것이 주된 이유가 되어 결국 2019년에 거부되었습니다 [1].
그러나 PEP 484에서 타입 힌트가 도입되면서, 예를 들어 정수 리스트를 Sequence[int]로 지정하는 것처럼 타입 어노테이션을 풍부하게 하는 데 대괄호 표기법이 일관되게 사용되어 왔습니다. 또한 pandas와 xarray 같은 데이터 분석용 패키지가 크게 늘어났습니다. 이 패키지들은 이름을 사용하여 테이블의 열(pandas)이나 nd-배열의 축(xarray)을 나타냅니다. 이러한 패키지는 사용자가 이름으로 특정 데이터에 접근할 수 있게 해 주지만, 현재 이 기능에 인덱스 표기법([])을 사용할 수는 없습니다.
그 결과, 이름이 붙은 정보를 허용하는 더 유연한 문법에 대한 관심이 python-ideas의 여러 스레드에서 이따금 다시 제기되었으며, 최근에는 2019년에 Caleb Donovick [2]가, 2020년에 Andras Tantos [3]가 이를 제기했습니다. 이러한 요청을 계기로 python-ideas 메일링 리스트에서 활발한 논의가 이루어졌고, 그 과정에서 여러 선택지가 다시 논의되어 이제 구현 전략에 대한 전반적인 합의에 이르렀습니다.
사용 사례
다음의 실용적인 사용 사례들은 키워드 명세가 표기법을 개선하고 추가적인 가치를 제공할 수 있는 여러 경우를 보여 줍니다:
- 인덱스에 더 명확한 의미를 부여하여, 예를 들어 인덱스 순서를 실수로 뒤바꾸는 일을 방지합니다:
>>> grid_position[x=3, y=5, z=8] >>> rain_amount[time=0:12, location=location] >>> matrix[row=20, col=40]
- 특히 제네릭을 사용할 때 키워드로 타입 표기법을 풍부하게 합니다:
def function(value: MyType[T=int]):
- 계산 물리학이나 화학 같은 일부 분야에서는
Basis[Z=5]같은 표기법이 정확도 수준을 나타내는 도메인 특화 언어 표기법으로 사용됩니다:>>> low_accuracy_energy = computeEnergy(molecule, BasisSet[Z=3])
- Pandas는 현재 다음과 같은 표기법을 사용합니다:
>>> df[df['x'] == 1]
이 표기법은
df[x=1]로 대체할 수 있습니다. - 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
- 다른 함수(와 그 함수의 인자)를 인자로 받는 함수나 메서드에는 어떤 인자가 대상 함수로 전달될 인자이고 어떤 인자가 대상 함수의 실행 방식을 설정하는 데 쓰이는지 구분할 방법이 필요합니다. 위치 매개변수에서는 (확장할 수는 없지만) 간단히 구분할 수 있으나, 키워드에서는 이를 구분할 방법이 필요합니다. [4]
인덱스 표기법을 사용하면 호출자의 코드를 어지럽히지 않고도 이러한 함수에 키워드 인자를 전달하는 Python다운 방법을 제공할 수 있습니다.
>>> # 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)
- 별표 인자를 사용할 수 있으면 PEP 646 가변 제네릭(Variadic Generics), 특히
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)
이는 __(get|set|del)item__ 던더 호출로 변환되며, 이 호출에는 인덱스를 담은 단일 매개변수(__getitem__ 및 __delitem__의 경우) 또는 인덱스와 값을 담은 두 매개변수(__setitem__의 경우)가 전달됩니다.
인덱싱 호출의 동작은 여러 측면에서 함수 호출과 근본적으로 다릅니다:
첫 번째 차이점은 코드를 읽는 사람에게 전달되는 의미입니다. 함수 호출은 “부작용이 있을 수 있는 임의의 함수 호출”을 뜻합니다. 인덱싱 연산은 “조회”를 뜻하며, 일반적으로 (타이핑 표기법의 경우처럼) 어떤 개체의 부분 집합이나 특정한 하위 측면을 가리킵니다. 이러한 근본적인 차이 때문에, 남용을 막을 수는 없더라도 구현자는 조회 동작을 바꾸기 위해 키워드 인자를 도입하면 이 본질적인 의미를 해칠 수 있다는 점을 알고 있어야 합니다.
함수와 비교했을 때 인덱스 표기법의 두 번째 차이점은 인덱싱을 값을 가져오는 연산과 설정하는 연산 모두에 사용할 수 있다는 것입니다. Python에서 함수는 대입의 좌변에 올 수 없습니다. 다시 말해, 다음 둘은 모두 유효합니다:
>>> x = a[1, 2]
>>> a[1, 2] = 5
하지만 다음 중에서는 첫 번째만 유효합니다:
>>> x = f(1, 2)
>>> f(1, 2) = 5 # invalid
이러한 비대칭성은 중요하며, 두 형태 사이에 자연스러운 불균형이 있음을 이해하게 해 줍니다. 따라서 두 형태가 투명하고 대칭적으로 동작해야 한다고 당연하게 여길 수는 없습니다.
세 번째 차이점은 함수의 인자에는 이름이 할당된다는 것입니다. 단, 전달된 매개변수가 *args로 포착되는 경우는 예외이며, 이때 매개변수는 args 튜플의 항목이 됩니다. 다시 말해, 함수는 이미 인덱싱 연산과 똑같이 익명 인자 의미를 가지고 있습니다. 그러나 __(get|set|del)item__이 index 인자로 항상 튜플을 받는 것은 아닙니다(*args와 동작이 일관되려면 항상 튜플을 받아야 합니다). 실제로, 다음과 같은 간단한 클래스가 주어졌을 때:
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)
다섯 번째 차이점은 인자가 없는 형태가 존재하지 않는다는 것입니다. 다음은 유효합니다:
f()
다음은 유효하지 않습니다:
a[]
명세
명세를 설명하기 전에 위치 인덱스, 최종 인덱스, 키워드 인자 사이의 명명상 차이를 강조해 두는 것이 중요합니다. 여기에 작용하는 근본적인 비대칭성을 이해하는 것이 중요하기 때문입니다. __(get|set|del)item__은 근본적으로 인덱싱 연산이며, 요소를 가져오거나 설정하거나 삭제하는 방법은 인덱스, 즉 최종 인덱스를 통하는 것입니다.
현재의 방식은 대괄호 사이에 전달된 것, 즉 위치 인덱스로부터 최종 인덱스를 직접 만드는 것입니다. 다시 말해, 대괄호 안에 전달된 것은 __getitem__ 안의 코드가 이후 인덱싱 연산에 사용할 값을 만드는 데 그대로 사용됩니다. dict에서 이미 보았듯이, d[1]은 위치 인덱스가 1이고 최종 인덱스도 1이며(이것이 이후 딕셔너리에 추가되는 요소이기 때문입니다), d[1, 2]는 위치 인덱스가 (1, 2)이고 최종 인덱스도 (1, 2)입니다(이번에도 이것이 딕셔너리에 추가되는 요소이기 때문입니다). 그러나 위치 인덱스 d[1,2:3]은 딕셔너리에서 허용되지 않습니다. 슬라이스 객체는 해시할 수 없으므로 위치 인덱스를 최종 인덱스로 변환할 방법이 없기 때문입니다. 위치 인덱스는 현재 __getitem__의 index 매개변수로 알려진 것입니다. 그럼에도 불구하고, 예를 들어 위치 인덱스를 문자열로 변환하는 방식으로 최종 인덱스를 만드는 딕셔너리 유사 클래스를 작성하는 것을 막는 것은 아무것도 없습니다.
이 PEP는 현재의 방식을 확장하여, 위치 인덱스와 (전달된 경우) 키워드 인자를 결합하는 향상된 문법을 통해 최종 인덱스를 더 유연하게 만들 수 있도록 합니다.
위 내용은 중요한 점을 분명히 보여 줍니다. 인덱스 연산의 맥락에서 키워드 인자는 최종 인덱스를 얻기 위한 인덱싱 결정을 내리는 데 사용될 수 있으며, 따라서 함수에서는 일반적이지 않은 값도 받아들여야 합니다. 예를 들어 슬라이스를 받아들이는 사용 사례 1을 참고하십시오.
이 PEP가 성공적으로 구현되면 다음과 같이 동작하게 됩니다:
- 빈 첨자는 맥락과 관계없이 여전히 허용되지 않습니다(기각된 아이디어 참고):
obj[] # SyntaxError
- 단일 인덱스 값은 전달될 때 단일 인덱스 값으로 유지됩니다:
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번 항목을 참고하십시오.
- 쉼표로 구분된 인자는 여전히 튜플로 파싱되어 단일 위치 인자로 전달됩니다:
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))
위의 항목들은 첨자에서 키워드 인자를 지원하지 않으려는 클래스는 아무것도 할 필요가 없다는 것을 의미하며, 따라서 이 기능은 완전한 하위 호환성을 갖습니다.
- 키워드 인자가 있다면 위치 인자 뒤에 와야 합니다:
obj[1, 2, spam=None, 3] # SyntaxError
이는 위치 인자와 키워드 인자를 섞어 쓰면 SyntaxError가 발생하는 함수 호출과 같습니다.
- 키워드 첨자가 있으면 함수 호출에서와 같은 방식으로 처리됩니다. 예시:
# 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__의 경우, 인덱스와 값의 순서가 그대로 유지됩니다. 함수 정의에서 일반적으로 그러하듯이, 키워드 인자는 맨 끝에 옵니다.
- 키워드 첨자에는 함수 호출의 키워드와 같은 규칙이 적용됩니다:
- 인터프리터는 각 키워드 첨자를 해당 메서드의 이름 있는 매개변수에 대응시킵니다;
- 이름 있는 매개변수가 두 번 사용되면 오류입니다;
- 키워드를 모두 사용한 뒤에 (값이 없는) 이름 있는 매개변수가 남아 있으면, 그 매개변수에는 (기본값이 있는 경우) 기본값이 할당됩니다;
- 그러한 매개변수 중 기본값이 없는 것이 있으면 오류입니다;
- 이름 있는 매개변수를 모두 채운 뒤에도 남은 키워드 첨자가 있고 메서드에
**kwargs매개변수가 있으면, 남은 키워드 첨자는 딕셔너리로**kwargs매개변수에 바인딩됩니다; - 하지만
**kwargs매개변수가 정의되어 있지 않으면 오류입니다.
- 첨자 안에서 시퀀스 언패킹이 허용됩니다:
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)
다시 말해, 단일 위치 인덱스는 시퀀스 언패킹이 없는 경우에만 “있는 그대로” 전달됩니다. 시퀀스 언패킹이 있으면, 언패킹이 이루어진 뒤 인덱스의 요소 개수와 관계없이 인덱스는 튜플이 됩니다.
- 딕셔너리 언패킹이 허용됩니다:
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]
- 키워드 전용 첨자가 허용됩니다. 위치 인덱스는 빈 튜플이 됩니다:
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)
빈 튜플을 센티널로 선택한 것에 대해서는 논쟁이 있었습니다. 자세한 내용은 기각된 아이디어 절에 나와 있습니다.
- 키워드 인자는 슬라이스 문법을 허용해야 합니다:
obj[3:4, spam=1:4, eggs=2] # calls type(obj).__getitem__(obj, slice(3, 4, None), spam=slice(1, 4, None), eggs=2)
이는 일반 함수 호출에서도 같은 문법을 허용할 가능성을 열어 줄 수 있지만, 이 권고안에 포함되는 내용은 아닙니다.
- 키워드 인자에는 기본값을 둘 수 있습니다:
# 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.
- 위에서 설명한 의미 체계를
__class__getitem__에도 똑같이 확장해야 합니다. PEP 560 이후로 타입 힌트는 다음과 같이 디스패치됩니다.x[y]에서__getitem__메서드를 찾을 수 없고,x가 타입(클래스) 객체이며,x에 클래스 메서드__class_getitem__이 있으면 그 메서드가 호출됩니다.list[T=int]와 같은 표기를 받아들일 수 있도록 이 메서드에도 같은 변경을 적용해야 합니다.
표준 클래스(dict, list 등)의 인덱싱 동작
이 PEP에서 제안하는 내용은 인덱싱을 사용하는 현재 핵심 클래스의 동작을 전혀 변경하지 않습니다. 사용자 정의 클래스의 인덱스 연산에 키워드를 추가하는 것은, 예를 들어 표준 dict 타입이 키워드 인자를 처리하도록 수정하는 것과는 다릅니다. 실제로 dict(그리고 list를 비롯해 인덱싱 의미 체계를 가진 다른 표준 라이브러리 클래스)는 그대로 유지되며, 앞으로도 키워드 인자를 받아들이지 않습니다. 다시 말해, d가 dict라면 d[1, a=2] 문은 TypeError를 발생시킵니다. 해당 구현이 키워드 인자 사용을 지원하지 않기 때문입니다. 다른 모든 클래스(list, dict 등)도 마찬가지입니다.
경계 사례와 주의할 점
새로운 표기법이 도입되면서 몇 가지 경계 사례를 분석해야 합니다.
- 기술적으로, 클래스가 게터를 다음과 같이 정의하면:
def __getitem__(self, index):
호출자는 다음 두 경우처럼 키워드 문법을 사용하여 이를 호출할 수 있습니다:
obj[3, index=4] obj[index=1]
이는
index인자에 두 개의 값을 넘겨 메서드를 호출하려는 것과 같으므로 자동으로 오류가 되며,TypeError가 발생합니다. 첫 번째 경우에는index가3이 되고, 두 번째 경우에는 빈 튜플()이 됩니다.이 동작은 인덱싱에 의존하는 현존하는 모든 클래스에 적용되므로, 이 점에서 새로운 동작이 하위 호환성 문제를 일으킬 여지는 없다는 점에 유의하십시오.
이 동작을 명시적으로 강조하고자 하는 클래스는 매개변수를 위치 전용으로 정의할 수 있습니다:
def __getitem__(self, index, /):
- setter 표기법에서도 비슷한 경우가 발생합니다:
# Given type(obj).__setitem__(obj, index, value): obj[1, value=3] = 5
값은 자동으로 전달되고 Python 인터프리터가
TypeError: got multiple values for keyword argument 'value'를 발생시키므로 이는 문제가 되지 않습니다 - 서브스크립트 던더 메서드가 위치-또는-키워드 매개변수를 사용하도록 선언되어 있으면, 메서드에 인자가 전달될 때 예상치 못한 경우가 생길 수 있습니다. 다음과 같은 시그니처가 주어졌을 때:
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')
이것이 의도한 동작인 시나리오도 있을 수 있으므로 인터프리터가 이 규칙을 강제할 필요는 없습니다. 하지만 린터는 키워드 전용 플래그를 사용하지 않는 첨자 메서드에 대해 경고하도록 선택할 수 있습니다.
- 앞서 보았듯이 단일 값 뒤에 키워드 인자가 오더라도 튜플로 바뀌지 않습니다. 즉,
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 인터페이스
인덱싱 연산은 다음 함수들을 호출하여 처리됩니다
- get 연산에는
PyObject_GetItem(PyObject *o, PyObject *key) - set 연산에는
PyObject_SetItem(PyObject *o, PyObject *key, PyObject *value) - del 연산에는
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_SUBSCR, DELETE_SUBSCR를 사용합니다. 새로운 연산을 위해 BINARY_SUBSCR_KW, STORE_SUBSCR_KW, DELETE_SUBSCR_KW를 제안합니다. 컴파일러가 이 새로운 옵코드들을 생성해야 합니다. 기존 C 구현들은 kwargs로 NULL을 전달하여 확장된 메서드들을 호출하게 됩니다.
마지막으로, PyMappingMethods 구조체에 다음과 같은 새로운 슬롯들을 추가해야 합니다:
mp_subscript_kwmp_ass_subscript_kw
이 슬롯들은 키워드를 담은 딕셔너리 객체를 처리하기에 적절한 시그니처를 갖게 됩니다.
“가르치는 방법”에 대한 권장 사항
피드백 세션 중에 나온 요청 중 하나는 학생, 데이터 과학자 및 이와 유사한 대상에게 이 기능을 가르칠 때 쓸 수 있는 설명 방식을 자세히 제시해 달라는 것이었습니다. 이 절은 그 요청에 대응합니다.
인덱싱은 구현의 관점이 아니라 사용의 관점에서만 설명하겠습니다. 앞서 언급한 대상이 접하게 될 가능성이 높은 측면이 바로 그것이기 때문입니다. 사용자 중 일부만이 자신만의 던더 함수를 구현해야 하며, 이는 고급 사용법으로 간주할 수 있습니다. 적절한 설명은 다음과 같습니다:
인덱싱 연산은 일반적으로 인덱스를 사용하여 더 큰 데이터셋의 부분 집합을 참조하는 데 사용됩니다. 흔히 볼 수 있는 경우에 인덱스는 하나 이상의 숫자, 문자열, 슬라이스 등으로 구성됩니다.일부 타입은 인덱스뿐만 아니라 이름이 있는 값으로도 인덱싱할 수 있게 허용할 수 있습니다. 이러한 이름이 있는 값은 함수 호출의 키워드 인자에 사용되는 것과 같은 문법으로 대괄호 안에 지정합니다. 이름의 의미와 사용법은 타입마다 다르므로 해당 타입의 문서에서 확인할 수 있습니다.
이제 교사는 몇 가지 실용적인 실제 사례를 보여 주면서, 제시된 라이브러리에서 이 기능이 갖는 의미를 설명합니다. 이 글을 쓰는 시점에는 당연히 이러한 사례가 존재하지 않지만, 이 기능을 구현할 가능성이 가장 높은 라이브러리는 pandas와 numpy이며, 아마도 이름으로 열을 참조하는 방법으로 구현될 것입니다.
참조 구현
참조 구현은 현재 여기에서 개발되고 있습니다 [6].
우회 방법
Python 언어를 변경하는 모든 PEP는 “PEP가 해결하려는 문제를 다루기에 기존 언어 명세가 왜 부적절한지 명확하게 설명”해야 합니다.
제안된 확장과 대략 동등한 몇 가지 방법이 이미 가능하며, 이를 우회 방법이라고 부릅니다. 우회 방법은 새로운 문법을 도입하는 대신 사용할 수 있는 대안을 제공하며, 의미는 다른 곳에서 정의하도록 남겨 둡니다.
우회 방법은 다음과 같습니다. 여기에 나오는 헬퍼 H와 P는 범용으로 의도된 것이 아닙니다. 예를 들어, 모듈이나 패키지가 자체 헬퍼를 사용하도록 요구할 수도 있습니다.
- 사용자 정의 클래스에 컨테이너에 저장된 값을 각각 가져오고 삭제하는
getitem과delitem메서드를 부여할 수 있습니다:>>> 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
- 여기서
H라고 부르는 헬퍼 클래스를 사용하여 컨테이너와 매개변수의 역할을 서로 바꿀 수 있습니다. 다시 말해, 다음 코드를:H(1, 2, a=3, b=4)[x]
아래 코드 대신 사용합니다:
x[1, 2, a=3, b=4]
이 방법은
getitem,delitem뿐만 아니라setitem에도 적용됩니다. 그 이유는 다음 코드가:>>> H(1, 2, a=3, b=4)[x] = val
유효한 문법이며, 적절한 의미를 부여할 수 있기 때문입니다.
- 여기서
P라고 부르는 헬퍼 함수를 사용하여 인자들을 하나의 객체에 저장할 수 있습니다. 예를 들어 다음 코드는:>>> x[P(1, 2, a=3, b=4)] = val
유효한 문법이며, 적절한 의미를 부여할 수 있습니다.
- 슬라이스를 위한
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__ 3종 대신 호출되도록 하자는 제안이 있었습니다.
이 선택의 근거는 대괄호에 키워드 인자 지원을 추가하는 방법에 대한 직관을 더 명확하게 하고 함수의 동작과 일치시키는 것입니다. 다음이 주어졌을 때:
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]가 있습니다. 그리고 가끔은 값이 하나도 없는 튜플을 표기해야 할 때가 있습니다. 그것이()의 타입이기 때문입니다. 하지만 현재는 이를Tuple[()]로 쓸 수밖에 없습니다.Tuple[]를 허용한다면 그 이상한 예외적인 경우가 사라질 것입니다.그러니 dict 타입이 이를 거부하도록 만들 수만 있다면, 저는 아마
obj[]를 문법적으로 허용해도 괜찮을 것 같습니다.
이 제안에서는 위치 인덱스가 주어지지 않은 경우 전달되는 값이 빈 튜플이어야 한다고 이미 정했습니다. 빈 인덱스 표기법을 허용하면 딕셔너리 타입이 이를 자동으로 받아들여, 빈 튜플을 키로 하는 값을 삽입하거나 참조하게 됩니다. 게다가 Tuple[] 같은 타이핑 표기법은 인덱싱 표기법 없이 Tuple로 쉽게 쓸 수 있습니다.
하지만 이후 구현 과정에서 Brandt Bucher와 논의한 결과, obj[] 경우가 가변 제네릭(variadic generics)의 자연스러운 발전 방향에 맞는다는 점이 드러났고, 이는 위 의견에 더 힘을 실어 주었습니다. 결국 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를 발생시킵니다. 두 인자가 사실상 위치 전용이라면, 사용자가 그 인자들의 실제 지역 이름을 신경 쓸 필요가 없어야 합니다. 안타깝게도 위치 전용 값을 사용하면 이러한 일이 발생하지 않도록 보장할 수 있지만, 인덱스가 제공되지 않았을 때에도 index와 value를 모두 전달해야 하는 문제는 여전히 해결되지 않습니다. 요점은 클래스 구현자가 우연히 매개변수 목록에 그러한 이름을 사용했다는 이유만으로, 사용자가 키워드 인자를 사용해 index, value(또는 self)라는 열을 가리키지 못하게 해서는 안 된다는 것입니다.
또한 세 가지 던더 메서드가 모두 같은 방식으로 동작해야 합니다. __setitem__만 이 센티널을 받고, __get|delitem__은 인덱스 명세가 없어도 되는 시그니처를 사용하여 사용자가 지정한 기본 인덱스를 허용할 수 있다는 이유로 이를 받지 않는다면 매우 불편할 것입니다.
센티널로 무엇을 선택하든, 다음 경우들은 퇴화되어 던더에서 구별할 수 없게 됩니다.:
obj[k=3]
obj[SENTINEL, k=3]
이제 문제는 어떤 개체가 센티널을 나타내야 하는지로 옮겨갑니다. 선택지는 다음과 같았습니다.
- 빈 튜플
- None
- NotImplemented
- 새로운 센티널 객체(예: NoIndex)
선택지 1의 경우, 호출은 다음과 같이 됩니다.:
type(obj).__getitem__(obj, (), k=3)
따라서 obj[k=3]과 obj[(), k=3]은 퇴화되어 구별할 수 없게 됩니다.
이 선택지는 다음과 같은 이유로 매력적으로 보입니다.
- numpy 커뮤니티에 의견을 물었으며 [5], 응답의 전반적인 합의는 빈 튜플이 적절해 보인다는 것이었습니다.
- 위치 인자가 주어지지 않았을 때 함수에서
*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)
흔한 반론
- 그냥 메서드 호출을 사용하십시오.
사용 사례 중 하나는 타입 지정인데, 여기서는 인덱싱만 사용되며 함수 호출은 고려 대상이 아닙니다. 게다가 함수 호출은 배열에서 일부 경우 흔히 사용되는 슬라이스 표기법을 처리하지 못합니다.
한 가지 문제는 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같은 연산은 허용되지 않지만, 인덱싱 연산에서는 허용됩니다.
참고 문헌
Copyright
This document has been placed in the public domain.