3

I understand that in-place list methods return None instead of the mutated list. As far as I can see, this makes it impossible to use these methods as part of the internal logic of a list comprehension.

What is the most pythonic way to create a list comprehension whose members result from mutating other lists? In other words: what is the best alternative to this (non-functioning) line:

new_list = [old_list.insert(0, "X") for old_list in list_of_old_lists]

Which results in a list of Nones because list.insert() returns None.

Is it simply not possible to do this in an elegant single-line of code without a lot of slicing and concatenating?

The example above is trivial for the sake of illustrating my question but in reality I'd like to do this in more complex situations in lieu of multiple nested 'for' loops.

Here's a simplified sample of what I'm trying to do:

word = 'abcdefg'

variations_list = []
characters_to_insert = ['X', 'Y', 'Z']

for character in characters_to_insert:
    for position in range(len(word) + 1):
        w = list(word)
        w.insert(position, character)
        this_variation = ''.join(w)
        variations_list.append(this_variation)

for v in variations_list:
    print(v)

This works fine using nested 'for' loops, like this (my real application is much more complex/verbose than this sample).

But I cannot do the same thing using list comprehension because the 'insert' method returns None:

variations_list_comprehension = [list(word).insert(position, character) for position in range(len(word) +1) for character in ['X', 'Y', 'Z']]

for v in variations_list_comprehension:
    print(v)

Results in a list of None values because the in-place mutations return "None".

MSeifert
  • 145,886
  • 38
  • 333
  • 352
Alabaster
  • 51
  • 6
  • List comprehensions are not replacements for `for` loops. – khelwood Jul 26 '18 at 19:41
  • Possible duplicate of [Is it Pythonic to use list comprehensions for just side effects?](https://stackoverflow.com/questions/5753597/is-it-pythonic-to-use-list-comprehensions-for-just-side-effects) – TemporalWolf Jul 26 '18 at 19:44
  • 1
    Can this be done? Yes. Should it be done? Absolutely not. – TemporalWolf Jul 26 '18 at 19:48
  • Note that I am creating a new list, not simply mutating an old list. I'm creating a new list whose items result from mutating individual items in a list_of_lists. – Alabaster Jul 26 '18 at 19:50
  • So I'm confused then, do you care about the mutation of the other list, or just the output ala Zabc, aZbc, abZc, etc... – TemporalWolf Jul 26 '18 at 19:57
  • The original lists are only temporary lists created from strings, and I'm not worried about mutating them. This is really what I'm trying to do: `new_list_of_strings = [''.join(list(my_string).insert(0, "X")) for my_string in string_list]` – Alabaster Jul 26 '18 at 20:10
  • @Alabaster As you haven't been active on StackOverflow for a while I wanted to mention that you have the options to [upvote posts that helped you](https://stackoverflow.com/help/privileges/vote-up) and that you can [accept the answer that was most helpful](https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work). They are optional (it's kind of a way to reward questions and answers that helped you) I just wanted to mention them. – MSeifert Jul 27 '18 at 18:38

3 Answers3

3

Not everything should or needs to be solved by a comprehension. for-loops aren't bad - sometimes they are better than comprehensions, especially because one tends to avoid doing too much in one line automatically.

But if you really want a list-comprehension solution I would use a helper function that wraps the in-place operation:

def insert(it, index, value):
    lst = list(it)
    lst.insert(index, value)
    return lst

[''.join(insert(word, position, character)) for position in range(len(word) +1) for character in ['X', 'Y', 'Z']]

However to be really equivalent to your loopy solution you need to swap the loops in the comprehension:

[''.join(insert(word, position, character)) for character in ['X', 'Y', 'Z'] for position in range(len(word) +1)]

The advantage here is that wrapping the in-place method can be applied in a lot of cases, it doesn't just work in this case (you can wrap any in-place function that way). It's verbose, but it's very readable and re-useable!


Personally I would use a generator function with loops, because you can use it to create a list but you could also create the items on demand (without needing the list):

def insert_characters_everywhere(word, chars):
    for character in characters_to_insert:
        for position in range(len(word) + 1):
            lst = list(word)
            lst.insert(position, character)
            yield ''.join(lst)

list(insert_characters_everywhere('abcdefg', ['X', 'Y', 'Z']))
MSeifert
  • 145,886
  • 38
  • 333
  • 352
3

If you don't care about the results of mutating the other list, then you don't need to use an interim list:

variations_list = [word[:i] + char + word[i:] 
                   for char in characters_to_insert 
                   for i in range(len(word) + 1)]

['Xabcdefg', 'aXbcdefg', 'abXcdefg', 'abcXdefg', 'abcdXefg', 'abcdeXfg', 'abcdefXg', 'abcdefgX', 
 'Yabcdefg', 'aYbcdefg', 'abYcdefg', 'abcYdefg', 'abcdYefg', 'abcdeYfg', 'abcdefYg', 'abcdefgY', 
 'Zabcdefg', 'aZbcdefg', 'abZcdefg', 'abcZdefg', 'abcdZefg', 'abcdeZfg', 'abcdefZg', 'abcdefgZ']

I would still say this is at best a borderline comprehension: it's much easier to follow as a for loop.

TemporalWolf
  • 7,727
  • 1
  • 30
  • 50
  • While I like the solution (+1) the question asks for an approach "to do this in an elegant single-line of code **without a lot of slicing and concatenating**?" - Emphasis mine – MSeifert Jul 26 '18 at 20:06
  • Fair, I guess it depends on what you consider "a lot". You could also technically avoid that via `"".join([word[:i], char, word[i:]])` but that's just obfuscation at that point :) – TemporalWolf Jul 26 '18 at 20:10
0

I think it is important to understand what are you actually trying to achieve here.

new_list = [old_list.insert(0, "X") for old_list in list_of_old_lists]

Case 1: In your code above, are you trying to create a new new_list containing old(!!!) lists updated to include 'X' character as their first element? If that is the case, then list_of_old_lists will be a list of "new lists".

For example,

list_of_old_lists = [['A'], ['B']]
new_list = []
for old_list in list_of_old_lists:
    old_list.insert(0, 'X')
    new_list.append(old_list)
print(list_of_old_lists)
print(new_list)
print(list_of_old_lists == new_list)

will print:

[['X', 'A'], ['X', 'B']]
[['X', 'A'], ['X', 'B']]
True

That is, new_list is a shallow copy of list_of_old_lists containing "updated" lists. If this is what you want, then you can do something like this using list comprehension:

[old_list.insert(0, "X") for old_list in list_of_old_lists]
new_list = list_of_old_lists[:]

instead of the for-loop in my example above.

Case 2: Or, are you trying to create a new list containing updated lists while having list_of_old_lists hold the original lists? In this case, you can use list comprehension in the following way:

new_list = [['X'] + old_list for old_list in list_of_old_lists]

Then:

In [14]:     list_of_old_lists = [['A'], ['B']]
    ...:     new_list = [['X'] + old_list for old_list in list_of_old_lists]
    ...:     print(list_of_old_lists)
    ...:     print(new_list)
    ...:     print(new_list == list_of_old_lists)
    ...: 
[['A'], ['B']]
[['X', 'A'], ['X', 'B']]
False
AGN Gazer
  • 8,025
  • 2
  • 27
  • 45