PEP 618 – zip에 선택적 길이 검사 추가
- Author:
- Brandt Bucher <brandt at python.org>
- Sponsor:
- Antoine Pitrou <antoine at python.org>
- BDFL-Delegate:
- Guido van Rossum <guido at python.org>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 01-May-2020
- Python-Version:
- 3.10
- Post-History:
- 01-May-2020, 10-May-2020, 16-Jun-2020
- Resolution:
- Python-Dev message
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 내장 zip에 선택적인 strict 불리언 키워드 매개변수를 추가할 것을 제안합니다. 활성화하면 인자 중 하나가 다른 인자보다 먼저 소진될 때 ValueError가 발생합니다.
동기
저자의 개인적인 경험과 표준 라이브러리에 대한 survey of the standard library를 통해, 많은(대부분은 아니더라도) zip 사용이 길이가 반드시 같은 이터러블을 포함한다는 사실이 분명합니다. 때로는 주변 코드의 맥락을 통해 이 불변 조건이 참임을 입증할 수 있지만, 흔히 zip으로 묶을 데이터는 호출자에게서 전달되거나, 별도로 가져오거나, 어떤 방식으로든 생성됩니다. 이러한 경우 zip의 기본 동작으로 인해 잘못된 리팩터링이나 논리 오류가 데이터를 아무런 알림 없이 쉽게 잃게 만들 수 있습니다. 이러한 버그는 진단하기 어려울 뿐만 아니라, 애초에 발견하기조차 어렵습니다.
이것이 문제가 될 수 있는 간단한 사례를 쉽게 생각해 낼 수 있습니다. 예를 들어 items가 시퀀스일 때는 다음 코드가 정상적으로 작동할 수 있지만, 호출자가 items를 소비 가능한 이터레이터로 리팩터링하면 아무런 알림 없이 짧고 서로 맞지 않는 결과를 생성하기 시작할 수 있습니다:
def apply_calculations(items):
transformed = transform(items)
for i, t in zip(items, transformed):
yield calculate(i, t)
zip이 일반적으로 사용되는 다른 방식도 여러 가지 있습니다. 관용적인 기법은 특히 취약한데, 코드가 어떻게 작동하는지 완전히 이해하지 못한 사용자가 이를 사용하는 경우가 많기 때문입니다. 한 가지 예는 중첩된 이터러블을 느리게 “압축 해제”하거나 “전치”하기 위해 zip에 언패킹을 적용하는 것입니다:
>>> x = [[1, 2, 3], ["one" "two" "three"]]
>>> xt = list(zip(*x))
또 다른 예는 데이터를 같은 크기의 그룹으로 “청크화”하는 것입니다:
>>> n = 3
>>> x = range(n ** 2),
>>> xn = list(zip(*[iter(x)] * n))
첫 번째 경우에는 직사각형이 아닌 데이터가 대개 논리 오류입니다. 두 번째 경우에도 길이가 n의 배수가 아닌 데이터는 흔히 오류입니다. 그러나 이 두 관용구는 모두 잘못된 입력의 끝부분 항목을 아무런 알림 없이 생략합니다.
아마도 가장 설득력 있는 사례로, 표준 라이브러리의 ast 모듈에서 zip을 사용한 결과 literal_eval 함수에 silently dropped parts of malformed nodes하는 버그가 발생했습니다.:
>>> from ast import Constant, Dict, literal_eval
>>> nasty_dict = Dict(keys=[Constant(None)], values=[])
>>> literal_eval(nasty_dict) # Like eval("{None: }")
{}
실제로 작성자는 Python의 표준 라이브러리와 도구에서 이 새로운 기능을 즉시 활성화하는 것이 적절한 다른 호출 지점을 수십 개 세었습니다.
근거
일부 비평가들은 상수 불리언 스위치가 “코드 냄새”이거나 Python의 설계 철학에 어긋난다고 주장합니다. 그러나 Python에는 현재 일반적으로 컴파일 시 상수로 호출되는 불리언 키워드 매개변수를 내장 함수에 사용하는 사례가 여러 가지 있습니다.
compile(..., dont_inherit=True)open(..., closefd=False)print(..., flush=True)sorted(..., reverse=True)
표준 라이브러리에는 이 밖에도 훨씬 많은 사례가 있습니다.
이 새로운 매개변수에 대한 아이디어와 이름은 Ram Rachum이 처음 제안했습니다. 이 스레드에는 100개가 넘는 답변이 달렸으며, 대안인 “equal”도 비슷한 수준의 지지를 받았습니다.
작성자는 두 선택지 중 어느 쪽도 강하게 선호하지 않지만, “equal equals”는 글에서 약간 어색합니다. 또한 zip으로 묶인 항목들 사이의 “동등성” 개념을 (잘못) 암시할 수도 있습니다.:
>>> z = zip([2.0, 4.0, 6.0], [2, 4, 8], equal=True)
명세
내장 zip을 키워드 전용 인자 strict=True와 함께 호출하면, 인자들이 서로 다른 길이로 소진될 경우 결과 이터레이터가 ValueError를 발생시킵니다. 이 오류는 현재 반복이 정상적으로 중지되는 지점에서 발생합니다.
하위 호환성
이 변경 사항은 완전히 하위 호환됩니다. zip은 현재 키워드 인자를 받지 않으며, strict를 생략했을 때의 “non-strict” 기본 동작도 변경되지 않습니다.
참조 구현
작성자는 C 구현을 작성했습니다.
대략적인 Python 번역은 다음과 같습니다.:
def zip(*iterables, strict=False):
if not iterables:
return
iterators = tuple(iter(iterable) for iterable in iterables)
try:
while True:
items = []
for iterator in iterators:
items.append(next(iterator))
yield tuple(items)
except StopIteration:
if not strict:
return
if items:
i = len(items)
plural = " " if i == 1 else "s 1-"
msg = f"zip() argument {i+1} is shorter than argument{plural}{i}"
raise ValueError(msg)
sentinel = object()
for i, iterator in enumerate(iterators[1:], 1):
if next(iterator, sentinel) is not sentinel:
plural = " " if i == 1 else "s 1-"
msg = f"zip() argument {i+1} is longer than argument{plural}{i}"
raise ValueError(msg)
거부된 아이디어
itertools.zip_strict 추가
이는 Python-Ideas 메일링 리스트에서 가장 많은 지지를 받은 대안이므로, 여기에서 어느 정도 자세히 논의할 가치가 있습니다. 이 방식에는 배제할 만한 결함이 없으며, 이 PEP가 거부되더라도 대체 수단으로 충분히 잘 작동할 수 있습니다.
이러한 점을 염두에 두고, 이 절에서는 zip에 선택적 매개변수를 추가하는 것이 더 작은 변경이며 궁극적으로 이 PEP의 동기가 된 문제를 더 잘 해결하는 이유를 설명하고자 합니다.
선례
이 대안을 이끄는 동기의 상당 부분은 zip_longest가 이미 itertools에 존재한다는 점에서 비롯된 것으로 보입니다. 그러나 zip_longest는 여러 면에서 훨씬 더 복잡하고 특수화된 유틸리티입니다. 누락된 값을 채우는 책임을 맡는데, 다른 두 변형은 어느 것도 신경 쓸 필요가 없는 작업입니다.
zip과 zip_longest가 모두 itertools에서 또는 내장 함수로 나란히 존재한다면, 같은 위치에 zip_strict를 추가하자는 주장은 실제로 훨씬 더 강력해질 것입니다. 그러나 새로운 “strict” 변형은 자체 내장 함수가 되기 위한 높은 기준을 충족하지 않으면서도, 인터페이스와 동작 면에서 zip_longest보다 zip에 much 더 가깝습니다. 이러한 상황에서는 zip이 이 새로운 옵션을 기존 위치에서 확장하는 것이 가장 자연스러워 보입니다.
사용 편의성
zip이 이러한 종류의 버그를 방지할 수 있다면, 사용자는 이 기능을 사용하여 호출 지점에서 검사를 활성화하기가 훨씬 간단해집니다. 이를 내장 유틸리티를 그대로 대체하는 것을 가져오는 경우와 비교해 보십시오. “항상” 참이어야 하는 까다로운 조건을 검사하기에는 다소 과한 느낌이 듭니다.
일부는 표준 라이브러리에 묻혀 있는 새로운 함수가 내장 함수 자체의 키워드 매개변수보다 어떻게든 더 “발견하기 쉽다”고 주장하기도 했습니다. 작성자는 이 평가에 동의하지 않습니다.
유지 관리 비용
사용 편의성을 개선할 때 구현은 부차적인 고려 사항에 불과해야 하지만, 새로운 유틸리티를 추가하는 일이 기존 유틸리티를 수정하는 것보다 훨씬 더 복잡하다는 점을 인식하는 것이 중요합니다. 이 PEP에 포함된 CPython 구현은 단순하며 기본 zip의 동작에 측정 가능한 성능 영향을 주지 않지만, itertools에 완전히 새로운 유틸리티를 추가하려면 다음 중 하나가 필요합니다.
zip_longest가 이미 하는 것처럼 기존zip로직의 상당 부분을 복제합니다.- 공통 구현 또는 상속된 구현을 공유하도록
zip,zip_longest또는 둘 다를 크게 리팩터링합니다(성능에 영향을 줄 수 있습니다).
전환할 수 있는 여러 “모드” 추가
세 가지 이상의 모드를 사용할 것으로 예상하는 경우에만 이 옵션이 이진 플래그보다 더 합리적입니다. 이러한 열거형 또는 상수 모드에서 “명백한” 세 가지 선택지는 “shortest”(현재 zip의 동작), “strict”(제안된 동작), “longest”(itertools.zip_longest의 동작)일 것입니다.
그러나 현재 기본 동작과 제안된 “strict” 모드 이외의 동작을 추가하는 것은 추가적인 복잡성을 감수할 만큼 가치가 있어 보이지 않습니다. 가장 명확한 후보인 “longest”는 새로운 fillvalue 매개변수를 필요로 합니다(다른 두 모드에서는 의미가 없습니다). 이 모드는 이미 itertools.zip_longest로 완벽하게 처리할 수 있으며, 이를 추가하면 같은 작업을 수행하는 두 가지 방법이 생깁니다. 무엇이 “분명한” 선택인지 명확하지 않습니다. 내장 zip의 mode 매개변수인지, 아니면 itertools에 오랫동안 존재해 온 동명의 유틸리티인지 알 수 없습니다.
zip 타입에 메서드 또는 대체 생성자 추가
다음 두 가지 옵션을 고려하십시오. 두 옵션 모두 이미 제안된 것입니다.:
>>> zm = zip(*iters).strict()
>>> zd = zip.strict(*iters)
어느 쪽이 성공하고 다른 쪽이 어떻게 실패할지는 명확하지 않습니다. zip.strict를 메서드로 구현하면 zm은 성공하지만, zd는 다음과 같이 여러 혼란스러운 방식 중 하나로 실패합니다.
- 튜플로 감싸지 않은 결과를 산출합니다(
iters에 항목이 하나만 포함된 경우에는zip이터레이터입니다). - 인자 유형이 잘못되면
TypeError를 발생시킵니다(iters에 항목이 하나만 포함된 경우에는zip이터레이터가 아닙니다). - 인자 개수가 잘못되면
TypeError를 발생시킵니다(그 외의 경우).
zip.strict를 classmethod 또는 staticmethod로 구현하면 zd는 성공하고 zm은 아무것도 산출하지 않은 채 조용히 종료됩니다(바로 이것이 애초에 피하려는 문제입니다).
CPython의 실제 zip타입이 현재 문서화되지 않은 구현 세부 사항이라는 사실 때문에 이 제안은 더욱 복잡해집니다. 이는 위 동작 중 하나를 선택하면 사실상 현재 구현을 앞으로 “고정”하게 된다는 의미입니다(또는 적어도 현재 구현을 에뮬레이션해야 합니다).
zip의 기본 동작 변경
zip의 기본 동작에는 “잘못된” 점이 없습니다. 크기가 서로 다른 입력을 처리하는 올바른 방법인 경우가 많기 때문입니다. 예를 들어 무한 이터레이터를 다룰 때 매우 유용합니다.
“추가” 꼬리 데이터가 여전히 필요한 경우를 처리하기 위해 이미 itertools.zip_longest가 존재합니다.
남은 항목을 처리할 콜백 허용
사용자가 필요로 할 수 있는 거의 모든 작업을 수행할 수 있지만, 이 해결책은 길이가 일치하지 않는 경우를 거부하는 것과 같은 더 일반적인 사례의 처리를 불필요하게 복잡하고 불분명하게 만듭니다.
AssertionError발생
API의 일부로 AssertionError를 발생시키는 내장 함수나 타입은 없습니다. 또한 공식 문서는 전체 내용이 다음과 같이 간단히 적혀 있습니다.
assert문이 실패할 때 발생합니다.
이 기능은 Python의 assert문과 아무런 관련이 없으므로 여기서 AssertionError를 발생시키는 것은 부적절합니다. 최적화 모드에서 비활성화되는 검사(assert문과 같은)를 원하는 사용자는 대신 strict=__debug__를 사용할 수 있습니다.
map에 유사한 기능 추가
이 PEP에서는 map에 대한 변경을 제안하지 않습니다. 여러 이터러블 인자와 함께 map을 사용하는 경우가 매우 드물기 때문입니다. 그러나 이 PEP의 결정은 향후 이러한 논의가 이루어질 경우를 위한 선례로 사용될 것입니다.
거부된다면 이 기능은 현실적으로 더 추진할 가치가 없습니다. 수락된다면 map에 대한 이러한 변경에는 자체 PEP가 필요하지 않습니다(모든 개선 사항과 마찬가지로 그 유용성은 신중하게 고려해야 하지만 그렇습니다). 일관성을 위해 zip에 대해 여기서 논의한 것과 동일한 API와 의미론을 따라야 합니다.
아무것도 하지 않기
이 선택지는 아마도 가장 매력적이지 않은 선택지입니다.
묵시적으로 잘린 데이터는 특히 골치 아픈 버그 유형이며, 이를 올바르게 처리하는 견고한 해결책을 손으로 직접 작성하는 일은 간단하지 않습니다. Python 자체의 표준 라이브러리에서 얻은 실제 동기 부여 사례는 이 기능이 피하려는 유형의 함정에 빠지기가 매우 쉽다는 증거입니다.
참고 자료
예제
Note
이 목록은 전부를 나열한 것은 아닙니다.
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/_pydecimal.py#L3394
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/_pydecimal.py#L3418
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/_pydecimal.py#L3435
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/ast.py#L94-L95
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/ast.py#L1184
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/ast.py#L1275
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/ast.py#L1363
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/ast.py#L1391
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/copy.py#L217
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/csv.py#L142
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/dis.py#L462
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/filecmp.py#L142
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/filecmp.py#L143
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/inspect.py#L1440
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/inspect.py#L2095
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/os.py#L510
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/plistlib.py#L577
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/tarfile.py#L1317
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/tarfile.py#L1323
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/tarfile.py#L1339
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/turtle.py#L3015
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/turtle.py#L3071
- https://github.com/python/cpython/blob/27c0d9b54abaa4112d5a317b8aa78b39ad60a808/Lib/turtle.py#L3901
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.