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

Python 개선 제안 한국어 번역

PEP 769 – ‘attrgetter’, ‘itemgetter’, ‘getitem’에 ‘default’ 키워드 인자 추가

Author:
Facundo Batista <facundo at taniquetil.com.ar>
Discussions-To:
Discourse thread
Status:
Rejected
Type:
Standards Track
Created:
22-Dec-2024
Python-Version:
3.14
Post-History:
07-Jan-2025
Resolution:
14-Mar-2025

Table of Contents

번역·라이선스 안내

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

초록

이 제안은 attrgetter, itemgetter, getitem 함수에 default 키워드 인자를 추가하여 operator 모듈을 개선하는 것을 목표로 합니다. 이를 추가하면 이 함수들은 대상 속성이나 항목이 없을 때 지정된 기본값을 반환할 수 있게 됩니다. 그 결과 예외가 발생하지 않고, 선택적 속성이나 항목을 다루는 코드도 단순해집니다.

동기

현재 attrgetter와 itemgetter는 지정된 속성이나 항목이 없으면 예외를 발생시킵니다. 이 제약 때문에 개발자는 추가적인 오류 처리를 구현해야 하며, 이는 더 복잡하고 읽기 어려운 코드로 이어집니다.

default 매개변수를 도입하면 선택적 속성이나 항목을 다루는 작업이 간결해집니다. 상용구 코드가 줄고 코드도 더 명확해집니다.

getitem에서도 비슷한 상황이 발생하는데, 이 경우에는 기본값의 명세를 허용하면 getattr() 내장 함수와의 오랜 비대칭이 해소된다는 미묘한 차이가 추가로 있습니다.

근거

핵심 설계 결정은 지정된 모든 속성이나 항목에 적용되는 단일 default 매개변수를 도입하는 것입니다.

이 방식은 단순함을 유지하며, 여러 속성이나 항목에 각각 기본값을 지정할 때 생기는 복잡성을 피합니다. 일부 논의에서는 여러 기본값을 허용하는 방안도 검토했지만, 복잡성이 늘어나고 혼란의 소지가 있어 모든 경우에 단일 기본값을 사용하는 쪽이 선택되었습니다(자세한 내용은 아래 기각된 아이디어를 참고하십시오).

명세

제안하는 동작:

  • attrgetter: f = attrgetter("name", default=XYZ) 다음에 f(obj)를 호출하면 속성이 있을 경우 obj.name을, 없을 경우 XYZ를 반환합니다.
  • itemgetter: f = itemgetter(2, default=XYZ) 다음에 f(obj)를 호출하면 obj[2]가 유효할 경우 그 값을, 그렇지 않으면 XYZ를 반환합니다.
  • getitem: getitem(obj, k, XYZ) 또는 getitem(obj, k, default=XYZ)는 obj[k]가 유효할 경우 그 값을, 그렇지 않으면 XYZ를 반환합니다.

처음 두 경우에서 이 개선 사항은 단일 속성/항목 조회와 여러 속성/항목 조회에 모두 적용되며, 없는 속성이나 항목에는 기본값이 반환됩니다.

추가 기본값(키워드) 인자를 사용하지 않으면 어떤 경우에도 기능 변경은 없습니다.

attrgetter 예제

현재 동작은 변경되지 않습니다:

>>> class C:
...   class D:
...     class X:
...       pass
...   class E:
...     pass
...
>>> attrgetter("D")(C)
<class '__main__.C.D'>
>>> attrgetter("badname")(C)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'C' has no attribute 'badname'
>>> attrgetter("D", "E")(C)
(<class '__main__.C.D'>, <class '__main__.C.E'>)
>>> attrgetter("D", "badname")(C)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'C' has no attribute 'badname'
>>> attrgetter("D.X")(C)
<class '__main__.C.D.X'>
>>> attrgetter("D.badname")(C)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'D' has no attribute 'badname'

이 PEP에서 제안하는 default 키워드를 사용하는 경우:

>>> attrgetter("D", default="noclass")(C)
<class '__main__.C.D'>
>>> attrgetter("badname", default="noclass")(C)
'noclass'
>>> attrgetter("D", "E", default="noclass")(C)
(<class '__main__.C.D'>, <class '__main__.C.E'>)
>>> attrgetter("D", "badname", default="noclass")(C)
(<class '__main__.C.D'>, 'noclass')
>>> attrgetter("D.X", default="noclass")(C)
<class '__main__.C.D.X'>
>>> attrgetter("D.badname", default="noclass")(C)
'noclass'

itemgetter 예제

현재 동작은 변경되지 않습니다:

>>> obj = ["foo", "bar", "baz"]
>>> itemgetter(1)(obj)
'bar'
>>> itemgetter(5)(obj)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> itemgetter(1, 0)(obj)
('bar', 'foo')
>>> itemgetter(1, 5)(obj)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

이 PEP에서 제안하는 default 키워드를 사용하는 경우:

>>> itemgetter(1, default="XYZ")(obj)
'bar'
>>> itemgetter(5, default="XYZ")(obj)
'XYZ'
>>> itemgetter(1, 0, default="XYZ")(obj)
('bar', 'foo')
>>> itemgetter(1, 5, default="XYZ")(obj)
('bar', 'XYZ')

getitem 예제

현재 동작은 변경되지 않습니다:

>>> obj = ["foo", "bar", "baz"]
>>> getitem(obj, 1)
'bar'
>>> getitem(obj, 5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

이 PEP에서 제안하는 추가 기본값을 위치 인자로 또는 키워드로 사용하는 경우:

>>> getitem(obj, 1, "XYZ")
'bar'
>>> getitem(obj, 5, "XYZ")
'XYZ'
>>> getitem(obj, 1, default="XYZ")
'bar'
>>> getitem(obj, 5, default="XYZ")
'XYZ'

가능한 구현에 관하여

attrgetter의 구현은 꽤 직접적입니다. getattr를 사용하고 발생할 수 있는 AttributeError를 잡으면 됩니다. 따라서 attrgetter("name", default=XYZ)(obj)는 다음과 같습니다:

try:
    value = getattr(obj, "name")
except AttributeError:
    value = XYZ

기본값과 함께 getattr을 사용하는 방식에는 의존할 수 없다는 점에 유의하십시오. 속성 체인이 지정된 경우(예: attrgetter("foo.bar.baz", default=XYZ)) 각 단계에서 무엇이 반환되었는지 구분할 수 없기 때문입니다.

itemgetter와 getitem의 구현은 그리 쉽지 않습니다. 더 직관적인 방법은 정의하고 이해하기도 간단합니다. __getitem__을 시도하고 발생할 수 있는 예외를 잡는 방법입니다(아래 참조). 이 방식에서 itemgetter(123, default=XYZ)(obj) 또는 getitem(obj, 123, default=XYZ)는 다음과 동등합니다:

try:
    value = obj[123]
except (IndexError, KeyError):
    value = XYZ

그러나 성능상의 이유로 구현은 다음과 더 비슷한 형태가 될 수 있으며, 동작은 완전히 동일합니다:

if type(obj) == dict:
    value = obj.get(123, XYZ)
else:
    try:
        value = obj[123]
    except (IndexError, KeyError):
        value = XYZ

검사가 isinstance를 사용하지 않고 정확한 타입을 대상으로 한다는 점에 주목하십시오. 이는 정확한 동작을 보장하기 위한 것입니다. 객체가 dict를 상속하면서 get을 재정의한 사용자 정의 객체라면 정확한 동작을 보장할 수 없기 때문입니다(객체에 get 메서드가 있는지 검사하지 않는 것도 비슷한 이유입니다).

이렇게 하면 성능은 더 좋아지지만 이는 구현 세부 사항일 뿐이므로, 동작 방식에 대한 원래 설명은 그대로 유지할 수 있습니다.

잡아야 할 예외와 관련해서는, __getitem__이 IndexError, KeyError, TypeError를 발생시킬 수 있지만(해당 reference를 참조하십시오), 컨테이너에 지정된 키나 인덱스가 없는 경우에 발생할 수 있는 것은 앞의 두 가지뿐이며, 마지막 것은 코드의 버그를 나타낼 가능성이 높으므로 기본 동작을 일으키기 위해 이를 잡지는 않습니다.

코너 케이스

default 옵션을 제공하는 것은 일반적인 경우에 항목/속성에 대한 접근이 실패할 때에만 의미가 있습니다. 다시 말해, 접근하는 객체가 스스로 기본값을 처리해서는 안 됩니다.

예를 들어, defaultdict는 항목에 접근할 때 절대 오류를 일으키지 않으므로 다음은 불필요하거나 혼란스럽습니다:

>>> from collections import defaultdict
>>> from operator import itemgetter
>>> dd = defaultdict(int)
>>> itemgetter("foo", default=-1)(dd)
0

자체적인 대체 동작을 구현하면서 __getitem__ 또는 __getattr__을 오버로드하는 모든 사용자 정의 객체에도 마찬가지입니다.

기각된 아이디어

여러 개의 기본값

여러 속성이나 항목에 대해 여러 개의 기본값을 허용하는 아이디어가 검토되었습니다.

두 가지 대안이 논의되었습니다. 하나는 attrgetter/itemgetter에 주어진 매개변수와 같은 개수의 항목을 가져야 하는 이터러블을 사용하는 방법이고, 다른 하나는 attrgetter/itemgetter에 전달된 이름과 일치하는 키를 가진 딕셔너리를 사용하는 방법입니다.

여기서 해결하기 정말 복잡한 문제(기능을 설명하기 어렵게 만들고 혼란스러운 코너 케이스를 낳는 문제)는, 이터러블이나 딕셔너리 자체가 모든 항목에 대해 원하는 실제 기본값인 경우에 어떤 일이 일어나야 하는가입니다. 예를 들면 다음과 같습니다:

>>> itemgetter("a", default=(1, 2))({})
(1, 2)
>>> itemgetter("a", "b", default=(1, 2))({})
((1, 2), (1, 2))

default를 사용해 “여러 개의 기본값”을 허용한다면, 위 예제의 첫 번째 경우에는 기본값에 포함된 항목이 이름보다 많기 때문에 예외가 발생하고, 두 번째 경우에는 (1, 2))를 반환할 것입니다. 이 때문에 여러 개의 기본값에 대해 다른 이름을 사용하는 방안도 검토했습니다(예: defaults. 표현력은 좋지만 default와 너무 비슷해서 실수를 유발할 수 있습니다).

여러 개의 기본값을 가능하게 하는 또 다른 제안은 attrgetter와 itemgetter의 조합을 허용하는 것입니다. 예를 들면 다음과 같습니다:

>>> ig_a = itemgetter("a", default=1)
>>> ig_b = itemgetter("b", default=2)
>>> ig_combined = itemgetter(ig_a, ig_b)
>>> ig_combined({"a": 999})
(999, 2)
>>> ig_combined({})
(1, 2)

그러나 itemgetter나 attrgetter를 조합하는 것은 완전히 새로운 동작이며 정의하기가 매우 복잡합니다. 불가능하지는 않지만, 이는 이 PEP의 범위를 벗어납니다.

결국 여러 개의 기본값을 두는 것은 지나치게 복잡하고 혼란을 줄 수 있다고 판단되어, 단순성과 예측 가능성을 위해 단일 default 매개변수가 채택되었습니다.

튜플 반환의 일관성

기각된 또 다른 제안은 주어진 키/이름/인덱스의 개수와 관계없이 항상 튜플을 반환하도록 하는 플래그를 추가하는 것이었습니다. 예를 들면 다음과 같습니다.:

>>> letters = ["a", "b", "c"]
>>> itemgetter(1, return_tuple=True)(letters)
('b',)
>>> itemgetter(1, 2, return_tuple=True)(letters)
('b', 'c')

이는 여러 개의 기본값의 일관성에 거의 도움이 되지 않고 추가 논의가 필요하며, 이 PEP의 범위를 벗어납니다.

미해결 문제

현재 미해결 문제는 없습니다.

교육 방법

기본 동작은 바뀌지 않으므로 attrgetter와 itemgetter를 처음 가르칠 때는 이 새로운 default를 다루지 않아도 됩니다. 이 기능은 필요할 때 비로소 소개하면 됩니다.

하위 호환성

제안된 변경 사항은 하위 호환성을 유지합니다. default 매개변수는 선택 사항이며, 이 매개변수를 사용하지 않는 기존 코드는 이전과 같이 함수로서 동작합니다. 새로운 default 매개변수를 명시적으로 사용하는 코드만 새로운 동작을 보이므로 현재 구현에는 아무런 지장이 없습니다.

보안 영향

default 매개변수를 도입한다고 해서 그 자체로 보안 취약점이 생기지는 않습니다.