7

Problem

I have a code like this

if condition:
    a = f(x)
else:
    a = g(y)

Initialization of a inside of the block looks bad for me. Can it be written better?

I cannot use ternary operator, because names of functions and/or lists of arguments are long. Saying "long" I mean that the following expression

a = f(x) if condition else g(y)

will take more than 79 (sometimes even more than 119) symbols with real names instead of a, f, g, x, y and condition. Usage of multiple slashes will make the code ugly and messy.

I don't want to initialize a with result of one of the functions by defaul, because both function are slow and I cannot allow such overhead

a = g(y)
if condition:
    a = f(x)

I can initialize the variable with None, but is this solution pretty enough?

a = None
if condition:
    a = f(x)
else:
    a = g(y)

Let me explain my position: in C and C++ variables inside of a block have the block as their scope. In ES6 the let keyword was introduced — it allows to create variables with the same scoping rules as variables in C and C++. Variables defined with old var keyword have similar scoping rules as in Python. That's why I think that initialization of variables should be made outside blocks if I want to use the variables outside these blocks.

Update

Here is more complicated example

for obj in gen:
    # do something with the `obj`
    if predicate(obj):
        try:
            result = f(obj)
        except Exception as e:
            log(e)
            continue
    else:
        result = g(obj)
    # do something useful with the `result`
else:
    result = h(obj)

display(result)

I go through elements of some generator gen, process them and perform some actions on the result on each iteration. Then I want to do something with the last result outside of the loop.

Is it pythonic enough to not assign a dummy value to the result beforehand? Doesn't this make the code less readable?

Question

Is it good to initialize variables inside if/else/for/etc. in Python?

Charlie
  • 826
  • 1
  • 11
  • 27
  • wrap the inline if-else in parens and then you can line break without the slashes, and add leading whitespace to improve readbility – f5r5e5d Dec 26 '17 at 22:07
  • Python doesn't have block scope. Your `a = None` is semantically equivalent to just leaving it out... – juanpa.arrivillaga Dec 26 '17 at 22:24
  • I tend to have the outer a = None. Reason is that, as your logic becomes more conditional and complicated, you may encounter NameError where you branches haven't assigned **a** and that will throw you. With **a** set, you can do **getattr(a, 'somevar','default')** against None for example. In your **simple** example, might not be worth it. – JL Peyret Dec 26 '17 at 23:44
  • @JLPeyret yes, you've got my point: I'm talking about the generic case, but maybe because of wrong terminology I was understood wrong – Charlie Dec 27 '17 at 09:36
  • @f5r5e5d multiline ternary operator will not be more readable than ordinary `if-else` at all – Charlie Dec 27 '17 at 09:39
  • No, I understood you fine. And I agree w "dont C++ in Python". But, code styling aside, there are reasons to be careful about conditional variable creation. Python is not JS and your variable wont be magically hoisted into existence. If you're totally on top of whether its created or not, including during exception handling when something went wrong and you to log or otherwise examine variables, then no, the dummy assignnment is just noise. Me, Ive found `a = b = None` where I make sure these vars exist, to be useful in *some* cases. – JL Peyret Dec 27 '17 at 09:51
  • @JLPeyret okay, as I understood, I don't need to use dummy values to try to increase readability if I'm sure that the value will be assigned? – Charlie Dec 27 '17 at 10:17
  • Correct. I agree with either answers so far, and my remark had nothing to do with style or readability. – JL Peyret Dec 27 '17 at 17:47

3 Answers3

7

Python has no block scope... the scope is the whole function and it's perfectly pythonic to write

if <condition>:
    a = f()
else:
    a = g()

If you want to write in C++ then write in C++ using C++, don't write C++ using Python... it's a bad idea.

6502
  • 112,025
  • 15
  • 165
  • 265
4

Ok, there are two points that need to be clarified here which are fundamental to python.

  1. There is no variable declaration/initialization in python. An expression like a = f(x) is simply a scheme to name the object that is returned by f as a. That namea can be later used to name any other object no matter what its type is. See this answer.

  2. A block in python is either the body of a module, a class or a function. Anything defined/named inside these objects are visible to later code until the end of a block. A loop or an if-else is not a block. So any name defined before outside the loop or if/else will be visible inside and vice versa. See this. global and nonlocal objects are a little different. There is no let in python since that is default behavior.

In your case the only concern is how you are using a further in the code. If you code expects the type of objects returned by f or g it should work fine unless there is an error. Because at least one of the if or the else should run in a normal operation so a will refer to some kind of an object (if the names were different in if and else that would be a problem). If you want to make sure that the subsequent code does not break you can use a try-except-else to catch any error generated by the functions and assign a default value to a in the except clause after appropriate reporting/logging of the error.

Hence to summarize and also to directly address your question, assigning names to objects inside an if-else statement or a loop is perfectly good practice provided:

  1. The same name is used in both if and else clause so that the name is guaranteed to refer to an object at the end of the statement. Additional try-except-else error catching can take care of exceptions raised by the functions.

  2. The names should not be too short, generic or something that does not make the intention of the code clear like a, res etc. A sensible name will lead to much better readability and prevent accidental use of the same name later for some other object thereby losing the original.

SigmaPiEpsilon
  • 678
  • 5
  • 15
  • Thanks for clarifying! Though, my question is about best practices. I know that the code is correct and I can use `a` further. The main point is whether it's readable enough and isn't it planned to change such behavior – Charlie Dec 27 '17 at 09:34
  • If I understood the last paragraph of your original question correctly you were confusing python blocks with C++ and JS blocks and something like initialization of variables should be made outside blocks to use them outside. That's why I clarified about why you do not need to initialize anything and what blocks specifically mean in python. Python has nothing like `let` because that is default behavior but using `globals` inside a python block makes it visible outside (not recommended though). – SigmaPiEpsilon Dec 27 '17 at 15:05
  • As long as you use sensible names for objects (not too short, generic or something that does not make the intention clear enough like `a`) the code should be readable and there will be less chance that you will use the same name for some other object thereby losing the original object. Hence it is perfectly good practice. – SigmaPiEpsilon Dec 27 '17 at 15:08
  • Please, add this comment to your answer. It actually answers my question, and clarification of fundamentals makes it better to understand the thing – Charlie Dec 28 '17 at 09:20
1

Let me clarify what I meant in my comments.

#this is not, strictly, needed, but it makes the 
#exception handler more robust
a = b = None

try:
    if condition:
        a = f(x)
        b = v(x)
    else:
        a = g(y)
        b = v2(x)

    return w(a, b)

except Exception, e:
    logger.exception("exception:%s" % (e))
    logger.exception("  the value of a was:%s" % (a))
    logger.exception("  the value of b was:%s" % (b))
    raise 

This is pretty std code, you just want to wrap the whole thing in some logging code in case of exceptions. I re-raise the original exception, but could just as easily return a default value.

Problem is, unless the exception waits until return w(a, b) to happen, any access to a and b will throw its own NameError based on those variables not having been declared.

This has happened to me, a lot, with custom web unittesting code - I get a response from a get or post to an url and I run a bunch of tests against the response. If the original get/post that failed, the response doesn't exist, so any diagnostics like pretty printing that response's attributes will throw an exception, forcing you to clean things up before your exception handler is useful.

So, to guard against this, I initialize any variable referred to in the exception handler to None. Of course, if needed, you also have to guard against a being None, with something like logger("a.attr1:%s" % (getattr(a, "attr1","?")

JL Peyret
  • 10,917
  • 2
  • 54
  • 73
  • For this technique, I think then it is much better to set the initializing value to something other than `None`. `None` in assigned by default in many other cases in python for example when the functions don't explicitly return a value. IMO from a user point of view it is better to check if `a` or `b` exists in `except` (in `vars()` or another try-except) and explicitly generate a message stating `a`/`b` does not have a value yet since one or more statements did not execute yet. Then the user does not have to check the code to find what the initial values are. Explicit is better than implicit. – SigmaPiEpsilon Dec 28 '17 at 15:14
  • `Simpler is better than too complicated.` – JL Peyret Dec 28 '17 at 18:23