3

I am working on a piece of code, and have to do some sorting stuff with the given list.

prices = [5, 11, 3, 50, 60, 90]
k = 2
all_posible_sales = []
i=0
for buy in prices[i:len(prices)]:
    for sell in prices[i:len(prices)]:
        a = tuple((buy, sell))
        all_posible_sales.append(a)
    i += 1

for data in all_posible_sales:
    if data[1] - data[0] < 0 or data[1] - data[0] == 0:
        all_posible_sales.remove(data)
print(all_posible_sales)

This code does is concatenating all possible sales (the 2 nested for loops), and removing the variants whose difference is a negative value (the final for loop).

When I check the output, there I find a very unpleasant thing: the tuple (11, 3) is in there, which must not be there following my logic

data[1] - data[0] < 0 | 3 - 11 < 0 (TRUE)

What is the matter with this value, have I done something wrong?

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
AppleCidar
  • 41
  • 5

3 Answers3

7

Introduction

Neither of the posted answers actually addresses the issues in your code. The problem is that you don't remove items from a list that you are iterating forward over. To begin to see why, print the tuples that are actually being removed:

(5, 5)
(5, 3)
(11, 11)
(3, 3)
(50, 50)
(60, 60)
(90, 90)

Notice that (11, 3) never gets checked. This happens because list iterators work based on index. Every time you remove an item, all following items shift back by one, but the iterator keeps incrementing regardless. Here is an example from the head of your starting all_possible_sales list:

  1. Start with the iterator at index 0:
    [(5, 5), (5, 11), (5, 3), ...
       ^
      i=0
    
  2. Discard the item, since 5 <= 5. Notice that the data shifts back, but the iterator stays at the same position in the list:
    [(5, 11), (5, 3), (5, 50), ...
       ^
      i=0
    
  3. Step for the next iteration of the loop:
    [(5, 11), (5, 3), (5, 50), ...
                ^
               i=1
    

Hopefully you can see how you end up skipping over (5, 11), and many elements after that (in fact just about every other element).

Solutions

Now let's look at some solutions. I start with some nearly cosmetic changes, and work up to completely overhauling your code, even more than the other answers recommend.

Backwards Iteration

When you iterate over a list backwards, removals do not affect the indices that you have not traversed. Lists have a reverse iterator, which means that calling reversed does not copy anything, and is therefore cheap in time and memory.

The simplest solution is therefore to replace the second loop with:

for data in reversed(all_posible_sales):

Copying the Input

If you make a copy of the input list, its elements will point to the same objects, but removing items from the original list will not affect the iterator over the copy. This is more expensive than reversing the original list because it actually allocates a second list.

This solution can be written in at least three different ways:

  1. for data in list(all_posible_sales):
  2. for data in all_posible_sales.copy():
  3. for data in all_posible_sales[:]:

Not Including Unnecessary Elements

As the other answers suggest, the best way is to exclude elements that don't belong, rather than removing them later. A partial answer to this approach is adjusting your loops so that you don't create tuple of the form (buy, buy):

for ibuy in range(len(prices) - 1):
    buy = prices[ibuy]
    for isell in range(ibuy + 1, len(prices)):
        sell = prices[isell]

Conditional in Loop

The simplest way to exclude items from the list is never to include them to begin with:

for ibuy in range(len(prices) - 1):
    buy = prices[ibuy]
    for isell in range(ibuy + 1, len(prices)):
        sell = prices[isell]
        if buy < sell:
            all_posible_sales.append((buy, sell))
print(all_posible_sales)

List Comprehensions

Any nested set of for loops that has nothing but a conditional and a list append can be written more efficiently, if not more legibly, as a list comprehension. In this particular case, it will involve a bit more indexing:

all_posible_sales = [(prices[ibuy], prices[isell]) for ibuy in range(len(prices) - 1) for isell in range(ibuy + 1, len(prices)) if prices[ibuy] < prices[isell]]

Notice that it's like writing the loops and conditional in the exact same order as before, but all on one line and without the colons. The only difference is that the contents of the append call goes at the beginning.

Numpy

Stuff like this with numbers is very well suited for numpy. If you are doing any sort of numerical processing, you will almost inevitably end up importing numpy, parts of scipy or pandas. Your code will be cleaner and faster.

If you turn prices into a numpy array, you can use the function np.triu_indices to find the same pairs as your loop goes over. You can then concatenate the selection into an Nx2 array of buy and sell prices:

import numpy as np

prices = np.array(prices)
ibuy, isell = np.triu_indices(len(prices), k=1)
all_possible_sales = np.stack((prices[ibuy], prices[isell]), axis=-1)[prices[ibuy] < prices[isell]]

Notes

  • You don't need to state len(prices) explicitly in your index: prices[i:] will take all the elements to the end, and prices[i:-1] will take all the elements until one from the end, etc.
  • The expression data[1] - data[0] < 0 or data[1] - data[0] == 0 can be written much more succinctly as data[1] - data[0] <= 0. Better yet (and I would argue, more straightforwardly) is the comparison data[1] <= data[0].
  • The expression tuple((buy, sell)) is exactly equivalent to just (buy, sell). I find the latter less confusing to read.
  • The expression for sell in prices[i:len(prices)]: makes a full copy of a segment of prices at each iteration of the outer loop. It is likely much cheaper to iterate over a range object, and index into prices.
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • 1
    IMO this should be the accepted answer as it addresses the actual problem in the provided code. The accepted answer gives a much better way to achieve the same results, but doesn't ever say why the original code was faulty. – Layne Bernardo Feb 12 '21 at 01:53
  • 1
    @LayneBernardo. I appreciate that. OP is able to change their selection any time they see fit. – Mad Physicist Feb 12 '21 at 01:55
2

Do not remove from the list while you are iterating over it, unless you know what you are doing. Use a list comprehension instead:

all_posible_sales = [s for s in all_posible_sales if s[0] < s[1]]
print(all_posible_sales)
# [(5, 11), (5, 50), (5, 60), (5, 90), (11, 50), (11, 60), (11, 90), (3, 50), (3, 60), (3, 90), (50, 60), (50, 90), (60, 90)]
Timur Shtatland
  • 12,024
  • 2
  • 30
  • 47
  • 1
    You totally can remove from a list if you're iterating over it. I suspect that's 90% of the reason `reversed` is a builtin to begin with. – Mad Physicist Feb 12 '21 at 01:50
  • 1
    @Mad Physicist it's not really that helpful to say things like that without clarifying that you can remove from a list *if you're iterating in reverse*. You're just going to confuse people. Also it's generally bad practice for a new programmer anyway, which the OP clearly is. – Layne Bernardo Feb 12 '21 at 01:51
  • @LayneBernardo. I assumed that mentioning `reversed` would make that clear. Apologies if not. My answer is very clear about how and why to do it properly. – Mad Physicist Feb 12 '21 at 01:52
  • @MadPhysicist just saw that you posted an answer alongside this which clarifies it. I feel like new C++ programmers might not automatically make the connection between reverse iteration and `reversed` but you might be right about that. – Layne Bernardo Feb 12 '21 at 01:54
  • @MadPhysicist: Thank you for you comment and your answer, explaining when and how to remove from the list while iterating over it. Updated my answer. – Timur Shtatland Feb 12 '21 at 02:16
0

Instead of adding elements to list and then removing, you can just add only valid ones into the list by doing this:

prices = [5, 11, 3, 50, 60, 90]
k = 2
all_posible_sales = []
i=0
for buy in prices[i:len(prices)]:
    for sell in prices[i:len(prices)]:
        if sell - buy > 0:
            a = tuple((buy, sell))
            all_posible_sales.append(a)

Also read this one to see how to remove items from list successfully for future application.

Rustam Garayev
  • 2,632
  • 1
  • 9
  • 13