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, itemgettergetitem 함수에 default 키워드 인자를 추가하여 operator 모듈을 개선하는 것을 목표로 합니다. 이 추가 기능을 사용하면 대상 속성이나 항목이 없을 때 이 함수들이 지정된 기본값을 반환할 수 있으므로, 예외를 방지하고 선택적 속성이나 항목을 처리하는 코드를 단순화할 수 있습니다.

동기

현재 attrgetteritemgetter는 지정된 속성이나 항목이 없으면 예외를 발생시킵니다. 이러한 제한으로 인해 개발자는 추가 오류 처리를 구현해야 하며, 그 결과 코드가 더 복잡해지고 읽기 어려워집니다.

default 매개변수를 도입하면 선택적 속성이나 항목과 관련된 작업을 간소화하고, 반복적인 코드를 줄이며, 코드의 명확성을 높일 수 있습니다.

getitem에서도 유사한 상황이 발생하며, 이 경우 기본값을 지정할 수 있도록 하면 오랫동안 지속된 getattr() 내장 함수와의 비대칭을 해소할 수 있다는 점이 추가됩니다.

근거

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

이 접근 방식은 단순성을 유지하고 여러 속성이나 항목에 개별 기본값을 할당하는 복잡성을 피합니다. 일부 논의에서는 여러 기본값을 허용하는 방안을 고려했지만, 복잡성 증가와 혼란 가능성으로 인해 모든 경우에 단일 기본값을 사용하는 방안을 선호하게 되었습니다(자세한 내용은 아래의 Rejected Ideas 항목을 참조하십시오).

명세

제안된 동작:

  • 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

제안된 추가 기본값을 위치 인자 또는 키워드로 사용:

>>> 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

속성 체인이 지정된 경우(예: attrgetter("foo.bar.baz", default=XYZ)), 각 단계에서 무엇을 반환했는지 구별할 수 없으므로 기본값과 함께 getattr을 사용하는 것에 의존할 수 없습니다.

itemgettergetitem의 구현은 그리 쉽지 않습니다. 더 직관적인 방법은 정의하고 이해하기도 간단합니다. 즉, __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와 너무 유사하여 오류가 발생하기 쉬울 수 있습니다.

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

>>> 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의 범위를 벗어납니다.

미해결 문제

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

이 내용을 가르치는 방법

기본 동작은 수정되지 않으므로, attrgetteritemgetter를 처음 가르칠 때는 이 새로운 default를 피할 수 있습니다. 기능이 필요할 때만 도입할 수 있습니다.

하위 호환성

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

보안 영향

default 매개변수를 도입해도 본질적으로 보안 취약점이 발생하지는 않습니다.