如何在Python中强制局部作用域?

29 投票
6 回答
8376 浏览
提问于 2025-04-17 20:43

在C++中,你可以这样做来强制创建一个局部作用域:

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

这样做的好处是在强制局部作用域结束时,括号内声明的任何变量都会消失。这可以帮助防止在后面的代码中不小心使用之前定义的变量x。

那么在Python中可以这样做吗?如果可以的话,怎么做呢?

==更新==

我知道函数是一个明显的选择。我想知道在代码比较简单、不值得单独创建函数的情况下,是否有快速的方法来实现上述功能——只是一些快速的标记,强调这个代码块中的变量不应该在函数的其他地方使用

根据目前人们的说法,简短的回答是不能。

(我明白有一些聪明的方法,比如使用“del”,或者说这种想要有块的需求可能暗示着应该重构成一个单独的函数。不过我想强调的是,这只是针对一些短小的代码片段,想要强调这些小块中的变量不应该在其他地方使用。)

6 个回答

1

之前提到的答案是为了防止一个变量函数泄露到父级作用域。我的需求是要防止一个变量父级作用域泄露到函数里。下面这个装饰器可以同时处理父级作用域和全局作用域的问题。装饰器的文档字符串里有一个示例。

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
1

如果你不喜欢用del这个方法,你可以把函数定义放在里面,也就是嵌套函数定义:

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

当然,我觉得更好的方法是把事情拆分成更小的函数,这样就不需要手动管理作用域了。在C++中,最酷的地方是析构函数会自动被调用,而在Python中,你不能保证析构函数一定会被调用,所以即使可以这样做,这种作用域管理也没什么用。

2

我决定用一些小技巧来解决这个问题。

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

顺便说一下,我发现使用虚拟类的解决方案可能会导致内存泄漏。比如,在被重写的类中创建的大型numpy数组,似乎没有被垃圾回收机制清理掉,从内存统计来看是这样的,不过这可能跟具体的实现有关。

4

我之前也有这个问题,结果发现你完全可以这样做

虽然这不是像C语言那样整洁的写法,但通过Python的两个特性,我们可以让它满足我们的需求。

这两个特性是:

  1. 类里面的代码会立即执行,即使这个类从来没有被使用。
  2. 你可以随便多次使用同一个类的名字。

下面是一个例子:

class DoStuff:
    i = 1
    # Do stuff

# local variable i is destroyed

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

为了更好地展示这种灵活性,我把这个类命名为“Scope”,因为这样可以和其他类区分开来。 当然,“Scope”可以是任何名字。

我建议你在整个项目中使用一个名字,并把这个名字写进文档里,这样大家就知道这是一个特殊的名字,绝对不能被实例化。

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!

输出结果:

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

所以这是可行的——接下来我们来看看它的优缺点。

优点:

  1. 代码保持线性,逻辑上没有不必要的跳跃,这样新手更容易阅读和理解代码的实际功能。
  2. 代码自我说明,未来的程序员看到这段代码会知道它只在这一处使用,这样修改起来更方便,因为他们不需要去找其他地方的实例。

缺点:

  1. 我们是利用Python的一些特性来实现这个功能,我觉得这种限制作用域的做法,与创建一次性使用的函数并不常见。这可能会在工作中引起一些摩擦,或者导致有人抱怨使用了“黑科技”,而不是遵循创建小函数的常规做法,无论这个函数是否会被多次使用。
  2. 如果你离开项目,新来的程序员看到这段代码,可能会感到困惑。需要一些文档来设定预期,并且要确保文档中的解释准确。

我认为在那些希望限制作用域但代码只用在一个地方,或者还不清楚如何写通用函数的情况下,这种做法是值得尝试的。

如果有谁觉得还有其他的利弊,欢迎在这里评论,我会确保它们被加入到“缺点”部分。

这里还有一些关于这个惯例的讨论,John Carmack、Jonathan Blow和Casey Muratori都比较喜欢这种做法。

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

10

在Python中,如果你在一个函数里面声明了一个变量,这个变量就是局部的,外面是无法访问到它的。

>>> 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
>>> 

另外,你可以在最后把这个变量从命名空间中删除,这样就不能再使用它了。

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

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

撰写回答