3

I have some code that should find the position of any 0 items in a list and move it to the back whilst preserving the order of the other elements.

def test2(array):
    try:
        for n in range(len(array)):
            array.append(array.pop(array.index(0)))
        return array
    except ValueError:
        return array

This code works fine for any list apart from one with False in. I think this is because the .index(0): will also return the position of any False in the list. Any way to get around this?

For example if array = [0,1,None,2,False,1,0] then the output should be [1,None,2,False,1,0,0]

With that same input my code produces: [1, None, 2, 1, False, 0, 0]

cs95
  • 379,657
  • 97
  • 704
  • 746
Harry Day
  • 378
  • 4
  • 13
  • 1
    `0 == False`. Booleans do that in Python. – user2357112 Dec 22 '18 at 19:39
  • @user2357112 So is there a different way I can do this then? – Harry Day Dec 22 '18 at 19:41
  • You just need to handle the `0 == False` is `True` in Python, that is main reason you are not able to get correct ans. More details please refer to: https://stackoverflow.com/questions/2764017/is-false-0-and-true-1-in-python-an-implementation-detail-or-is-it-guarante – krishna Prasad Dec 22 '18 at 20:02
  • Does this answer your question? [Moving all zeros to the end of the list while leaving False alone](https://stackoverflow.com/questions/42187105/moving-all-zeros-to-the-end-of-the-list-while-leaving-false-alone) – Georgy Jan 07 '20 at 16:25

6 Answers6

3

This is a consequence of the fact that bool is a subclass of int in python, so searching for the first index of 0 will return the index of False, if it is in the list before a 0 because False == 0.

What you can do is check whether a list element is an instance of int, and at the same time, not an instance of bool. This way, you avoid matching other false values (like empty containers and None).

def is_zero(v):
    # return is instance(v, int) and v is not False and not v
    # return isinstance(v, int) and not isinstance(v, bool) and not v
    return type(v) in (int, float) and not v

You can then iterate over lst in reverse and update in-place.

lst = [1, 0, None, False, 0, 3, True] # For example.

for i in reversed(range(len(lst))):
    if is_zero(lst[i]):
        lst.append(lst.pop(i))

print(lst)
# [1, None, False, 3, True, 0, 0]

This is amortised linear time complexity, if I'm not mistaken.

cs95
  • 379,657
  • 97
  • 704
  • 746
  • 1
    Just curious, instead of checking `isinstance`, is it safe in python to simply check `v is not False`? – Mark Dec 22 '18 at 19:52
  • 1
    @MarkMeyer I would be _very_ careful of using `is` for anything besides reference checks. – cs95 Dec 22 '18 at 19:53
  • I guess that's what I was asking: does python make any guarantees here. Seems like the answer is no. Thanks. – Mark Dec 22 '18 at 19:54
  • 1
    @MarkMeyer I believe True and False values are interned (cached), so with cpython the reference _should_ be the same... but it is an implementation detail and in general you should not rely on it. :-) – cs95 Dec 22 '18 at 19:55
  • @coldspeed Just curious, what does the `and not v` do? – Harry Day Dec 22 '18 at 20:00
  • @HarryDay 0 is falsey, so not 0 evaluates to True. It's cleaner than v == 0 IMO, because it's more "pythonic", so to speak. – cs95 Dec 22 '18 at 20:01
  • Python does guarantee that `True` and `False` are the *only* instances of `bool`, so it is safe to use them in `is` checks. This also applies to `None`, `NotImplemented`, and `Ellipsis`. – gilch Dec 22 '18 at 20:16
  • @coldspeed how could I expand this code to also change `0.0` to `0` and move it to the back also? – Harry Day Dec 22 '18 at 20:21
  • @HarryDay you can pass a tuples of types as the second argument to isinstance. So, isinstance(v, (int, float)), see if you can figure it out from there! – cs95 Dec 22 '18 at 20:27
  • 1
    @coldspeed Thanks! I got there anyway before I read your hint ;) – Harry Day Dec 22 '18 at 20:30
2

If creating another list is of no concern, you can use list-comprehensions:

def test2(array):
    la = len(array)
    return ([x for x in array if not isinstance(x,int) or x]+[0]*la)[:la]

The first part filters out any 0 integerbut lets any non-int pass. The second part adds (too many) zeros and trims the result back down to the original lenght.

Caveat: This will create up to twice the original list-length in data - so not good for "big" lists or lists with few 0.

Usage:

k = [1,2,3,0,4,5,False,92,-3,0,1,0,0,True,[],None,True]
print(k)
print(test2(k))

Output:

[1, 2, 3, 0, 4, 5, False, 92, -3, 0, 1, 0, 0, True, [], None, True]
[1, 2, 3, 4, 5, 92, -3, 1, True, True, 0, 0, 0, 0, 0, 0, 0]

Doku:

  • (related) Truth-testing - there are other values that are "False" as well.
Patrick Artner
  • 50,409
  • 9
  • 43
  • 69
1

A variation of @coldspeed's solution:

array = [0, 1, None, 2, False, 1, 0]
nonzero = [x for x in array if x or x is None or isinstance(x,bool)]
nonzero + [0] * (len(array) - len(nonzero))
# [1, None, 2, False, 1, 0, 0]
DYZ
  • 55,249
  • 10
  • 64
  • 93
1

You can use a wrapper with a custom __eq__:

class Typed:
    def __init__(self, val):
        self.val = val
    def __hash__(self):
        return hash(self.val)
    def __eq__(self, other):
        if isinstance(other, Typed):
            other = other.val
        return type(self.val) is type(other) and self.val == other

Then replace array.index(0) with array.index(Typed(0)) - you don't need to use this within the array itself.

Extending this to containers (so that (0, 1) and (False, True) won't be equal) is left as an exercise for the reader.

o11c
  • 15,265
  • 4
  • 50
  • 75
0

I think this is as concise as this can be. Thanks for the help everyone

l = [i for i in array if isinstance(i, bool) or i!=0]
return l+[0]*(len(array)-len(l))
Harry Day
  • 378
  • 4
  • 13
0

Python guarantees True and False are the only instances of bool, so you can use is to distinguish False from 0.

z = []
return [e for e in array if e is False or e != 0 or z.append(e)] + z

This will preserve the order of the various non-False zeros (0, 0.0, 0j, Decimal(0), Fraction(0, 1)), that may be in the list.

gilch
  • 10,813
  • 1
  • 23
  • 28