GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing peps.python.org — PEP 798 – Unpacking in Comprehensions | peps.python.org
PEP 798 – Unpacking in Comprehensions
PEP 798 – Unpacking in Comprehensions
Important This PEP is a historical document. The up-to-date, canonical documentation can now be found at Displays for lists, sets and dictionaries, Dictionary displays. × See PEP 1 for how to propose changes. AbstractThis PEP proposes extending list, set, and dictionary comprehensions, as well
as generator expressions, to allow unpacking notation ( [*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' MotivationExtended unpacking notation ( [*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 But if we want to similarly combine an arbitrary number of iterables, we cannot use unpacking in this same way. That said, we do have a few options for combining multiple iterables. We could, for example, use explicit looping structures and built-in means of combination: 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 Or, we could be more concise by using a comprehension with two loops: [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) Or, we could use 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) Or, for all but the generator, we could use functools.reduce(operator.iconcat, its, (new_list := [])) functools.reduce(operator.ior, its, (new_set := set())) functools.reduce(operator.ior, its, (new_dict := {})) This PEP proposes allowing unpacking operations to be used in comprehensions as an additional alternative: [*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' This proposal also extends to asynchronous comprehensions and generator
expressions, such that, for example, RationaleCombining multiple iterable objects together into a single object is a common task. For example, one StackOverflow post asking about flattening a list of lists has been viewed 4.6 million times, and there are several examples of code from the standard library that perform this operation (see Code Examples). While Python provides a means of combining a small, known number of iterables using extended unpacking from PEP 448, no comparable syntax currently exists for combining an arbitrary number of iterables. This proposal represents a natural extension of the language, paralleling
existing syntactic structures: where We expect this syntax to be intuitive and familiar to programmers already
comfortable with both comprehensions and unpacking notation. This proposal was
motivated in part by a written exam in a Python programming class, where
several students used the proposed notation (specifically the SpecificationSyntaxThe grammar should be changed to allow the expression in list/set
comprehensions and generator expressions to be preceded by a This can be accomplished by updating the listcomp[expr_ty]:
| '[' a=star_named_expression b=for_if_clauses ']'
setcomp[expr_ty]:
| '{' a=star_named_expression b=for_if_clauses '}'
The rule for genexp[expr_ty]:
| '(' a=(assignment_expression | expression !':=' | starred_expression) b=for_if_clauses ')'
The rule for dictionary comprehensions would need to be adjusted as well, to allow for this new form: dictcomp[expr_ty]:
| '{' a=double_starred_kvpair b=for_if_clauses '}'
No change should be made to the way that argument unpacking is handled in
function calls, i.e., the general rule that generator expressions provided as
the sole argument to functions do not require additional redundant parentheses
should be retained. Note that this implies that, for example,
Semantics: List/Set/Dict ComprehensionsThe meaning of a starred expression in a list comprehension Said another way, the objects created by the following comprehensions: new_list = [*expr for x in its] new_set = {*expr for x in its} new_dict = {**expr for d in dicts} should be equivalent to the objects created by the following pieces of code, respectively: 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) Semantics: Generator ExpressionsGenerator expressions using the unpacking syntax should form new generators
producing values from the concatenation of the iterables given by the
expressions. Specifically, the behavior is defined to be equivalent to the
following (though without defining or referencing the looping variable # 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() See Alternative Generator Expression Semantics for more discussion of these semantics. Interaction with Assignment ExpressionsNote that this proposal does not suggest changing the order of evaluation of
the various pieces of the comprehension, nor any rules about scoping. This is
particularly relevant for generator expressions that make use of the “walrus
operator” As an example, consider the generator that results from evaluating the
expression def generator(): for i in (0, 2, 4): for j in (y := [i, i+1]): yield j In this example, the subexpression >>> 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] Error ReportingCurrently, the proposed syntax generates a Additional specific error messages should be provided in at least the following cases:
Reference ImplementationThe reference implementation implements this functionality, including draft documentation and additional test cases. Backwards CompatibilityThe behavior of all comprehensions that are currently syntactically valid would
be unaffected by this change, so we do not anticipate much in the way of
backwards-incompatibility concerns. In principle, this change would only
affect code that relied on the fact that attempting to use unpacking operations
in comprehensions would raise a One related concern is that a hypothetical future decision to change the
semantics of generator expressions to make use of Code ExamplesThis section shows some illustrative examples of how small pieces of code from the standard library could be rewritten to make use of this new syntax. The Reference Implementation continues to pass all tests with these replacements made. Replacing Explicit LoopsReplacing explicit loops compresses multiple lines into one, and avoids the need for defining and referencing an auxiliary variable.
Replacing from_iterable and FriendsWhile not always the right choice, replacing
Replacing Double Loops in ComprehensionsReplacing double loops in comprehensions avoids the need for defining and referencing an auxiliary variable.
How to Teach ThisCurrently, a common way to introduce the notion of comprehensions (which is
employed by the Python Tutorial) is to demonstrate equivalent code. For
example, this method would say that, for example, out = [] for x in it: out.append(expr) Taking this approach, we can introduce out = [] for x in it: out.extend(expr) Set and dict comprehensions that make use of unpacking can also be introduced by a similar analogy: # 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) And we can take a similar approach to illustrate the behavior of generator expressions that involve unpacking: # 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() We can then generalize from these specific examples to the idea that, wherever a non-starred comprehension/genexp would use an operator that adds a single element to a collection, the starred would instead use an operator that adds multiple elements to that collection. Alternatively, we don’t need to think of the two ideas as separate; instead,
with the new syntax, we can think of out = [] for x in it: out.extend([...x...]) Similarly, we can think of out = set() # or out = {} for x in it: out.update({...x...}) These examples are equivalent in the sense that the output they produce would
be the same in both the version with the comprehension and the version without
it, but note that the non-comprehension version is slightly less efficient due
to making new lists/sets/dictionaries before each Rejected Alternative ProposalsThe primary goal when thinking through the specification above was consistency with existing norms around unpacking and comprehensions / generator expressions. One way to interpret this is that the goal was to write the specification so as to require the smallest possible change(s) to the existing grammar and code generation, letting the existing code inform the surrounding semantics. Below we discuss some of the common concerns/alternative proposals that came up in discussions but that are not included in this proposal. Starred Generators as Function ArgumentsOne common concern that has arisen multiple times (not only in the discussion
threads linked above but also in previous discussions around this same idea) is
a possible syntactical ambiguity when passing a starred generator as the sole
argument to This proposal suggests that
The reason to prefer this proposal over these alternatives is the preservation of existing conventions for punctuation around generator expressions. Currently, the general rule is that generator expressions must be wrapped in parentheses except when provided as the sole argument to a function, and this proposal suggests maintaining that rule even as we allow more kinds of generator expressions. This option maintains a full symmetry between comprehensions and generator expressions that use unpacking and those that don’t. Currently, we have the following conventions: 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) This proposal opts to maintain those conventions even when the comprehensions make use of unpacking: 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) Further Generalizing Unpacking OperatorsAnother suggestion that came out of the discussion involved further
generalizing the
These variants were considered substantially more complex (both to understand
and to implement) and of only marginal utility, so neither is included in this
PEP. As such, these forms should continue to raise a Alternative Generator Expression SemanticsAnother point of discussion centered around the semantics of unpacking in
generator expressions, particularly the relationship between the semantics of
synchronous and asynchronous generator expressions given that async generators
do not support The core question centered around whether sync and async generator expressions
should use Several reasonable options were considered, none of which was a clear winner in a poll in the Discourse thread. Beyond the proposal outlined above, the following were also considered:
Each of these options (including the one presented in this PEP) has its benefits and drawbacks, with no option being clearly superior on all fronts. The semantics proposed in Semantics: Generator Expressions above represent a reasonable compromise by allowing exactly the same kind of unpacking in synchronous and asynchronous generator expressions and retaining an existing property of generator expressions (that they do not delegate to subgenerators). This decision should be revisited in the event that asynchronous generators
receive support for Concerns and DisadvantagesAlthough the general consensus from the discussion thread seemed to be that this syntax was clear and intuitive, several concerns and potential downsides were raised as well. This section aims to summarize those concerns.
Appendix: Other LanguagesQuite a few other languages support this kind of flattening with syntax similar to what is already available in Python, but support for using unpacking syntax within comprehensions is rare. This section provides a brief summary of support for similar syntax in a few other languages. Many languages that support comprehensions support double loops: # 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) Several other languages (even those without comprehensions) support these operations via a built-in function or method to support flattening of nested structures: # 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} However, languages that support both comprehension and unpacking do not tend to allow unpacking within a comprehension. For example, the following expression in Julia currently leads to a syntax error: [xs... for xs in [[1,2,3], [], [4,5]]] As one counterexample, support for a similar syntax was recently added to Civet. For example, the following is a valid comprehension in
Civet, making use of JavaScript’s for xs of [[1,2,3], [], [4,5]] then ...(xs++xs) Appendix: Semantics of Generator DelegationOne of the common questions about the semantics outlined above had to do with
the difference between using Basic BehaviorFor simple iteration over values, which we expect to be by far the most-common use of unpacking in generator expressions, both approaches produce identical results: 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 Advanced Generator Protocol DifferencesThe differences become apparent when using the advanced generator protocol
methods Delegation with
|
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |