Pythonic method of determining if a list's contents change from odd to even values
Writing some test cases and my mind wanders, assuming there is a better way to write something like this. I have a list, its numbers transition from all odd values to all even, doesn't matter where. I need to assert this is the case, here's what I came up with:
values = [1, 3, 5, 7, 5, 3, 5, 3, 5, 7, 4, 6, 8, 4, 2, 2, 8, 6]
# find all the indexes of odd and even values
odds = [i for (i, v) in enumerate(values) if v % 2 == 1]
evens = [i for (i, v) in enumerate(values) if v % 2 == 0]
# indexes should be a continuous sequence: 0, 1, 2, 3 ... n
assert odds + evens == range(evens[-1开发者_Go百科] + 1)
Seems like a long way to go. Suggestions on how this could be reduced?
A possible solution is to consider that you allow only
odd->odd
odd->even
even->even
in other words the only forbidden transition is
even->odd
and this translates to
(0, 1) not in ((x%2, y%2) for x, y in zip(values, values[1:]))
[x for x in values if x % 2 == 1] + [x for x in values if x % 2 == 0] == values
This is only true, if values
starts with all of it's own odd values, followed by all of its even values.
Well, you don't need to calculate evens
:
assert odds == range(len(odds))
Inspired by the clarifications and solution from @6502, this generator approach uses any()
to short circuit the iteration as soon as the test fails and only fails if an even to odd transition is detected. The worst case performance is one full iteration if the test passes:
iter_val = iter(values)
assert not any(next(iter_val)%2 < v%2 for v in values[1:])
or
from itertools import izip
assert not any(i[0]%2 < i[1]%2 for i in izip(vals, vals[1:]))
(values[0] % 2) and (len(list(itertools.groupby(values, lambda x: x%2))) == 2)
Rather than collecting the indices, you can just calculate the transition point based on the assumption that all the odd values are at the start; and then check that there are no more odd values after that transition point.
Case where the assertion is true:
values = [1, 3, 5, 7, 5, 3, 5, 3, 5, 7, 4, 6, 8, 4, 2, 2, 8, 6]
odd_count = len([x for x in values if (x % 2)])
assert (not any(x for x in values[odd_count:] if (x % 2) != 0))
Case where the assertion is false:
values = [1, 3, 5, 7, 5, 3, 5, 3, 44, 5, 7, 4, 6, 8, 4, 2, 2, 8, 6]
odd_count = len([x for x in values if (x % 2)])
assert (not any(x for x in values[odd_count:] if (x % 2) != 0))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
assert zip(*itertools.groupby(x%2 for x in values))[0] == (1, 0)
Or an easier to understand two-liner:
odds_and_evens = [x%2 for x in values]
assert odds_and_evens.index(0) == odds_and_evens.count(1)
If values
is valid then odds_and_evens
will be some number of 1
followed by only 0
, so it is valid if the first 0
comes after every 1
.
Both of these methods assume you need to have at least one odd followed by at least one even, which I don't think the OP has clarified.
If empty lists, all odd, or all even should be considered valid the following method works:
odds_and_evens = [x%2 for x in values]
assert odds_and_evens == sorted(odds_and_evens, reverse=True)
I think filter
reads better than list comprehensions here, e.g.,
filter(isodd, values) + filter(iseven, values) == values
Another option is to sort values
by parity and see if anything changed:
assert sorted(values, key=lambda x: x % 2, reverse=True) == values
Somewhat longer, but this would seem to capture all (even only, odd only, empty) in addition to requirements. It only requires one modulo and one compare on the full list. Not nearly as succinct (or clever) as Andrew's sorted answer, but faster(?) for long lists.
values= [1, 3, 5, 7, 5, 3, 5, 3, 5, 7, 2, 4, 6, 8, 10]
evenOdd = [x%2 for x in values]
try:
evenLoc=evenOdd.index(0)
assert evenLoc != 0
except ValueError:
evenLoc=len(evenOdd)
try:
badActor=evenOdd[evenLoc:].index(1)
assert False
except ValueError:
pass
精彩评论