44

In short, the question: Is there a way to prevent Python from looking up variables outside the current scope?

Details:

Python looks for variable definitions in outer scopes if they are not defined in the current scope. Thus, code like this is liable to break when not being careful during refactoring:

def line(x, a, b):
    return a + x * b

a, b = 1, 1
y1 = line(1, a, b)
y2 = line(1, 2, 3)

If I renamed the function arguments, but forgot to rename them inside the function body, the code would still run:

def line(x, a0, b0):
    return a + x * b  # not an error

a, b = 1, 1
y1 = line(1, a, b)  # correct result by coincidence
y2 = line(1, 2, 3)  # wrong result

I know it is bad practice to shadow names from outer scopes. But sometimes we do it anyway...

Is there a way to prevent Python from looking up variables outside the current scope? (So that accessing a or b raises an Error in the second example.)

MB-F
  • 22,770
  • 4
  • 61
  • 116
  • 3
    No, there is not. Otherwise no built-in would ever work. – Martijn Pieters Jun 24 '15 at 09:41
  • 2
    The better solution is to use *proper testing*, rather than trying to break Python. – Martijn Pieters Jun 24 '15 at 09:41
  • 2
    put the last three lines into another function... – LittleQ Jun 24 '15 at 09:44
  • @MartijnPieters Good point. What about redirecting outer scope lookups directly to built-ins? I disagree with your point on breaking Python. While testing is good practice in general, I think it is not feasible in prototyping situations. – MB-F Jun 24 '15 at 09:46
  • @LittleQ Indeed. Clean and simple. I wonder why I didn't consider this in the first place... – MB-F Jun 24 '15 at 09:47
  • @kazemakase: no, there is no such option in Python. – Martijn Pieters Jun 24 '15 at 09:47
  • 1
    @kazemakase: instead, try to avoid creating too many globals. Put your other code in a `main()` function, for example. – Martijn Pieters Jun 24 '15 at 09:48
  • 2
    @kazemakase: take into account that all functions and classes in a module are globals too, it is not just built-ins your code will use. It'll be impossible to factor out anything into functions if globals were disabled. – Martijn Pieters Jun 24 '15 at 09:49
  • @MartijnPieters Right. I was thinking too much in C again. – MB-F Jun 24 '15 at 09:53
  • Avoid global variables? – Eric Levieil Jun 24 '15 at 09:55
  • @MartijnPieters: built-in names are in a different namespace: the builtins namespace: [*"The global namespace is searched first. If the name is not found there, the builtins namespace is searched."*](https://docs.python.org/3/reference/executionmodel.html) – jfs Jun 25 '15 at 10:03
  • @J.F.Sebastian: yes, but you can still import them as there is a [dedicated module](https://docs.python.org/2/library/__builtin__.html) ([New name in Python 3](https://docs.python.org/3/library/builtins.html)). – Martijn Pieters Jun 25 '15 at 10:24
  • @kazemakase when you posted this question, you found that SO q&a quoted in your question above. Now, you might also find [my different answer to that thread](https://stackoverflow.com/questions/20125172/how-bad-is-shadowing-names-defined-in-outer-scopes/40008745#40008745) relevant. – RayLuo Aug 28 '18 at 05:40
  • @RayLuo Thank you. Your answer is similar in spirit as the answer I accepted here - "be reasonable and avoid unnecessary globals" :) – MB-F Aug 28 '18 at 08:41

6 Answers6

37

Yes, maybe not in general. However you can do it with functions.

The thing you want to do is to have the function's global to be empty. You can't replace the globals and you don't want to modify it's content (becaus that would be just to get rid of global variables and functions).

However: you can create function objects in runtime. The constructor looks like types.FunctionType((code, globals[, name[, argdefs[, closure]]]). There you can replace the global namespace:

def line(x, a0, b0):
   return a + x * b  # will be an error

a, b = 1, 1
y1 = line(1, a, b)  # correct result by coincidence

line = types.FunctionType(line.__code__, {})
y1 = line(1, a, b)  # fails since global name is not defined

You can of course clean this up by defining your own decorator:

import types
noglobal = lambda f: types.FunctionType(f.__code__, {}, argdefs=f.__defaults__)

@noglobal
def f():
    return x

x = 5
f() # will fail

Strictly speaking you do not forbid it to access global variables, you just make the function believe there is no variables in global namespace. Actually you can also use this to emulate static variables since if it declares an variable to be global and assign to it it will end up in it's own sandbox of global namespace.

If you want to be able to access part of the global namespace then you'll need to populate the functions global sandbox with what you want it to see.

skyking
  • 13,817
  • 1
  • 35
  • 57
  • Something like this was what I had originally in mind when asking the question. However, simply using a main function suits my actual needs better. – MB-F Jun 25 '15 at 10:27
  • 4
    Great answer! I extended it a little to keep imports and functions defined: https://gist.github.com/ax3l/59d92c6e1edefcef85ac2540eb056da3 – Ax3l Mar 07 '17 at 10:53
  • @Ax3l nice extension! Could you make it cover imported objects such as `from win32con import MEM_COMMIT`? (I kind if doubt it, but maybe...) – bers Jan 18 '19 at 07:23
  • I think imported vars might not be distinguishable from inline defined global variables. – Ax3l Jan 21 '19 at 09:19
  • @Ax3l I'm not sure what you mean. In python there's local and global namespace, what you mean by "inline defined global variables" is unclear to me. However if you mean that imported variables in the global namespace will be invisible too, then yes, but that's because they're in the global namespace. Note that imports need not be placed in global namespace - for example if you do an import from within a function then the imported symbol will be placed in the local namespace and then visible even if you've hidden the global namespace. – skyking Jan 21 '19 at 18:18
  • yes, that's what I mean. Of course you can reorder your global defines and imports. but as soon as a variable is imported into global namespace one can not differentiate it from an otherwise globally defined variable, afaik. – Ax3l Jan 22 '19 at 14:24
  • This seems to be difficult with optional arguments, such as here: `def myFunction(x=0): pass`. When calling myFunction(), I get `TypeError: myFunction() missing 1 required positional argument: 'x'`. Any idea how to solve this? – bers Mar 04 '19 at 14:31
  • @bers One can solve that by using the `argdefs` parameter of `FunctionType` constructor. I've updated my answer for that. – skyking Mar 05 '19 at 06:17
20

No, you cannot tell Python not to look names up in the global scope.

If you could, you would not be able to use any other classes or functions defined in the module, no objects imported from other modules, nor could you use built-in names. Your function namespace becomes a desert devoid of almost everything it needs, and the only way out would be to import everything into the local namespace. For every single function in your module.

Rather than try to break global lookups, keep your global namespace clean. Don't add globals that you don't need to share with other scopes in the module. Use a main() function for example, to encapsulate what are really just locals.

Also, add unittesting. Refactoring without (even just a few) tests is always prone to create bugs otherwise.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 10
    I've always considered use of a `main()` function in Python scripts a relict of old C practice. Thinking in terms of global namespace polution makes me see it in a different light, now. Thank you :) – MB-F Jun 24 '15 at 10:49
  • @kazemakase Another important reason is to allow a program to also be imported as a module. If your code is not wrapped in a function, then it would execute on import. – Michael Mior Apr 20 '21 at 19:32
7

With @skyking's answer, I was unable to access any imports (I could not even use print). Also, functions with optional arguments are broken (compare How can an optional parameter become required?).

@Ax3l's comment improved that a bit. Still I was unable to access imported variables (from module import var).

Therefore, I propose this:

def noglobal(f):
    return types.FunctionType(f.__code__, globals().copy(), f.__name__, f.__defaults__, f.__closure__)

For each function decorated with @noglobal, that creates a copy of the globals() defined so far. This keeps imported variables (usually imported at the top of the document) accessible. If you do it like me, defining your functions first and then your variables, this will achieve the desired effect of being able to access imported variables in your function, but not the ones you define in your code. Since copy() creates a shallow copy (Understanding dict.copy() - shallow or deep?), this should be pretty memory-efficient, too.

Note that this way, a function can only call functions defined above itself, so you may need to reorder your code.

For the record, I copy @Ax3l's version from his Gist:

def imports():
    for name, val in globals().items():
        # module imports
        if isinstance(val, types.ModuleType):
            yield name, val
        # functions / callables
        if hasattr(val, '__call__'):
            yield name, val

noglobal = lambda fn: types.FunctionType(fn.__code__, dict(imports()))
bers
  • 4,817
  • 2
  • 40
  • 59
  • 1
    If one needs to use `@noglobal` somewhere in the middle of the code, the approach "`globals()` defined so far" may not be ideal because `globals` has been mixed up with new variables along the way. In this case, I define `init_globals = globals().copy()` right behind the all imports, and `noglobal = lambda f: types.FunctionType(f.__code__, init_globals, argdefs=f.__defaults__)`, then use `@noglobal` whenever I want, without worrying about mixing up with new variables. – JoyfulPanda Jun 03 '21 at 00:15
  • @JoyfulPanda with this approach, a function `f` defined in your module cannot call some other function `g` defined in the module, even when it is defined before it, right? https://www.mycompiler.io/view/GPb2ZJJ – bers Jun 14 '21 at 14:57
5

To discourage global variable lookup, move your function into another module. Unless it inspects the call stack or imports your calling module explicitly; it won't have access to the globals from the module that calls it.

In practice, move your code into a main() function, to avoid creating unnecessary global variables.

If you use globals because several functions need to manipulate shared state then move the code into a class.

jfs
  • 399,953
  • 195
  • 994
  • 1,670
3

As mentioned by @bers the decorator by @skykings breaks most python functionality inside the function, such as print() and the import statement. @bers hacked around the import statement by adding the currently imported modules from globals() at the time of decorator definition.

This inspired me to write yet another decorator that hopefully does what most people who come looking at this post actually want. The underlying problem is that the new function created by the previous decorators lacked the __builtins__ variable which contains all of the standard built-in python functions (e.g. print) available in a freshly opened interpreter.

import types
import builtins

def no_globals(f):
    '''
    A function decorator that prevents functions from looking up variables in outer scope.
    '''
    # need builtins in globals otherwise can't import or print inside the function
    new_globals = {'__builtins__': builtins} 
    new_f = types.FunctionType(f.__code__, globals=new_globals, argdefs=f.__defaults__)
    new_f.__annotations__ = f.__annotations__ # for some reason annotations aren't copied over
    return new_f

Then the usage goes as the following

@no_globals
def f1():
    return x

x = 5
f1() # should raise NameError

@no_globals
def f2(x):
    import numpy as np
    print(x)
    return np.sin(x)

x = 5
f2(x) # should print 5 and return -0.9589242746631385
1

Theoretically you can use your own decorator that removes globals() while a function call. It is some overhead to hide all globals() but, if there are not too many globals() it could be useful. During the operation we do not create/remove global objects, we just overwrites references in dictionary which refers to global objects. But do not remove special globals() (like __builtins__) and modules. Probably you do not want to remove callables from global scope too.

from types import ModuleType
import re

# the decorator to hide global variables
def noglobs(f):
    def inner(*args, **kwargs):
        RE_NOREPLACE = '__\w+__'
        old_globals = {}
        # removing keys from globals() storing global values in old_globals
        for key, val in globals().iteritems():
            if re.match(RE_NOREPLACE, key) is None and not isinstance(val, ModuleType) and not callable(val):
                old_globals.update({key: val})

        for key in old_globals.keys():
            del globals()[key]  
        result = f(*args, **kwargs)
        # restoring globals
        for key in old_globals.iterkeys():
            globals()[key] = old_globals[key]
        return result
    return inner

# the example of usage
global_var = 'hello'

@noglobs
def no_globals_func():
    try:
        print 'Can I use %s here?' % global_var
    except NameError:
        print 'Name "global_var" in unavailable here'

def globals_func():
    print 'Can I use %s here?' % global_var 

globals_func()
no_globals_func()
print 'Can I use %s here?' % global_var

...

Can I use hello here?
Name "global_var" in unavailable here
Can I use hello here?

Or, you can iterate over all global callables (i.e. functions) in your module and decorate them dynamically (it's little more code).

The code is for Python 2, I think it's possible to create a very similar code for Python 3.

sergzach
  • 6,578
  • 7
  • 46
  • 84