PEP 455 – collections에 키를 변환하는 딕셔너리 추가
- Author:
- Antoine Pitrou <solipsis at pitrou.net>
- BDFL-Delegate:
- Raymond Hettinger
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 13-Sep-2013
- Python-Version:
- 3.5
- Post-History:
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 collections 모듈에 새로운 자료 구조를 추가할 것을 제안하며, 이 PEP에서는 이를 “TransformDict”라고 부릅니다. 이 구조는 조회할 때는 주어진 함수로 키를 변환하지만, 읽을 때는 원래 키를 유지하는 가변 매핑입니다.
거부
근거는 https://mail.python.org/pipermail/python-dev/2015-May/140003.html 을 참고하시고, 이전의 부분적인 검토는 https://mail.python.org/pipermail/python-dev/2013-October/129937.html 을 참고하십시오.
근거
이 패턴을 특수화한 버전은 수없이 많습니다. 가장 흔한 것은 대소문자를 구분하지 않으면서 대소문자를 보존하는 딕셔너리, 즉 대소문자를 구분하지 않고 키를 매칭하지만 원래의 대소문자 표기는 유지하는 딕셔너리 유사 컨테이너입니다. 이는 네트워크 프로그래밍에서 매우 흔한 요구 사항입니다. 많은 프로토콜이 메시지 안에 “키 / 값” 속성의 배열을 두는데, 이때 키는 텍스트 문자열이며, 수신할 때는 대소문자를 무시하도록 규정되어 있지만 재전송할 때는 명세나 관례에 따라 보존하거나 단순하지 않은 방식으로 정규화해야 하기 때문입니다.
또 다른 흔한 요청은 아이덴티티 딕셔너리로, 일반적인 매칭 대신 각 키의 id()에 따라 키를 매칭하는 것입니다.
두 경우 모두 키를 조회할 때 주어진 변환 함수를 키에 적용하는 더 일반적인 패턴의 사례입니다. 그 함수는 앞의 예에서는 str.lower 또는 str.casefold이고, 뒤의 예에서는 내장 id 함수입니다.
(이 패턴은 사용자에게 보이는 집합의 키를 내부 조회 집합으로 투영한다고 말할 수도 있습니다.)
의미론
TransformDict는 MutableMapping 구현체입니다. 즉, dict 자체 및 표준 라이브러리의 다른 딕셔너리 유사 클래스처럼 잘 알려진 가변 매핑 API를 충실하게 구현합니다. 따라서 이 PEP에서는 TransformDict 메서드 대부분의 의미론을 다시 설명하지 않습니다.
변환 함수는 전단사일 필요가 없으며, 대소문자를 구분하지 않는 예처럼 엄밀하게 전사이기만 해도 됩니다(다시 말해, 서로 다른 키로 같은 값을 조회할 수 있습니다):
>>> d = TransformDict(str.casefold)
>>> d['SomeKey'] = 5
>>> d['somekey']
5
>>> d['SOMEKEY']
5
TransformDict는 항목을 생성할 때 사용한 첫 번째 키를 유지합니다:
>>> d = TransformDict(str.casefold)
>>> d['SomeKey'] = 1
>>> d['somekey'] = 2
>>> list(d.items())
[('SomeKey', 2)]
변환 함수가 해시 가능한 값을 반환하기만 하면, 원래 키는 해시 가능하지 않아도 됩니다:
>>> d = TransformDict(id)
>>> l = [None]
>>> d[l] = 5
>>> l in d
True
생성자
위의 예에서 보듯이, TransformDict를 생성하려면 키 변환 함수를 첫 번째 인자로 전달해야 합니다(defaultdict를 생성할 때 팩토리 함수를 첫 번째 인자로 전달해야 하는 것과 매우 비슷합니다).
생성자는 TransformDict를 특정 키-값 쌍으로 초기화하는 데 사용할 수 있는 다른 선택적 인자도 받습니다. 이 선택적 인자는 dict 및 defaultdict 생성자의 선택적 인자와 같습니다:
>>> d = TransformDict(str.casefold, [('Foo', 1)], Bar=2)
>>> sorted(d.items())
[('Bar', 2), ('Foo', 1)]
원래 키 얻기
TransformDict는 저장된 키를 해당 값과 함께 반환하는 조회 메서드도 제공합니다:
>>> d = TransformDict(str.casefold, {'Foo': 1})
>>> d.getitem('FOO')
('Foo', 1)
>>> d.getitem('bar')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'bar'
메서드 이름 getitem()은 가변 매핑의 표준 popitem() 메서드를 따른 것입니다.
변환 함수 얻기
TransformDict에는 변환 함수를 돌려주는 간단한 읽기 전용 속성 transform_func이 있습니다.
대안 제안 및 질문
마지막 원래 키 유지
python-dev 응답자 대부분은 마지막 키를 유지하는 것보다 사용자가 처음 제공한 키를 유지하는 쪽이 더 직관적이라고 보았습니다. 또한 이는 서로 다르지만 동등한 키를 사용할 때 dict 객체 자체가 동작하는 방식과도 일치합니다:
>>> d = {}
>>> d[1] = 'hello'
>>> d[1.0] = 'world'
>>> d
{1: 'world'}
또한 첫 번째 키를 유지하는 방식에서도 다음 방법을 사용하면 마지막 키를 명시적으로 유지할 수 있습니다:
d.pop(key, None)
d[key] = value
반면 그 반대(마지막 키를 유지하는 방식에서 첫 번째 키를 유지하는 것)는 컨테이너 코드의 일부를 다시 작성하지 않고서는 가능해 보이지 않습니다.
인코더 / 디코더 쌍 사용하기
원래 키는 컨테이너가 유지하므로 함수 쌍을 사용할 필요가 없습니다. 게다가 인코더 / 디코더 쌍을 사용하려면 변환이 전단사여야 하는데, 이렇게 되면 대소문자를 구분하지 않는 매칭과 같은 중요한 사용 사례를 지원할 수 없습니다.
값에 대한 변환 함수 제공하기
딕셔너리 값은 조회에 사용되지 않으며, 값의 의미는 컨테이너의 동작과 전혀 관계가 없습니다. 따라서 “원래” 값과 “변환된” 값을 모두 두는 것은 의미가 없습니다. 변환된 값은 아무 데도 쓰이지 않을 것이기 때문입니다.
범용이 아닌 특수화된 컨테이너 제공하기
특수화된 대소문자 비구분 딕셔너리 변형 대신 범용 TransformDict 구조를 제공하는 이유가 무엇인지 질문이 있었습니다. 그 답은 범용 구조를 제공하는 비용이 (코드 측면에서나 성능 측면에서나) 거의 같으면서도 더 많은 사용 사례를 충족할 수 있다는 것입니다.
대소문자를 구분하지 않는 딕셔너리조차도 실제로는 서로 다른 변환 함수를 요구할 수 있습니다. 예를 들어 str.lower, str.casefold, 또는 ASCII 호환 인코딩으로 인코딩된 텍스트를 다룰 때는 경우에 따라 bytes.lower가 있습니다.
다른 생성자 패턴
Serhiy Storchaka가 두 가지 다른 생성자 패턴을 제안했습니다:
- 타입 팩토리 방식:
d = TransformDict(str.casefold)(Foo=1)
- 서브클래싱 방식:
class CaseInsensitiveDict(TransformDict): __transform__ = str.casefold d = CaseInsensitiveDict(Foo=1)
두 접근 방식 모두 타당성을 주장할 수는 있지만, 표준 라이브러리의 확립된 관행을 따르지 않으므로 기각되었습니다.
구현
collections 모듈에 대한 패치는 버그 트래커 http://bugs.python.org/issue18986 에서 추적되고 있습니다.
기존 작업
대소문자를 구분하지 않는 딕셔너리는 자주 요청되는 기능입니다:
- http://twistedmatrix.com/documents/current/api/twisted.python.util.InsensitiveDict.html
- https://mail.python.org/pipermail/python-list/2013-May/647243.html
- https://mail.python.org/pipermail/python-list/2005-April/296208.html
- https://mail.python.org/pipermail/python-list/2004-June/241748.html
- http://bugs.python.org/msg197376
- http://stackoverflow.com/a/2082169
- http://stackoverflow.com/a/3296782
- http://code.activestate.com/recipes/66315-case-insensitive-dictionary/
- https://gist.github.com/babakness/3901174
- http://www.wikier.org/blog/key-insensitive-dictionary-in-python
- http://en.sharejs.com/python/14534
- http://www.voidspace.org.uk/python/archive.shtml#caseless
동일성 딕셔너리(identity dict)에 대한 요청도 있었습니다:
- https://mail.python.org/pipermail/python-ideas/2010-May/007235.html
- http://www.gossamer-threads.com/lists/python/python/209527
표준 라이브러리의 여러 모듈이 객체 메모이제이션을 위해 동일성 조회를 사용합니다. 예를 들어 pickle, json, copy, cProfile, doctest, _threading_local 모듈이 그렇습니다.
다른 언어
C# / .Net
.Net에는 사용자 정의 IEqualityComparer를 지정할 수 있는 제네릭 Dictionary 클래스가 있습니다: http://msdn.microsoft.com/en-us/library/xfhwa508.aspx
대소문자를 구분하지 않는 딕셔너리를 작성할 때는 이를 사용하는 것이 권장되는 방법입니다: http://stackoverflow.com/questions/13230414/case-insensitive-access-for-generic-dictionary
Java
Java에는 특화된 CaseInsensitiveMap이 있습니다: http://commons.apache.org/proper/commons-collections/apidocs/org/apache/commons/collections4/map/CaseInsensitiveMap.html
또한 별도의 IdentityHashMap도 있습니다: http://docs.oracle.com/javase/6/docs/api/java/util/IdentityHashMap.html
C++
C++ 표준 템플릿 라이브러리는 해시 함수와 동등성 함수를 사용자가 지정할 수 있는 unordered_map을 제공합니다: http://www.cplusplus.com/reference/unordered_map/unordered_map/
Copyright
This document has been placed in the public domain.