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??
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.
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!
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
(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.
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!
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.
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!
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.