在Python中禁用全局变量查找

问题描述 投票:15回答:5

简而言之,问题是:有没有办法阻止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版本方面不明确,我最感兴趣的是Python 3.3及更高版本。

python global-variables
5个回答
8
投票

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

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

而不是试图打破全局查找,保持您的全局命名空间清洁。不要添加您不需要与模块中的其他范围共享的全局变量。例如,使用main()函数来封装真正只是本地的东西。

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


13
投票

是的,也许不是一般的。但是你可以用功能来做。

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

但是:您可以在运行时创建函数对象。构造函数看起来像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

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

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


2
投票

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

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

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


1
投票

从理论上讲,你可以使用自己的装饰器在函数调用时删除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?

或者,您可以迭代模块中的所有全局callables(即函数)并动态修饰它们(它只需要更多的代码)。

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


0
投票

有了@ 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()))
© www.soinside.com 2019 - 2024. All rights reserved.