Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schrödinger's variable #245

Closed
charles-l opened this issue Dec 21, 2020 · 5 comments
Closed

Schrödinger's variable #245

charles-l opened this issue Dec 21, 2020 · 5 comments

Comments

@charles-l
Copy link

charles-l commented Dec 21, 2020

▶ Schrödinger's variable *

The asterisk at the end of the title indicates the example was not present in the first release and has been recently added.

def make_func():
    for target in ('a', 2, None):
        if isinstance(target, str):
            def f():
                print(f'inside f(), {target=}')
            print(f'just created f(), {target=} at this point!')
    return f

Output (Python version):

>>> f = make_func()
target='a' at this point!

>>> f()
inside f(), target=None

💡 Explanation:

  • Python doesn't actually bind the value of target in f(). It just creates a function that will look up target in the surrounding context.

  • Since target will continue to be updated until it is set to None in the last iteration of the for loop, target=None when it prints inside of f()

    Output:

>>> f.__code__.co_freevars
('target',)
>>> import inspect
>>> print(inspect.getclosurevars(f).nonlocals['target'])
None

This might be made a bit clearer if we use a global variable rather than a free variable:

for target in ('a', 2, None):
    if isinstance(target, str):
        def f():
            print(f'inside f(), {target=}')
        print(f'just created f(), {target=} at this point!')

Output.

just created f(), target='a' at this point!
>>> print(target)
None
>>> f()
inside f(), target=None
>>> target = "whoa"
inside f(), target='whoa'
@charles-l
Copy link
Author

Maybe this is intuitive to other folks, but I got burned pretty badly by this one recently when writing some code that used closures. I would have thought that on the definition of f(), target would have been bound in the function. But that's not the case (as shown above).

@satwikkansal
Copy link
Owner

Hi Charles, thanks for sharing this.

I think the concept overlaps with The sticky output function example, but I do like the title and certain parts of your explanation. So I think the best way out is to combine these two in the next release :)

@charles-l
Copy link
Author

@satwikkansal ahh, I missed that one. Merging them sounds good! Let me know if you want me to take a shot a combining the explanations.

@satwikkansal
Copy link
Owner

Let me know if you want me to take a shot a combining the explanations.

@charles-l Sure, that'd be helpful, thanks

@charles-l
Copy link
Author

▶ Schrödinger's variable *

The asterisk at the end of the title indicates the example was not present in the first release and has been recently added.

funcs = []
results = []
for x in range(7):
    def some_func():
        return x
    funcs.append(some_func)
    results.append(some_func())  # note the function call here

funcs_results = [func() for func in funcs]

Output (Python version):

>>> results
[0, 1, 2, 3, 4, 5, 6]
>>> funcs_results
[6, 6, 6, 6, 6, 6, 6]

The values of x were different in every iteration prior to appending some_func to funcs, but all the functions return 6 when they're evaluated after the loop completes.

>>> powers_of_x = [lambda x: x**i for i in range(10)]
>>> [f(2) for f in powers_of_x]
[512, 512, 512, 512, 512, 512, 512, 512, 512, 512]

💡 Explanation:

  • When defining a function inside a loop that uses the loop variable in its body, the loop function's closure is bound to the variable, not its value. The function looks up x in the surrounding context, rather than using the value of x at the time the function is created. So all of the functions use the latest value assigned to the variable for computation. We can see that it's using the x from the surrounding context (i.e. not a local variable) with:
>>> import inspect
>>> inspect.getclosurevals(funcs[0])
ClosureVars(nonlocals={}, globals={'x': 6}, builtins={}, unbound=set())

Since x is a global value, we can change the value that the funcs will lookup and return by updating x:

>>> x = 42
>>> [func() for func in funcs]
[42, 42, 42, 42, 42, 42, 42]
  • To get the desired behavior you can pass in the loop variable as a named variable to the function. Why does this work? Because this will define the variable inside the function's scope. It will no longer go to the surrounding (global) scope to look up the variables value but will create a local variable that stores the value of x at that point in time.
funcs = []
for x in range(7):
    def some_func(x=x):
        return x
    funcs.append(some_func)

Output:

>>> funcs_results = [func() for func in funcs]
>>> funcs_results
[0, 1, 2, 3, 4, 5, 6]

It is not longer using the x in the global scope:

>>> inspect.getclosurevars(funcs[0])
ClosureVars(nonlocals={}, globals={}, builtins={}, unbound=set())

satwikkansal added a commit that referenced this issue Jan 17, 2021
muscliary pushed a commit to muscliary/wtfpython that referenced this issue Sep 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants