Reputation: 41
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?
Upvotes: 3
Views: 106
Reputation: 12347
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)]
Upvotes: 2
Reputation: 114290
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:
[(5, 5), (5, 11), (5, 3), ...
^
i=0
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
[(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).
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.
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):
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:
for data in list(all_posible_sales):
for data in all_posible_sales.copy():
for data in all_posible_sales[:]:
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]
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)
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.
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]]
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.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]
.tuple((buy, sell))
is exactly equivalent to just (buy, sell)
. I find the latter less confusing to read.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
.Upvotes: 7
Reputation: 2692
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.
Upvotes: 0