Search code examples
pythonperl

Behavior of python method in absence of return statement


I have a question related to change in program behavior the absence of return statement leads to in a python method.

The count method below prints number of digits in a given integer. With the below chunk of code I get the result as 4, which is the expected result.

def count(x,acc=0):
    if x==0:
        return acc        
    return count(x/10,acc+1)

print "Count is %s" %(count(1234))

Result: Count is 4

If I modify the above method so that the last statement does not contain the 'return' statement the result I get is 'None'.

def count(x,acc=0):
    if x==0:
        return acc        
    count(x/10,acc+1)

print "Count is %s" %(count(1234))

Result: Count is None

(The version of Python I used is: 2.7.3)

Does the above behavior results due to the fact that Python doesn't do tail call optimization or is there any other reasoning involved ?

A similar chunk of code in perl (which AFAIK doesn't do tail call optimization) provides the expected result without 'return' being part of the last statement.

sub counter {
    my ($n,$acc) = @_;
    return $acc if ($n==0);
    counter(int($n/10), $acc+1);
}
print "Count is:" . counter(1234,0) ."\n"

Result: Count is:4

(The versions of Perl I ran above code chunk are : 5.14.4 and 5.8.5).

My questions are:

  • Is Tail Call optimization the reason for the behavior shown in above chunk of Python code.
  • If that were the case then why behavior of perl code differs, which doesn't do TCO either.

Solution

  • Doesn't have to do with the missing of tail optimization at all. Functions in Python need the keyword return explicitly, otherwise is assumed their return None.

    I know Ruby doesn't behave that way, it returns the value of the last executed expression. With Perl it must be the same.

    It is nothing that clever, just the fact that Python programs behave that way :)

    See the disassembly for both Python functions. You may see how the one having the return value actually calls the function and return the value on top of the stack. The one that doesn't have it, see that have two instructions after the funciont call, which load constant None and return it.

    def count(x,acc=0):
        if x==0:
            return acc        
        return count(x/10,acc+1)
    
    def count2(x,acc=0):
        if x==0:
            return acc        
        count(x/10,acc+1)
    
    In [7]: import dis    
    In [8]: dis.dis(count)
      2           0 LOAD_FAST                0 (x)
                  3 LOAD_CONST               1 (0)
                  6 COMPARE_OP               2 (==)
                  9 POP_JUMP_IF_FALSE       16
    
      3          12 LOAD_FAST                1 (acc)
                 15 RETURN_VALUE
    
      4     >>   16 LOAD_GLOBAL              0 (count)
                 19 LOAD_FAST                0 (x)
                 22 LOAD_CONST               2 (10)
                 25 BINARY_DIVIDE
                 26 LOAD_FAST                1 (acc)
                 29 LOAD_CONST               3 (1)
                 32 BINARY_ADD
                 33 CALL_FUNCTION            2
                 36 RETURN_VALUE
    
    In [9]: dis.dis(count2)
      2           0 LOAD_FAST                0 (x)
                  3 LOAD_CONST               1 (0)
                  6 COMPARE_OP               2 (==)
                  9 POP_JUMP_IF_FALSE       16
    
      3          12 LOAD_FAST                1 (acc)
                 15 RETURN_VALUE
    
      4     >>   16 LOAD_GLOBAL              0 (count)
                 19 LOAD_FAST                0 (x)
                 22 LOAD_CONST               2 (10)
                 25 BINARY_DIVIDE
                 26 LOAD_FAST                1 (acc)
                 29 LOAD_CONST               3 (1)
                 32 BINARY_ADD
                 33 CALL_FUNCTION            2
                 36 POP_TOP
                 37 LOAD_CONST               0 (None)
                 40 RETURN_VALUE