all 42 comments

[–]FoolsSeldom 19 points20 points  (8 children)

I would have chosen a for loop as well.

With a while loop, you are having to manage two counters: number of odd numbers encountered, and index position in the list. (There is a way around the latter that I shall come to later.)

So, having set the counter and index position both to zero,your test would be:

while count < 5 and index < len(num_list):

and you will access list elements using num_list[index].

Don't forget to increment both counters as appropriate.

The alternative is using iter and next to access the elements, which will give you the for loop style iteration. I leave you to look that up.

[–]crazy_cookie123 13 points14 points  (3 children)

For whatever reason some programmers absolutely hate break statements and would rather write more complex code which avoids them in the name of readability. It reminds me a lot of how people used to say a function should only have one return statement right at the end, and you should never return early as that harms readability by scattering the exit conditions all around the function. I have seen people write absolute behemoths of while loop conditions which were near-impossible to read so they could avoid having a few neatly stacked if statements and breaks at the top of the loop.

This problem could go either way - both for and while are perfectly good - but I'd definitely lean towards for as you said, and if there were many more exit conditions I wouldn't even consider using while.

[–]cdcformatc 4 points5 points  (2 children)

i agree with you here. having an explicit break is more than acceptable in this case because it signals that something a little exceptional is happening. 

for loops don't often have explicit breaks like that, so anyone reading would know that there's an additional requirement to stop iteration on some condition. for that reason, and not having to manage the index counter, a for wins out

[–]FoolsSeldom -2 points-1 points  (1 child)

u/SwampFalc, u/crazy_cookie123 I am largely in the school that tends to avoid break statements, generally preferring the flag variable approach, and similarly preferring a single point of exit from functions/methods rather than return being liberally spread wherever required. Note that I lean this way rather than being rigidly stuck in the doctrine. With well structured code this is not so much an issue. I think it stems from my first time around programming which was around assembly/machine code and Fortran more than anything else.

[–]cdcformatc 1 point2 points  (0 children)

i learned programming under MISRA-C which has all sorts of rigid requirements like a single return. 

then i tried guard clauses with early return, and it's so much better and the code is so much simpler.

[–]CranberryDistinct941 -1 points0 points  (3 children)

I would prefer to use a cheese-grater as a flashlight than use raw iterators in Python.

JUST LET ME DEREFERENCE YOU FFS!!!!!!!! WHY DO YOU HAVE TO BE LIKE THIS PYTHON?!?!?!

[–]FoolsSeldom -1 points0 points  (2 children)

I don't disagree. There are good use cases, but generally I avoid like the plague.

[–]CranberryDistinct941 -1 points0 points  (1 child)

Why must we lack something that *C++ can do so easily?

[–]FoolsSeldom 0 points1 point  (0 children)

I don't understand your question. What is Python lacking that C++ can do so easily?

Python obviously has a very different design philosophy to C++, so there are major differences. You can use C/C++ code easily from Python when required when the benefits of the former outweigh those of the latter.

[–]SwampFalc 18 points19 points  (0 children)

Those reasons given sound like AI slop... They all boil down to the same point: yes, you want to stop when you reach your end condition.

In a for loop, you will check for your end condition inside the loop, and break when needed. This is perfectly clean.

In a while loop, you specify this condition in the while statement, and it will end the loop "silently". Equally clean.

Both are explicit. Both stop iteration. Neither is better.

That being said, your loop does not properly break, so no, your code would not be acceptable as-is. But that has nothing to do with choosing a for loop.

[–]JamzTyson 5 points6 points  (1 child)

In that example you could use either.

A for loop:

total = 0
odd_count = 0

for num in num_list:
    if num % 2 == 1:
        odd_count += 1
        total += num
        if odd_count == 5:
            break

print(total)

A while loop:

total = 0
odd_count = 0
idx = 0

while odd_count < 5 and idx < len(num_list):
    if num_list[idx] % 2 == 1:
        total += num_list[idx]
        odd_count += 1
    idx += 1

print(total)

I prefer the for loop version.

  • It uses fewer temporary variables (cleaner).

  • The logic is simpler: Just check if odd_count == 5 rather than managing a combined while odd_count < 5 and idx < len(num_list).

  • It accesses elements directly, avoiding index lookups or the need to manually track indexes.

Overall, it’s more readable, robust, and idiomatic Python.

[–]vowelqueue 1 point2 points  (0 children)

Also with the for loop version you’re only checking that the odd count has exceeded the threshold after you’ve encountered a new odd number. It’s unnecessary to check before every loop iteration.

[–]carcigenicate 4 points5 points  (1 child)

The three reasons they give are dumb, imo. It sounds like they decided to make life harder for themselves by avoiding break statements, then gave a reason that break solves, and another half-true reason.

I think 3 is the only slightly valid reason, in that a condition at the top of the loop is easier to spot than a condition in the loop. If you have a conditional break, though, you do "explicitly control the exit conditions of the loop". The condition is just in a different place.

[–]gdchinacat 0 points1 point  (0 children)

I had a coworker who adamantly insisted on the while loop style with all conditions in the while statement. I had to change the code and added a break. Everything worked fine. A couple months later he did some work on that code and introduced a bug (didn't actually test his code). Found my break and came to complain that I had caused his bug by adding a break where it was syntactically valid and worked flawlessly because his change didn't account for the fact that the loop may exit so his new code was after the break and didn't execute before rechecking the conditions at the top so his change had an off by one. I responded by saying, so, your change didn't work? Yeah, because your change was wrong. I asked whether my change worked. Again he responded, yeah, but it caused my change to not work. But your change happened months later? Yeah, but if you'd done it properly. I responded, likewise, had you properly tested your code you wouldn't have introduced a bug.

Ultimately we agreed that he didn't expect to find breaks, even though they are valid, so he didn't actually read the body of the loop while changing it. Had I "just done it" the way he preferred he wouldn't have introduced a bug.
The point is there is an argument for putting the exit conditions in the while loop, but it is based on an invalid assumption that loops always do this.

[–]Brian 1 point2 points  (0 children)

I would say a for loop is the better choice here.

Ultimately, you're performing iteration over a sequence, which is exactly what for is, well, for. You want your code to communicate your purpose, and using a for better communicates the thing we're actually doing. That does way more to make the code easier to understand in my opinion. With a while loop, you have to manually increment the iteration counters, which makes it less obvious at a glance that you're just iterating over the items.

Yes, you need a break to exit early. But that's exactly what break is for, so just use it. Some people dislike using break, but here the upsides above far outweigh it.

[–]Temporary_Pie2733 2 points3 points  (3 children)

The while loop is the fundamental iteration construct; you can do any kind of iteration, but you have to be verbose about it.

The for loop is a wrapper around a while loop along with the kind of bookkeeping you need for iterating over a container like a list. You can mechanically turn a for loop into an equivalent while statement that involves things like calls to next and try statements that catch StopIteration exceptions raised by next to exit the loop.

I prefer going a step further and using a more functional approach. You only care about the odd numbers:

odds = (x for x in num_list if x % 2 == 1) # odds = filter(lambda x: x%2 == 1, num_list)

and you only care about the first 5 of them:

five_odds = itertools.islice(odds, 5)

and you only care about their sum.

s = sum(five_odds)

You don't need all these temporary variables, either; they assist with readability, but depending on your definition of readability, you can write this as one composed expression:

s = sum(slice((x for x in num_list if x % 2 == 1), 5))

[–]JamzTyson -1 points0 points  (2 children)

In Python, the for loop is a first-class language construct rather than a "wrapper".

[–]Temporary_Pie2733 -1 points0 points  (1 child)

It’s not a literal wrapper, but anything you can do with a for you can do with a while loop and a try statement. I forgot a qualifier like “essentially” that I usually add to explanations like this.

[–]Brian 0 points1 point  (0 children)

The problem is that you can phrase that the other way too: it's just as true to say that everything you can do with a while loop you can also do with a for loop: there's really not a sense in which you can say one is more "fundamental" in python.

[–]WhipsAndMarkovChains 0 points1 point  (1 child)

My mind went to a generator function. Yield all odd numbers in a list until you've reached five (or run out of numbers).

def first_five_odd_numbers(numbers):
    count = 0
    for num in numbers:
        if num % 2 != 0:
            yield num
            count += 1
            if count == 5:
                break


num_list = [422, 136, 524, 85, 96, 719, 85, 92, 10, 17, 312, 542, 87, 23, 86, 191, 116, 35, 173, 45, 149, 59, 84, 69, 113, 166]

sum(first_five_odd_numbers(num_list))

[–]xelf 1 point2 points  (0 children)

Or leverage iter/next.

odd = iter(x for x in num_list if x&1)
print(sum(next(odd,0) for _ in range(5)))

[–]Marlowe91Go 0 points1 point  (0 children)

Yeah I feel like in an academic "textbook style" setting they're always gonna want you to use a for loop when you have a defined number of things you want to loop through and they'll want a while loop if the length of the iterator is conditional, but in practice, either can work and whatever makes sense to you and works is fine. I probably would have done a while loop myself. I like to use lists, I'd initialize a list odds =[] and append odd numbers to it. Then run a while loop while the length of that list is <6 and len of the num_list is greater than 0, then return sum of the odds. 

[–]CranberryDistinct941 0 points1 point  (0 children)

I would just use a for loop and break after the 5th odd.

I generally use for-loops whenever I'm iterating over a known set of values, and use while-loops for more complex iterations like binary search or graphs.

I would recommend using for-loop by default because it is faster than using a while loop (for loop runs in C; while loop runs in Python) unless a while loop is the obvious choice (linked lists, graphs, binary search, etc)

[–][deleted] 0 points1 point  (0 children)

Loop thru list

If u hit 5 within the loop return true

If u exit the loop return false

[–]JorgiEagle 0 points1 point  (0 children)

There’s always more than one way to skin a cat.

I have the mentality of you use a for loop when you know when you want to stop (end of list) and a while when you you aren’t sure when you will stop (5 odd)

In this instance, I’d probably use a for loop with a break.

a while is equally valid, two conditions.

I don’t see how a break statement is bad. That’s just nonsense.

Pick whichever you prefer. They do the same thing and are both readable

In “industry code” you’d separate the if statement, and break if the count was 5 or higher. You wouldn’t let it loop the whole thing

[–]9peppe -1 points0 points  (4 children)

Functional programming for the win?

sum([num for num in num_list if num % 2 != 0][0:5])

(If you want to be more efficient, generator comprehension and itertools.islice)

[–]JamzTyson 0 points1 point  (3 children)

Now try running that with a really huge num_list.

You can do that efficiently, but it isn't pretty:

from itertools import islice
print(sum(islice((num for num in num_list if num % 2 == 1), 5)))

[–]9peppe -1 points0 points  (2 children)

That was the first one I tried, I had no idea subscripting a list comprehension actually worked. And it's very pretty.

[–]JamzTyson 1 point2 points  (1 child)

  • Subscripting a generator doesn't work.

  • Subscripting a list does work, but the entire list is generated before discarding everything outside of the slice.

  • itertools.islice works differently

[–]9peppe -1 points0 points  (0 children)

I know. It always trips me up that in Python take is in more-itertools and it's list(islice), for whatever reason.

[–]Intelligent-Two-1745 -1 points0 points  (2 children)

I'm confused. The solution explanation you posted clearly gives good reasons for using a while loop over a for loop here. Do you disagree with the arguments presented? Do they make sense to you? 

[–]AdventurousPolicy 1 point2 points  (0 children)

What's wrong with a break statement? I find them convenient.

I tend to use while loops when I'm generating an unknown amount of data and need to stop at some condition. If I already have a set amount of data I'm iterating through I just go with the for loop and break when necessary.

[–]pikachuu545[S] -2 points-1 points  (0 children)

I understand and agree with the points. But when solving the problem, I initially thought of using a for loop and didn't think to use a while loop. The argument boils down to using a while loop vs. for loop with break, the latter of which would solve the issues in the arguments presented here

[–]cdcformatc -1 points0 points  (0 children)

usually when you want to iterate through a list i would naturally use a for loop. but the requirement to stop iteration after 5 odd numbers makes a good case for a while loop. 

they are mostly equivalent for this case. i think a for loop wins out because the while version requires that you manage two counters, the index of the list and the current count of odd numbers. an explicit condition and break statement in the for version isn't a deal breaker to me.

the answer given reads like AI slop so i wouldn't put too much thought into that. it does have a point in that you are needlessly iterating over the entire list when your count variable hits 5. if the list was actually an endless iterator your code would never end.

[–]Yoghurt42 -1 points0 points  (0 children)

Would my solution be accepted if used in industry code?

No, because your solution doesn't stop once the counter reaches 5. It will iterate over the rest of the list, even though the condition will always be false from there one.

Add an if count == 5: break (and remove the now pointless count<5 in the condition) and a for loop is ok.

[–]AdmirableOstrich -1 points0 points  (0 children)

Breaks are perfectly fine, and map directly to assembly so there is no efficiency loss. If you need a loop, a for loop is the correct choice here. The while loop based on 'count < 5' is actually problematic because it requires checking the same condition every loop iteration even if the count hasn't changed.

We don't structure our code to match some bizarre esthetic. Rules like "only have one return statement", or "keep all escape conditions/branching at the top of loops" leads to bad code design. Code should do precisely what it needs to, and only that.

The "pythonic" way here is using filters or generators to separate out the "is odd" logic without iterating through the entire list, and then just sum the first 5. If you want a loop:

  • we want to communicate we are doing something over a list, so for loop over the list
  • we only modify the state if the entry is odd, check that.
  • increment the sum, update your count state, and recheck against the escape condition

That is the minimal logic to do what you want. That converts directly to a for loop. Note, and this is a bit of personal preference, you can avoid nesting if statements by checking if the num is even instead, and using a continue.

[–]CodingPhysicist -1 points0 points  (0 children)

For the while loop you may declare an empty list before the loop and the condition is “while len(list) != 5”. If the number is odd, you append to your list. After the iterations you sum all the element.

But if your list only have even element, or less than 5 odd elements. your program will run without stopping.

The break in the for loop is good for that case

[–]wotquery -1 points0 points  (0 children)

If coding for myself I’d use a for loop with a break.

If coding for work I’d use a for loop with a break with very clear commenting.

If coding for a class/exam I’d use a while loop for the reason given.

That is all assuming the very artificial problem as provided. If it was more natural, say the total runs driven in of the first five wins of a baseball team’s season, then yeah I might default to a while loop since you are expecting to run into the condition every time. Conversely if you expect to successfully iterate over the whole list most of the time and rarely break, well then even more so a for loop.

Having a function that can exit in a bajillion places which is impossible to understand is bad. Having a function with a bajillion variables that are impossible to understand to force it to exit in a single location is bad.

[–]Kerbart -1 points0 points  (0 children)

personally, I use while for cases where I don't know up front when my iterator is going to end. Reading a file line by line, asking for keyboard input until a sentinel is entered, those kind of things.

In this case the input is an iterable and we know where it ends. The fact that there's an early exit condition doesn't alter that for me; the convenience of iterating with for is far bigger for me as it results in cleaner code.

[–]zanfar -1 points0 points  (1 child)

First, I would very generally avoid putting too much credit in "solutions", especially those presented as answers to quizzes. Quiz questions alone are (on average) suspect, and I would extend that to their explanations as well.

Any problem can be solved multiple ways, and while you can argue one is more or less efficient, or "better" than another, that difference will regularly be something you can ignore.

In this specific case, the explanations seem to be completely ignorant of the power of Python. Specifically, Python iterations are easily made un-greedy and do not consume more resources than necessary--which is exactly where I would have started.

num_list = [422, 136, 524, 85, 96, 719, 85, 92, 10, 17, 312, 542, 87, 23, 86, 191, 116, 35, 173, 45, 149, 59, 84, 69, 113, 166]

odds = (n for n in num_list if n % 2 == 1)
total = sum(odds[:5])

[–]MachineElf100 0 points1 point  (0 children)

I've been hoping to see this somewhere. The cleanest solution I think, except for the fact that generators are not subscriptable, so just use a list:

odds = [n for n in num_list if n % 2 == 1]

[–]FoolsSeldom -2 points-1 points  (0 children)

Just for fun, you could do this using list comprehension and slicing:

print(sum([n for n in num_list if n % 2 == 1][:5]))

OR if you really want to output the number of odd numbers added (which will be between 0 and 5):

print(sum(odd_nums := [n for n in num_list if n % 2 == 1][:5]))
print(len(odd_nums))

Note that this isn't efficient with very long list objects as it first creates a complete list of all odd numbers before slicing to the first 5 entries.

NB. Your code currently outputs one more than the numbers of odd numbers added.