[Draft] PEP 798 - Unpacking in Comprehensions

원문 링크: PEP 798 - Unpacking in Comprehensions

상태: Draft 유형: Standards Track 작성일: 19-Jul-2025

PEP 798 – Comprehension 내 언패킹 (Unpacking)

개요 (Abstract)

이 PEP는 list, set, dictionary Comprehension (컴프리헨션) 및 generator expression (제너레이터 표현식)을 확장하여 표현식 시작 부분에 언패킹(Unpacking) 표기법(***)을 허용할 것을 제안합니다. 이는 임의의 수의 iterable을 하나의 list, set 또는 generator로, 또는 임의의 수의 딕셔너리를 하나의 딕셔너리로 결합하는 간결한 방법을 제공합니다.

예시는 다음과 같습니다:

[*it for it in its] # 'its' 내의 iterable들을 연결한 list
{*it for it in its} # 'its' 내의 iterable들을 합집합(union)한 set
{**d for d in dicts} # 'dicts' 내의 딕셔너리들을 조합한 dict
(*it for it in its) # 'its' 내의 iterable들을 연결한 generator

동기 (Motivation)

PEP 448에 도입된 확장 언패킹 표기법(***)은 몇 개의 iterable 또는 딕셔너리를 쉽게 결합할 수 있도록 합니다.

[*it1, *it2, *it3] # 세 개의 iterable을 연결한 list
{*it1, *it2, *it3} # 세 개의 iterable을 합집합(union)한 set
{**dict1, **dict2, **dict3} # 세 개의 딕셔너리를 조합한 dict

하지만 임의의 수의 iterable을 유사하게 결합하려는 경우에는 동일한 방식으로 언패킹을 사용할 수 없습니다.

여러 iterable을 결합하는 몇 가지 기존 방법이 있습니다. 예를 들어, 명시적인 루프 구조와 내장된 결합 방법을 사용할 수 있습니다:

new_list = []
for it in its:
    new_list.extend(it)

new_set = set()
for it in its:
    new_set.update(it)

new_dict = {}
for d in dicts:
    new_dict.update(d)

def new_generator():
    for it in its:
        yield from it

또는 두 개의 루프를 사용하는 Comprehension으로 더 간결하게 표현할 수 있습니다:

[x for it in its for x in it]
{x for it in its for x in it}
{key: value for d in dicts for key, value in d.items()}
(x for it in its for x in it)

itertools.chain 또는 itertools.chain.from_iterable을 사용할 수도 있습니다:

list(itertools.chain(*its))
set(itertools.chain(*its))
dict(itertools.chain(*(d.items() for d in dicts)))
itertools.chain(*its)
list(itertools.chain.from_iterable(its))
set(itertools.chain.from_iterable(its))
dict(itertools.chain.from_iterable(d.items() for d in dicts))
itertools.chain.from_iterable(its)

또는 generator를 제외한 모든 경우에 functools.reduce를 사용할 수 있습니다:

functools.reduce(operator.iconcat, its, (new_list := []))
functools.reduce(operator.ior, its, (new_set := set()))
functools.reduce(operator.ior, its, (new_dict := {}))

이 PEP는 Comprehension 내에서 언패킹 연산을 추가적인 대안으로 허용할 것을 제안합니다.

[*it for it in its] # 'its' 내의 iterable들을 연결한 list
{*it for it in its} # 'its' 내의 iterable들을 합집합(union)한 set
{**d for d in dicts} # 'dicts' 내의 딕셔너리들을 조합한 dict
(*it for it in its) # 'its' 내의 iterable들을 연결한 generator

이 제안은 비동기 Comprehension 및 generator expression에도 확장되어, 예를 들어 (*ait async for ait in aits())(x async for ait in aits() for x in ait)와 동일합니다.

근거 (Rationale)

iterable 객체들을 하나의 더 큰 객체로 결합하는 것은 흔한 작업입니다. 예를 들어, 리스트의 리스트를 평탄화(flattening)하는 방법에 대한 StackOverflow 게시물은 460만 회 조회되었습니다. 이처럼 흔한 작업임에도 불구하고, 현재 간결하게 수행할 수 있는 옵션들은 간접적인 수준을 요구하여 결과 코드를 읽고 이해하기 어렵게 만들 수 있습니다.

제안된 표기법은 간결하며(보조 변수의 사용과 반복을 피함), Comprehension과 언패킹 표기법 모두에 익숙한 프로그래머에게 직관적이고 친숙할 것으로 예상됩니다. 표준 라이브러리의 코드를 제안된 구문으로 더 명확하고 간결하게 다시 작성할 수 있는 예시는 Code Examples 섹션을 참조하십시오.

이 제안은 부분적으로 Python 프로그래밍 수업의 필기시험에서 동기를 얻었습니다. 여러 학생들이 Python에 이미 존재한다고 가정하고 이 표기법(특히 set 버전)을 솔루션에 사용했습니다. 이는 이 표기법이 초보자에게도 직관적임을 시사합니다. 반대로, 기존 구문인 [x for it in its for x in it]는 학생들이 종종 틀리는 부분이며, 많은 학생들이 for 절의 순서를 뒤바꾸는 경향이 있습니다.

또한, 이 PEP가 발표된 후 Reddit 게시물의 댓글 섹션은 이 제안에 대한 상당한 지지를 보여주며, 여기에 제안된 구문이 가독성이 좋고, 직관적이며, 유용하다는 점을 시사합니다.

명세 (Specification)

구문 (Syntax)

문법은 list/set Comprehension 및 generator expression에서 표현식 앞에 *를 허용하도록 변경되어야 합니다. 또한 dictionary comprehension의 대체 형식으로 key: value 쌍 대신 double-starred 표현식을 사용할 수 있도록 허용해야 합니다.

이는 listcompsetcomp 규칙을 named_expression 대신 star_named_expression을 사용하도록 업데이트함으로써 달성할 수 있습니다:

listcomp[expr_ty]:
    | '[' a=star_named_expression b=for_if_clauses ']'
setcomp[expr_ty]:
    | '{' a=star_named_expression b=for_if_clauses '}'

genexp 규칙도 유사하게 starred_expression을 허용하도록 수정해야 합니다:

genexp[expr_ty]:
    | '(' a=(assignment_expression | expression !':=' | starred_expression) b=for_if_clauses ')'

dictionary comprehension 규칙도 이 새로운 형식을 허용하도록 조정해야 합니다:

dictcomp[expr_ty]:
    | '{' a=double_starred_kvpair b=for_if_clauses '}'

함수 호출에서 인자 언패킹이 처리되는 방식에는 변경이 없어야 합니다. 즉, 함수에 유일한 인수로 제공되는 generator expression이 추가적인 중복 괄호를 필요로 하지 않는다는 일반 규칙은 유지되어야 합니다. 이는 예를 들어 f(*x for x in it)f((*x for x in it))와 동일함을 의미합니다. (Starred Generators as Function Arguments 섹션에서 더 자세한 논의를 참조하십시오).

***는 Comprehension 내 표현식의 최상위 레벨에서만 허용되어야 합니다. (Further Generalizing Unpacking Operators 섹션에서 더 자세한 논의를 참조하십시오).

의미론: List/Set/Dict Comprehension (Semantics: List/Set/Dict Comprehensions)

list comprehension 내에서 별표가 붙은 표현식 [*expr for x in it]의 의미는 각 표현식을 iterable로 취급하고, [*expr1, *expr2, ...]와 같이 명시적으로 나열된 것처럼 이들을 연결하는 것입니다. 유사하게, {*expr for x in it}{*expr1, *expr2, ...}와 같이 명시적으로 나열된 것처럼 set union을 형성합니다. 그리고 {**expr for x in it}{**expr1, **expr2, ...}와 같이 명시적으로 나열된 것처럼 딕셔너리를 결합합니다. 이러한 연산은 이 방식으로 컬렉션을 결합하는 모든 동등한 의미론(예: 딕셔너리를 결합할 때 중복된 키의 경우 나중 값이 이전 값을 덮어쓰는 것 포함)을 유지해야 합니다.

다시 말해, 다음 Comprehension에 의해 생성된 객체들은:

new_list = [*expr for x in its]
new_set = {*expr for x in its}
new_dict = {**expr for d in dicts}

각각 다음 코드 조각에 의해 생성된 객체들과 동등해야 합니다:

new_list = []
for x in its:
    new_list.extend(expr)

new_set = set()
for x in its:
    new_set.update(expr)

new_dict = {}
for x in dicts:
    new_dict.update(expr)

의미론: Generator Expression (Semantics: Generator Expressions)

언패킹 구문을 사용하는 generator expression은 표현식에 의해 주어진 iterable들을 연결하여 값을 생성하는 새로운 generator를 형성해야 합니다. 특히, 동작은 다음 코드와 동일하게 정의됩니다:

# g = (*expr for x in it) 와 동일
def generator():
    for x in it:
        yield from expr
g = generator()

yield from은 비동기 generator 내에서 허용되지 않으므로 (PEP 525의 Asynchronous yield from 섹션 참조), (*expr async for x in ait())의 동등한 표현은 다음과 같습니다 (물론 이 새로운 형식은 루프 변수 i를 정의하거나 참조하지 않아야 합니다):

# g = (*expr async for x in ait()) 와 동일
async def generator():
    async for x in ait():
        for i in expr:
            yield i
g = generator()

이러한 의미론의 세부 사항은 미래에 재검토되어야 합니다. 특히 비동기 generatoryield from을 지원하게 된다면 (async 변형이 명시적인 루프 대신 yield from을 사용하도록 변경될 수 있음). (Alternative Generator Expression Semantics 섹션에서 더 자세한 논의를 참조하십시오).

할당 표현식과의 상호작용 (Interaction with Assignment Expressions)

이 제안은 Comprehension의 다양한 부분의 평가 순서나 스코프(scope) 규칙을 변경하지 않습니다. 이는 PEP 572의 “walrus operator” :=를 사용하는 generator expression에 특히 관련이 있습니다. 이 연산자는 Comprehension 또는 generator expression에서 사용될 때, 변수 바인딩을 Comprehension 내의 지역 스코프가 아닌 포함하는 스코프(containing scope)에서 수행합니다.

예를 들어, (*(y := [i, i+1]) for i in (0, 2, 4)) 표현식의 평가에서 발생하는 generator를 고려해 봅시다. 이는 다음 generator와 거의 동일하지만, generator expression 형태에서는 y가 지역적으로 바인딩되는 대신 포함하는 스코프에서 바인딩됩니다.

def generator():
    for i in (0, 2, 4):
        yield from (y := [i, i+1])

이 예제에서, 서브 표현식 (y := [i, i+1])generator가 소진되기 전에 정확히 세 번 평가됩니다: Comprehension에서 i가 각각 0, 2, 4로 할당된 직후입니다. 따라서 y(포함하는 스코프 내)는 해당 시점에 수정됩니다:

>>> g = (*(y := [i, i+1]) for i in (0, 2, 4))
>>> y
Traceback (most recent call last):
  File "<python-input-1>", line 1, in <module>
NameError: name 'y' is not defined
>>> next(g)
0
>>> y
[0, 1]
>>> next(g)
1
>>> y
[0, 1]
>>> next(g)
2
>>> y
[2, 3]

오류 보고 (Error Reporting)

현재 제안된 구문은 SyntaxError를 발생시킵니다. 이러한 형식이 구문적으로 유효한 것으로 인식되도록 허용하려면 invalid_comprehensioninvalid_dict_comprehension에 대한 문법 규칙을 각각 *** 사용을 허용하도록 조정해야 합니다.

최소한 다음 경우에 추가적인 특정 오류 메시지가 제공되어야 합니다:

list comprehension 또는 generator expression에서 **를 사용하려고 시도하면 딕셔너리 언패킹이 해당 구조에서 사용될 수 없음을 보고해야 합니다. 예를 들면:

>>> [**x for x in y]
  File "<stdin>", line 1
    [**x for x in y]
    ^^^
SyntaxError: cannot use dict unpacking in list comprehension
>>> (**x for x in y)
  File "<stdin>", line 1
    (**x for x in y)
    ^^^
SyntaxError: cannot use dict unpacking in generator expression

dictionary key/value에서 *를 사용하려고 시도할 때의 기존 오류 메시지는 유지되어야 하지만, dictionary key 또는 value에서 ** 언패킹을 사용하려고 시도할 때도 유사한 메시지가 보고되어야 합니다. 예를 들면:

>>> {*k: v for k,v in items}
  File "<stdin>", line 1
    {*k: v for k,v in items}
    ^^
SyntaxError: cannot use a starred expression in a dictionary key
>>> {k: *v for k,v in items}
  File "<stdin>", line 1
    {k: *v for k,v in items}
    ^^
SyntaxError: cannot use a starred expression in a dictionary value
>>> {**k: v for k,v in items}
  File "<stdin>", line 1
    {**k: v for k,v in items}
    ^^^
SyntaxError: cannot use dict unpacking in a dictionary key
>>> {k: **v for k,v in items}
  File "<stdin>", line 1
    {k: **v for k,v in items}
    ^^^
SyntaxError: cannot use dict unpacking in a dictionary value

다른 기존 오류 메시지의 문구도 새 구문의 존재를 설명하고, 또는 언패킹과 일반적으로 관련된 모호하거나 혼란스러운 경우(특히 Further Generalizing Unpacking Operators에서 언급된 경우)를 명확히 하기 위해 조정되어야 합니다. 예를 들면:

>>> [*x if x else y]
  File "<stdin>", line 1
    [*x if x else y]
    ^^^^^^^^^^^^^^
SyntaxError: invalid starred expression. Did you forget to wrap the conditional expression in parentheses?
>>> {**x if x else y}
  File "<stdin>", line 1
    {**x if x else y}
    ^^^^^^^^^^^^^^^
SyntaxError: invalid double starred expression. Did you forget to wrap the conditional expression in parentheses?
>>> [x if x else *y]
  File "<stdin>", line 1
    [x if x else *y]
    ^
SyntaxError: cannot unpack only part of a conditional expression
>>> {x if x else **y}
  File "<stdin>", line 1
    {x if x else **y}
    ^^
SyntaxError: cannot use dict unpacking on only part of a conditional expression

참조 구현 (Reference Implementation)

참조 구현은 이 기능을 구현하며, 초안 문서와 추가 테스트 케이스를 포함합니다.

하위 호환성 (Backwards Compatibility)

현재 구문적으로 유효한 모든 Comprehension의 동작은 이 변경의 영향을 받지 않으므로, 하위 호환성 문제가 많지는 않을 것으로 예상됩니다. 원칙적으로 이 변경은 Comprehension에서 언패킹 연산을 시도하면 SyntaxError가 발생한다는 사실에 의존하는 코드, 또는 대체되는 이전 오류 메시지의 특정 문구에 의존하는 코드에만 영향을 미칠 것입니다. 이는 드물 것으로 예상됩니다.

하나의 관련 우려는 미래에 비동기 generator expression의 의미론을 변경하여 언패킹 시 yield from을 사용하도록(언패킹되는 generator에게 위임하도록) 결정하는 경우입니다. 이는 .asend(), .athrow(), .aclose()와 함께 사용될 때 결과 generator의 동작에 영향을 미치므로 하위 호환성이 없을 것입니다. 그러나 하위 호환성이 없음에도 불구하고, 이러한 변경은 이 제안 하에서 특별히 유용하지 않은 구조의 동작에만 영향을 미치므로 큰 영향을 미치지 않을 가능성이 높습니다. (Alternative Generator Expression Semantics 섹션에서 더 자세한 논의를 참조하십시오).

코드 예시 (Code Examples)

이 섹션은 표준 라이브러리의 작은 코드 조각들을 어떻게 이 새로운 구문을 사용하여 간결성과 가독성을 향상시키도록 다시 작성할 수 있는지 보여주는 예시를 제시합니다. 참조 구현은 이러한 대체가 이루어진 후에도 모든 테스트를 통과합니다.

명시적 루프 대체 (Replacing Explicit Loops)

명시적 루프를 대체하면 여러 줄이 한 줄로 압축되고, 보조 변수를 정의하고 참조할 필요가 없어집니다.

email/_header_value_parser.py에서:

# 현재:
comments = []
for token in self:
    comments.extend(token.comments)
return comments

# 개선:
return [*token.comments for token in self]

shutil.py에서:

# 현재:
ignored_names = []
for pattern in patterns:
    ignored_names.extend(fnmatch.filter(names, pattern))
return set(ignored_names)

# 개선:
return {*fnmatch.filter(names, pattern) for pattern in patterns}

http/cookiejar.py에서:

# 현재:
cookies = []
for domain in self._cookies.keys():
    cookies.extend(self._cookies_for_domain(domain, request))
return cookies

# 개선:
return [
    *self._cookies_for_domain(domain, request) for domain in self._cookies.keys()
]

from_iterable 및 관련 함수 대체 (Replacing from_iterable and Friends)

항상 올바른 선택은 아니지만, itertools.chain.from_iterablemap을 대체하면 추가적인 간접 레벨을 피할 수 있어, Comprehension이 map/filter보다 가독성이 좋다는 일반적인 통념을 따르는 코드가 됩니다.

dataclasses.py에서:

# 현재:
inherited_slots = set(
    itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1]))
)

# 개선:
inherited_slots = {*_get_slots(c) for c in cls.__mro__[1:-1]}

importlib/metadata/__init__.py에서:

# 현재:
return itertools.chain.from_iterable(
    path.search(prepared) for path in map(FastPath, paths)
)

# 개선:
return (*FastPath(path).search(prepared) for path in paths)

collections/__init__.py (Counter 클래스)에서:

# 현재:
return _chain.from_iterable(_starmap(_repeat, self.items()))

# 개선:
return (*_repeat(elt, num) for elt, num in self.items())

zipfile/_path/__init__.py에서:

# 현재:
parents = itertools.chain.from_iterable(map(_parents, names))

# 개선:
parents = (*_parents(name) for name in names)

_pyrepl/_module_completer.py에서:

# 현재:
search_locations = set(chain.from_iterable(
    getattr(spec, 'submodule_search_locations', []) for spec in specs if spec
))

# 개선:
search_locations = {
    *getattr(spec, 'submodule_search_locations', []) for spec in specs if spec
}

Comprehension 내 이중 루프 대체 (Replacing Double Loops in Comprehensions)

Comprehension 내 이중 루프를 대체하면 보조 변수를 정의하고 참조할 필요가 없어 혼란을 줄일 수 있습니다.

importlib/resources/readers.py에서:

# 현재:
children = (child for path in self._paths for child in path.iterdir())

# 개선:
children = (*path.iterdir() for path in self._paths)

asyncio/base_events.py에서:

# 현재:
exceptions = [exc for sub in exceptions for exc in sub]

# 개선:
exceptions = [*sub for sub in exceptions]

_weakrefset.py에서:

# 현재:
return self.__class__(e for s in (self, other) for e in s)

# 개선:
return self.__class__(*s for s in (self, other))

교육 방법 (How to Teach This)

현재 Comprehension의 개념을 소개하는 일반적인 방법(Python 튜토리얼에서 사용됨)은 동등한 코드를 보여주는 것입니다. 예를 들어, 이 방법은 out = [expr for x in it]가 다음 코드와 동일하다고 말할 것입니다:

out = []
for x in it:
    out.append(expr)

이 접근 방식을 취하면 out = [*expr for x in it]를 대신 다음 코드와 동일하다고 소개할 수 있습니다 (append 대신 extend 사용):

out = []
for x in it:
    out.extend(expr)

언패킹을 사용하는 setdict comprehension도 유사한 비유를 통해 소개될 수 있습니다:

# out = {expr for x in it} 와 동일
out = set()
for x in it:
    out.add(expr)

# out = {*expr for x in it} 와 동일
out = set()
for x in it:
    out.update(expr)

# out = {k_expr: v_expr for x in it} 와 동일
out = {}
for x in it:
    out[k_expr] = v_expr

# out = {**expr for x in it} 와 동일 (expr이 **로 언패킹될 수 있는 매핑으로 평가되는 경우)
out = {}
for x in it:
    out.update(expr)

그리고 언패킹을 포함하는 generator expression의 동작을 설명하기 위해 유사한 접근 방식을 취할 수 있습니다:

# g = (expr for x in it) 와 동일
def generator():
    for x in it:
        yield expr
g = generator()

# g = (*expr for x in it) 와 동일
def generator():
    for x in it:
        yield from expr
g = generator()

이러한 특정 예시들로부터, 별표가 없는 Comprehension/genexp가 컬렉션에 단일 요소를 추가하는 연산자를 사용하는 모든 곳에서, 별표가 있는 버전은 대신 해당 컬렉션에 여러 요소를 추가하는 연산자를 사용할 것이라는 아이디어로 일반화할 수 있습니다.

대안으로, 두 아이디어를 별개로 생각할 필요 없이, 새로운 구문을 통해 out = [...x... for x in it]를 다음 코드와 동일하다고 생각할 수 있습니다 (여기서 ...x...는 임의의 코드를 나타냅니다), ...x...*를 사용하는지 여부와 관계없이:

out = []
for x in it:
    out.extend([...x...])

마찬가지로, out = {...x... for x in it}를 다음 코드와 동일하다고 생각할 수 있습니다. ...x...* 또는 ** 또는 :를 사용하는지 여부와 관계없이:

out = set() # 또는 out = {}
for x in it:
    out.update({...x...})

이러한 예시는 Comprehension이 있는 버전과 없는 버전 모두에서 동일한 출력을 생성한다는 의미에서 동등하지만, Comprehension이 없는 버전은 각 extend 또는 update 전에 새로운 list/set/dictionary를 만들기 때문에 약간 덜 효율적입니다. 이는 Comprehension을 사용하는 버전에서는 불필요합니다.

거부된 대안 제안 (Rejected Alternative Proposals)

위 명세에 대해 생각할 때 주된 목표는 언패킹 및 Comprehension / generator expression에 대한 기존 규범과의 일관성이었습니다. 이를 해석하는 한 가지 방법은 기존 문법 및 코드 생성에 가능한 가장 작은 변경 사항만 요구하도록 명세를 작성하여, 기존 코드가 주변 의미론을 형성하도록 하는 것이 목표였다는 것입니다.

아래에서는 논의에서 제기되었지만 이 제안에 포함되지 않은 일반적인 우려/대안 제안 중 일부를 논의합니다.

함수 인수로 사용되는 Starred Generator (Starred Generators as Function Arguments)

여러 번 제기된 일반적인 우려(위에서 링크된 논의 스레드뿐만 아니라 이와 동일한 아이디어에 대한 이전 논의에서도)는 f(*x for x in y)와 같이 별표가 붙은 generator를 함수의 단일 인수로 전달할 때 발생할 수 있는 구문적 모호성입니다. 원래 PEP 448에서는 이 모호성이 제안의 일부로 유사한 일반화를 포함하지 않은 이유로 언급되었습니다.

이 제안은 f(*x for x in y)f((*x for x in y))로 해석되어야 하며, 결과 generator에 대한 추가 언패킹을 시도해서는 안 된다고 제안합니다. 그러나 논의에서는 몇 가지 대안이 제시되었습니다(및/또는 과거에 제시된 바 있음), 예를 들어:

  • f(*x for x in y)f(*(x for x in y))로 해석하거나,
  • f(*x for x in y)f(*(*x for x in y))로 해석하거나,
  • 이 제안의 다른 측면이 수락되더라도 f(*x for x in y)에 대해 계속 SyntaxError를 발생시키는 것입니다.

이러한 대안보다 이 제안을 선호하는 이유는 generator expression 주변의 기존 구두점 규칙을 유지하기 위함입니다. 현재 일반적인 규칙은 generator expression이 함수의 단일 인수로 제공되는 경우를 제외하고는 괄호로 묶여야 한다는 것입니다. 이 제안은 더 많은 종류의 generator expression을 허용하더라도 이 규칙을 유지할 것을 제안합니다. 이 옵션은 언패킹을 사용하는 Comprehension 및 generator expression과 그렇지 않은 것들 사이에 완전한 대칭을 유지합니다.

현재 우리는 다음 규칙을 가지고 있습니다:

f([x for x in y])   # 단일 list 전달
f({x for x in y})   # 단일 set 전달
f(x for x in y)     # 단일 generator 전달 (genexp 주변에 추가 괄호 불필요)
f(*[x for x in y])  # list의 요소들을 개별적으로 전달
f(*{x for x in y})  # set의 요소들을 개별적으로 전달
f(*(x for x in y)) # generator의 요소들을 개별적으로 전달 (괄호 필요)

이 제안은 Comprehension이 언패킹을 사용하는 경우에도 이러한 규칙을 유지하기로 선택합니다:

f([*x for x in y])   # 단일 list 전달
f({*x for x in y})   # 단일 set 전달
f(*x for x in y)     # 단일 generator 전달 (genexp 주변에 추가 괄호 불필요)
f(*[*x for x in y])  # list의 요소들을 개별적으로 전달
f(*{*x for x in y})  # set의 요소들을 개별적으로 전달
f(*(*x for x in y)) # generator의 요소들을 개별적으로 전달 (괄호 필요)

언패킹 연산자 추가 일반화 (Further Generalizing Unpacking Operators)

논의에서 나온 또 다른 제안은 Comprehension 내에서 표현식을 언패킹하는 것을 허용하는 것 이상으로 *를 더욱 일반화하는 것이었습니다. 이 확장의 두 가지 주요 유형이 고려되었습니다:

  • ***를 새로운 종류의 Unpackable 객체(또는 유사한 것)를 생성하는 진정한 단항 연산자로 만들어서, Comprehension이 이를 언패킹하여 처리할 수 있지만 다른 컨텍스트에서도 사용될 수 있도록 하는 것; 또는
  • ***를 이 제안에서 허용되는 다른 곳(표현식 리스트, Comprehension, generator expression, 인자 리스트)에서만 허용하되, Comprehension 내의 서브 표현식에서도 사용할 수 있도록 허용하여, 예를 들어 일부 iterable 객체와 일부 non-iterable 객체를 포함하는 리스트를 평탄화하는 방법으로 다음을 허용하는 것:

    [*x if isinstance(x, Iterable) else x for x in [[1,2,3], 4]]
    

이러한 변형은 (이해하고 구현하기에) 실질적으로 더 복잡하고 유용성이 미미하다고 간주되어 이 PEP에 포함되지 않았습니다. 따라서 이러한 형식은 계속 SyntaxError를 발생시켜야 하지만, 위에서 설명한 새로운 오류 메시지와 함께 제공되어야 합니다. 그러나 미래 제안을 위한 고려 사항으로 배제되어서는 안 됩니다.

대체 Generator Expression 의미론 (Alternative Generator Expression Semantics)

또 다른 논의 지점은 generator expression의 언패킹 의미론, 특히 비동기 generatoryield from을 지원하지 않는다는 점을 고려할 때 동기 및 비동기 generator expression의 의미론 간의 관계에 중점을 두었습니다 (PEP 525의 Asynchronous yield from 섹션 참조).

핵심 질문은 동기 및 비동기 generator expression이 언패킹할 때 명시적인 루프 대신 yield from(또는 동등한 것)을 사용해야 하는지에 대한 것이었습니다. 이러한 옵션 간의 주요 차이점은 결과 generator가 언패킹되는 객체에 위임하는지 여부입니다. 이는 언패킹되는 객체 자체가 generator인 경우 .send()/.asend(), .throw()/.athrow(), .close()/.aclose()와 함께 사용될 때 이러한 generator expression의 동작에 영향을 미칠 것입니다. 이러한 옵션 간의 차이점은 Appendix: Semantics of Generator Delegation에 요약되어 있습니다.

몇 가지 합리적인 옵션이 고려되었으며, Discourse 스레드의 투표에서 명확한 승자는 없었습니다. 위에서 설명한 제안 외에 다음도 고려되었습니다:

  • 동기 및 비동기 generator expression 모두에 명시적 루프를 사용하는 것.

    이 전략은 동기 및 비동기 generator expression 사이에 대칭성을 가져왔겠지만, 동기 generator expression의 경우 위임을 허용하지 않아 잠재적으로 유용한 도구를 막았을 것입니다. 이 접근 방식의 한 가지 특정 우려는 동기 및 비동기 generator 간의 비대칭성을 도입하는 것이지만, 이러한 비대칭성이 이미 동기 및 비동기 generator 사이에 더 일반적으로 존재한다는 사실로 인해 이 우려는 완화됩니다.

  • 동기 generator expression의 언패킹에 yield from을 사용하고, 비동기 generator expression의 언패킹에 yield from의 동작을 모방하는 것.

    이 전략은 동기 및 비동기 generator의 언패킹 동작을 대칭적으로 만들겠지만, 더 복잡할 것이며, 그 비용이 이점만큼 가치가 없을 수도 있습니다. 따라서 이 PEP는 언패킹 연산자를 사용하는 generator expressionyield from이 비동기 generator에서 더 일반적으로 지원될 때까지 yield from과 유사한 의미론을 사용해서는 안 된다고 제안합니다.

  • 동기 generator expression의 언패킹에 yield from을 사용하고, 비동기 generator expressionyield from을 지원할 때까지 비동기 generator expression에서의 언패킹을 금지하는 것.

    이 전략은 비동기 generator expression이 미래에 yield from에 대한 지원을 얻게 된다면, 그때 내려지는 모든 결정이 완전히 하위 호환성을 갖도록 보장함으로써 마찰을 줄일 수 있을 것입니다. 그러나 그 컨텍스트에서 언패킹의 유용성은 비동기 generator expressionyield from에 대한 지원을 받는 경우 미래에 최소한의 침해적인 하위 비호환성 변경의 잠재적인 단점보다 클 것으로 보입니다.

  • 모든 generator expression에서 언패킹을 금지하는 것.

    이는 두 경우 사이에 대칭성을 유지하겠지만, 매우 표현적인 형태를 잃는 단점이 있습니다.

이러한 각 옵션(이 PEP에 제시된 옵션 포함)은 장점과 단점을 가지고 있으며, 모든 면에서 명확하게 우월한 옵션은 없습니다. Semantics: Generator Expressions에 제안된 의미론은 동기 및 비동기 generator expression 모두에서 언패킹이 현재 동등한 generator를 작성하는 일반적인 방법을 반영하는 합리적인 절충안을 나타냅니다. 또한, 이러한 미묘한 차이는 일반적인 사용 사례에 큰 영향을 미치지 않을 가능성이 높습니다 (예를 들어, 단순 컬렉션을 결합하는 가장 흔한 사용 사례에는 차이가 없습니다).

위에서 제안된 바와 같이, 이 결정은 비동기 generator가 미래에 yield from에 대한 지원을 받을 경우 재검토되어야 하며, 이 경우 비동기 generator expression에서 언패킹의 의미론을 yield from을 사용하도록 조정하는 것을 고려해야 합니다.

우려 사항 및 단점 (Concerns and Disadvantages)

논의 스레드에서 전반적인 합의는 이 구문이 명확하고 직관적이라는 것이었지만, 몇 가지 우려 사항과 잠재적인 단점도 제기되었습니다. 이 섹션은 이러한 우려 사항을 요약하는 것을 목표로 합니다.

  • 기존 대안과의 중복: 제안된 구문이 더 명확하고 간결하다고 주장할 수 있지만, Python에는 이미 동일한 작업을 수행하는 여러 가지 방법이 있습니다.
  • 함수 호출 모호성: f(*x for x in y)와 같은 표현식은 generator를 언패킹하려는 것인지 또는 단일 인수로 전달하려는 것인지가 명확하지 않아 처음에는 모호하게 보일 수 있습니다. 이 제안은 이 형식을 f((*x for x in y))와 동일하게 처리하여 기존 규칙을 유지하지만, 이 동등성이 즉시 명확하지 않을 수 있습니다.
  • 과도한 사용 또는 오용 가능성: Comprehension에서 언패킹을 복잡하게 사용하면 명시적 루프에서 더 명확할 수 있는 로직을 모호하게 만들 수 있습니다. 이는 Comprehension 전반에 걸쳐 이미 우려되는 사항이지만, ***의 추가는 특히 복잡한 사용을 한눈에 읽고 이해하기 훨씬 더 어렵게 만들 수 있습니다. 예를 들어, 이러한 상황이 드물기는 하지만, 여러 방식으로 언패킹을 사용하는 Comprehension은 무엇이 언제 언패킹되는지 알기 어렵게 만들 수 있습니다: f(*(*x for *x, _ in list_of_lists)).
  • 스코프 제한의 불명확성: 이 제안은 언패킹을 Comprehension 표현식의 최상위 레벨로 제한하지만, 일부 사용자는 Further Generalizing Unpacking Operators에서 논의된 바와 같이 언패킹 연산자가 추가로 일반화될 것이라고 예상할 수 있습니다.
  • 외부 도구에 미치는 영향: Python 구문의 다른 변경 사항과 마찬가지로, 이 변경 사항은 코드 포매터, 린터, 타입 체커 등의 유지보수자에게 새로운 구문이 지원되도록 작업을 생성할 것입니다.

부록: 다른 언어 (Appendix: Other Languages)

꽤 많은 다른 언어들이 Python에 이미 있는 것과 유사한 구문으로 이러한 종류의 평탄화(flattening)를 지원하지만, Comprehension 내에서 언패킹 구문을 사용하는 것에 대한 지원은 드뭅니다. 이 섹션은 몇 가지 다른 언어에서 유사한 구문에 대한 지원을 간략하게 요약합니다.

Comprehension을 지원하는 많은 언어들은 이중 루프를 지원합니다:

# python
[x for xs in [[1,2,3], [], [4,5]] for x in xs * 2]

-- haskell
[x | xs <- [[1,2,3], [], [4,5]], x <- xs ++ xs]

# julia
[x for xs in [[1,2,3], [], [4,5]] for x in [xs; xs]]

; clojure
(for [xs [[1 2 3] [] [4 5]] x (concat xs xs)] x)

몇몇 다른 언어들(Comprehension이 없는 언어들도)은 중첩된 구조의 평탄화를 지원하기 위해 내장 함수나 메서드를 통해 이러한 연산을 지원합니다:

# python
list(itertools.chain(*(xs*2 for xs in [[1,2,3], [], [4,5]])))

// javascript
[[1,2,3], [], [4,5]].flatMap(xs => [...xs, ...xs])

-- haskell
concat (map (\x -> x ++ x) [[1,2,3], [], [4,5]])

# ruby
[[1, 2, 3], [], [4, 5]].flat_map {|e| e * 2}

그러나 Comprehension과 언패킹을 모두 지원하는 언어는 Comprehension 내에서 언패킹을 허용하는 경향이 없습니다. 예를 들어, Julia에서 다음 표현식은 현재 구문 오류로 이어집니다:

[xs... for xs in [[1,2,3], [], [4,5]]]

한 가지 반례로, Civet에는 최근 유사한 구문이 추가되었습니다. 예를 들어, 다음은 Civet의 유효한 Comprehension이며, JavaScript의 ... 구문을 사용하여 언패킹을 활용합니다:

for xs of [[1,2,3], [], [4,5]] then ...(xs++xs)

부록: Generator 위임의 의미론 (Appendix: Semantics of Generator Delegation)

위에서 설명된 의미론에 대한 일반적인 질문 중 하나는 generator expression 내에서 언패킹할 때 yield from을 사용하는 것과 명시적인 루프를 사용하는 것의 차이점에 관한 것이었습니다. 이는 generator의 상당히 고급 기능이므로, 이 부록은 yield from을 사용하는 generator와 명시적인 루프를 사용하는 generator 간의 몇 가지 주요 차이점을 요약합니다.

기본 동작 (Basic Behavior)

값에 대한 단순한 반복의 경우, 이는 generator expression에서 언패킹의 가장 일반적인 용도가 될 것으로 예상되며, 두 접근 방식 모두 동일한 결과를 생성합니다:

def yield_from(iterables):
    for iterable in iterables:
        yield from iterable

def explicit_loop(iterables):
    for iterable in iterables:
        for item in iterable:
            yield item

# 두 가지 모두 동일한 값 시퀀스를 생성합니다.
x = list(yield_from([[1, 2], [3, 4]]))
y = list(explicit_loop([[1, 2], [3, 4]]))
print(x == y) # True 출력

고급 Generator 프로토콜 차이 (Advanced Generator Protocol Differences)

차이점은 고급 generator 프로토콜 메서드인 .send(), .throw(), .close()를 사용할 때, 그리고 서브 iterable이 단순 시퀀스가 아닌 generator 자체일 때 나타납니다. 이러한 경우, yield from 버전은 관련 신호가 서브 generator에 도달하게 하지만, 명시적 루프가 있는 버전은 그렇지 않습니다.

.send()를 사용한 위임 (Delegation with .send())

def sub_generator():
    x = yield "first"
    yield f"received: {x}"
    yield "last"

def yield_from():
    yield from sub_generator()

def explicit_loop():
    for item in sub_generator():
        yield item

# yield from을 사용하면 값이 서브-generator로 전달됩니다.
gen1 = yield_from()
print(next(gen1)) # "first" 출력
print(gen1.send("hello")) # "received: hello" 출력
print(next(gen1)) # "last" 출력

# 명시적 루프를 사용하면 .send()가 외부 generator에만 영향을 미칩니다.
# 값은 서브-generator에 도달하지 않습니다.
gen2 = explicit_loop()
print(next(gen2)) # "first" 출력
print(gen2.send("hello")) # "received: None" 출력 (서브-generator는 "hello" 대신 None을 받습니다)
print(next(gen2)) # "last" 출력

.throw()를 사용한 예외 처리 (Exception Handling with .throw())

def sub_generator_with_exception_handling():
    try:
        yield "first"
        yield "second"
    except ValueError as e:
        yield f"caught: {e}"

def yield_from():
    yield from sub_generator_with_exception_handling()

def explicit_loop():
    for item in sub_generator_with_exception_handling():
        yield item

# yield from을 사용하면 예외가 서브-generator로 전달됩니다.
gen1 = yield_from()
print(next(gen1)) # "first" 출력
print(gen1.throw(ValueError("test"))) # "caught: test" 출력

# 명시적 루프를 사용하면 예외가 외부 generator에만 영향을 미칩니다.
gen2 = explicit_loop()
print(next(gen2)) # "first" 출력
print(gen2.throw(ValueError("test"))) # ValueError가 발생합니다. 서브-generator는 이를 보지 못합니다.

.close()를 사용한 Generator 정리 (Generator Cleanup with .close())

# 서브-generator에 대한 참조를 유지하여 GC가 명시적 루프 버전을 닫지 않도록 합니다.
references = []

def sub_generator_with_cleanup():
    try:
        yield "first"
        yield "second"
    finally:
        print("sub-generator received GeneratorExit")

def yield_from():
    try:
        g = sub_generator_with_cleanup()
        references.append(g)
        yield from g
    finally:
        print("outer generator received GeneratorExit")

def explicit_loop():
    try:
        g = sub_generator_with_cleanup()
        references.append(g)
        for item in g:
            yield item
    finally:
        print("outer generator received GeneratorExit")

# yield from을 사용하면 GeneratorExit이 서브-generator로 전달됩니다.
gen1 = yield_from()
print(next(gen1)) # "first" 출력
gen1.close() # 서브-generator를 닫은 다음 외부 generator를 닫습니다.

# 명시적 루프를 사용하면 GeneratorExit이 외부 generator에만 전달됩니다.
gen2 = explicit_loop()
print(next(gen2)) # "first" 출력
gen2.close() # 외부 generator만 닫습니다.
print('program finished; GC will close the explicit loop subgenerator')
# 두 번째 내부 generator는 GC가 프로그램을 마칠 때 닫힙니다.

⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments