PEP 469 – dict 반복 코드의 Python 3 마이그레이션
- 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와 3의 공통 부분집합으로 안정적으로 변환하는 방법에 대해 현재 널리 합의된 모범 사례는 없으며, 포팅된 코드의 테스트 커버리지가 제한적인 경우에는 더욱 그렇습니다. 이 PEP는 Python 2 반복 API에 접근하는 다양한 방식을 검토하고, Python 2.6+와 Python 3.0+의 공통 부분집합을 거쳐 해당 코드를 Python 3으로 마이그레이션하는 데 사용할 수 있는 선택지를 살펴봅니다.
또한 이 PEP는 이전 버전 지원을 신경 쓸 필요가 없는 애플리케이션 코드가 마침내 Python 3으로 넘어갈 때 전환 과정을 쉽게 해 줄 수 있도록 Python 3.5에 추가할 만한 기능이 있는지도 검토합니다.
PEP 철회
이 PEP의 두 번째 초안을 작성하면서, 저는 Python 2/3 혼용 매핑 코드의 가독성을 가장 잘 높이는 방법이 사실 Python 3.5+를 변경하는 것이 아니라 더 나은 헬퍼 함수를 제공하는 것이라는 결론에 도달했습니다. 이제 제가 보는 이 PEP의 주된 가치는 Python 2에서 Python 3으로 매핑 반복 코드를 마이그레이션하는 권장 접근 방식을 명확하게 기록하고, 두 버전을 모두 지원하는 혼용 코드를 작성할 때 가독성과 유지 보수성을 지키는 방법을 제안하는 데 있습니다.
특히 저는 혼용 코드에서 매핑 반복 메서드를 직접 호출하지 말고, 가능한 경우 내장 함수에 의존하며, 순수 Python 3 코드라면 내장 함수와 매핑 메서드의 단순한 조합이 되겠지만 Python 2에서 정확히 같은 의미를 얻으려면 약간 다르게 처리해야 하는 경우에는 몇 가지 추가 헬퍼 함수에 의존할 것을 권장합니다.
pylint 같은 정적 코드 검사기를 확장하여, 혼용 코드 베이스에서 매핑 반복 메서드를 직접 사용하는 것에 대한 선택적 경고를 제공하도록 할 수도 있습니다.
매핑 반복 모델
Python 2.7은 dict 인스턴스에서 키, 값, 항목을 추출하는 서로 다른 세 가지 메서드 집합을 제공하며, 이는 dict 타입의 공개 메서드 18개 중 9개를 차지합니다.
Python 3에서는 이것이 공개 메서드 11개 중 단 3개로 정리되었습니다(has_key 메서드도 제거되었기 때문입니다).
변경 가능한 스냅숏으로서의 리스트
이는 세 가지 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 호환성 모듈도 모두 iterkeys(), itervalues(), iteritems() 헬퍼 함수를 제공합니다. 이 함수들은 Python 2와 3 모두에서 효율적인 이터레이터 의미 체계를 제공합니다.
집합 기반 동적 뷰
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개뿐입니다. 이 메서드들은 관련 내장 함수 2개와 간단하게 조합되어 Python 2.7의 dict 메서드가 제공하는 모든 동작을 다룹니다.
많은 경우 d.keys()를 d로만 바꿔도 되지만, 2to3 마이그레이션 도구는 이렇게 바꾸려고 시도하지 않습니다.
2to3 마이그레이션 도구는 이 객체들을 바운드 메서드나 언바운드 메서드로 참조하는 코드의 마이그레이션도 자동으로 지원하지 않습니다. API를 즉시 호출하는 경우에만 자동으로 변환합니다.
Python 2와 3의 공통 부분 집합으로 마이그레이션하기
Python 2와 3의 공통 부분 집합으로 마이그레이션할 때는 일반적으로 위의 변환이 적절하지 않습니다. 이 변환들은 모두 Python 2에서 불필요한 리스트를 만들거나, 적어도 일부 경우에 예상과 다른 의미 체계를 가지거나, 두 문제를 모두 일으키기 때문입니다.
Python 2와 3의 공통 부분 집합에서 실행되는 코드는 대부분 적어도 Python 2.6까지 지원하므로, 매핑 이터레이션 연산을 변환할 때 현재 권장하는 방법은 매핑 값과 매핑 항목 튜플을 효율적으로 이터레이션하는 헬퍼 함수 2개를 사용하는 것입니다.
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 3으로 포팅하거나 Python 3 호환 대체품을 만드는 일은 결코 간단하지 않습니다).
이러한 경우에는 Python 2.7 호환성만으로 충분할 때가 많으며, future.utils가 제공하는 2.7 이상 전용의 뷰 기반 헬퍼 함수를 사용하면 Python 3 매핑 뷰 메서드에 직접 접근하는 부분을 Python 2.7과 Python 3 모두와 호환되는 코드로 바꿀 수 있습니다(참고로, 이 PEP에서 변환의 왼쪽에 Python 3 코드가 있는 마이그레이션 표는 이것 하나뿐입니다):
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로 마이그레이션하는 데 도움이 될 수 있도록 제시된 주요 제안은 대체 이터레이션 API의 일부 또는 전부를 Python 3 매핑 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()
이러한 메서드를 복원하면 Python 3의 언어 복잡성이 늘어나는데, 이를 완화할 수 있는 방안으로는 해당 메서드를 즉시 폐지 예정(deprecated)으로 지정하는 것과 dir() 함수에서 숨기는 것(또는 더 나아가 pydoc이 함수의 폐지 예정 여부를 인식하도록 하는 방법을 정의하는 것)이 있었습니다.
그러나 실제로 리스트 출력이 필요한 경우에는 그 제안의 최종 결과가 적절히 정의된 헬퍼 함수보다 오히려 가독성이 떨어지며, 이터레이터 버전의 경우 함수 형태와 메서드 형태는 가독성 측면에서 사실상 동등합니다.
따라서 제가 중요한 무언가를 놓치지 않았다면, 바로 사용할 수 있는 listvalues() 및 listitems() 헬퍼 함수가 Python 3.5+ 매핑 API에 다시 추가할 수 있는 그 어떤 것보다도 하이브리드 코드의 가독성을 더 높여 줄 것으로 보이며, Python 3 자체의 복잡성에도 장기적인 영향을 주지 않을 것입니다.
논의
Python 3 마이그레이션이 시작된 지 5년이 지난 지금도 dict API 변경을 마이그레이션의 중대한 장벽으로 여기는 사용자가 있다는 사실은 이전에 권장된 접근 방식에 문제가 있음을 시사합니다. 이 PEP는 그러한 문제를 살펴보고, 이전의 조언(그런 것이 있었다면)이 문제가 될 수 있는 경우를 가려내고자 합니다.
저의 평가(주로 Twisted 개발자들의 피드백에 기반함)로는, 하이브리드 코드에서 d.keys(), d.values(), d.items()를 사용하려고 할 때 문제가 발생할 가능성이 가장 높습니다. 겉보기에는 의미 차이를 무시해도 안전한 경우가 있을 것처럼 보이지만, 실제로는 “변경 가능한 스냅숏”에서 “동적 뷰”로의 변화가 상당히 크기 때문에, 하이브리드 코드에서는 리스트 의미나 이터레이터 의미 중 하나를 강제로 사용하게 하고 뷰 의미의 사용은 순수 Python 3 코드에 맡기는 편이 더 나을 것입니다.
또한 이 접근 방식은 충분히 단순하고 안전한 규칙을 만들어 내므로, 순수 Python 3 코드를 대상으로 할 때 2to3가 이를 자동으로 변환하는 것처럼, Python 2와 Python 3의 공통 부분집합을 대상으로 하는 코드 현대화 스크립트에서도 이 규칙을 자동화할 수 있을 것입니다.
감사의 글
이 아이디어(와 여러 다른 주제)에 대해 매우 열띤 토론을 벌여 준 PyCon의 Twisted 스프린트 테이블 참석자 여러분께 감사드리며, 특히 논의가 조금 지나치게 과열되었을 때 중재자 역할을 해 준 Hynek Schlawack에게 감사드립니다 :)
이메일로 피드백을 준 JP Calderone과 Itamar Turner-Trauring, 그리고 이 PEP 초기 버전의 python-dev 리뷰에 참여한 분들께도 감사드립니다.
Copyright
This document has been placed in the public domain.