PEP 469 – Python 3로의 dict 반복 코드 마이그레이션
- Author:
- Alyssa Coghlan <ncoghlan at gmail.com>
- Status:
- Withdrawn
- Type:
- Standards Track
- Created:
- 18-Apr-2014
- Python-Version:
- 3.5
- Post-History:
- 18-Apr-2014, 21-Apr-2014
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
Python 3에서는 PEP 3106이 dict 내장 기능의 설계와 전반적인 매핑 API를 변경하여, Python 2의 분리된 리스트 기반 API와 이터레이터 기반 API를 병합된 메모리 효율적인 집합 및 멀티셋 뷰 기반 API로 대체했습니다. 이 새로운 방식의 dict 반복은 새로운 반복 메서드 집합으로 Python 2.7의 dict 형에도 추가되었습니다.
이는 애플리케이션을 전환할 때 Python 3으로 마이그레이션해야 할 수 있는 dict 반복 방식이 이제 3가지 종류라는 의미입니다.
- 변경 가능한 스냅샷으로서의 리스트:
d.items()->list(d.items()) - 이터레이터 객체:
d.iteritems()->iter(d.items()) - 집합 기반 동적 뷰:
d.viewitems()->d.items()
현재 모든 Python 2 dict 반복 코드를 Python 2와 Python 3의 공통 부분집합으로 안정적으로 변환하는 방법에 대해 널리 합의된 모범 사례는 없습니다. 특히 포팅된 코드의 테스트 커버리지가 제한적인 경우에는 더욱 그렇습니다. 이 PEP는 Python 2 반복 API에 접근할 수 있는 다양한 방법을 검토하고, Python 2.6 이상과 Python 3.0 이상의 공통 부분집합을 통해 해당 코드를 Python 3으로 마이그레이션하는 데 사용할 수 있는 선택지를 살펴봅니다.
또한 이 PEP는 이전 버전 지원을 걱정할 필요가 없는 애플리케이션 코드를 결국 Python 3으로 전환할 때, 전환 과정을 용이하게 하기 위해 Python 3.5에 추가할 만한 사항이 있는지 여부도 검토합니다.
PEP 철회
이 PEP의 두 번째 초안을 작성하면서, Python 3.5+를 변경하는 것보다 더 나은 헬퍼 함수를 통해 하이브리드 Python 2/3 매핑 코드의 가독성을 실제로 더 향상할 수 있다는 결론에 도달했습니다. 이제 제가 이 PEP에서 보는 주요 가치는 매핑 반복 코드를 Python 2에서 Python 3으로 마이그레이션하기 위한 권장 접근법을 명확히 기록하고, 두 버전을 모두 지원하는 하이브리드 코드를 작성할 때 가독성과 유지 관리성을 유지하는 방법을 제안하는 데 있습니다.
특히 하이브리드 코드에서는 매핑 반복 메서드를 직접 호출하지 말고 가능한 경우 내장 함수에 의존할 것을 권장합니다. 또한 순수 Python 3 코드에서는 내장 함수와 매핑 메서드의 간단한 조합으로 처리할 수 있지만 Python 2에서 정확히 동일한 의미를 얻으려면 약간 다르게 처리해야 하는 경우를 위한 추가 헬퍼 함수도 사용하십시오.
pylint와 같은 정적 코드 검사기는 하이브리드 코드베이스에서 매핑 반복 메서드를 직접 사용하는 것에 관한 선택적 경고를 제공하도록 확장할 수 있습니다.
매핑 반복 모델
Python 2.7은 dict인스턴스에서 키, 값 및 항목을 추출하는 세 가지 서로 다른 메서드 집합을 제공하며, 이는 dict 형의 공개 메서드 18개 중 9개를 차지합니다.
Python 3에서는 has_key 메서드도 제거되었으므로, 이것이 공개 메서드 11개 중 3개로 정리되었습니다.
변경 가능한 스냅샷으로서의 리스트
이는 세 가지 dict 반복 방식 중 가장 오래된 것이며, 따라서 Python 2에서 d.keys(), d.values() 및 d.items() 메서드로 구현된 방식입니다.
이러한 메서드는 모두 메서드가 호출된 시점의 매핑 상태를 담은 스냅샷인 리스트를 반환합니다. 여기에는 몇 가지 결과가 따릅니다.
- 원본 객체는 스냅샷에 대한 반복에 영향을 주지 않고 자유롭게 변경할 수 있습니다.
- 스냅샷은 원본 객체와 독립적으로 수정할 수 있습니다.
- 스냅샷은 원본 매핑의 크기에 비례하는 메모리를 사용합니다.
Python 3에서 이러한 연산에 의미상 대응하는 것은 list(d.keys()), list(d.values()) 및 list(d.iteritems())입니다.
이터레이터 객체
Python 2.2에서 dict 객체는 당시 새로 도입된 이터레이터 프로토콜을 지원하게 되었으며, 이를 통해 딕셔너리에 저장된 키를 직접 반복할 수 있게 되었습니다. 따라서 딕셔너리의 내용을 한 항목씩 반복하기 위해 리스트를 만들 필요가 없어졌습니다. iter(d)는 키에 대한 이터레이터 객체에 직접 접근할 수 있도록 합니다.
Python 2는 iter(d)와 사실상 동의어인 d.iterkeys() 메서드와 함께 d.itervalues() 및 d.iteritems() 메서드도 제공합니다.
이러한 이터레이터는 기본 객체의 실시간 뷰를 제공하므로, 반복 중에 기본 객체의 키 집합이 변경되면 실패할 수 있습니다.:
>>> d = dict(a=1)
>>> for k in d:
... del d[k]
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration
이 객체들에 대한 반복도 이터레이터의 특성상 한 번만 수행할 수 있습니다. 이터레이터가 소진되면 다시 반복하려면 원래 매핑으로 돌아가야 합니다.
Python 3에서 매핑을 직접 반복하는 방식은 Python 2에서와 동일하게 작동합니다. 메서드 기반 대응물은 없으며, Python 3에서 d.itervalues() 및 d.iteritems()의 의미상 대응물은 iter(d.values()) 및 iter(d.items())입니다.
six 및 future.utils 호환성 모듈은 모두 Python 2와 3에서 효율적인 이터레이터 의미론을 제공하는 iterkeys(), itervalues() 및 iteritems() 헬퍼 함수도 제공합니다.
집합 기반 동적 뷰
Python 3에서 메서드 기반 API로 제공되는 모델은 집합 기반 동적 뷰의 모델입니다(values() 뷰의 경우 기술적으로 멀티셋입니다).
Python 3에서 d.keys(), d.values() 및 d. items()가 반환하는 객체는 Python 2에서처럼 현재 상태의 전체 스냅샷을 생성하는 대신 기본 객체의 현재 상태를 실시간으로 보여 줍니다. 이러한 변경은 많은 상황에서 안전하지만, 직접 반복 API와 마찬가지로 반복 중에 키를 추가하거나 제거하지 않아야 다음 오류가 발생하는 것을 피할 수 있습니다.:
>>> d = dict(a=1)
>>> for k, v in d.items():
... del d[k]
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration
반복 API와 달리 이러한 객체는 이터레이터가 아니라 이터러블입니다. 여러 번 반복할 수 있으며, 반복할 때마다 기본 매핑 전체를 반복합니다.
이러한 의미론은 Python 2.7에서도 d.viewkeys(), d.viewvalues() 및 d.viewitems() 메서드로 사용할 수 있습니다.
future.utils 호환성 모듈은 Python 2.7 또는 Python 3.x에서 실행할 때 viewkeys(), viewvalues() 및 viewitems() 헬퍼 함수도 제공합니다.
Python 3으로 직접 마이그레이션하기
2to3 마이그레이션 도구는 위에서 설명한 의미상 대응물에 따라 Python 3으로의 직접 마이그레이션을 처리합니다.
d.keys()->list(d.keys())d.values()->list(d.values())d.items()->list(d.items())d.iterkeys()->iter(d.keys())d.itervalues()->iter(d.values())d.iteritems()->iter(d.items())d.viewkeys()->d.keys()d.viewvalues()->d.values()d.viewitems()->d.items()
반복을 위한 서로 다른 매핑 메서드는 9개가 아니라 이제 3개의 뷰 메서드만 있으며, 이 메서드들은 관련된 두 내장 함수와 간단한 방식으로 결합되어 Python 2.7에서 dict 메서드로 사용할 수 있는 모든 동작을 지원합니다.
많은 경우 d.keys()는 단순히 d로 대체할 수 있지만, 2to3 마이그레이션 도구는 이러한 대체를 시도하지 않습니다.
2to3 마이그레이션 도구는 이러한 객체에 대한 참조가 바인딩된 메서드 또는 바인딩되지 않은 메서드인 경우의 마이그레이션에 대해 자동 지원을 제공하지 않으며, API가 즉시 호출되는 경우에만 변환을 자동화합니다.
Python 2와 3의 공통 부분집합으로 마이그레이션하기
Python 2와 3의 공통 부분집합으로 마이그레이션할 때는 위 변환이 일반적으로 적절하지 않습니다. 이러한 변환은 모두 Python 2에서 불필요한 리스트를 생성하거나, 적어도 일부 경우에 예상과 다른 의미론을 갖게 하거나, 두 가지 모두를 초래하기 때문입니다.
Python 2와 3의 공통 부분집합에서 실행되는 대부분의 코드는 최소한 Python 2.6까지 지원하므로, 매핑 반복 작업을 변환할 때 현재 권장되는 방식은 매핑 값과 매핑 항목 튜플을 효율적으로 반복하기 위한 두 헬퍼 함수에 의존합니다.
d.keys()->list(d)d.values()->list(itervalues(d))d.items()->list(iteritems(d))d.iterkeys()->iter(d)d.itervalues()->itervalues(d)d.iteritems()->iteritems(d)
six 및 future.utils는 itervalues() 및 iteritems()에 대한 적절한 정의를 제공하며, iterkeys()에 대한 정의도 사실상 중복되지만 함께 제공합니다. 사용자 정의 호환성 모듈에서 이러한 함수의 정의를 직접 만드는 작업도 비교적 간단합니다.:
try:
dict.iteritems
except AttributeError:
# Python 3
def itervalues(d):
return iter(d.values())
def iteritems(d):
return iter(d.items())
else:
# Python 2
def itervalues(d):
return d.itervalues()
def iteritems(d):
return d.iteritems()
현재 가독성이 가장 크게 저하되는 경우는 Python에서 기본값이었던 리스트 기반 스냅샷을 실제로 필요로 하는코드를 변환할 때 발생합니다.
2. 이러한 가독성 저하는 다음도 제공함으로써 완화할 수 있습니다.
listvalues 및 listitems 도우미 함수를 제공하면, 영향을 받는 변환을 다음과 같이 단순화할 수 있습니다:
d.values()->listvalues(d)d.items()->listitems(d)
이에 대응하는 호환성 함수 정의는 이터레이터에 대응하는 함수 정의만큼 간단합니다.:
try:
dict.iteritems
except AttributeError:
# Python 3
def listvalues(d):
return list(d.values())
def listitems(d):
return list(d.items())
else:
# Python 2
def listvalues(d):
return d.values()
def listitems(d):
return d.items()
이렇게 확장된 호환성 함수 집합을 사용하면 Python 2 코드는 다음과 같은 “관용적인” 혼합 2/3 코드로 변환됩니다:
d.keys()->list(d)d.values()->listvalues(d)d.items()->listitems(d)d.iterkeys()->iter(d)d.itervalues()->itervalues(d)d.iteritems()->iteritems(d)
이는 매핑 메서드와 내장 함수를 직접 사용하는 관용적인 순수 Python 3 코드와 가독성 측면에서 잘 비교됩니다:
d.keys()->list(d)d.values()->list(d.values())d.items()->list(d.items())d.iterkeys()->iter(d)d.itervalues()->iter(d.values())d.iteritems()->iter(d.items())
이 접근 방식을 사용할 때 하이브리드 코드가 매핑 메서드를 직접 절대로 호출하지 않는다는 점도 주목할 만합니다. Python 2와 3에서 정확히 동일한 의미론을 보장하기 위해, 대신 항상 내장 함수나 도우미 함수 중 하나를 호출합니다.
Python 3에서 Python 2.7과의 공통 부분집합으로 마이그레이션하기
현재 대부분의 마이그레이션은 Python 2에서 Python 3으로 직접 진행되거나 Python 2와 Python 3의 공통 부분집합으로 진행되지만, Python 3에서 시작한 후 사용자 요구에 따라 Python 2 지원을 추가하거나 Python 3에서 아직 사용할 수 없는 Python 2 라이브러리에 접근하기 위해 나중에 Python 2 지원을 추가하는 최신 프로젝트의 마이그레이션도 일부 있습니다. 이러한 라이브러리를 Python 3으로 포팅하거나 Python 3 호환 대체물을 만드는 일은 간단한 작업이 아니기 때문입니다.
이러한 경우에는 Python 2.7 호환성으로 충분한 경우가 많으며, future.utils가 제공하는 2.7 이상 전용 뷰 기반 도우미 함수를 사용하면 Python 3의 매핑 뷰 메서드에 대한 단순한 접근을 Python 2.7과 Python 3 모두와 호환되는 코드로 대체할 수 있습니다(참고로 이는 변환 전 왼쪽에 Python 3 코드가 있는 PEP의 유일한 마이그레이션 표입니다):
d.keys()->viewkeys(d)d.values()->viewvalues(d)d.items()->viewitems(d)list(d.keys())->list(d)list(d.values())->listvalues(d)list(d.items())->listitems(d)iter(d.keys())->iter(d)iter(d.values())->itervalues(d)iter(d.items())->iteritems(d)
Python 2에서 공통 부분집합으로 마이그레이션할 때와 마찬가지로, 하이브리드 코드는 결국 매핑 메서드를 직접 호출하지 않고 내장 함수와 헬퍼 메서드만 호출하며, 후자는 Python 2와 Python 3 간의 의미적 차이를 처리한다는 점에 유의하십시오.
Python 3.5+에 대한 가능한 변경 사항입니다.
기존 Python 2 코드의 Python 3으로의 마이그레이션을 지원하기 위해 제안된 주요 방안은 Python 3 매핑 API에 대체 이터레이션 API의 일부 또는 전부를 복원하는 것입니다. 특히 이 PEP의 초기 초안에서는 Python 2와 Python 3.5+의 공통 부분집합으로 마이그레이션할 때 다음 변환이 가능하도록 제안했습니다.
d.keys()->list(d)d.values()->list(d.itervalues())d.items()->list(d.iteritems())d.iterkeys()->d.iterkeys()d.itervalues()->d.itervalues()d.iteritems()->d.iteritems()
이러한 메서드를 즉시 사용 중단으로 지정하는 것과 더불어, dir() 함수에서 해당 메서드를 숨기거나 함수의 사용 중단을 pydoc이 인식하도록 하는 방법을 정의하는 것까지, 이러한 메서드를 복원함으로써 Python 3에 추가되는 언어 복잡성을 완화할 수 있는 방안이 있었습니다.
그러나 실제로 리스트 출력이 필요한 경우에는 적절하게 정의된 헬퍼 함수보다 이 제안의 최종 결과가 오히려 가독성이 떨어지며, 이터레이터 버전의 함수 형식과 메서드 형식은 가독성 측면에서 거의 동등합니다.
따라서 제가 중요한 무언가를 놓친 것이 아니라면, 바로 사용할 수 있는 listvalues() 및 listitems() 헬퍼 함수는 Python 3.5+ 매핑 API에 무엇을 되돌려 추가할 수 있는지보다 하이브리드 코드의 가독성을 더 크게 향상할 것으로 보이며, Python 3 자체의 복잡성에도 장기적인 영향을 주지 않을 것입니다.
토론입니다.
Python 3으로의 마이그레이션이 시작된 지 5년이 지난 지금도 딕셔너리 API 변경을 마이그레이션의 중요한 장벽으로 여기는 사용자가 있다는 사실은, 이전에 권장된 접근 방식에 문제가 있음을 시사합니다. 이 PEP는 이러한 문제를 살펴보고, 이전의 조언이(그러한 조언이 존재했다면) 문제가 될 수 있는 경우를 분리해 보려고 합니다.
제 평가(대체로 Twisted 개발자들의 피드백에 기반함)는 하이브리드 코드에서 d.keys(), d.values(), d.items()를 사용하려고 할 때 문제가 발생할 가능성이 가장 높다는 것입니다. 표면적으로는 의미적 차이를 무시해도 안전한 경우가 있을 것처럼 보이지만, 실제로는 “변경 가능한 스냅샷”에서 “동적 뷰”로의 변경이 충분히 중요하므로, 하이브리드 코드에서는 리스트 의미 체계나 이터레이터 의미 체계 중 하나만 사용하도록 강제하고 뷰 의미 체계의 사용은 순수 Python 3 코드에 맡기는 편이 나을 가능성이 높습니다.
이 접근 방식은 Python 2와 Python 3의 공통 부분집합을 대상으로 하는 코드 현대화 스크립트에서 자동화할 수 있을 만큼 단순하고 안전한 규칙도 만들며, 순수 Python 3 코드를 대상으로 할 때 2to3이 이를 자동으로 변환하는 것과 같습니다.
감사의 말입니다.
이 아이디어와 여러 다른 주제에 대해 매우 열띤 토론을 벌여 주신 PyCon의 Twisted 스프린트 테이블 참가자분들께 감사드리며, 상황이 조금 지나치게 과열되었을 때 중재자 역할을 해 주신 Hynek Schlawack에게 특히 감사드립니다 :)
또한 이메일로 의견을 보내 주신 JP Calderone과 Itamar Turner-Trauring, 그리고 이 PEP의 초기 버전에 대한 python-dev review 에 참여해 주신 분들께도 감사드립니다.
Copyright
This document has been placed in the public domain.