10

I have a C++ background and I'm learning Python. I am writing code which needs to extract a particular value from a for loop:

seventh_value = None   # ** my question is about this line
for index in range(1, 10):
    value = f(index)
    # we have a special interest in the seventh value
    if index == 7:
        seventh_value = value
    # (do other things with value here)

# make use of seventh_value here

In C++ I'd need to declare seventh_value before the for loop to ensure its scope is not limited to the for loop. In Python I do not need to do this. My question is whether it's good style to omit the initial assignment to seventh_value.

I understand that if the loop doesn't iterate at least 7 times then I can avoid a NameError by assigning to seventh_value prior to the loop. Let's assume that it is clear it will iterate at least 7 times (as in the above example where I've hard-coded 10 iterations).

I also understand that there may be other ways to extract a particular value from an iteration. I'm really just wondering about whether it's good style to introduce variables before a loop if they will be used after the loop.

The code I wrote above looks clear to me, but I think I'm just seeing it with C++ eyes.

nonagon
  • 3,271
  • 1
  • 29
  • 42
  • In the general case where `seventh_value` may or may not be set in the loop, I think this is a reasonable approach. At the end of the loop you could check if `seventh_value` is still `None` (assuming you won't encounter `None` as a value.) This is probably just opinion, but I think checking if the value is still `None` at the end of the list to say it wasn't set is far nicer than trying to handle it by working around a `NameError`. An alternate approach is to have a separate boolean such as `seventh_value_set = False` and then set that to true if the value is set in the loop. – Chris Sprague Aug 12 '16 at 17:04
  • It is subjective, but personally I would initialise it to `None` before the loop. – cdarke Aug 12 '16 at 17:06
  • In this case, I would declare before the loop, and unwrap. As long as f isn't too expensive, I would declare `seventh_value = f(7)` I realize that this is a hugely simplified case, but this type of logic works fairly often. – Oscar Smith Aug 12 '16 at 17:06
  • Take a look at http://stackoverflow.com/questions/24089924/skip-over-a-value-in-the-range-function-in-python You can construct a range that simply excludes the 7th value and then call f(7) when you assign. – pvg Aug 12 '16 at 17:15
  • This post has some relevant discussion http://stackoverflow.com/questions/3611760/scoping-in-python-for-loops – Chris_Rands Aug 12 '16 at 17:56

4 Answers4

2

This is a fine approach to set the value to None before the for-loop. Do not worry too much. Personally, I feel as long as code is readable by someone who has no idea at all -- it is fine.

That being said, there's a slightly better (pythonic) way to avoid the problem altogether. Note that the following will not do anything with the value at every iteration -- just the "special interest" part. Further, I am assuming f(..) does not cause any side-effects (like changes states of variables outside (like global variables). If it does, the line below is definitely not for you.

seventh_value = next(f(i) for i in range(1,10) if i == 7)

The above construct run until i==7, and calls f(i) only when i=7 never else.

UltraInstinct
  • 43,308
  • 12
  • 81
  • 104
  • This code is not equivalent to the code the question is about since that code calls `f` with every index and also does something with the return value. Yours looks like `f(7)`. – pvg Aug 12 '16 at 17:13
  • Yes, if there is a "special interest" in only the 7th index, then why call `f(..)` on every index -- as I have called out in the last line of my answer. – UltraInstinct Aug 12 '16 at 17:16
  • 1
    Because f might have side effects and because, as the question says, the loop does something with the return value of _every_ call. What you've ended up is a confusing non-answer and you should probably remove it. – pvg Aug 12 '16 at 17:18
  • If `f` is a pure function, this solution should work exactly as OP's does. If `f` has side effects (as @pvg mentions), I think this should work: `[f(i) for i in range(1,10)][6])`. – Tagc Aug 12 '16 at 17:21
  • @Tagc no it doesn't. see '# (do other things with value here)' in the question. Also this is basically a really convoluted way to call `f(someconstant)`. It is not 'pythonic', just pointless. – pvg Aug 12 '16 at 17:23
  • @pvg: Fair enough. I have updated the answer with a few details on purity of functions, and when that generator expression can/must not be used. – UltraInstinct Aug 12 '16 at 17:25
  • @Tagc: I missed it too! – UltraInstinct Aug 12 '16 at 17:26
1

It's only partially a matter of style. You need to do something to ensure your code doesn't raise an (uncaught) NameError following the loop, so your two choices are

  1. Ensure that the NameError cannot happen by making an unconditional assignment to seventh_value.

  2. Wrap the code that accesses seventh_value in a try block to catch and (presumably) ignore the possible NameError.

View from this perspective, and without knowing more about your code, I think you would agree that #1 is preferable.

chepner
  • 497,756
  • 71
  • 530
  • 681
1

If you are sure that seventh_value should be set and you count on it in your program logic, you might even want your code to crash with NameError and write it without prior seventh_value definition.

If you are sure that seventh_value might not be set when all code works just fine, you should probably define seventh_value as you do in the example and check later if it was set at all.

user1687327
  • 176
  • 1
  • 9
  • Ignoring an exception that you can do something about is not a good idea. – chepner Aug 13 '16 at 01:57
  • I can't imagine it's pythonic to attempt to handle every possible exception in a program. For example every time we divide integers we don't say "if x != 0" or "catch ZeroDivisionError". It must depend on the context, and in my case I was tried to emphasize that it's clear seventh_value will indeed be assigned. – nonagon Aug 14 '16 at 01:12
0

You can do it exactly how you suggest, and if you try it, you will find it works exactly how you want.

However, there is a better, (imho more pythonic) way, that totally avoids the need to pre-initialize the variable.

results = []
for index in range(1, 10):
    value = f(index)
    # (do other things with value here)

    results.append(value)     

seventh_value = results[6] #6 because index 1 is at location 0 in the list.
# make use of seventh_value here

Now that you have a simple for loop, it can easily be refactored into a list comprehension:

results = [f(index) for index in range(1, 10)]

for value in results:
    # (do other things with value here)

seventh_value = results[7]
# make use of seventh_value here

Whether you decide to go that far will depend on how complex your (do other things with value here) is.

Graham
  • 7,431
  • 18
  • 59
  • 84
RoadieRich
  • 6,330
  • 3
  • 35
  • 52