[Tutor] inter-module global variable

Steven D'Aprano steve at pearwood.info
Sun Mar 28 12:50:46 CEST 2010


On Sun, 28 Mar 2010 08:31:57 pm spir ☣ wrote:
> Hello,
>
> I have a main module importing other modules and defining a top-level
> variable, call it 'w' [1]. I naively thought that the code from an
> imported module, when called from main, would know about w, 

Why would it?

If you write a module M, you can't control what names exist in the 
calling module, and you shouldn't have to. Imagine if you wrote a 
module containing a function f, and it was imported by another module 
also containing f, and then suddenly all your module's functions 
stopped working! This would be a disaster:

# mymodule.py
def f(n):
    return n+1

def spam(n):
    return "spam"*f(n)


# caller.py
def f(n):
    return range(23, 45+n, 6)

from mymodule import spam
print spam(2)  # expect "spamspamspam", get TypeError instead


> but I 
> have name errors. The initial trial looks as follows (this is just a
> sketch, the original is too big and complicated):
>
> # imported "code" module
> __all__ = ["NameLookup", "Literal", "Assignment", ...]
>
> # main module
> from parser import parser

By the way, have you looked at PyParsing? This is considered by many to 
be the gold standard in Python parsing libraries.


> from code import *

This is discouraged strongly. What happens if the code module has 
something called parser? Or len?

> from scope import Scope, World
> w = World()
>
> This pattern failed as said above. 

What do you mean "failed"? Nothing you show is obviously broken.



> So, I tried to "export" w: 
>
> # imported "code" module
> __all__ = ["NameLookup", "Literal", "Assignment", ...]
>
> # main module
> from parser import parser
> from scope import Scope, World
> w = World()
> import code		#    new
> code.w = w		### "export"
> from code import *
>
> And this works. I had the impression that the alteration of the
> "code" module object would not propagate to objects imported from
> "code". But it works. 

It sounds like you are trying to write PHP code in Python.


> But I find this terribly unclear, fragile, and 
> dangerous, for any reason. (I find this "dark", in fact ;-) Would
> someone try to explain what actually happens in such case? 

Yep, sounds like PHP code :)

Every function and class in a module stores a reference to their 
enclosing globals, so that when you do this:

# module A.py
x = "Hello world"

def f():
    print x


# module B.py
from A import f
f()
=> prints "Hello world" as expected.


You don't have to do anything to make this work: every class and 
function knows what namespace it belongs to.

I can only imagine you're trying to do this:

# module A.py
x = "Hello world"

def f():
    print x


# module B.py
x = "Goodbye cruel world!"
from A import f
f()
=> prints "Goodbye cruel world!"


This is bad design. You might think you need it, but in the long run you 
will regret it. You are mixing up arguments and globals. If you want 
the result of f() to depend on the local value of x, then make it take 
an argument:

def f(x):
    print x

and call it:

f(x)


http://c2.com/cgi/wiki?GlobalVariablesAreBad
http://discuss.joelonsoftware.com/default.asp?design.4.249182.18



> Also, why 
> is a global variable not actually global, but in fact only "locally"
> global (at the module level)? It's the first time I meet such an
> issue. What's wrong in my design to raise such a problem, if any?

In Python, that is a deliberate choice. All globals are deliberately 
global to the module. The closest thing to "globally global" is the 
builtins namespace, which is where builtins like len, map, str, etc. 
are found.

Any design which relies on modifying global variables is flawed. Global 
variables are a poor design:

http://weblogs.asp.net/wallen/archive/2003/05/08/6750.aspx

Slightly better than global variables is a design where you use a module 
or class as a namespace, put all your globals in that namespace, then 
pass it to your other classes as an argument:

class SettingsNamespace:
    pass

settings = SettingsNamespace()
settings.x = 42
settings.y = 23
settings.z = "magic"

instance = MyOtherClass(a, b, c, settings)

This is still problematic. For example, if I change settings.x, will the 
result of MyOtherClass be different? Maybe, maybe not... you have to 
dig deep into the code to know which settings are used and which are 
not, and you never know if an innocent-looking call to a function or 
class will modify your settings and break things.



> My view is a follow: From the transparency point of view (like for
> function transparency), the classes in "code" should _receive_ as
> general parameter a pointer to 'w', before they do anything.

Yes, this is better than "really global" globals, but not a lot better.


> In other 
> words, the whole "code" module is like a python code chunk
> parameterized with w. If it would be a program, it would get w as
> command-line parameter, or from the user, or from a config file.
> Then, all instanciations should be done using this pointer to w.
> Meaning, as a consequence, all code objects should hold a reference
> to 'w'. This could be made as follows:

If every code object has a reference to the same object w, that defeats 
the purpose of passing it as an argument. It might be local in name, 
but in practice it is "really global", which is dangerous.


> # main module
> import code
> code.Code.w = w

Why not just this?

code.w = w

And where does w come from in the first place? Shouldn't it be defined 
in code.py, not the calling module?


> from code import *

This is generally frowned upon. You shouldn't defeat Python's 
encapsulation of namespaces in that way unless you absolutely have to.


> # "code" module
> class Code(object):
>     w = None	### to be exported from importing module

That sets up a circular dependency that should be avoided: Code objects 
are broken unless the caller initialises the class first, but you can't 
initialise the class unless you import it. Trust me, you WILL forget to 
initialise it before using it, and then spend hours trying to debug the 
errors.



>     def __init__(self, w=Code.w):
>         # the param allows having a different w eg for testing
>         self.w = w

This needlessly gives each instance a reference to the same w that the 
class already has. Inheritance makes this unnecessary. You should do 
this instead:

class Code(object):
    w = None  # Better to define default settings here.
    def __init__(self, w=None):
        if w is not None:
            self.w = w

If no w is provided, then lookups for instance.w will find the shared 
class attribute w.


[...]
> But the '###' line looks like  an ugly trick to me. (Not the fact
> that it's a class attribute; as a contrary, I often use them eg for
> config, and find them a nice tool for clarity.) The issue is that
> Code.w has to be exported. 

It is ugly, and fragile. It means any caller is *expected* to modify the 
w used everywhere else, in strange and hard-to-predict ways.





-- 
Steven D'Aprano


More information about the Tutor mailing list