[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
표현식을 사용할 수 있도록 허용해야 합니다.
이는 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 ')'
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()
이러한 의미론의 세부 사항은 미래에 재검토되어야 합니다. 특히 비동기 generator
가 yield 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_comprehension
및 invalid_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_iterable
및 map
을 대체하면 추가적인 간접 레벨을 피할 수 있어, 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)
언패킹을 사용하는 set
및 dict 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
의 언패킹 의미론, 특히 비동기 generator
가 yield 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 expression
이yield from
이 비동기generator
에서 더 일반적으로 지원될 때까지yield from
과 유사한 의미론을 사용해서는 안 된다고 제안합니다. -
동기
generator expression
의 언패킹에yield from
을 사용하고, 비동기generator expression
이yield from
을 지원할 때까지 비동기generator expression
에서의 언패킹을 금지하는 것.이 전략은 비동기
generator expression
이 미래에yield from
에 대한 지원을 얻게 된다면, 그때 내려지는 모든 결정이 완전히 하위 호환성을 갖도록 보장함으로써 마찰을 줄일 수 있을 것입니다. 그러나 그 컨텍스트에서 언패킹의 유용성은 비동기generator expression
이yield 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