Dictionary Comprehension from List of Dictionaries [duplicate] - python

I have a list of tuples with duplicates and I've converted them to a dictionary using this code I found here:
https://stackoverflow.com/a/61201134/2415706
mylist = [(a,1),(a,2),(b,3)]
result = {}
for i in mylist:
result.setdefault(i[0],[]).append(i[1])
print(result)
>>> result = {a:[1,2], b:[3]}
I recall learning that most for loops can be re-written as comprehensions so I wanted to practice but I've failed for the past hour to make one work.
I read this: https://stackoverflow.com/a/56011919/2415706 and now I haven't been able to find another library that does this but I'm also not sure if this comprehension I want to write is a bad idea since append mutates things.

Comprehension is meant to map items in a sequence independent of each other, and is not suitable for aggregations such as the case in your question, where the sub-list an item appends to depends on a sub-list that a previous item appends to.
You can produce the desired output with a nested comprehension if you must, but it would turn what would've been solved in O(n) time complexity with a loop into one that takes O(n ^ 2) instead:
{k: [v for s, v in mylist if s == k] for k, _ in mylist}

Related

dictionary comprehension to append variable length lists

I’m looking for the pythonic way to create a dictionary of lists where you append values to a list for a series of keys. So a dictionary n which stores the sum of digits for the values up to 1000, with the sum of the digits the key, for example:
n[25] = [799, 889, 898, 979, 988, 997]
Using a basic list comprehension doesn't work as it overwrites smaller values and only allows one, the largest, value per key,
n = {sumdigits(i): i for i in range(1000)}
I've got a two line working version below, but I am curious whether there is a neat one line solution to create a dictionary of variable length lists.
def sumdigits(x):
return sum(int(i) for i in str(x))
n = defaultdict(list)
for i in range(1000):
n[sumdigits(i)].append(i)
What you already have is very pythonic. There's absolutely nothing wrong with it and crucially it's clear what's happening.
If you really want a one line solution, I think you need 2 loops. One loop for values of n, which can be anything from 1 to 27 (sumdigits(999)). Then another loop to go over the items in the range 1-1000.
Here's what that would look like, but it's very inefficient from a time complexity view. What you have has time complexity O(n) which is good. But doing it in a comprehension has complexity
O(n^sumdigits(n-1)). Because for every key you have to iterate over the entire range 1-1000, but most of those are discarded.
{n: [i for i in range(1000) if sumdigits(i) == n] for n in range(sumdigits(999) + 1)}
This is also a possibility that is one line (as you don't count the defaultdict initialization in your 2-line solution). With the advantage that it is significantly faster than the other solutions.
n = defaultdict(list)
{n[sum(int(d) for d in str(nb))].append(nb) for nb in range(1000)}
or really in one line (using the walrus operator python3.8 +)
{n := collections.defaultdict(list)[sum(int(i) for i in str(x))].append(x) for x in range(NB)}
If you really want a one-line solution, you can do the following combining list and dict comprehension:
dct = {sumdigits(i): [j for j in range(1000) if sumdigits(i)==sumdigits(j)] for i in range(1000)}
That said, I do not think that it gets more pythonic than the simple for loop you've suggested yourself and I think you should stick to that due to performance reasons as well.

Dictionary comprehension to create dictionary of even numbers

I want to create a dictionary of even numbers with the keys being consecutive integers using dictionary comprehension
The output should be:
{1:2,2:4,3:6,4:8}
I used 2 lines of code ie one line being the list comprehension to get the even numbers and the second being the dictionary comprehension to get the desired output.
The code i used is as follows:
evens=[number for number in range(1,10) if number%2==0]
even_dict={k:evens[k-1] for k in range(1,len(evens)+1)}
My question is instead of using 2 lines of code, can we use a single line of code which involves only dictionary comprehension to get the desired output?
According to what is your desired output, you can simply do:
d = {x: 2*x for x in range(1, 5)}
The way you have it now, you have to define evens before since you are using it in two places in the dict comprehension: To iterate the indices, and to get the actual element. Generally, whenever you need both the index and the element, you can use enumerate instead, possible with start parameter if you want to offset the index:
even_dict = {i: x for i, x in enumerate(evens, start=1)}
Now you only need evens once, and thus you could "inline" it into the dict comprehension:
even_dict = {i: x for i, x in enumerate([number for number in range(1,10) if number%2==0], start=1)}
But you do not need that inner list comprehension at all; to get the even numbers, you could just use range with step parameter:
even_dict = {i: x for i, x in enumerate(range(2, 10, 2), start=1)}
And, finally, in this particular case, you would not even need that, either, as you can just multiply the key with two to get the value, as shown in #olinox's answer.

Creating a list by iterating over a dictionary

I defined a dictionary like this (list is a list of integers):
my_dictionary = {'list_name' : list, 'another_list_name': another_list}
Now, I want to create a new list by iterating over this dictionary. In the end, I want it to look like this:
my_list = [list_name_list_item1, list_name_list_item2,
list_name_list_item3, another_list_name_another_list_item1]
And so on.
So my question is: How can I realize this?
I tried
for key in my_dictionary.keys():
k = my_dictionary[key]
for value in my_dictionary.values():
v = my_dictionary[value]
v = str(v)
my_list.append(k + '_' + v)
But instead of the desired output I receive a Type Error (unhashable type: 'list') in line 4 of this example.
You're trying to get a dictionary item by it's value whereas you already have your value.
Do it in one line using a list comprehension:
my_dictionary = {'list_name' : [1,4,5], 'another_list_name': [6,7,8]}
my_list = [k+"_"+str(v) for k,lv in my_dictionary.items() for v in lv]
print(my_list)
result:
['another_list_name_6', 'another_list_name_7', 'another_list_name_8', 'list_name_1', 'list_name_4', 'list_name_5']
Note that since the order in your dictionary is not guaranteed, the order of the list isn't either. You could fix the order by sorting the items according to keys:
my_list = [k+"_"+str(v) for k,lv in sorted(my_dictionary.items()) for v in lv]
Try this:
my_list = []
for key in my_dictionary:
for item in my_dictionary[key]:
my_list.append(str(key) + '_' + str(item))
Hope this helps.
Your immediate problem is that dict().values() is a generator yielding the values from the dictionary, not the keys, so when you attempt to do a lookup on line 4, it fails (in this case) as the values in the dictionary can't be used as keys. In another case, say {1:2, 3:4}, it would fail with a KeyError, and {1:2, 2:1} would not raise an error, but likely give confusing behaviour.
As for your actual question, lists do not attribute any names to data, like dictionaries do; they simply store the index.
def f()
a = 1
b = 2
c = 3
l = [a, b, c]
return l
Calling f() will return [1, 2, 3], with any concept of a, b, and c being lost entirely.
If you want to simply concatenate the lists in your dictionary, making a copy of the first, then calling .extend() on it will suffice:
my_list = my_dictionary['list_name'][:]
my_list.extend(my_dictionary['another_list_name'])
If you're looking to keep the order of the lists' items, while still referring to them by name, look into the OrderedDict class in collections.
You've written an outer loop over keys, then an inner loop over values, and tried to use each value as a key, which is where the program failed. Simply use the dictionary's items method to iterate over key,value pairs instead:
["{}_{}".format(k,v) for k,v in d.items()]
Oops, failed to parse the format desired; we were to produce each item in the inner list. Not to worry...
d={1:[1,2,3],2:[4,5,6]}
list(itertools.chain(*(
["{}_{}".format(k,i) for i in l]
for (k,l) in d.items() )))
This is a little more complex. We again take key,value pairs from the dictionary, then make an inner loop over the list that was the value and format those into strings. This produces inner sequences, so we flatten it using chain and *, and finally save the result as one list.
Edit: Turns out Python 3.4.3 gets quite confused when doing this nested as generator expressions; I had to turn the inner one into a list, or it would replace some combination of k and l before doing the formatting.
Edit again: As someone posted in a since deleted answer (which confuses me), I'm overcomplicating things. You can do the flattened nesting in a chained comprehension:
["{}_{}".format(k,v) for k,l in d.items() for v in l]
That method was also posted by Jean-François Fabre.
Use list comprehensions like this
d = {"test1":[1,2,3,],"test2":[4,5,6],"test3":[7,8,9]}
new_list = [str(item[0])+'_'+str(v) for item in d.items() for v in item[1]]
Output:
new_list:
['test1_1',
'test1_2',
'test1_3',
'test3_7',
'test3_8',
'test3_9',
'test2_4',
'test2_5',
'test2_6']
Let's initialize our data
In [1]: l0 = [1, 2, 3, 4]
In [2]: l1 = [10, 20, 30, 40]
In [3]: d = {'name0': l0, 'name1': l1}
Note that in my example, different from yours, the lists' content is not strings... aren't lists heterogeneous containers?
That said, you cannot simply join the keys and the list's items, you'd better cast these value to strings using the str(...) builtin.
Now it comes the solution to your problem... I use a list comprehension
with two loops, the outer loop comes first and it is on the items (i.e., key-value couples) in the dictionary, the inner loop comes second and it is on the items in the corresponding list.
In [4]: res = ['_'.join((str(k), str(i))) for k, l in d.items() for i in l]
In [5]: print(res)
['name0_1', 'name0_2', 'name0_3', 'name0_4', 'name1_10', 'name1_20', 'name1_30', 'name1_40']
In [6]:
In your case, using str(k)+'_'+str(i) would be fine as well, but the current idiom for joining strings with a fixed 'text' is the 'text'.join(...) method. Note that .join takes a SINGLE argument, an iterable, and hence in the list comprehension I used join((..., ...))
to collect the joinands in a single argument.

How do you nest list comprehensions

I have been assigned homework and have spent hours running in circles attempting to nest comprehensions. Specifically, I am attempting to find vowels in a string (say, S = 'This is an easy assignment') and have it return the vowels from the string in a list (so, [1], [1], [1], [2], [3])
I figured out quickly [len(x) for x in S.lower().split()]
to give the length of the words, but cannot successfully get it to produce the required output. This problem cannot use anything but list comprehensions.
Something like:
[[len([v for v in x if v in 'aeiou'])] for x in S.lower().split()]
Not sure why you'd want the counts to be single element lists themselves, but that would do it with only list comprehensions (plus a len call).
Note: A suggested edit was to use [sum(x.count(v) for v in 'aeiou') for x in S.lower().split()]; this is a reasonable way to do it (though x.count would mean traversing the string five times; in CPython it might still be faster, but in JIT-ed interpreters a single pass is likely superior), and to keep it use list comprehensions, not generator expressions, you'd have to pointlessly realize the list before summing: [sum([x.count(v) for v in 'aeiou']) for x in S.lower().split()]

python list.iteritems replacement

I've got a list in which some items shall be moved into a separate list (by a comparator function). Those elements are pure dicts. The question is how should I iterate over such list.
When iterating the simplest way, for element in mylist, then I don't know the index of the element. There's no .iteritems() methods for lists, which could be useful here. So I've tried to use for index in range(len(mylist)):, which [1] seems over-complicated as for python and [2] does not satisfy me, since range(len()) is calculated once in the beginning and if I remove an element from the list during iteration, I'll get IndexError: list index out of range.
Finally, my question is - how should I iterate over a python list, to be able to remove elements from the list (using a comparator function and put them in another list)?
You can use enumerate function and make a temporary copy of the list:
for i, value in enumerate(old_list[:]):
# i == index
# value == dictionary
# you can safely remove from old_list because we are iterating over copy
Creating a new list really isn't much of a problem compared to removing items from the old one. Similarly, iterating twice is a very minor performance hit, probably swamped by other factors. Unless you have a very good reason to do otherwise, backed by profiling your code, I'd recommend iterating twice and building two new lists:
from itertools import ifilter, ifilterfalse
l1 = list(ifilter(condition, l))
l2 = list(ifilterfalse(condition, l))
You can slice-assign the contents of one of the new lists into the original if you want:
l[:] = l1
If you're absolutely sure you want a 1-pass solution, and you're absolutely sure you want to modify the original list in place instead of creating a copy, the following avoids quadratic performance hits from popping from the middle of a list:
j = 0
l2 = []
for i in range(len(l)):
if condition(l[i]):
l[j] = l[i]
j += 1
else:
l2.append(l[i])
del l[j:]
We move each element of the list directly to its final position without wasting time shifting elements that don't really need to be shifted. We could use for item in l if we wanted, and it'd probably be a bit faster, but when the algorithm involves modifying the thing we're iterating over, I prefer the explicit index.
I prefer not to touch the original list and do as #Martol1ni, but one way to do it in place and not be affected by the removal of elements would be to iterate backwards:
for i in reversed(range(len()):
# do the filtering...
That will affect only the indices of elements that you have tested/removed already
Try the filter command, and you can override the original list with it too if you don't need it.
def cmp(i): #Comparator function returning a boolean for a given item
...
# mylist is the initial list
mylist = filter(cmp, mylist)
mylist is now a generator of suitable items. You can use list(mylist) if you need to use it more than once.
Haven't tried this yet but.. i'll give it a quick shot:
new_list = [old.pop(i) for i, x in reversed(list(enumerate(old))) if comparator(x)]
You can do this, might be one line too much though.
new_list1 = [x for x in old_list if your_comparator(x)]
new_list2 = [x for x in old_list if x not in new_list1]

Categories