【问题标题】:Disable global variable lookup in Python在 Python 中禁用全局变量查找
【发布时间】:2015-06-24 09:40:33
【问题描述】:

简而言之,问题是:有没有办法阻止 Python 查找当前范围之外的变量?

详情:

如果没有在当前范围内定义,Python 会在外部范围内查找变量定义。因此,如果在重构过程中不小心,这样的代码很容易被破坏:

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

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

如果我重命名了函数参数,但忘记在函数体内重命名它们,代码仍然会运行:

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

我知道it is bad practice to shadow names from 外部范围。但有时我们还是会这样做......

有没有办法阻止 Python 在当前范围之外查找变量? (因此在第二个示例中访问 ab 会引发错误。)

【问题讨论】:

  • 不,没有。否则任何内置的都不会工作。
  • 更好的解决方案是使用适当的测试,而不是试图破坏Python。
  • 把最后三行放到另一个函数中...
  • @kazemakase:尽量避免创建太多全局变量。例如,将您的其他代码放在 main() 函数中。
  • @kazemakase:考虑到模块中的所有函数和类也是全局的,它不仅仅是您的代码将使用的内置函数。如果禁用全局变量,就不可能将任何内容分解为函数。

标签: python global-variables


【解决方案1】:

是的,也许不是一般的。但是,您可以使用函数来完成。

您要做的就是让函数的全局为空。您不能替换全局变量,也不想修改它的内容(因为 那只是为了摆脱全局变量和函数)。

但是:您可以在运行时创建函数对象。构造函数看起来像types.FunctionType((code, globals[, name[, argdefs[, closure]]])。在那里你可以替换全局命名空间:

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

您当然可以通过定义自己的装饰器来清理它:

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

@noglobal
def f():
    return x

x = 5
f() # will fail

严格来说你并没有禁止它访问全局变量,你只是让函数相信全局命名空间中没有变量。实际上,您也可以使用它来模拟静态变量,因为如果它将变量声明为全局变量并分配给它,它将最终出现在它自己的全局命名空间沙箱中。

如果您希望能够访问全局命名空间的一部分,那么您需要使用您希望它看到的内容填充函数全局沙箱。

【讨论】:

  • 这样的事情是我在问这个问题时最初想到的。但是,简单地使用 main 函数更适合我的实际需求。
  • 很好的答案!我对其进行了一些扩展以保持导入和函数的定义:gist.github.com/ax3l/59d92c6e1edefcef85ac2540eb056da3
  • @Ax3l 不错的扩展!你能让它覆盖诸如from win32con import MEM_COMMIT 之类的导入对象吗? (如果怀疑,我很乐意,但也许......)
  • 我认为导入的变量可能无法与内联定义的全局变量区分开来。
  • @Ax3l 我不确定你的意思。在 python 中有本地和全局命名空间,我不清楚“内联定义的全局变量”是什么意思。但是,如果您的意思是全局命名空间中的导入变量也将不可见,那么是的,但那是因为它们位于全局命名空间中。请注意,导入不需要放置在全局命名空间中 - 例如,如果您从函数中进行导入,则导入的符号将放置在本地命名空间中,然后即使您隐藏了全局命名空间也是可见的。
【解决方案2】:

不,你不能告诉 Python 不要在全局范围内查找名称。

如果可以,您将不能使用模块中定义的任何其他类或函数,不能从其他模块导入对象,也不能使用内置名称。你的函数命名空间变成了一片沙漠,几乎没有它需要的一切,唯一的出路是将所有东西都导入本地命名空间。 对于你模块中的每一个函数

与其尝试破坏全局查找,不如保持全局命名空间干净。不要添加不需要与模块中的其他范围共享的全局变量。例如,使用main() 函数来封装真正的本地人。

另外,添加单元测试。没有(甚至只是几个)测试的重构总是容易产生错误。

【讨论】:

  • 我一直认为在 Python 脚本中使用 main() 函数是旧 C 实践的遗留物。现在,从全局命名空间污染的角度思考让我从不同的角度看待它。谢谢你:)
  • @kazemakase 另一个重要原因是允许程序也可以作为模块导入。如果您的代码没有包装在函数中,那么它将在导入时执行。
【解决方案3】:

有了@skyking 的回答,我无法访问任何导入(我什至无法使用print)。此外,带有可选参数的函数被破坏(比较 How can an optional parameter become required?)。

@Ax3l 的评论稍微改善了这一点。我仍然无法访问导入的变量 (from module import var)。

因此,我建议:

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

对于每个用@noglobal 修饰的函数,它会创建一个globals() 的副本到目前为止已定义。这使导入的变量(通常在文档顶部导入)保持可访问性。如果你像我一样,先定义函数,然后定义变量,这将达到预期的效果,即能够访问函数中导入的变量,而不是你在代码中定义的变量。由于copy() 创建了一个浅拷贝(Understanding dict.copy() - shallow or deep?),这也应该非常节省内存。

注意这样,一个函数只能调用上面定义的函数,所以你可能需要重新排序你的代码。

为了记录,我从his Gist复制@Ax3l的版本:

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

【讨论】:

  • 如果需要在代码中间的某处使用@noglobal,“globals() defined so far”方法可能并不理想,因为globals 已经与新变量混合在一起道路。在这种情况下,我在所有导入的后面定义 init_globals = globals().copy()noglobal = lambda f: types.FunctionType(f.__code__, init_globals, argdefs=f.__defaults__),然后在需要时使用 @noglobal,而不必担心与新变量混淆。
  • @JoyfulPanda 使用这种方法,你的模块中定义的函数f 不能调用模块中定义的其他函数g,即使它在它之前定义,对吧? mycompiler.io/view/GPb2ZJJ
【解决方案4】:

要阻止全局变量查找,请将您的函数移到另一个模块中。除非它检查调用堆栈或显式导入您的调用模块;它无法从调用它的模块访问全局变量。

在实践中,将您的代码移动到 main() 函数中,以避免创建不必要的全局变量。

如果您使用全局变量是因为多个函数需要操作共享状态,那么请将代码移到一个类中。

【讨论】:

    【解决方案5】:

    正如@bers 所提到的,@skykings 的装饰器破坏了函数内部的大多数python 功能,例如print()import 语句。 @bers 通过在装饰器定义时添加当前从 globals() 导入的模块来绕过 import 语句。

    这激发了我编写另一个装饰器的灵感,希望它能够满足大多数来看这篇文章的人真正想要的。潜在的问题是,之前的装饰器创建的新函数缺少 __builtins__ 变量,该变量包含新打开的解释器中可用的所有标准内置 Python 函数(例如 print)。

    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
    

    那么用法如下

    @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
    

    【讨论】:

      【解决方案6】:

      理论上,您可以使用自己的装饰器在函数调用时删除globals()。隐藏所有 globals() 是一些开销,但是,如果没有太多 globals(),它可能会很有用。在操作期间,我们不创建/删除全局对象,我们只是覆盖字典中引用全局对象的引用。但不要删除特殊的globals()(如__builtins__)和模块。可能您也不想从全局范围中删除 callables。

      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?
      

      或者,您可以遍历模块中的所有全局可调用对象(即函数)并动态装饰它们(代码少一点)。

      代码用于Python 2,我认为可以为Python 3 创建一个非常相似的代码。

      【讨论】:

        猜你喜欢
        • 2012-01-31
        • 1970-01-01
        • 1970-01-01
        • 2019-07-24
        • 2011-07-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多