28

In C++, you can do this to force local scope:

{
    int i = 1;
    // Do stuff
}
// local variable i is destroyed
{
    int i = 7;
    // Do more stuff
}

This has the benefit that by the end of a forced local scope, any variables declared in the bracket are gone. This can help prevent using a previously defined variable x in a place later on where you didn't intend to use x.

Can you do this in Python? If so, how?

==UPDATE==

I'm aware of functions - which is the obvious thing. I was wondering if there was a quick way to do the above when the code is simple and not worth creating separate a function for - just some quick notation to emphasize that the variables in this block are not to be used anywhere else in the function.

From what people have said so far the short answer is no.

(I understand that there are clever ways like "del", or that this desire to have blocks may suggest refactoring into a separate function anyway. However I would like to emphasize this is just for short snippets where you want to emphasize the variables in this small block are not to be used elsewhere.)

sleblanc
  • 3,821
  • 1
  • 34
  • 42
dmonopoly
  • 3,251
  • 5
  • 34
  • 49

6 Answers6

10

In Python, if you declare a variable inside a function, it is local and cannot be accessed outside the function

>>> def x():
    i = 5


>>> x()
>>> i

Traceback (most recent call last):
  File "<pyshell#5>", line 1, in <module>
    i
NameError: name 'i' is not defined
>>> 

Alternatively, you can delete the variable from the namespace at the end so that you cannot reuse it.

>>> i = 5
>>> del i
>>> i

Traceback (most recent call last):
  File "<pyshell#8>", line 1, in <module>
    i
NameError: name 'i' is not defined
>>> 
icedtrees
  • 6,134
  • 5
  • 25
  • 35
  • 1
    Technically, "cannot be accessed outside the function" is not quite true. It can be accessed by any function called by the function declaring the variable, which could be any place. It might be better to say after the function ends, the scope does not contain the variable anymore. – tkruse Oct 15 '18 at 12:01
4

I had this same question, and found out that you absolutely can!

It's not as clean as the c style blocks, but through two quirks of python we can make it serve our purposes.

The Quirks:

  1. Whatever code is inside a class runs immediately, even if the class is never used.
  2. You can reuse the name of a class as many times as you want.

Here's your example:

class DoStuff:
    i = 1
    # Do stuff

# local variable i is destroyed

class DoStuff:
    i = 7
    # Do more stuff
# local variable i is destroyed

To fully represent the flexibility here, see this example. I've named the class "Scope", because that's probably what I'd call it to differentiate from other named classes. Note that "Scope" can of course be anything.

I'd recommend you stick with one name for the entire project and add that name to your documentation, so that there is an understanding that this is a special name that should never ever be instantiated.

outer = 1

class Scope:
    inner = outer
    print("runs first ---")
    print("outer %d" % outer)
    print("inner %d" % inner)

class Scope:
    inner = outer + 1
    print("runs second ---")
    print("outer %d" % outer)
    print("inner %d" % inner)

print("runs last ---")
print("outer %d" % outer)
print("inner %d" % inner) # This will give an error. Inner does not exist in this scope!

Output:

runs first ---
outer 1
inner 1
runs second ---
outer 1
inner 2             
runs last ---
outer 1
Traceback (most recent call last):
  File "test.py", line 18, in <module>
    print("inner %d" % inner) # This will give an error. Inner does not exist in this scope!
NameError: name 'inner' is not defined

So it is doable - let's take a look at the benefits / downsides tradeoffs.

Benefits:

  1. Code remains linear and no unnecessary leaps in logic are needed to follow the code flow. This linearity will make it easier for newcomers to read and understand what a section of code actually does.
  2. Code is self-documenting to future coders that this code is only used in this one place, making it easier to edit, as the coder will not need to do an unnecessary search to find other instances.

Downsides:

  1. We're using quirks of Python to make this work, and I sense that this very idea of limiting scope as opposed to creating new one-time-use functions is not something that Python programmers tend to do. This may cause tensions in the workplace, or result in complaints of using a hack as opposed to following conventions on creating small functions whether or not something is used more than once.
  2. If you leave the project and new programmers come onboard and see this code, they will probably be confused initially. Some documentation will be needed in order to set expectations, and care must be taken to make sure the explanation in the documentation remains accurate.

I think this is a worthwhile effort for all code where you'd like to limit the scope but there are not multiple places this code is used, or it is not yet clear how to write a generic function to address all those situations.

If anyone reading this feels there are other tradeoffs, comment here and I'll make sure they're represented in the "Downsides" section.

Here's some more discussion around this convention, which has been preferred by John Carmack, Jonathan Blow, and Casey Muratori.

https://news.ycombinator.com/item?id=12120752

Seth
  • 134
  • 1
  • 14
  • 1
    One downside: you cannot use comprehension constructs (or anything that makes a nested scope that relies on closures over that class scope, since *class scope doesn't create an enclosing scope*). I really don't see many advantages over just using a function, `def _(): ; _()` Wouldn't have all the weird scoping quirks that class bodies have – juanpa.arrivillaga Apr 24 '19 at 18:59
2

I have committed to solve this with trickery.

from scoping import scoping
a = 2
with scoping():
    assert(2 == a)
    a = 3
    b = 4
    scoping.keep('b')
    assert(3 == a)
assert(2 == a)
assert(4 == b)

https://github.com/l74d/scoping

By the way, I found that the dummy class solution might result in memory leak. For example, large numpy arrays created in the overwritten class did not seem to be garbage collected by watching the memory statistics, which may be an implementation-dependent thing though.

l74d
  • 177
  • 1
  • 2
1

If you don't like the del solution, you can nest function definitions:

def one_function():
    x=0
    def f():
        x = 1
    f()
    print(x) # 0

Of course, I think the better approach is to just split things up into smaller functions, so there's no need for this manual scoping. In C++, the coolest thing about it is that the destructor is automatically called -- in Python, you can't really guarantee that the destructor will be called, so this scoping wouldn't be very useful even if it were possible.

Scott Lawrence
  • 1,023
  • 6
  • 14
0

The previously mentioned answers prevent leaking a variable from a function to parent scopes. My use case is to prevent a variable from leaking into a function from parent scopes. The following decorator does the job for both parent scopes as well as the global scope. An example is listed in the decorator's docstring.

class NonlocalException(Exception):
    pass


import inspect
import sys
import functools
def force_local(allowed_nonlocals=[], clear_globals=True, verbose=False):
    """
    Description:
      Decorator that raises NonlocalException if a function uses variables from outside its local scope.
      Exceptions can be passed in the variable "allowed_nonlocals" as a list of variable names.
      Note that for avoiding the usage of global variables, this decorator temporarily clears the "globals()" dict
      during the execution of the function. This might not suit all situations. It can be disabled with "clear_globals=False"

    Parameters:
      allowed_nonlocals: list of variable names to allow from outside its local scope.
      clear_globals    : set to False to skip clearing/recovering global variables as part of avoiding non-local variables
      verbose          : True to print more output
    
    Example:
        # define 2 variables
        x = 1
        g = 25

        # Define function doit
        # Decorate it to only allow variable "g" from the parent scope
        # Add first parameter being "raise_if_nonlocal", which is a callable passed from the decorator
        @force_local(allowed_nonlocals=["g"])
        def doit(raise_if_nonlocal, z, a=1, *args, **kwargs):
            raise_if_nonlocal()
            y = 2
            print(y) # <<< this is ok since variable y is local
            print(x) # <<< this causes the above "raise_if_nonlocal" to raise NonlocalException
            print(g) # <<< this is ok since "g" is in "allowed_nonlocals"

        # call the function
        doit(3, 20, 27, b=3)

        print(x) # <<< using "x" here is ok because only function "doit" is protected by the decorator


    Dev notes:
      - Useful decorator tutorial: https://realpython.com/primer-on-python-decorators/
      - Answers https://stackoverflow.com/questions/22163442/how-to-force-local-scope-in-python
      - Notebook with unit tests: https://colab.research.google.com/drive/1pRFXRMbl0hXV99zsHwHEwH5A-v2rGZaL?usp=sharing
    """
    def inner_dec(func):
        var_ok = inspect.getfullargspec(func)
        var_ok = (
            var_ok.args
            + ([] if var_ok.varargs is None else [var_ok.varargs])
            + ([] if var_ok.varkw is None else [var_ok.varkw])
            + allowed_nonlocals
        )
        if verbose: print("var_ok", var_ok)
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            def raise_if_nonlocal():
                #var_found = locals().keys() # like sys._getframe(0).f_locals.keys()
                var_found = sys._getframe(1).f_locals.keys()
                if verbose: print("var_found", var_found)
                var_uhoh = [x for x in
                                    #locals().keys()
                                    sys._getframe(1).f_locals.keys()
                                    if x not in var_ok
                                    #and x not in ["var_ok_2734982","NonlocalException","sys"]
                                  ]
                if any(var_uhoh):
                    raise NonlocalException(f"Using non-local variable(s): {var_uhoh}")
            # backup and clear globals
            if clear_globals:
              bkp_globals = globals().copy()
              globals().clear()
              for fx_i in ["sys", "NonlocalException"]+allowed_nonlocals:
                if fx_i not in bkp_globals.keys(): continue
                globals()[fx_i] = bkp_globals[fx_i]
            # call function
            try:
              o = func(raise_if_nonlocal, *args, **kwargs)
            except NameError as e:
              #print("NameError", e.name, e.args, dir(e))
              if e.name in bkp_globals.keys():
                raise NonlocalException(f"Using non-local variable(s): {e.name}") from e
              else:
                raise e
            finally:
              if clear_globals:
                # recover globals
                for k,v in bkp_globals.items(): globals()[k] = v
            # done
            return o
        return wrapper
    return inner_dec
Shadi
  • 9,742
  • 4
  • 43
  • 65
-2

In C++, you use local scope with brackets {} to avoid variable redefinitions or naming conflicts:

{
    int var=3;
}
{
    float var=1.0f;
}

While in python, there are no explicit variable definition , you just assign some objects to a var name when you want to use it, and rebind the same name to some new variable:

var=3
#do something
var=1.0  #no need to "del var", var refers to a float object now
#do more stuff

Note that the use of scope block in C++ might be indicating your code needs to be refactored to functions or methods, which can be named and reused. And it's the same with python.

zhangxaochen
  • 32,744
  • 15
  • 77
  • 108