3

I was playing with scopes and namespaces and I found a weird behaviour which I'm not sure how to explain. Say we have a file called new_script.py with inside

a = 0

def func():
    import new_script #import itself
    new_script.a += 1
    print(new_script.a)

func()
print(a)

when executing it prints

1 
1 
2 
0

I didn't expect the last print of the number 0. From what I understand, it prints the first two 1 executing the self-import statement incrementing the global a, then it prints 2 because it increments again the global a from inside the function, but then why the last print is 0 instead of 2?

Tortar
  • 625
  • 5
  • 15

2 Answers2

4

TL;DR: you have two different variables __main__.a and new_script.a. You only change new_script.a.

To trace it through:

a = 0

defines a variable a in __main__ module.

def func(): ...

defines a function func in __main__ module.

func()

calls this function in __main__ module. In function:

import new_script

imports new_script module in which:

a = 0
def func(): ...

defines a and func in new_script module (new_script.a and new_script.func)

func()

calls func from new_script module. In function:

import new_script

well, we have it imported already so we don't import it again

new_script.a += 1
print(new_script.a)

increment new_script.a and print it (our first 1). Then

print(a)

printing a (a.k.a new_script.a) from new_script module (our second 1)

new_script is finished. Back to execution in the __main__:

new_script.a += 1
print(new_script.a)

incrementing new_script.a second time and printing it (our 2).

Finally:

print(a)

printing a (a.k.a. __main__.a) that has never been changed (so 0)

Yevhen Kuzmovych
  • 10,940
  • 7
  • 28
  • 48
2

Well, this has lead down a very interesting rabit hole. So thanks you for that.

Here are the key points:

  • Imports will not recurse. If it's imported once, it will execute the module level code, but it will not execute again if it's imported again. Hence you only see 4 values.
  • Imports are singletons. If you try this code:
# singleton_test.py
import singleton_test

def func():
    import singleton_test #import itself
    print(singleton_test.singleton_test == singleton_test)

func()

It will print:

True
True
  • The imported singleton version of a module is different from the original ran version of the module

With this in mind, we can explore your code, by enriching it with a few more comments, particularly using __name__ which contains the name of the current module, which will be __main__ if the current module is what was ran originally:

a = 0

print("start", __name__)

def func():
    print("Do import", __name__)
    import new_script #import itself
    new_script.a += 1
    print(new_script.a, "func", __name__)

func()
print(a, "outr", __name__)

This will print

start __main__
Do import __main__
start new_script
Do import new_script
1 func new_script
1 outr new_script
2 func __main__
0 outr __main__

This shows you quite well, given that the imported module is a singleton (but not the module that was ran), that you

  • first print 1 in the function after you incremented value inside the function inside the module
  • then you print 1 at the end of the imported module
  • then you print 2 after incrementing the value on the singleton on the original run code
  • then finally you print 0 for the unchanged outer module that you originally ran, but have not touched.
tituszban
  • 4,797
  • 2
  • 19
  • 30
  • thank for your explaination! the key point I think is that importing a module creates a different namespace from the original one! I have just one question, could you specify a bit better what you mean by singleton? – Tortar Aug 24 '22 at 12:16
  • Maybe do you mean that importing it in two different scopes doesn't make them different? – Tortar Aug 24 '22 at 12:20
  • @Tortar By Singleton I refer to the common [Singleton pattern](https://en.wikipedia.org/wiki/Singleton_pattern) where you only have one instance of an entity in your codebase – tituszban Jun 06 '23 at 10:59