<div dir="ltr"><div><div><div><div><div class="gmail_extra"><div class="gmail_quote">On Fri, Jun 29, 2018 at 10:53 AM, Michael Selik <span dir="ltr"><<a href="mailto:mike@selik.org" target="_blank">mike@selik.org</a>></span> wrote:<br><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><div dir="ltr"><div>I've drafted a PEP for an easier way to construct groups of elements from a sequence. <a href="https://github.com/selik/peps/blob/master/pep-9999.rst" target="_blank">https://github.com/selik/peps/<wbr>blob/master/pep-9999.rst</a><br></div><div><br></div></div></blockquote><div>I'm really warming to the:</div><div><br></div>Alternate: <span style="font-family:monospace,monospace">collections.Grouping</span></div><div class="gmail_quote"><br></div><div class="gmail_quote">version -- I really like this as a kind of custom mapping, rather than "just a function" (or alternate constructor) -- and I like your point that it can have a bit of functionality built in other than on construction.</div><div class="gmail_quote"><br></div><div class="gmail_quote">But I think it should be more like the other collection classes -- i.e. a general purpose class that can be used for grouping, but also used more general-purpose-y as well. That way people can do their "custom" stuff (key function, etc.) with comprehensions.<br><br></div><div class="gmail_quote">The big differences are a custom <span style="font-family:monospace,monospace">__setitem__</span>:<br><br><span style="font-family:monospace,monospace">    def __setitem__(self, key, value):<br>        self.setdefault(key, []).append(value) <br></span></div><div class="gmail_quote"><br></div><div class="gmail_quote">And the __init__ and update would take an iterable of (key, value) pairs, rather than a single sequence.<br><br></div><div class="gmail_quote">This would get away from the<span style="font-family:monospace,monospace"> itertools.groupby</span> approach, which I find kinda awkward:<br><br>* How often do you have your data in a single sequence?<br></div><div class="gmail_quote"><br></div><div class="gmail_quote">* Do you need your keys (and values!) to be sortable???)<br><br></div><div class="gmail_quote">* Do we really want folks to have to be writing custom key functions and/or lambdas for really simple stuff?<br><br></div><div class="gmail_quote">* and you may need to "transform" both your keys and values<br></div><div class="gmail_quote"><br></div><div class="gmail_quote">I've enclosed an example implementation, borrowing heavily from Michael's code.<br><br></div><div class="gmail_quote">The test code has a couple examples of use, but I'll put them here for the sake of discussion.<br><br></div><div class="gmail_quote">Michael had:<br><br><span style="font-family:monospace,monospace">Grouping('AbBa', key=c.casefold))</span><br><br>with my code, that would be:<br><span style="font-family:monospace,monospace"><br>Grouping(((c.casefold(), c) for c in 'AbBa'))</span><br><br></div><div class="gmail_quote">Note that the key function is applied outside the Grouping object, it doesn't need to know anything about it -- and then users can use an expression in a comprehension rather than a key function.<br></div><div class="gmail_quote"><br></div><div class="gmail_quote">This looks a tad clumsier with my approach, but this is a pretty contrived example -- in the more common case [*], you'd be writing a bunch of lambdas, etc, and I'm not sure there is a way to get the values customized as well, if you want that. (without applying a map later on)<br><br></div><div class="gmail_quote">Here is the example that the OP posted that kicked off this thread:<br><span style="font-family:monospace,monospace"><br>In [37]: student_school_list = [('Fred', 'SchoolA'),<br>    ...:                        ('Bob', 'SchoolB'),<br>    ...:                        ('Mary', 'SchoolA'),<br>    ...:                        ('Jane', 'SchoolB'),<br>    ...:                        ('Nancy', 'SchoolC'),<br>    ...:                        ]                 <br><br>In [38]: Grouping(((item[1], item[0]) for item in student_school_list))<br>Out[38]: Grouping({'SchoolA': ['Fred', 'Mary'],<br>                   'SchoolB': ['Bob', 'Jane'],<br>                   'SchoolC': ['Nancy']})<br></span></div><div class="gmail_quote"><span style="font-family:monospace,monospace"><br></span></div><div class="gmail_quote"><span style="font-family:monospace,monospace">or <br><br>In [40]: Grouping((reversed(item) for item in student_school_list))<br>Out[40]: Grouping({'SchoolA': ['Fred', 'Mary'], 'SchoolB': ['Bob', 'Jane'], 'SchoolC': ['Nancy']})<br></span></div><div class="gmail_quote"><span style="font-family:monospace,monospace"><br></span></div>(note that if those keys and values were didn't have to be reversed, you could just pass the list in raw.<div class="gmail_quote"><br>I really like how I can use a generator expression and simple expressions to transform the data in the way I need, rather than having to make key functions.<span style="font-family:monospace,monospace"><br><br></span></div>And with Michael's approach, I think you'd need to call <span style="font-family:monospace,monospace">.map()</span> after generating the grouping -- a much klunkier way to do it. (and you'd get  plain dict rather than a Grouping that you could add stuff too later...)<br><br></div><div class="gmail_extra">I'm sure there are ways to improve my code, and maybe <span style="font-family:monospace,monospace">Grouping</span> isn't the best name, but I think something like this would be a nice addition to the collections module.<br><br></div><div class="gmail_extra">-CHB<br><br></div>[*] -- before making any decisions about the best API, it would probably be a good idea to collect examples of the kind of data that people really do need to group like this. Does it come in (key, value) pairs naturally? or in one big sequence with a key function that's easy to write? who knows without examples of real world use cases.<br><br></div>I will show one "real world" example here:<br><br></div>In my Python classes, I like to use Dave Thomas' trigrams: "code kata":<br><br><a href="http://codekata.com/kata/kata14-tom-swift-under-the-milkwood/" target="_blank">http://codekata.com/kata/<wbr>kata14-tom-swift-under-the-<wbr>milkwood/</a><br><br></div>A key piece of this is building up a data structure with word pairs, and a list of all the words that follow the pair in a piece of text.<br><br></div>This is a nice exercise to help people think about how to use dicts, etc. Currently the most clean code uses .<span style="font-family:monospace,monospace">setdefault</span>:<br><br><span style="font-family:monospace,monospace">    word_pairs = {}<br>    # loop through the words<br>    # (rare case where using the index to loop is easiest)<br>    for i in range(len(words) - 2):  # minus 2, 'cause you need a pair<br>        pair = tuple(words[i:i + 2])  # a tuple so it can be a key in the dict<br>        follower = words[i + 2]<br>        word_pairs.setdefault(pair, []).append(follower)<br></span><div><div><div><div><div><div class="gmail_extra"><div class="gmail_quote"><br></div><div class="gmail_quote">if this were done with my <span style="font-family:monospace,monospace">Grouping</span> class, it would be:<br><br><span style="font-family:monospace,monospace">In [53]: word_pairs = Grouping()<br><br>In [54]: for i in range(len(words) - 2):<br>    ...:     pair = tuple(words[i:i + 2])  # a tuple so it can be a key in the dict<br>    ...:     follower = words[i + 2]<br>    ...:     word_pairs[pair] = follower<br>    ...:   </span>  <br><span style="font-family:monospace,monospace"><br>In [55]: word_pairs<br>Out[55]: Grouping({('I', 'wish'): ['I', 'I'], ('wish', 'I'): ['may', 'might'], ('I', 'may'): ['I'], ('may', 'I'): ['wish']})</span><br><br>Not that different, really, but it saves folks from having to find and understand setdefault. But you could also make it into a generator expression like so:<br><br><span style="font-family:monospace,monospace">In [56]: Grouping(((w1, w2), w3) for w1, w2, w3, in zip(words[:], words[1:], words[2:]))<br>Out[56]: Grouping({('I', 'wish'): ['I', 'I'], ('wish', 'I'): ['may', 'might'], ('I', 'may'): ['I'], ('may', 'I'): ['wish']})</span><br></div><div class="gmail_quote"><br>which I think is pretty slick. And satisfies the OP's desire for a comprehension-like approach, rather than the:<br><br></div><div class="gmail_quote">- create an empty dict<br></div><div class="gmail_quote">- loop through the iterable<br></div><div class="gmail_quote">- use setdefault in the loop<br></div><div class="gmail_quote"><br></div><div class="gmail_quote">approach.<br></div><div class="gmail_quote"><br></div>> As a teacher, I've found that grouping is one of the most awkward tasks for beginners to learn in Python. While this proposal requires understanding a key-function, in my experience that's easier to teach than the nuances of setdefault or defaultdict.<br><br></div><div class="gmail_extra">well, yes, and no -- as above, I use an example of this in teaching so that I CAN teach the nuances of setdefault -- or at least dicts themselves (most student use a if key in dict" construct before I tell them about setdefault)<br><br></div><div class="gmail_extra">So if you are teaching, say data analysis with Python -- it might be nice to have this builtin, but if you are teaching "programming with Python" I'd probably encourage them to do it by hand first anyway :-)<br></div><div class="gmail_extra"><br>> Defaultdict requires passing a factory function or class, similar to a key-function. Setdefault is awkwardly named and requires a discussion of references and mutability.<br><br></div><div class="gmail_extra">I agree that the naming is awkward, but I haven't found confusion with references an mutabilty from this --though I do keep hammering those points throughout the class anyway :-)<br></div><div class="gmail_extra"><br></div><div class="gmail_extra">and my approach doesn't require any key functions either :-)<br><br><br></div><div class="gmail_extra"><br clear="all"><div><br></div>-- <br><div class="gmail-m_5112643852996694157gmail-m_-6921579776099953516gmail_signature"><br>Christopher Barker, Ph.D.<br>Oceanographer<br><br>Emergency Response Division<br>NOAA/NOS/OR&R            (206) 526-6959   voice<br>7600 Sand Point Way NE   (206) 526-6329   fax<br>Seattle, WA  98115       (206) 526-6317   main reception<br><br><a href="mailto:Chris.Barker@noaa.gov" target="_blank">Chris.Barker@noaa.gov</a></div>
</div></div></div></div></div></div></div>