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 프로그래밍 수업의 필기시험에서 비롯되었습니다. 여러 학생이 이미 Python에 존재한다고 가정하고 제안된 표기법, 특히 set 버전을 답안에 사용했습니다. 이는 이 표기법이 Python의 기존 구문에 논리적이고 일관된 확장임을 시사합니다. 이와 대조적으로 기존의 이중 반복문 버전인 [x for it in its for x in it]은 학생들이 자주 틀리는 방식이며, 많은 학생은 자연스럽게 for 절의 순서를 뒤집으려 합니다. 제안된 구문의 직관성은 이 PEP가 처음 게시된 후 작성된 Reddit 게시물 의 댓글 섹션에서도 뒷받침됩니다. 해당 댓글은 더 광범위한 커뮤니티의 지지를 보여 줍니다.

사양

구문

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

이는 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 ')'

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

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_comprehensioninvalid_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_iterablemap을 대체하면 추가적인 간접 참조 단계를 피할 수 있으며, 컴프리헨션이 일반적으로 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__.pyCounter 클래스에서:
    # 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))
    

이것을 가르치는 방법

현재 컴프리헨션의 개념을 소개하는 일반적인 방법은(파이썬 튜토리얼에서 사용하는 방법이기도 합니다) 동등한 코드를 보여 주는 것입니다. 예를 들어 이 방법에서는 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()

그런 다음 이러한 구체적인 예에서, 별표가 없는 컴프리헨션/제너레이터 식이 컬렉션에 단일 요소를 추가하는 연산자를 사용하는 모든 곳에서 별표가 붙은 식은 대신 해당 컬렉션에 여러 요소를 추가하는 연산자를 사용한다는 개념으로 일반화할 수 있습니다.

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

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

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

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

참고 자료