0

Here is an example of my initial list of lists:

trip_chains = [['London', 'Paris', 'Madrid'], 
               ['Delhi', 'London', 'New York'],
            ...['Jerusalem', 'Cairo', 'Paris']]

I want to extract another list out of trip_chain, to be like this:

chains_steps = [[['London', 'Paris'], ['Paris', 'Madrid'], ['Madrid', 'London']], 
                [['Delhi', 'London'], ['London', 'New York'], ['New York', 'Delhi']],
             ...[['Jerusalem', 'Cairo'], ['Cairo', 'Paris'], ['Paris', 'Jerusalem']]]

So I am using a list comprehension with two for loops in it:

chains_steps = [[chain_[stepscount_], chain_[stepscount_+1]] for chain_ in trip_chains for stepscount_ in range(len(chain_)-1)]

but because I'm loosing the step returning me to the starting city, the result will look like this:

chains_steps = [[['London', 'Paris'], ['Paris', 'Madrid']], 
                [['Delhi', 'London'], ['London', 'New York']],
             ...[['Jerusalem', 'Cairo'], ['Cairo', 'Paris']]]

Since we can think of this list comprehension as a double nested for loop, and as we know that for loops come with an optional else clause:

chain_steps = []
for chain_ in trip_chains:
    for stepscount_ in range(len(chain_)-1):
        chain_steps.append([chain_[stepscount_], chain_[stepscount_+1])
    else:
        chain_steps.append([chain_[-1], chain_[0]])

I am wondering if I could use this else clause in the list comprehension in this way:

    chains_steps = [[chain_[stepscount_], chain_[stepscount_+1]] for chain_ in trip_chains for stepscount_ in range(len(chain_)-1) else [chain_[-1], chain_[0]]]

I know there may be a bunch of alternative solution's for this specific problem, but I'm asking it for sake of curiosity and knowledge.

P.S. One may want to run a function just after every time the inner loop is finished. So this issue is not specific to my example.

Thanks.

S.Khajeh
  • 327
  • 1
  • 9
  • Does this answer your question? [Does Python have a ternary conditional operator?](https://stackoverflow.com/questions/394809/does-python-have-a-ternary-conditional-operator) – mkrieger1 Dec 29 '20 at 19:41
  • Not really, I'm not talking about if else... thanks. – S.Khajeh Dec 29 '20 at 20:03
  • Oh, sorry, I misunderstood what you were asking. But why not just `chains_steps = [[chain_[stepscount_], chain_[stepscount_+1]] for chain_ in trip_chains for stepscount_ in range(len(chain_)-1)] + [chain_[-1], chain_[0]]`? – mkrieger1 Dec 29 '20 at 20:05
  • It also isn't really necessary to use the `else` clause of the `for` loop in your example, it only makes a difference if there is a `break` in the loop. In your case you could have just moved the statement from the `else` part to after the loop so that it is executed afterwards unconditionally. – mkrieger1 Dec 29 '20 at 20:08
  • Assume that I wanted to run a function, like print, or rechecking connection status to a server etc... right after every iteration of the outer for loop, inside the list comprehension. – S.Khajeh Dec 29 '20 at 20:19
  • `else` only makes sense if the loop has a `break` statement somewhere in it; and `break` statements cannot be used in loop comprehensions. So the question seems to be based on a misunderstanding of what `for`/`else` actually does. – kaya3 Dec 29 '20 at 23:30
  • @kaya3 Sorry but disagree. Because this is a nested loop situation. – S.Khajeh Dec 30 '20 at 02:55
  • It's not a matter of opinion; the only reason to ever have an `else` block after a `for` loop is if there is a `break` statement in the `for` loop, and list comprehensions cannot have `break` statements in them. Those are facts, and it has nothing to do with whether it is a nested loop or a nested list comprehension. As I said, your question seems to be based on a misunderstanding, and you thinking it matters that the loops are nested only confirms that. – kaya3 Dec 30 '20 at 03:04
  • Please make your answer more clear. There are two `for` loops, and when you say `for` loop I don't know which one do you mean! I want to have an operation triggered before every time the outer `for` loop goes into the next iteration, and this is only possible if in the inner for loop, an `else` clause be fitted. – S.Khajeh Dec 30 '20 at 03:14
  • It is worth noticing that your example using two explicit `for` statements does not produce your desired output! It produces a list of lists (starts with `[[`) whereas your desired output is a list of lists of lists (starts with `[[[`). – Ture Pålsson Dec 30 '20 at 05:25
  • @Ture Pålsson I agree, to circumvent this, I'm using another list which contains element count of each of the lists, and use elementary math to index into the desired elements. – S.Khajeh Dec 30 '20 at 05:35

3 Answers3

1

for...else

I recommend that you study the for...else semantic. The comment by @mkrieger1 is a maybe a bit concise. The doc states:

a loop’s else clause runs when no break occurs.

For instance:

>>> for i in range(1):
...     print("first and only iteration")
...     break
... else: # not executed since there is a break
...     print("no break was met")
first and only iteration

But:

>>> for i in range(1):
...     print("first and only iteration")
... else: # executed since the loop exits normally
...     print("no break was met")
first and only iteration
no break was met

If no break ever happens in the body of the loop, the else clause always run, and thus the following pieces of code are equivalent:

for ...:
    <loop body>
else:
    <else statement>

And:

for ...:
    <loop body>
<else statement>

A list of lists of lists or a list of lists?

You write:

chain_steps = []
for chain_ in trip_chains:
    for stepscount_ in range(len(chain_)-1):
        chain_steps.append([chain_[stepscount_], chain_[stepscount_+1]])
    else:
        chain_steps.append([chain_[-1], chain_[0]])

As @Ture Pålsson write, thats only produces a list of lists. The correct version would be:

trip_chains = [['London', 'Paris', 'Madrid'],
               ['Delhi', 'London', 'New York'],
               ['Jerusalem', 'Cairo', 'Paris']]

chain_steps = []
for chain_ in trip_chains:
    chain_step = [] # create a new list in the outer loop
    # and fill it in the inner loop
    for stepscount_ in range(len(chain_)-1):
        chain_step.append([chain_[stepscount_], chain_[stepscount_+1]])
    chain_step.append([chain_[-1], chain_[0]])
    chain_steps.append(chain_step)

from pprint import pprint
pprint(chain_steps)

Output:

[[['London', 'Paris'], ['Paris', 'Madrid'], ['Madrid', 'London']],
 [['Delhi', 'London'], ['London', 'New York'], ['New York', 'Delhi']],
 [['Jerusalem', 'Cairo'], ['Cairo', 'Paris'], ['Paris', 'Jerusalem']]]

No "elementary math" is needed.

Can we add an else clause in a list comprehension?

The main question is: can we have a break in a list comprehension? The answer is no (Python 3):

>>> [i if i<5 else break for i in range(10)]
Traceback (most recent call last):
    [i if i<5 else break for i in range(10)]
                       ^
SyntaxError: invalid syntax

(See Using break in a list comprehension for more details).

Hence, the else clause has no meaning in a list comprehension. Consequence: we cannot have an else clause in a list comprehension.

Can we add an extra element in a list comprehension?

I will focus on the inner loop:

chain_ = ['London', 'Paris', 'Madrid']

chain_step = []
for stepscount_ in range(len(chain_)-1):
    chain_step.append([chain_[stepscount_], chain_[stepscount_+1]])
chain_step.append([chain_[-1], chain_[0]]) # extra element

pprint(chain_step)

Output:

[['London', 'Paris'], ['Paris', 'Madrid'], ['Madrid', 'London']]

Obviously, we can add an iteration with the last element:

chain_step = []
for stepscount_ in list(range(len(chain_)-1))+[-1]:
    chain_step.append([chain_[stepscount_], chain_[stepscount_+1]])

And then:

>>> chain_ = ['London', 'Paris', 'Madrid']
>>> [[chain_[s], chain_[s+1]] for s in list(range(len(chain_)-1))+[-1]]
[['London', 'Paris'], ['Paris', 'Madrid'], ['Madrid', 'London']]

That's a bit overkill when you can write:

>>> chain_ = ['London', 'Paris', 'Madrid']
>>> [[chain_[s], chain_[s+1]] for s in range(len(chain_)-1)] + [[chain_[-1], chain_[0]]]
[['London', 'Paris'], ['Paris', 'Madrid'], ['Madrid', 'London']]

(You can easily wrap this in an outer list comprehension:

chain_steps = [
    [[chain_[s], chain_[s+1]] for s in range(len(chain_)-1)] + [[chain_[-1], chain_[0]]]
    for chain_ in trip_chains
]

)

Bonus: is there a cleaner way to write this?

YES (inner loop):

chain_step = []
for step in zip(chain_, chain_[1:]+[chain_[0]]):
    chain_step.append(step)

pprint(chain_step)

Output:

[('London', 'Paris'), ('Paris', 'Madrid'), ('Madrid', 'London')]

The zip function will pair the elements:

chain[0], chain[1], ... chain_[n-2], chain_[n-1]  <-  chain_
chain[1], chain[2], ... chain_[n-1], chain_[0]    <-  chain_[1:]+[chain_[0]]

As you see, you have a list of tuples, not a list of lists. That's better, because a step is a pair, not a list (a tuple size won't change, a list size might change).

Hence the final double list comprehension:

>>> trip_chains = [['London', 'Paris', 'Madrid'],
...                ['Delhi', 'London', 'New York'],
...                ['Jerusalem', 'Cairo', 'Paris']]
>>> [[step for step in zip(chain_, chain_[1:]+[chain_[0]])] for chain_ in trip_chains]
[[('London', 'Paris'), ('Paris', 'Madrid'), ('Madrid', 'London')], [('Delhi', 'London'), ('London', 'New York'), ('New York', 'Delhi')], [('Jerusalem', 'Cairo'), ('Cairo', 'Paris'), ('Paris', 'Jerusalem')]]

Or, because the inner list comprehension just takes the elements of zip:

>>> [list(zip(chain_, chain_[1:]+[chain_[0]])) for chain_ in trip_chains]
[[('London', 'Paris'), ('Paris', 'Madrid'), ('Madrid', 'London')], [('Delhi', 'London'), ('London', 'New York'), ('New York', 'Delhi')], [('Jerusalem', 'Cairo'), ('Cairo', 'Paris'), ('Paris', 'Jerusalem')]]

Beware: the code becomes hard to maintain...

jferard
  • 7,835
  • 2
  • 22
  • 35
0

No, you can't have else branches on list comprehensions.

You could loop over chain + [chain[0]] or play tricks with the % operator to "fold" stepscount back to the beginning. Or just add the last pair manually, as @mkrieger suggests in a comment.

Ture Pålsson
  • 6,088
  • 2
  • 12
  • 15
  • Yes I see. Currently I'm using some additional ternary operators inside the chain_ indexing operators: `chain_[stepcounts_ if stepcounts_ – S.Khajeh Dec 29 '20 at 20:29
0

Another New Answer

Based on the S.Khajeh'comment, I am adding another one-line solution, which is more generic and cover situation which has strokes with non-uniform unconfined stroke lengths

Modified to be more effecient than double comparison :)

chains_steps = [[[row[i], (row + [row[0]])[i + 1]] for i in range(len(row))] for row in trip_chains]

Before

chains_steps = [[[(row + row)[i], (row + row)[i + 1]] for i in range(len(row))] for row in trip_chains]

The old answer

You can also achieve your target, by directly coding as in the following line:

chains_steps = [ [ [row[0],row[1]] , [row[1],row[2]] , [row[2],row[0]] ]  for row in trip_chains]

the output is

chains_steps
Out[7]: 
[[['London', 'Paris'], ['Paris', 'Madrid'], ['Madrid', 'London']],
 [['Delhi', 'London'], ['London', 'New York'], ['New York', 'Delhi']],
 [['Jerusalem', 'Cairo'], ['Cairo', 'Paris'], ['Paris', 'Jerusalem']]]

as you wanted.

Nour-Allah Hussein
  • 1,439
  • 1
  • 8
  • 17
  • Thanks for your help. Now please consider another situation which has strokes with non-uniform unconfined stroke lengths. – S.Khajeh Dec 29 '20 at 21:53
  • Yes this trick handles the situation. But a small draw back of it, IMO is, it is adding an extend operation to every iteration of the outer loop which may reduce the efficiency of the comprehension list. – S.Khajeh Dec 30 '20 at 03:05
  • Currently I am using this solution: `chains_steps = [[chain_[stepscount_ if stepscount_ – S.Khajeh Dec 30 '20 at 03:33
  • I added another modification, but in your final solution, `len()` is performed twice with two `if` statements and one addition, and all of them are repeated every iteration, so It is better to compare the different solutions based on time measurement. it will be a nice trip :} Thanks – Nour-Allah Hussein Dec 30 '20 at 04:41
  • 1
    I tried your three solutions on a larger list with 2598 elements. The first one being the fastest one took 0.002s, the second one took a bit more than 0.0045 on average, which is the time consumed by the solution I suggested. And the third one is a bit faster than my solution. All of your suggestions are satisfying for this problem. Except for the second one, in cases in which memory efficiency is a matter. Thank you. – S.Khajeh Dec 30 '20 at 05:32