Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 798 – 컴프리헨션에서의 언패킹

Author:
Adam Hartz <hz at mit.edu>, Erik Demaine <edemaine at mit.edu>
Sponsor:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Created:
19-Jul-2025
Python-Version:
3.15
Post-History:
16-Oct-2021, 22-Jun-2025, 19-Jul-2025
Resolution:
03-Nov-2025

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at 리스트, 집합, 딕셔너리 디스플레이, 딕셔너리 디스플레이.

×

See PEP 1 for how to propose changes.

초록

이 PEP는 리스트, 집합, 딕셔너리 컴프리헨션과 제너레이터 표현식을 확장하여 표현식의 시작 부분에 언패킹 표기법(* 및 **)을 허용할 것을 제안합니다. 이를 통해 임의 개수의 이터러블을 하나의 리스트나 집합 또는 제너레이터로, 혹은 임의 개수의 딕셔너리를 하나의 딕셔너리로 결합하는 간결한 방법을 제공합니다. 예를 들면 다음과 같습니다:

[*it for it in its]  # list with the concatenation of iterables in 'its'
{*it for it in its}  # set with the union of iterables in 'its'
{**d for d in dicts} # dict with the combination of dicts in 'dicts'
(*it for it in its)  # generator of the concatenation of iterables in 'its'

동기

PEP 448의 확장된 언패킹 표기법(* 및 **)을 사용하면 몇 개의 이터러블이나 딕셔너리를 쉽게 결합할 수 있습니다:

[*it1, *it2, *it3]  # list with the concatenation of three iterables
{*it1, *it2, *it3}  # set with the union of three iterables
{**dict1, **dict2, **dict3}  # dict with the combination of three dicts

하지만 임의 개수의 이터러블을 이와 비슷하게 결합하려는 경우에는 언패킹을 같은 방식으로 사용할 수 없습니다.

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

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

또는 두 개의 루프가 있는 컴프리헨션을 사용하여 더 간결하게 작성할 수 있습니다:

[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)

또는 제너레이터를 제외한 모든 경우에 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는 추가적인 대안으로 컴프리헨션에서 언패킹 연산을 사용할 수 있도록 허용할 것을 제안합니다:

[*it for it in its]  # list with the concatenation of iterables in 'its'
{*it for it in its}  # set with the union of iterables in 'its'
{**d for d in dicts} # dict with the combination of dicts in 'dicts'
(*it for it in its)  # generator of the concatenation of iterables in 'its'

이 제안은 비동기 컴프리헨션과 제너레이터 표현식에도 적용되므로, 예를 들어 (*ait async for ait in aits())는 (x async for ait in aits() for x in ait)와 동등합니다.

근거

여러 이터러블 객체를 하나의 객체로 결합하는 것은 흔한 작업입니다. 예를 들어, 리스트의 리스트를 평탄화하는 방법을 묻는 한 StackOverflow 게시물은 조회수가 460만 회에 달하며, 표준 라이브러리에도 이 연산을 수행하는 코드의 예가 여럿 있습니다(코드 예제 참조). Python은 PEP 448의 확장 언패킹을 사용해 개수가 적고 미리 알려진 이터러블을 결합하는 수단을 제공하지만, 임의 개수의 이터러블을 결합하기 위한 그에 상응하는 문법은 현재 존재하지 않습니다.

이 제안은 기존 구문 구조와 나란히 가는, 언어의 자연스러운 확장입니다. [x, y, z]가 고정된 개수의 값으로 리스트를 만들고 [item for item in items]가 임의 개수의 값으로 리스트를 만드는 것처럼, 이 제안은 그 개념을 언패킹이 포함된 리스트 생성으로 확장하여 [*item for item in items]를 [*x, *y, *z]에 대응하는 것으로 만듭니다.

저희는 이 문법이 컴프리헨션과 언패킹 표기법 모두에 이미 익숙한 프로그래머에게 직관적이고 친숙할 것으로 기대합니다. 이 제안은 부분적으로 한 Python 프로그래밍 수업의 필기시험에서 비롯되었습니다. 그 시험에서 여러 학생이 제안된 표기법(구체적으로는 set 버전)이 Python에 이미 존재한다고 가정하고 답안에 사용했습니다. 이는 이 표기법이 Python의 기존 문법을 논리적이고 일관되게 확장한 것임을 시사합니다. 반면에 기존의 이중 루프 버전인 [x for it in its for x in it]는 학생들이 자주 틀리는 방식으로, 많은 학생이 자연스럽게 for 절의 순서를 뒤집어 쓰려고 합니다. 제안된 문법이 직관적이라는 점은 이 PEP가 처음 공개된 후 작성된 Reddit 게시물의 댓글란에서도 뒷받침되며, 이는 더 넓은 커뮤니티의 지지를 보여 줍니다.

명세

문법

리스트/집합 컴프리헨션과 제너레이터 표현식의 표현식 앞에 *가 올 수 있도록 문법을 변경하고, key: value 쌍 대신 이중 별표 표현식을 사용할 수 있는 딕셔너리 컴프리헨션의 대체 형식을 허용해야 합니다.

이는 listcomp와 setcomp 규칙이 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 ')'

딕셔너리 컴프리헨션 규칙도 이 새로운 형식을 허용하도록 조정해야 합니다:

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

함수 호출에서 인자 언패킹을 처리하는 방식은 변경하지 않아야 합니다. 즉, 함수의 유일한 인자로 제공되는 제너레이터 표현식에는 불필요한 괄호를 추가할 필요가 없다는 일반 규칙은 유지되어야 합니다. 이는 예를 들어 f(*x for x in it)가 f((*x for x in it))와 동등하다는 것을 의미한다는 점에 유의하십시오(자세한 논의는 함수 인자로서의 별표 붙은 제너레이터를 참조하십시오).

*와 **는 컴프리헨션 안에 있는 표현식의 최상위 수준에서만 허용되어야 합니다(자세한 논의는 언패킹 연산자의 추가 일반화을 참조하십시오).

의미: 리스트/집합/딕셔너리 컴프리헨션

리스트 컴프리헨션 [*expr for x in it]에서 별표 표현식의 의미는 각 표현식을 이터러블로 취급하여, [*expr1, *expr2, ...]처럼 명시적으로 나열한 경우와 같은 방식으로 이어 붙이는 것입니다. 마찬가지로 {*expr for x in it}은 표현식들을 {*expr1, *expr2, ...}처럼 명시적으로 나열한 것과 같이 합집합을 만들고, {**expr for x in it}은 표현식들을 {**expr1, **expr2, ...}처럼 명시적으로 나열한 것과 같이 딕셔너리들을 결합합니다. 이러한 연산은 이런 방식으로 컬렉션을 결합할 때 적용되는 의미를 모두 동일하게 유지해야 합니다(예를 들어 딕셔너리를 결합할 때 키가 중복되면 나중 값이 이전 값을 대체하는 것도 포함합니다).

다시 말해, 다음 컴프리헨션들이 생성하는 객체는:

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)

의미: 제너레이터 표현식

언패킹 문법을 사용하는 제너레이터 표현식은 표현식들이 제공하는 이터러블들을 이어 붙인 결과에서 값을 생성하는 새로운 제너레이터를 만들어야 합니다. 구체적으로, 그 동작은 다음과 동등하도록 정의됩니다(단, 루프 변수 i를 정의하거나 참조하지는 않습니다):

# equivalent to g = (*expr for x in it)
def generator():
    for x in it:
        for i in expr:
            yield i

g = generator()
# equivalent to g = (*expr async for x in ait())
async def generator():
    async for x in ait():
        for i in expr:
            yield i

g = generator()

이 의미에 대한 자세한 논의는 제너레이터 표현식 의미론의 대안를 참조하십시오.

대입 표현식과의 상호 작용

이 제안은 컴프리헨션을 이루는 여러 부분의 평가 순서나 스코프에 관한 규칙을 변경하자는 것이 아니라는 점에 유의하십시오. 이는 PEP 572의 “바다코끼리 연산자” :=를 사용하는 제너레이터 표현식과 특히 관련이 있습니다. 이 연산자는 컴프리헨션이나 제너레이터 표현식에서 사용될 때 변수 바인딩을 컴프리헨션 내부가 아니라 이를 포함하는 스코프에서 수행합니다.

예를 들어, 표현식 (*(y := [i, i+1]) for i in (0, 2, 4))를 평가하여 만들어지는 제너레이터를 생각해 봅시다. 이는 다음 제너레이터와 대략 동등합니다. 다만 제너레이터 표현식 형태에서는 y가 지역적으로 바인딩되지 않고 이를 포함하는 스코프에 바인딩된다는 점이 다릅니다:

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

이 예에서 하위 표현식 (y := [i, i+1])은 제너레이터가 소진되기 전까지 정확히 세 번 평가됩니다. 평가 시점은 컴프리헨션에서 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>
    y
NameError: name 'y' is not defined
>>> next(g)
0
>>> y
[0, 1]
>>> next(g)
1
>>> y
[0, 1]
>>> next(g)
2
>>> y
[2, 3]

오류 보고

현재 제안된 문법은 SyntaxError를 발생시킵니다. 이러한 형태를 문법적으로 유효하다고 인식하게 하려면 invalid_comprehension과 invalid_dict_comprehension의 문법 규칙을 조정하여 각각 *와 **를 사용할 수 있도록 해야 합니다.

최소한 다음 경우에는 추가적인 구체적 오류 메시지를 제공해야 합니다:

  • 리스트 컴프리헨션이나 제너레이터 표현식에서 **를 사용하려고 하면 해당 구조에서는 딕셔너리 언패킹을 사용할 수 없다고 보고해야 합니다. 예를 들면 다음과 같습니다:
    >>> [**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
    
  • 딕셔너리 키/값에 *를 사용하려고 할 때 나오는 기존 오류 메시지는 유지하되, 딕셔너리 키나 값에 ** 언패킹을 사용하려고 할 때에도 비슷한 메시지를 보고해야 합니다. 예를 들면 다음과 같습니다:
    >>> {*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
    
  • 기존의 다른 오류 메시지 가운데 일부도 마찬가지로 새 문법이 생긴 것을 반영하거나, 언패킹 전반과 관련하여 모호하거나 혼란스러운 경우(특히 언패킹 연산자의 추가 일반화에서 언급한 경우)를 명확히 하도록, 또는 두 가지 모두를 위해 문구를 조정해야 합니다. 예를 들면 다음과 같습니다:
    >>> [*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
    

참조 구현

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

하위 호환성

현재 문법적으로 유효한 모든 컴프리헨션의 동작은 이 변경의 영향을 받지 않으므로, 하위 호환성 문제는 크지 않을 것으로 예상합니다. 원칙적으로 이 변경의 영향을 받는 코드는 컴프리헨션에서 언패킹 연산을 사용하려고 하면 SyntaxError가 발생한다는 사실에 의존하는 코드나, 교체되는 기존 오류 메시지의 특정 문구에 의존하는 코드뿐이며, 이러한 코드는 드물 것으로 예상합니다.

이와 관련하여 한 가지 우려가 있습니다. 앞으로 언패킹 중에 yield from을 사용하도록(언패킹되는 제너레이터에 위임하도록) 제너레이터 표현식의 의미를 바꾸기로 결정한다고 가정하면, 그 변경은 결과 제너레이터를 .send()/.asend(), .throw()/.athrow(), .close()/.aclose()와 함께 사용할 때의 동작에 영향을 주므로 하위 호환성을 깨뜨리게 됩니다. 그렇기는 하지만, 그러한 변경은 하위 호환성을 깨뜨리더라도 큰 영향을 미칠 가능성은 낮습니다. 이 제안에서 특별히 유용하지 않은 구조의 동작에만 영향을 주기 때문입니다. 더 자세한 논의는 제너레이터 표현식 의미론의 대안를 참조하십시오.

코드 예제

이 절에서는 표준 라이브러리의 작은 코드 조각을 이 새 문법을 사용하도록 다시 작성하는 방법을 몇 가지 예제로 보여 줍니다. 이렇게 대체하더라도 참조 구현은 모든 테스트를 계속 통과합니다.

명시적 루프 대체하기

명시적 루프를 대체하면 여러 줄을 한 줄로 줄일 수 있고, 보조 변수를 정의하고 참조할 필요도 없어집니다.

  • 출처: email/_header_value_parser.py:
    # current:
    comments = []
    for token in self:
        comments.extend(token.comments)
    return comments
    
    # proposed:
    return [*token.comments for token in self]
    
  • 출처: shutil.py:
    # current:
    ignored_names = []
    for pattern in patterns:
        ignored_names.extend(fnmatch.filter(names, pattern))
    return set(ignored_names)
    
    # proposed:
    return {*fnmatch.filter(names, pattern) for pattern in patterns}
    
  • 출처: http/cookiejar.py:
    # current:
    cookies = []
    for domain in self._cookies.keys():
        cookies.extend(self._cookies_for_domain(domain, request))
    return cookies
    
    # proposed:
    return [
        *self._cookies_for_domain(domain, request)
        for domain in self._cookies.keys()
    ]
    

from_iterable 및 관련 함수 대체하기

항상 올바른 선택은 아니지만, itertools.chain.from_iterable과 map을 대체하면 한 단계의 추가적인 간접 호출을 피할 수 있습니다. 그 결과 코드는 일반적으로 컴프리헨션이 map/filter보다 읽기 쉽다는 통념을 따르게 됩니다.

  • 출처: dataclasses.py:
    # current:
    inherited_slots = set(
        itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1]))
    )
    
    # proposed:
    inherited_slots = {*_get_slots(c) for c in cls.__mro__[1:-1]}
    
  • 출처: importlib/metadata/__init__.py:
    # current:
    return itertools.chain.from_iterable(
        path.search(prepared) for path in map(FastPath, paths)
    )
    
    # proposed:
    return (*FastPath(path).search(prepared) for path in paths)
    
  • 출처: collections/__init__.py (Counter 클래스):
    # current:
    return _chain.from_iterable(_starmap(_repeat, self.items()))
    
    # proposed:
    return (*_repeat(elt, num) for elt, num in self.items())
    
  • 출처: zipfile/_path/__init__.py:
    # current:
    parents = itertools.chain.from_iterable(map(_parents, names))
    
    # proposed:
    parents = (*_parents(name) for name in names)
    
  • 출처: _pyrepl/_module_completer.py:
    # current:
    search_locations = set(chain.from_iterable(
        getattr(spec, 'submodule_search_locations', [])
        for spec in specs if spec
    ))
    
    # proposed:
    search_locations = {
        *getattr(spec, 'submodule_search_locations', [])
        for spec in specs if spec
    }
    

컴프리헨션의 이중 루프 대체하기

컴프리헨션의 이중 루프를 대체하면 보조 변수를 정의하고 참조할 필요가 없어집니다.

  • 출처: importlib/resources/readers.py:
    # current:
    children = (child for path in self._paths for child in path.iterdir())
    
    # proposed:
    children = (*path.iterdir() for path in self._paths)
    
  • 출처: asyncio/base_events.py:
    # current:
    exceptions = [exc for sub in exceptions for exc in sub]
    
    # proposed:
    exceptions = [*sub for sub in exceptions]
    
  • 출처: _weakrefset.py:
    # current:
    return self.__class__(e for s in (self, other) for e in s)
    
    # proposed:
    return self.__class__(*s for s in (self, other))
    

교육 방법

현재 컴프리헨션 개념을 소개하는 일반적인 방법(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)

언패킹을 사용하는 집합 컴프리헨션과 딕셔너리 컴프리헨션도 비슷한 비유로 소개할 수 있습니다:

# equivalent to out = {expr for x in it}
out = set()
for x in it:
    out.add(expr)

# equivalent to out = {*expr for x in it}
out = set()
for x in it:
    out.update(expr)

# equivalent to out = {k_expr: v_expr for x in it}
out = {}
for x in it:
    out[k_expr] = v_expr

# equivalent to out = {**expr for x in it}, provided that expr evaluates to
# a mapping that can be unpacked with **
out = {}
for x in it:
    out.update(expr)

언패킹을 포함하는 제너레이터 표현식의 동작도 비슷한 방식으로 설명할 수 있습니다:

# equivalent to g = (expr for x in it)
def generator():
    for x in it:
        yield expr
g = generator()

# equivalent to g = (*expr for x in it)
def generator():
    for x in it:
        yield from expr
g = generator()

그런 다음 이 구체적인 예제들로부터 다음과 같이 일반화할 수 있습니다. 별표가 없는 컴프리헨션/제너레이터 표현식이 컬렉션에 요소 하나를 추가하는 연산자를 사용하는 곳이라면 어디서든, 별표가 있는 쪽은 그 대신 해당 컬렉션에 여러 요소를 추가하는 연산자를 사용합니다.

또는 두 개념을 별개로 생각할 필요도 없습니다. 새 문법에서는 ...x...가 *를 사용하는지와 관계없이 out = [...x... for x in it]을 다음 코드와 동등하다고 생각할 수 있습니다 [1] (여기서 ...x...는 임의의 코드를 나타냅니다):

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

마찬가지로, ...x...가 *, **, : 중 무엇을 사용하는지와 관계없이 out = {...x... for x in it}을 다음 코드와 동등하다고 생각할 수 있습니다:

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

이 예제들은 컴프리헨션을 사용한 버전과 사용하지 않은 버전이 같은 출력을 만든다는 의미에서 동등합니다. 다만 컴프리헨션을 사용하지 않은 버전은 매번 extend 또는 update를 호출하기 전에 새 리스트/집합/딕셔너리를 만들기 때문에 약간 덜 효율적이라는 점에 유의하십시오. 컴프리헨션을 사용하는 버전에서는 이런 작업이 필요 없습니다.

거부된 대안 제안

위의 명세를 고안할 때의 주된 목표는 언패킹 및 컴프리헨션 / 제너레이터 표현식에 관한 기존 규범과의 일관성이었습니다. 이를 해석하는 한 가지 방법은 기존 문법과 코드 생성에 필요한 변경을 가능한 한 최소화하고 주변 의미론은 기존 코드에 따르도록 명세를 작성하는 것이 목표였다고 보는 것입니다.

아래에서는 논의 중에 제기되었으나 이 제안에는 포함되지 않은 일반적인 우려와 대안 제안 몇 가지를 다룹니다.

함수 인자로서의 별표 붙은 제너레이터

여러 차례 제기된 일반적인 우려 중 하나는(위에 링크된 논의 스레드뿐 아니라 같은 아이디어를 다룬 이전 논의에서도 제기되었습니다) f(*x for x in y)처럼 별표 붙은 제너레이터를 함수의 유일한 인자로 전달할 때 생길 수 있는 구문상의 모호성입니다. 원래의 PEP 448에서는 이 모호성이 비슷한 일반화를 제안에 포함하지 않은 이유로 언급되었습니다.

이 제안은 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)를 f(*(*x for x in y))로 해석하는 것, 또는
  • 이 제안의 다른 부분이 채택되더라도 f(*x for x in y)에 대해서는 계속 SyntaxError를 발생시키는 것.

이러한 대안보다 이 제안을 선호하는 이유는 제너레이터 표현식을 둘러싼 구두점에 관한 기존 관례를 유지할 수 있기 때문입니다. 현재 일반적인 규칙은 제너레이터 표현식을 함수의 유일한 인자로 전달하는 경우를 제외하고는 반드시 괄호로 감싸야 한다는 것이며, 이 제안은 더 많은 종류의 제너레이터 표현식을 허용하더라도 이 규칙을 유지할 것을 제안합니다. 이 방식은 언패킹을 사용하는 컴프리헨션 및 제너레이터 표현식과 그렇지 않은 것 사이에 완전한 대칭성을 유지합니다.

현재는 다음과 같은 관례가 있습니다:

f([x for x in y])  # pass in a single list
f({x for x in y})  # pass in a single set
f(x for x in y)  # pass in a single generator (no additional parentheses required around genexp)

f(*[x for x in y])  # pass in elements from the list separately
f(*{x for x in y})  # pass in elements from the set separately
f(*(x for x in y))  # pass in elements from the generator separately (parentheses required)

이 제안은 컴프리헨션이 언패킹을 사용하는 경우에도 이러한 관례를 유지하기로 합니다:

f([*x for x in y])  # pass in a single list
f({*x for x in y})  # pass in a single set
f(*x for x in y)  # pass in a single generator (no additional parentheses required around genexp)

f(*[*x for x in y])  # pass in elements from the list separately
f(*{*x for x in y})  # pass in elements from the set separately
f(*(*x for x in y))  # pass in elements from the generator separately (parentheses required)

언패킹 연산자의 추가 일반화

논의에서 나온 또 다른 제안은 *를 컴프리헨션 안의 표현식을 언패킹하는 용도로만 허용하는 데서 더 나아가 일반화하자는 것이었습니다. 이 확장은 크게 두 가지 형태로 검토되었습니다:

  • *와 **를 새로운 종류의 Unpackable 객체(또는 그와 비슷한 것)를 만드는 진정한 단항 연산자로 만들어, 컴프리헨션에서는 이 객체를 언패킹하여 처리하되 다른 문맥에서도 사용할 수 있게 하는 것, 또는
  • *와 **를 이 제안의 다른 부분에서 허용하는 위치(표현식 목록, 컴프리헨션, 제너레이터 표현식, 인자 목록)에서만 계속 허용하되, 컴프리헨션 안의 하위 표현식에서도 사용할 수 있게 하여, 예를 들어 이터러블과 이터러블이 아닌 객체가 섞인 리스트를 평탄화하는 방법으로 다음과 같은 형태를 허용하는 것:
    [*x if isinstance(x, Iterable) else x for x in [[1,2,3], 4]]
    

이러한 변형은 이해하기에도 구현하기에도 상당히 더 복잡하고 유용성은 미미하다고 판단되어, 둘 다 이 PEP에 포함하지 않았습니다. 따라서 이러한 형태는 계속 SyntaxError를 발생시켜야 하지만 오류 메시지는 위에서 설명한 새 메시지를 사용해야 합니다. 다만 향후 제안에서 이를 검토할 가능성을 배제해서는 안 됩니다.

제너레이터 표현식 의미론의 대안

또 다른 논의 주제는 제너레이터 표현식에서의 언패킹 의미론, 특히 비동기 제너레이터가 yield from을 지원하지 않는다는 점을 고려할 때 동기 제너레이터 표현식과 비동기 제너레이터 표현식의 의미론 사이의 관계였습니다(PEP 525의 비동기 yield from 절을 참조하십시오).

핵심 질문은 동기 및 비동기 제너레이터 표현식이 언패킹할 때 명시적 루프 대신 yield from(또는 그에 상응하는 방식)을 사용해야 하는가였습니다. 이 선택지들의 주된 차이는 결과 제너레이터가 언패킹되는 객체에 위임하는지 여부이며, 언패킹되는 객체 자체가 제너레이터인 경우 이 차이는 해당 제너레이터 표현식을 .send()/.asend(), .throw()/.athrow(), .close()/.aclose()와 함께 사용할 때의 동작에 영향을 줍니다. 이 선택지들의 차이는 부록: 제너레이터 위임의 의미론에 요약되어 있습니다.

여러 합리적인 선택지가 검토되었지만, Discourse 스레드의 투표에서 뚜렷하게 우세한 것은 없었습니다. 위에서 설명한 제안 외에 다음 방안도 검토되었습니다:

  1. 동기 제너레이터 표현식에서는 언패킹에 yield from을 사용하고 비동기 제너레이터 표현식에서는 명시적 루프를 사용하는 방식(이 PEP의 원래 초안에서 제안한 방식).

    이 전략을 택했다면 제너레이터 표현식에서의 언패킹이 이 연산을 수행하는 제너레이터를 작성할 때 널리 쓰이는 방식(yield from 사용)을 거의 그대로 흉내 낼 수 있었겠지만, 동기 버전과 비동기 버전 사이에 비대칭이 생겼을 것입니다. 이 새 문법과 itertools.chain 및 이중 루프 버전 사이에도 비대칭이 생겼을 것입니다.

  2. 동기 제너레이터 표현식의 언패킹에는 yield from을 사용하고, 비동기 제너레이터 표현식의 언패킹에는 yield from의 동작을 흉내 내는 방식.

    이 전략 역시 동기 제너레이터와 비동기 제너레이터에서 언패킹이 대칭적으로 동작하게 하지만, 더 복잡해집니다. 특히 위임이 필요한 설득력 있는 사용 사례가 없는 상황에서는 그 비용이 이점에 비해 가치가 없을 수도 있을 만큼 복잡합니다.

  3. 동기 제너레이터 표현식의 언패킹에는 yield from을 사용하고, 비동기 제너레이터 표현식에서는 yield from을 지원하게 될 때까지 언패킹을 허용하지 않는 방식.

    이 전략은 향후 비동기 제너레이터 표현식이 yield from을 지원하게 되는 경우 그 시점에 내려지는 어떤 결정이든 하위 호환성을 완전히 유지하도록 보장하여 마찰을 줄일 수도 있습니다. 하지만 그때까지는 동기 제너레이터 표현식과 비동기 제너레이터 표현식 사이에 1번 선택지보다도 더 큰 불일치가 생길 것입니다.

  4. 모든 제너레이터 표현식에서 언패킹을 허용하지 않는 방식.

    이 방식은 두 경우 사이의 대칭성을 유지하지만, 표현력 있는 형태 하나를 잃고 리스트/집합 컴프리헨션과 제너레이터 표현식 사이의 대칭성이 줄어든다는 단점이 있습니다.

이 선택지들(이 PEP에서 제시하는 선택지 포함)에는 각각 장단점이 있으며, 모든 면에서 확실히 우월한 선택지는 없습니다. 위의 의미: 제너레이터 표현식에서 제안한 의미론은 합리적인 절충안입니다. 동기 제너레이터 표현식과 비동기 제너레이터 표현식에서 정확히 같은 종류의 언패킹을 허용하고, 제너레이터 표현식의 기존 속성(하위 제너레이터에 위임하지 않는다는 점)을 유지하기 때문입니다.

향후 비동기 제너레이터가 yield from을 지원하게 되면 이 결정을 재검토해야 합니다. 그 경우 제너레이터 표현식에서의 언패킹이 yield from을 사용하도록 의미론을 조정하는 방안을 고려해야 합니다.

우려 사항과 단점

논의 스레드에서는 이 문법이 명확하고 직관적이라는 데 전반적으로 의견이 모인 것으로 보였지만, 몇 가지 우려 사항과 잠재적인 단점도 제기되었습니다. 이 절에서는 그러한 우려 사항을 요약합니다.

  • 기존 대안과의 중복: 제안된 문법은 언어를 일관되게 확장한 것이고 더 간결한 코드로 이어질 가능성이 높지만, Python에는 이미 같은 작업을 수행하는 방법이 여러 가지 있습니다.
  • 함수 호출의 모호성: f(*x for x in y) 같은 표현식은 처음에는 모호해 보일 수 있습니다. 제너레이터를 언패킹하려는 것인지 단일 인자로 전달하려는 것인지 분명하지 않기 때문입니다. 이 제안은 그 형태를 f((*x for x in y))와 동등하게 취급하여 기존 관례를 유지하지만, 그 동등성이 곧바로 명백하지는 않을 수 있습니다.
  • 과용 또는 남용 가능성: 컴프리헨션에서 언패킹을 복잡하게 사용하면, 명시적인 루프로 작성했다면 더 명확했을 로직이 가려질 수 있습니다. 이는 컴프리헨션 전반에 이미 존재하는 우려이기도 하지만, *와 **가 추가되면 특히 복잡한 사용 사례는 한눈에 읽고 이해하기가 더욱 어려워질 수 있습니다. 예를 들어, 이런 상황은 드물겠지만, 언패킹을 여러 방식으로 사용하는 컴프리헨션에서는 무엇이 언제 언패킹되는지 파악하기 어려울 수 있습니다(예: f(*(*x for *x, _ in list_of_lists))).
  • 불분명한 범위 제한: 이 제안은 언패킹을 컴프리헨션 표현식의 최상위 수준으로 제한하지만, 일부 사용자는 언패킹 연산자의 추가 일반화에서 논의한 것처럼 언패킹 연산자가 더 일반화되리라고 기대할 수 있습니다.
  • 외부 도구에 대한 영향: Python 문법에 대한 모든 변경과 마찬가지로, 이 변경을 적용하면 코드 포매터, 린터, 타입 검사기 등의 유지 관리자가 새 문법을 지원하도록 작업해야 합니다.

부록: 다른 언어

상당수의 다른 언어가 Python에서 이미 사용할 수 있는 것과 유사한 문법으로 이러한 평탄화를 지원하지만, 컴프리헨션 안에서 언패킹 문법을 사용하는 것을 지원하는 경우는 드뭅니다. 이 절에서는 몇몇 다른 언어가 비슷한 문법을 어떻게 지원하는지 간략히 요약합니다.

컴프리헨션을 지원하는 많은 언어가 이중 루프를 지원합니다:

# 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)

다른 여러 언어(컴프리헨션이 없는 언어 포함)는 중첩 구조의 평탄화를 지원하는 내장 함수나 메서드를 통해 이러한 연산을 지원합니다:

# 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}

그러나 컴프리헨션과 언패킹을 모두 지원하는 언어는 대체로 컴프리헨션 안에서의 언패킹을 허용하지 않습니다. 예를 들어, Julia에서 다음 표현식은 현재 문법 오류를 일으킵니다:

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

한 가지 반례로, 최근 Civet에 비슷한 문법에 대한 지원이 추가되었습니다. 예를 들어, 다음은 언패킹에 JavaScript의 ... 문법을 사용하는 유효한 Civet 컴프리헨션입니다:

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

부록: 제너레이터 위임의 의미론

위에서 설명한 의미론과 관련하여 자주 제기된 질문 중 하나는 제너레이터 표현식 안에서 언패킹할 때 yield from을 사용하는 것과 명시적 루프를 사용하는 것의 차이에 관한 것이었습니다. 이는 제너레이터의 상당히 고급 기능이므로, 이 부록에서는 yield from을 사용하는 제너레이터와 명시적 루프를 사용하는 제너레이터 사이의 주요 차이점 몇 가지를 요약합니다.

기본 동작

값을 단순히 순회하는 경우에는 두 방식 모두 동일한 결과를 냅니다. 제너레이터 표현식에서 언패킹은 대부분 이런 용도로 쓰일 것으로 예상합니다.:

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

# Both produce the same sequence of values
x = list(yield_from([[1, 2], [3, 4]]))
y = list(explicit_loop([[1, 2], [3, 4]]))
print(x == y)  # prints True

고급 제너레이터 프로토콜의 차이점

차이점은 고급 제너레이터 프로토콜 메서드인 .send(), .throw(), .close()를 사용할 때, 그리고 하위 이터러블이 단순한 시퀀스가 아니라 그 자체로 제너레이터일 때 드러납니다. 이러한 경우 yield from 버전에서는 해당 신호가 하위 제너레이터에 전달되지만, 명시적 루프를 사용하는 버전에서는 전달되지 않습니다.

.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

# With yield from, values are passed through to sub-generator
gen1 = yield_from()
print(next(gen1))  # prints "first"
print(gen1.send("hello"))  # prints "received: hello"
print(next(gen1))  # prints "last"

# With explicit loop, .send() affects the outer generator; values don't reach the sub-generator
gen2 = explicit_loop()
print(next(gen2))  # prints "first"
print(gen2.send("hello"))  # prints "received: None" (sub-generator receives None instead of "hello")
print(next(gen2))  # prints "last"

.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

# With yield from, exceptions are passed to sub-generator
gen1 = yield_from()
print(next(gen1))  # prints "first"
print(gen1.throw(ValueError("test")))  # prints "caught: test"

# With explicit loop, exceptions affect the outer generator only
gen2 = explicit_loop()
print(next(gen2))  # prints "first"
print(gen2.throw(ValueError("test")))  # ValueError is raised; sub-generator doesn't see it

.close()를 사용한 제너레이터 정리

# hold references to sub-generators so GC doesn't close the explicit loop version
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")

# With yield from, GeneratorExit is passed through to sub-generator
gen1 = yield_from()
print(next(gen1))  # prints "first"
gen1.close()  # closes sub-generator and then outer generator

# With explicit loop, GeneratorExit goes to outer generator only
gen2 = explicit_loop()
print(next(gen2))  # prints "first"
gen2.close()  # only closes outer generator

print('program finished; GC will close the explicit loop subgenerator')
# second inner generator closes when GC closes it at the end

참고 문헌