[Python-Dev] (no subject)

Donald Stufft donald at stufft.io
Tue Feb 10 02:48:01 CET 2015


> On Feb 9, 2015, at 8:34 PM, Neil Girdhar <mistersheik at gmail.com> wrote:
> 
> 
> 
> On Mon, Feb 9, 2015 at 7:53 PM, Donald Stufft <donald at stufft.io <mailto:donald at stufft.io>> wrote:
> 
>> On Feb 9, 2015, at 7:29 PM, Neil Girdhar <mistersheik at gmail.com <mailto:mistersheik at gmail.com>> wrote:
>> 
>> For some reason I can't seem to reply using Google groups, which is is telling "this is a read-only mirror" (anyone know why?)  Anyway, I'm going to answer as best I can the concerns.
>> 
>> Antoine said:
>> 
>> To be clear, the PEP will probably be useful for one single line of 
>> Python code every 10000. This is a very weak case for such an intrusive 
>> syntax addition. I would support the PEP if it only added the simple 
>> cases of tuple unpacking, left alone function call conventions, and 
>> didn't introduce **-unpacking. 
>> 
>> To me this is more of a syntax simplification than a syntax addition.  For me the **-unpacking is the most useful part. Regarding utility, it seems that a many of the people on r/python were pretty excited about this PEP: http://www.reddit.com/r/Python/comments/2synry/so_8_peps_are_currently_being_proposed_for_python/ <http://www.reddit.com/r/Python/comments/2synry/so_8_peps_are_currently_being_proposed_for_python/>
>> 
>>>> 
>> Victor noticed that there's a mistake with the code:
>> 
>> >>> ranges = [range(i) for i in range(5)] 
>> >>> [*item for item in ranges] 
>> [0, 0, 1, 0, 1, 2, 0, 1, 2, 3] 
>> 
>> It should be a range(4) in the code.  The "*" applies to only item.  It is the same as writing:
>> 
>> [*range(0), *range(1), *range(2), *range(3), *range(4)]
>> 
>> which is the same as unpacking all of those ranges into a list.
>> 
>> > function(**kw_arguments, **more_arguments) 
>> If the key "key1" is in both dictionaries, more_arguments wins, right? 
>> 
>> There was some debate and it was decided that duplicate keyword arguments would remain an error (for now at least).  If you want to merge the dictionaries with overriding, then you can still do:
>> 
>> function(**{**kw_arguments, **more_arguments})
>> 
>> because **-unpacking in dicts overrides as you guessed.
>> 
>>>> 
>> 
>> 
>> On Mon, Feb 9, 2015 at 7:12 PM, Donald Stufft <donald at stufft.io <mailto:donald at stufft.io>> wrote:
>> 
>>> On Feb 9, 2015, at 4:06 PM, Neil Girdhar <mistersheik at gmail.com <mailto:mistersheik at gmail.com>> wrote:
>>> 
>>> Hello all,
>>> 
>>> The updated PEP 448 (https://www.python.org/dev/peps/pep-0448/ <https://www.python.org/dev/peps/pep-0448/>) is implemented now based on some early work by Thomas Wouters (in 2008) and Florian Hahn (2013) and recently completed by Joshua Landau and me.
>>> 
>>> The issue tracker http://bugs.python.org/issue2292 <http://bugs.python.org/issue2292>  has  a working patch.  Would someone be able to review it?
>>> 
>> 
>> I just skimmed over the PEP and it seems like it’s trying to solve a few different things:
>> 
>> * Making it easy to combine multiple lists and additional positional args into a function call
>> * Making it easy to combine multiple dicts and additional keyword args into a functional call
>> * Making it easy to do a single level of nested iterable "flatten".
>> 
>> I would say it's:
>> * making it easy to unpack iterables and mappings in function calls
>> * making it easy to unpack iterables  into list and set displays and comprehensions, and
>> * making it easy to unpack mappings into dict displays and comprehensions.
>> 
>>  
>> 
>> Looking at the syntax in the PEP I had a hard time detangling what exactly it was doing even with reading the PEP itself. I wonder if there isn’t a way to combine simpler more generic things to get the same outcome.
>> 
>> Looking at the "Making it easy to combine multiple lists and additional positional args into a  function call" aspect of this, why is:
>> 
>> print(*[1], *[2], 3) better than print(*[1] + [2] + [3])?
>> 
>> That's already doable in Python right now and doesn't require anything new to handle it.
>> 
>> Admittedly, this wasn't a great example.  But, if [1] and [2] had been iterables, you would have to cast each to list, e.g.,
>> 
>> accumulator = []
>> accumulator.extend(a) 
>> accumulator.append(b)
>> accumulator.extend(c)
>> print(*accumulator)
>> 
>> replaces
>> 
>> print(*a, b, *c)
>> 
>> where a and c are iterable.  The latter version is also more efficient because it unpacks only a onto the stack allocating no auxilliary list.
> 
> Honestly that doesn’t seem like the way I’d write it at all, if they might not be lists I’d just cast them to lists:
> 
> print(*list(a) + [b] + list(c))
> 
> Sure, that works too as long as you put in the missing parentheses.

There are no missing parentheses, the * and ** is last in the order of operations (though the parens would likely make that more clear).

>  
> 
> But if casting to list really is that big a deal, then perhaps a better solution is to simply make it so that something like ``a_list + an_iterable`` is valid and the iterable would just be consumed and +’d onto the list. That still feels like a more general solution and a far less surprising and easier to read one.
> 
> I understand.  However I just want to point out that 448 is more general.  There is no binary operator for generators.  How do you write (*a, *b, *c)?  You need to use itertools.chain(a, b, c).

I don’t feel like using itertools.chain is a bad thing TBH, it’s extremely clear as to what’s going on, you’re chaining a bunch a bunch of iterables together. I would not however be super upset if the ability to do * and ** multiple times in a function was added, I just don’t think it’s very useful for * (since you can already get that behavior with things I believe are clear-er) and I think getting similar constructs for ** would bring that up to parity.

I am really really -1 on the comprehension syntax.

>  
> 
> 
>> 
>> 
>> Looking at the "making it easy to do a single level of nsted iterable 'flatten'"" aspect of this, the example of:
>> 
>> >>> ranges = [range(i) for i in range(5)]
>> >>> [*item for item in ranges]
>> [0, 0, 1, 0, 1, 2, 0, 1, 2, 3]
>> 
>> Conceptually a list comprehension like [thing for item in iterable] can be mapped to a for loop like this:
>> 
>> result = []
>> for item in iterable:
>>     result.append(thing)
>> 
>> However [*item for item in ranges] is mapped more to something like this:
>> 
>> result = []
>> for item in iterable:
>>     result.extend(*item)
>> 
>> I feel like switching list comprehensions from append to extend just because of a * is really confusing and it acts differently than if you just did *item outside of a list comprehension. I feel like the itertools.chain() way of doing this is *much* clearer.
>> 
>> Finally there's the "make it easy to combine multiple dicts into a function call" aspect of this. This I think is the biggest thing that this PEP actually adds, however I think it goes around it the wrong way. Sadly there is nothing like [1] + [2] for dictionaries. The closest thing is:
>> 
>> kwargs = dict1.copy()
>> kwargs.update(dict2)
>> func(**kwargs)
>> 
>> So what I wonder is if this PEP wouldn't be better off just using the existing methods for doing the kinds of things that I pointed out above, and instead defining + or | or some other symbol for something similar to [1] + [2] but for dictionaries. This would mean that you could simply do:
>> 
>> func(**dict1 | dict(y=1) | dict2)
>> 
>> instead of
>> 
>> dict(**{'x': 1}, y=2, **{'z': 3})
>> 
>> I feel like not only does this genericize way better but it limits the impact and new syntax being added to Python and is a ton more readable.
> 
> 
> Honestly the use of * and ** in functions doesn’t bother me a whole lot, though i don’t see much use for it over what’s already available for lists (and I think doing something similarly generic for mapping is a better idea). What really bothers me is these parts:
> 
> * making it easy to unpack iterables  into list and set displays and comprehensions, and
> * making it easy to unpack mappings into dict displays and comprehensions.
> 
> I feel like these are super wrong and if they were put in I’d probably end up writing a linter to disallow them in my own code bases.
> 
> I feel like adding a special case for * in list comprehensions breaks the “manually expanded” version of those. Switching from append to extend inside of a list comprehension because of a * doesn’t make any sense to me. I can’t seem to construct any for loop that mimics what this PEP proposes as [*item for item in iterable] without fundamentally changing the operation that happens in each loop of the list comprehension.
> 
> 
> I don't know what you mean by this.  You can write [*item for item in iterable] in current Python as [it for item in iterable for it in item].  You can unroll that as:
> a = []
> for item in iterable:
>   for it in item:
>     a.append(it)
> 
> — or yield for generators or add for sets.

I don’t think * means “loop” anywhere else in Python and I would never “guess” that [*item for item in iterable] meant that. It’s completely non intuitive. Anywhere else you see *foo it’s unpacking a tuple not making an inner loop. That means that anywhere else in Python *item is the same thing as item[0], item[1], item[2], …, but this PEP makes it so just inside of a comprehension it actually means “make a second, inner loop” instead of what I think anyone who has learned that syntax would expect, which is it should be equivalent to [(item[0], item[1], item[2], …) for item in iterable].

---
Donald Stufft
PGP: 7C6B 7C5D 5E2B 6356 A926 F04F 6E3C BCE9 3372 DCFA

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-dev/attachments/20150209/e0cb6a65/attachment.html>


More information about the Python-Dev mailing list