Python: A Brief Analysis of the Pit Excavated by return and finally

Posted by sidhumaharaj on Tue, 04 Jun 2019 04:04:03 +0200

Initial knowledge of return

_Believe that every child shoe that has used Python function will definitely use return statement. As the name implies, return is used to return value to the caller, for example:

def test():
    a = 2
    return a

s = test()
print s

# Output result
2

For the above results, I believe that everyone will not be surprised, then a little more difficult, if there is code in the return statement? What about that code?

def test():
    a = 2
    return a
    s = 3
    print s

s = test()
print s

# What is the result?

Old drivers are sure to see the results at a glance, but for children's shoes that are still at the beginning or don't know much about return, they may be confused. Will the next two sentences of code be executed?
The answer is: No execution.

returnJust like its name, When executing this code, The entire function returns, The whole call is over.~ So inreturnLater code, Neither will be executed.!
  It is precisely because of this characteristic., So there's a code specification calledearly returnCoding specifications are advocated

It probably means: When the condition has been met and returned, Come back right away.
Take an example to illustrate.:

def test():
    a = 2
    if a > 2:
        result = 'more than'
    else:
        result = 'less than'
    return result

s = test()
print s

The above code should be easy to understand, that is, to decide what result to return based on the value of A. This code is believed to be popular with most children's shoes, because this is more intuitive, however, it seems a bit wasteful to write, because when the first judgment is over, if the result is true, it should return more than, and then end the function, otherwise it must be more than. Return less than, so we can adjust the code to this:

def test():
    a = 2
    if a > 2:
        return 'more than'
    else:
        return 'less than'

s = test()
print s

Or even:

def test():
    a = 2
    if a > 2:
        return 'more than'
    return 'less than'

s = test()
print s

The result is the same as that of the first writing. The first time I saw children's shoes like this, I might find it more difficult to accept, even worse readability, but in fact, I think it would be slightly better. Because:

  1. With fewer code running, callers can get results faster

  2. It helps to reduce the number of nested layers and is easy to understand.

As for point 2, it needs to be explained that most of the time we write code with deep nesting because of the pot of if/else. Because there are more if/else nested, a lot of code is deeply nested. This is a disaster for other small partners because they may forget the previous logic when they read this part of the code.
To make it easier to understand, here's a code example:

def test():
    a = 2
    if a > 2:
        result = 'not 2'
    else:
        a += 2
        if a < 2:
            result = 'not 2'
        else:
            for i in range(2):
                print 'test ~'
            result = 'Target !'
    return result

s = test()
print s

# Output result
test ~
test ~
Target !

Code Simplification Optimized Version:

def test():
    a = 2
    if a > 2:
        return 'not 2'
    
    a += 2
    if a < 2:
        return 'not 2'
    
    for i in range(2):
        print 'test ~'

    return 'Target !'

s = test()
print s

# Output result
test ~
test ~
Target !

By contrast, we should be able to better understand why early return can reduce the number of nested layers.~

Talking about deep pit

Just now, I spent a long time to introduce return. I believe that I have a basic understanding of return here. So let's talk about a more confusing topic.

What happens when return meets try..finally?

If I had just taken it seriously, I would have noticed a sentence that is:

Return represents the return of the entire function, and the function call ends.

But is that really the case? Usually when asked, the answer is not always the same.~~
Let's start with an example:

def test():
    try:
        a = 2
        return a
    except:
        pass

    finally:
        print 'finally'

s = test()
print s

Can you guess if this print a will print? I believe many children's shoes think for a while and then say no. However, the answer is wrong, the real output is:

finally
2

You feel as if you have seen the New World. In the first example, the sentence behind return has not been executed, but here, so far apart, you still haven't forgotten. Maybe this is true love.

However, because of this kind of "true love", a bunch of old and new drivers always fall into the pit. Then they don't know for Mao.

In order to avoid them continuing to use the pretext of "true love" to bully us, let's uncover the true face of "true love" together!

So, we have to use the spy artifact: dis, think about it all a little bit exciting! ___________

import dis
def test():
    try:
        a = 2
        return a
    except:
        pass

    finally:
        print 'finally'

print dis.dis(test)

Output is longer, write separately:

# Output result
  6           0 SETUP_FINALLY           28 (to 31)
              3 SETUP_EXCEPT            14 (to 20)

  7           6 LOAD_CONST               1 (2)
              9 STORE_FAST               0 (a)

  8          12 LOAD_FAST                0 (a)
             15 RETURN_VALUE        
             16 POP_BLOCK           
             17 JUMP_FORWARD             7 (to 27)

  9     >>   20 POP_TOP             
             21 POP_TOP             
             22 POP_TOP             

 10          23 JUMP_FORWARD             1 (to 27)
             26 END_FINALLY         
        >>   27 POP_BLOCK           
             28 LOAD_CONST               0 (None)

 13     >>   31 LOAD_CONST               2 ('finally')
             34 PRINT_ITEM          
             35 PRINT_NEWLINE       
             36 END_FINALLY         
             37 LOAD_CONST               0 (None)
             40 RETURN_VALUE  

Here's a brief description of what these columns mean:

1. The first column is the line number of the code in the file.
2. The offset of the second column bytecode
 3. Name of bytecode
 4. parameter
 5. Final results of bytecode processing parameters

As you can see from the bytecode, SETUP_FINALLY and SETUP_EXCEPT correspond to final and try. Although final is behind try, although we usually treat them as a whole, they are actually separate... Because we focus on final, we just look at SETUP_FINALLY.

// ceval.c
TARGET(SETUP_FINALLY)
        _setup_finally:
        {
            /* NOTE: If you add any new block-setup opcodes that
               are not try/except/finally handlers, you may need
               to update the PyGen_NeedsFinalizing() function.
               */

            PyFrame_BlockSetup(f, opcode, INSTR_OFFSET() + oparg,
                               STACK_LEVEL());
            DISPATCH();
        }


// fameobject.c
void
PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level)
{
    PyTryBlock *b;
    if (f->f_iblock >= CO_MAXBLOCKS)
        Py_FatalError("XXX block stack overflow");
    b = &f->f_blockstack[f->f_iblock++];
    b->b_type = type;
    b->b_level = level;
    b->b_handler = handler;
}

From the above code, it is obvious that SETUP_FINALLY calls PyFrame_BlockSetup to create a Block and then sets it up:

  1. b_type (opcode is SETUP_FINALLY)

  2. b_level

  3. b_handler (INSTR_OFFSET() + oparg)

handler may be more difficult to understand, actually look at the dis output just now, you can see which is 13 > > > 31 LOAD_CONST 2 ('finally'). This arrow tells us where to jump, why to jump to this sentence? Because 60 SETUP_FINALLY 28 (to 31) has told us to jump to 31.~~~

If this is clear, let's look at return again. The bytecode for return is: RETURN_VALUE, so the source code for return is: RETURN_VALUE.

// ceval.c
TARGET_NOARG(RETURN_VALUE)
        {
            retval = POP();
            why = WHY_RETURN;
            goto fast_block_end;
        }

We used to understand that return is a false return! Instead of returning directly, this return pops up the value of the stack, assigns a retval, then sets why to WHY_RETURN, and runs away! Running to a place called fast_block_end, there's no way, in order to expose the true face, we have to dig three feet:

while (why != WHY_NOT && f->f_iblock > 0) {
            fast_block_end:
        while (why != WHY_NOT && f->f_iblock > 0) {
            /* Peek at the current block. */
            PyTryBlock *b = &f->f_blockstack[f->f_iblock - 1];

            assert(why != WHY_YIELD);
            if (b->b_type == SETUP_LOOP && why == WHY_CONTINUE) {
                why = WHY_NOT;
                JUMPTO(PyInt_AS_LONG(retval));
                Py_DECREF(retval);
                break;
            }

            /* Now we have to pop the block. */
            f->f_iblock--;

            while (STACK_LEVEL() > b->b_level) {
                v = POP();
                Py_XDECREF(v);
            }
            if (b->b_type == SETUP_LOOP && why == WHY_BREAK) {
                why = WHY_NOT;
                JUMPTO(b->b_handler);
                break;
            }
            if (b->b_type == SETUP_FINALLY ||
                (b->b_type == SETUP_EXCEPT &&
                 why == WHY_EXCEPTION) ||
                b->b_type == SETUP_WITH) {
                if (why == WHY_EXCEPTION) {
                    PyObject *exc, *val, *tb;
                    PyErr_Fetch(&exc, &val, &tb);
                    if (val == NULL) {
                        val = Py_None;
                        Py_INCREF(val);
                    }
                    /* Make the raw exception data
                       available to the handler,
                       so a program can emulate the
                       Python main loop.  Don't do
                       this for 'finally'. */
                    if (b->b_type == SETUP_EXCEPT ||
                        b->b_type == SETUP_WITH) {
                        PyErr_NormalizeException(
                            &exc, &val, &tb);
                        set_exc_info(tstate,
                                     exc, val, tb);
                    }
                    if (tb == NULL) {
                        Py_INCREF(Py_None);
                        PUSH(Py_None);
                    } else
                        PUSH(tb);
                    PUSH(val);
                    PUSH(exc);
                }
                else {
                    if (why & (WHY_RETURN | WHY_CONTINUE))
                        PUSH(retval);
                    v = PyInt_FromLong((long)why);
                    PUSH(v);
                }
                why = WHY_NOT;
                JUMPTO(b->b_handler);
                break;
            }
        } /* unwind stack */

In this need to review some of the knowledge just now, we just looked at the return code and saw that it set why to WHY_RETURN, so in such a series of judgments, it just went to the last other, the action is very simple, that is to return the stored value retval and push back to the stack, at the same time, why into long and then press back to the stack, and then set the why, then the buttocks. Turn to the b_handler code set by SETUP_FINALLY just now. ~When this b_handler code is executed, do it back through END_FINALLY, and here it is, return retval.

conclusion

So, we should know why when we execute the return code, why final code will execute first, because the essence of return is to set why and retval, then go to a big judgment, and finally perform the corresponding operation according to the value of why! So it can be said that it is not really substantive return. Hope we can use them later, don't drop them again. In the pit!

Welcome to the discussion group of QQ: 258498217
For reprinting, please indicate the source: https://segmentfault.com/a/11...

Topics: Python less