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.
Part of my initiative to bring more questions to the community.
– Anthony Accioly
I don’t think your code isn’t pythonic. :-)
– Luiz Felipe
Hehehe, fair. Let’s say I’m looking for equally idiomatic aternative solutions :).
– Anthony Accioly