Python Forum
In this function y initially has no value, but a call to foo() gives no error. Why? - Printable Version

+- Python Forum (https://python-forum.io)
+-- Forum: Python Coding (https://python-forum.io/forum-7.html)
+--- Forum: General Coding Help (https://python-forum.io/forum-8.html)
+--- Thread: In this function y initially has no value, but a call to foo() gives no error. Why? (/thread-31506.html)



In this function y initially has no value, but a call to foo() gives no error. Why? - Pedroski55 - Dec-16-2020

This is from Reuven Lerner's book Python Workout.

I don't really understand why I don't get an error when no value is passed to y.
Or why, the second time around, I get the expected result.
(I added the print()s to show what is happening.

def foo(x):
    def bar(y):
        print(f'y is {y}')
        print(f'x is {x}')
        print(f'x * y is {x*y}')
        return x * y
    print(f'bar is {bar}')
    return bar
I call the function with g = foo(2), but y has no value. I expected an error, but I get:

Quote:g = foo(2) # returns: bar is <function foo.<locals>.bar at 0x7ff37b475268>

Why is there no error when y has no value??

Normally, if a function requires an argument and none is supplied, you will get an error.

Now I try:

print(g(20))
but I don't get:

Quote:foo(20) # returns: bar is <function foo.<locals>.bar at 0x7ff37b475268>

I get the intended result:

Quote:y is 20
x is 2
x * y is 40
40

How did 20 get passed to y??


RE: In this function y initially has no value, but a call to foo() gives no error. Why? - deanhystad - Dec-16-2020

The function foo returns the function bar, it does not call the function. You then use that return value to call function bar(). So when you called g(20) you were actually calling foo.bar(20).

The more interesting thing in this example is where does bar get a value for x? Why is x hanging around after foo is done executing? What is keeping garbage collection from throwing x away? Ponder that for a bit.

And if you are under the impression that each Python function has some static namespace where it keeps all this info, look at this example:
def foo(x):
    def bar(y):
        return x*y
    return bar

a = foo(10)
b = foo(20)
print(a(2), b(2))
Output:
20, 40
a is the function bar() with a context for bar() that remembers x == 10. b is also the function bar() with a context that remembers x == 20. Interesting.


RE: In this function y initially has no value, but a call to foo() gives no error. Why? - Pedroski55 - Dec-16-2020

Well, "where does bar get a value for x?"

Reuven Lerner talked about LEGB: Python looks in: Local, Enclosing, Global, then Builtin for x

Still not sure I understand this, but I am a slow learner!


RE: In this function y initially has no value, but a call to foo() gives no error. Why? - DeaD_EyE - Dec-16-2020

This is a closure. A function inside a function.
The inner function has access to objects of the outer function.
The function bar access x from foo's scope.

def foo(x):
    def bar(y):
        return x*y
    return bar
Calling foo(), will return a function object:
Output:
<function __main__.foo.<locals>.bar(y)>
You've to call the returned function bar to get the return value from bar.

func = foo(13)
# or in one line
# result = foo(13)(42)
result = func(42)
print(result)
Output:
546



RE: In this function y initially has no value, but a call to foo() gives no error. Why? - deanhystad - Dec-16-2020

(Dec-16-2020, 10:09 AM)Pedroski55 Wrote: Well, "where does bar get a value for x?"

Reuven Lerner talked about LEGB: Python looks in: Local, Enclosing, Global, then Builtin for x

Still not sure I understand this, but I am a slow learner!
LEGB describes why bar() can see x (though it cannot change x), but it does not explain why in my example a sees x=10 and b sees x=20. In C it is easy to pass s function pointer around, but that pointer does not come with it's own little piece of frozen space time. This is a very cool, and slightly frightening, demonstration of a feature that may be uniquely Python.


RE: In this function y initially has no value, but a call to foo() gives no error. Why? - Pedroski55 - Dec-16-2020

Thanks a lot!

I'll let this sink in for a while, maybe I'll have a "Eureka!"

All I know is, try calling a function that requires 1 or more parameters, but don't pass a parameter, and you get this:

Quote:>>> foo()
Traceback (most recent call last):
File "<pyshell#2>", line 1, in <module>
foo()
TypeError: foo() missing 1 required positional argument: 'x'

Apparently, if the function is inside another function, you're OK.

Don't know when I might need this fuctionality, but I will remember it!


RE: In this function y initially has no value, but a call to foo() gives no error. Why? - ndc85430 - Dec-17-2020

Remember that calling a function and defining a function are two completely different things. The latter creates a function that can later be called and declares what parameters must be given at call time. Since there is no call to the function then, there's no checking of what parameters are declared (how could there be?). That is the same regardless of whether that definition is nested inside another function or not. Calling a function necessitates passing the right number of arguments and obviously checking against the definition only happens at call time. This happens regardless of what scope that call is in (whether inside a function or at a global level, for example).

Let's say you want to measure execution times of function calls and record those somewhere. Of course, you could just change all your functions to do that, but you'd be repeating similar code everywhere. Another approach is to write a function that takes as its argument a function f and produces a new one that calls f and does the measurement and recording of the time:

def measure_time(f):
  def measured():
     start_time = calculate_time()
     result = f()
     end_time = calculate_time()
     record_time(end_time - start_time)
     return result
  return measured
(I've used fictional functions and just taken the case that f takes no arguments, for the sake of simplicity).

This is known as a decorator and Python has syntax to decorate functions:

@measure_time
def do_something_useful():
  ...
Here, basically, with the presence of the @measure_time decorator, do_something_useful will really be the function that measure_time returns (i.e. one that does exactly the same as do_something_useful but with the additional recording of the execution time.


RE: In this function y initially has no value, but a call to foo() gives no error. Why? - Pedroski55 - Dec-17-2020

Thanks!

A little further in my book, Reuven Lerner notes:

Quote:Working with inner functions and closures can be quite surprising and
confusing at first. That’s particularly true because our instinct is to believe
that when a function returns, its local variables and state all go away. Indeed,
that’s normally true—but remember that in Python, an object isn’t released
and garbage-collected if there’s at least one reference to it. And if the inner
function is still referring to the stack frame in which it was defined, then the
outer function will stick around as long as the inner function exists.

Then he gives this example of "function in function":

#! /usr/bin/python3
# password generator

import random

def create_password_generator(characters):
    def create_password(length):
        output = []
        for i in range(length):
            output.append(random.choice(characters))
        return ''.join(output)
    return create_password

alpha_password = create_password_generator('abcdef')
symbol_password = create_password_generator('!@#$%')

print(alpha_password(5))
print(alpha_password(10))

print(symbol_password(5))
print(symbol_password(10))
Things become clearer!


RE: In this function y initially has no value, but a call to foo() gives no error. Why? - ndc85430 - Dec-19-2020

You don't necessarily need the nested function for that; you can use partial application with partial from the functools module. Have create_password take both parameters, characters and length:

def create_password(characters, length):
  # Code as above
partial allows you to freeze the arguments of a function, returning a function with a lower arity:

from functools import partial

alpha_password = partial(create_password, characters='abcdef')
symbol_password = partial(create_password, characters='!@#$%')
Both alpha_password and symbol_password are functions that just take the length, as in your example.