David Bridgeland
David Bridgeland

Reputation: 545

Python namespaces and import: setting the value of a global variable in an imported module creates a second, unrelated global?

(Python 3.6)

Consider a package odd with two modules foo and bar. I want a customer of odd to use a single import for everything in the package, without having to know what's defined in foo and what's defined in bar. So I define an __init__.py with the following:

from .foo import *
from .bar import *

foo.py contains a global variable baz and a function set_baz() that sets the value of baz, as follows:

baz = 12

def set_baz():
    global baz 
    print('Baz was {}'.format(baz))
    baz = 13
    print('Now it is changed to {}'.format(baz))

When I import odd (from elsewhere) and look at the value of odd.baz, I get what I expect, the original value of 12.

In [2]: import odd

In [3]: odd.baz
Out[3]: 12

But when I run set_baz(), it does not change the value of odd.baz, as expected, but instead changes a different global variable: odd.foo.baz.

In [4]: odd.set_baz()
Baz was 12
Now it is changed to 13

In [5]: odd.baz
Out[5]: 12

In [6]: odd.foo.baz
Out[6]: 13

Apparently there are two unrelated namespaces, one for odd and one for odd.foo, and each namespace defines baz, differently.

How can I accomplish what I want, a single global variable, accessed by customers of odd as odd.baz?

(And yes, I know that I should not be using global variables like this. I have distilled the reasonable goal I am actually trying to accomplish into this silly example with odd.baz that illustrates the underlying problem.)

Upvotes: 2

Views: 72

Answers (1)

Richard Neumann
Richard Neumann

Reputation: 3361

Python has names, not variables.
If you import odd.foo.baz in odd.__init__, it will create a new name odd.__init__.baz pointing to the same int object 12.
If you change the name odd.foo.baz to point to another int, 13, this has no effect whatsoever on the name odd.__init__.baz.

Such quirks are one of the reasons why globals are considered bad practice (except very special cases).

You can solve the issue by encapsulating baz in a mutable object, like a dict:

odd.foo.py:

GLOBALS = {'baz': 12}

def set_baz():
    print('Baz was {}'.format(GLOBALS['baz']))
    GLOBALS['baz'] = 13
    print('Now it is changed to {}'.format(GLOBALS['baz']))

def get_baz():
    return GLOBALS['baz']

To make a name baz that behaves similar to a int, you can use werkzeug.local.LocalProxy:

from werkzeug.local import LocalProxy

baz = LocalProxy(lambda: GLOBALS['baz'])

Example:

>>> test.baz
12
>>> test.set_baz()
Baz was 12
Now it is changed to 13
>>> test.baz
13
>>> test.baz + 4
17
>>> test.baz.__class__
<class 'werkzeug.local.LocalProxy'>

Upvotes: 1

Related Questions