How to get the index of the first positive element in a list?

Asked

Viewed 190 times

8

I am looking for a solution to a very simple problem. I need to get the first element of a list greater than or equal to zero.

I was able to do this with the following function:

def index_of_first_positive_element(values):
    for idx, val in enumerate(values):
        if val >= 0:
            return idx
    return -1

first_positive_idx = index_of_first_positive_element([-10, -5, 3, 15])
print(first_positive_idx) # 2

My function works, but my intuition tells me that there must be a shorter, more direct and/or idiomatic way to do this. Maybe some function built-in, functional technique or any pythonism (e. g., comprehensilist on).

As far as possible I would like an efficient technique. I prefer techniques that, as in the code above, stop at the first positive element rather than techniques that accumulate all positive elements in an intermediate list.

Does anyone have any idea how to do that?

P.S.: Solutions involving libraries are also welcome (just please be sure to leave a link to the library(s) used and include the Imports necessary to make your code work).

Attribution:

The question is original, but the idea was adapted from First Python list index Greater than x?

  • 1

    Part of my initiative to bring more questions to the community.

  • 3

    I don’t think your code isn’t pythonic. :-)

  • 2

    Hehehe, fair. Let’s say I’m looking for equally idiomatic aternative solutions :).

3 answers

9

A built-in function usable in your example would be the next. Given a rule, it then returns the next item in your iterator. Example of use:

seq = [-10, -5, 3, 15]
print(next((x for x in seq if x >= 0) , -1 ))

Updating:

This answer does not correctly reproduce the request by the question, recovering only the value and not the index. For correct answer look at the answer of above

  • 1

    You’re correct, I updated passing the default value

  • 4

    print(next((i for i,x in enumerate(seq) if x >= 0) , -1 ))

  • Why would you judge this pythonica solution?

  • @Woss, actually I do not consider, I did not know the terminology and I am looking now, I believe that the most complete answer is that of hkotsubo

  • 1

    @Lucasmiranda, that was not the purpose of Woss' question. It’s a rhetoric reminding him that the question that prompted his answer before editing started out like this I’m looking for a pythonica solution.... then the question Why would you judge this pythonica solution? is a suggestion to explain in your answer why or what makes your code idiomatic. For my part, review my previous comment and see if it can contribute your answer. get more information here.

  • I got it! In fact I answered this question in an unpretentious way, I don’t know much about python, as my answer is wrong I avoided giving too much attention to her not to give the correct answer

Show 1 more comment

6


The question asks that the index of the first positive element, but one of the answers is returning the element itself. Therefore, a small modification would suffice:

def index_of_first_positive_element(values):
    return next((idx for idx, val in enumerate(values) if val >= 0), -1)

print(index_of_first_positive_element([-10, -5, 3, 15])) # 2
print(index_of_first_positive_element([-10, -5, -15])) # -1

The built-in next takes two arguments:

  • an iterator: in the case, (idx for idx, val in enumerate(values) if val >= 0), which is a Generator Expression. It is similar to comprehensilist on, but the difference is that it does not create a list. The Generator is "Lazy", in the sense that it only returns its elements as needed (that is, there is no creation - in this case unnecessary - of a list)
  • a value default, if there are no more elements in the iterator. How are we calling next only once in a Generator newly created, then the value will be returned if it is empty (in this case, if the list has no element greater than or equal to zero)

So every call of next consumes the next element of Generator, and as I only called once, the first will be returned (or -1 if there is no element that makes the condition val >= 0).

I think that’s the most succinct (and pythonic?) that we can reach.


But of course there are other ways (maybe not as "simple" as the above method).

One option is to use filter:

def index_of_first_positive_element(values):
    return next(filter(lambda x: x[1] >= 0, enumerate(values)), (-1, ))[0]

But how we need the index, and each element returned by enumerate is a tuple (containing the index and its element), the value default of next must also be a tuple.

Of course it could be used map to obtain the first element of the tuple:

def index_of_first_positive_element(values):
    return next(map(lambda x: x[0], filter(lambda x: x[1] >= 0, enumerate(values))), -1)

But in my opinion, we are already complicating too much something that should be simple (and in fact it is, just use the first solution above).

You also have options with module itertools (that for me, are equally - or perhaps more - complicated than the previous options with filter and map, but stay here as a curiosity):

from itertools import dropwhile
def index_of_first_positive_element(values):
    return next(dropwhile(lambda x: x[1] < 0, enumerate(values)), (-1, ))[0]

Just for the record, we can generalize this problem to the umpteenth occurrence (instead of just the first one), and then the module itertools comes in handy:

from itertools import islice

def index_of_nth_element(values, n, predicate, default=None):
    return next(
        islice((idx for idx, val in enumerate(values) if predicate(val)), n - 1, None),
        default
    )

lista = [-1, -2, 3, -5, -7, 10, -1]
# pega a primeira ocorrência
print(index_of_nth_element(lista, 1, lambda x: x >= 0, -1)) # 2
# pega a segunda ocorrência
print(index_of_nth_element(lista, 2, lambda x: x >= 0, -1)) # 5
# pega a terceira ocorrência (que não existe)
print(index_of_nth_element(lista, 3, lambda x: x >= 0, -1)) # -1
# não tem elementos que satisfazem a condição
print(index_of_nth_element(lista, 2, lambda x: x >= 1000000, -1)) # -1

But of course if you always want the first occurrence, use itertools is unnecessary.

  • 3

    I particularly found the solution of the most pythonica question so far hahaha simple, direct, easy to understand and easy to maintain - considering only the problem of first occurrence.

  • 1

    @Woss Yeah, I also think the code of the simplest question, that’s why I wrote it "(and pythonic ?)", because I think being "more pythonic" is kind of subjective in some cases... But finally, the question asked for alternatives, whether it will be more pythonic or not, goes from each one :-)

  • 2

    I think the problem here was more in my question than in the answers. I took the passage on pythonica solution and reinforced that I am seeking something short direct and/or idiomatic, so we avoid the debate about what is or is not pythonico. Anyway the combination of next + iterators works very well. And in the absence of something like the indexWhere in Scala I would say that Luke’s answer and adaptation to return hkotsubo content represent a good middle ground between short and expressive code.

3

If instead of the list we are working with a 1D arrangement of the numpy, we can do it this way:

import numpy as np

def index_of_first_positive_element(array: np.ndarray):
    bool_array = array >= 0
    return bool_array.argmax() if bool_array.sum() else -1

a = np.array([-10, -5, 3, 15])
first_positive_idx = index_of_first_positive_element(a)
print(first_positive_idx) # 2

Explanation:

Here I used the fact that the boolean values True and False correspond to the numbers 1 and 0, respectively. We can confirm this with common lists:

>>> sum([True, True])
2

in the first line of the function, the variable bool_array is defined as being array >= 0. Like array is an arrangement of numpy, this syntax means create an array of boolean values representing whether each element of the initial arrangement is greater than or equal to zero. That is, if

array = np.array([-1, 0, 5]),

then

bool_array = array >= 0

is an arrangement with values [False, True, True].

In the second line, I use the method np.ndarray.argmax, returning the index of the highest value element of an arrangement (and if there is a tie between 2 elements, it returns the first occurrence). Since the arrangement in question is only composed of True and False, this equals to return the index of the first value True (remembering that True vale 1, False vale 0), which is precisely the first positive element.

We still have to deal with the case when no value is greater than zero; in this case, bool_array has only elements False, and np.ndarray.argmax will return (erroneously) the index 0. For this, I used the conditional expression testing the condition if bool_array.sum(). This condition will only be false when the sum of the Boolean arrangement is zero, or, in other words, when there are no positive values in the initial arrangement. In this case, we return the sentinel value -1.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.