8

我现在有几次遇到麻烦,因为意外(无意地)在函数或方法定义中引用了全局变量。

我的问题是:有什么方法可以禁止 python 让我引用全局变量?或者至少警告我我正在引用一个全局变量?

x = 123

def myfunc() :
    print x    # throw a warning or something!!!

让我补充一点,我的典型情况是使用 IPython 作为交互式 shell。我使用“execfile”来执行定义类的脚本。在解释器中,我直接访问类变量以做一些有用的事情,然后决定将其作为方法添加到我的类中。当我在解释器中时,我正在引用类变量。但是,当它成为一个方法时,它需要引用'self'。这是一个例子。

class MyClass :

    a = 1
    b = 2

    def add(self) :
        return a+b


m = MyClass()

现在在我的解释器中运行脚本'execfile('script.py')',我正在检查我的类并输入:'ma * mb'并决定,这将是一个有用的方法。因此,我将代码修改为非故意复制/粘贴错误:

class MyClass :

    a = 1
    b = 2

    def add(self) :
        return a+b


    def mult(self) :
        return m.a * m.b   # I really meant this to be self.a * self.b

这当然仍然在 IPython 中执行,但它真的让我感到困惑,因为它现在引用了之前定义的全局变量!

鉴于我典型的 IPython 工作流程,也许有人有建议。

4

2 回答 2

4

首先,您可能不想这样做。正如 Martijn Pieters 所指出的,很多东西,比如顶级函数和类,都是全局的。

您可以仅过滤不可调用的全局变量。从 C 扩展模块导入的函数、类、内置函数或方法等都是可调用的。您可能还想过滤掉模块(任何你import是全局的)。这仍然无法捕捉到您在def. 您可以为此添加某种白名单(这也将允许您创建可以在没有警告的情况下使用的全局“常量”)。真的,你想出的任何东西充其量只是一个非常粗略的指南,而不是你想当作绝对警告的东西。

此外,无论您如何操作,尝试检测隐式全局访问,而不是显式访问(使用global语句)都将非常困难,所以希望这并不重要。


没有明显的方法可以在源代码级别检测全局变量的所有隐式使用。

但是,从解释器内部进行反射非常容易。

inspect模块的文档有一个漂亮的图表,向您展示了各种类型的标准成员。请注意,其中一些在Python 2.xPython 3.x中有不同的名称。

此函数将为您提供两个版本中绑定方法、未绑定方法、函数或代码对象访问的所有全局名称的列表:

def get_globals(thing):
    thing = getattr(thing, 'im_func', thing)
    thing = getattr(thing, '__func__', thing)
    thing = getattr(thing, 'func_code', thing)
    thing = getattr(thing, '__code__', thing)
    return thing.co_names

如果你只想处理不可调用的,你可以过滤它:

def get_callable_globals(thing):
    thing = getattr(thing, 'im_func', thing)
    func_globals = getattr(thing, 'func_globals', {})
    thing = getattr(thing, 'func_code', thing)
    return [name for name in thing.co_names
            if callable(func_globals.get(name))]

这并不完美(例如,如果一个函数的全局变量有一个自定义的内置替换,我们将无法正确查找它),但它可能已经足够好了。


一个简单的使用例子:

>>> def foo(myparam):
...     myglobal
...     mylocal = 1
>>> print get_globals(foo)
('myglobal',)

你可以很容易地import一个模块并递归地遍历它的可调用对象并调用get_globals()每个模块,这将适用于主要情况(顶级函数,以及顶级和嵌套类的方法),尽管它不适用于任何事情动态定义(例如,函数内部定义的函数或类)。


如果您只关心 CPython,另一种选择是使用dis模块扫描模块或 .pyc 文件(或类或其他)中的所有字节码,并记录每个LOAD_GLOBAL操作。

与该inspect方法相比,它的一个主要优点是它会找到已编译的函数,即使它们尚未创建。

缺点是无法查找名称(如果其中一些尚未创建,怎么可能?),因此您无法轻松过滤掉可调用对象。你可以尝试做一些花哨的事情,比如将LOAD_GLOBAL操作连接到相应的CALL_FUNCTION(和相关的)操作,但是……这开始变得相当复杂。


最后,如果你想动态地钩住东西,你总是可以globals用一个每次访问它时发出警告的包装器来替换。例如:

class GlobalsWrapper(collections.MutableMapping):
    def __init__(self, globaldict):
        self.globaldict = globaldict
    # ... implement at least __setitem__, __delitem__, __iter__, __len__
    # in the obvious way, by delegating to self.globaldict
    def __getitem__(self, key):
        print >>sys.stderr, 'Warning: accessing global "{}"'.format(key)
        return self.globaldict[key]

globals_wrapper = GlobalsWrapper(globals())

同样,您可以很容易地过滤不可调用的对象:

    def __getitem__(self, key):
        value = self.globaldict[key]
        if not callable(value):
            print >>sys.stderr, 'Warning: accessing global "{}"'.format(key)
        return value

显然,对于 Python 3,您需要将print语句更改为print函数调用。

您也可以很容易地引发异常而不是警告。或者您可能要考虑使用该warnings模块。

您可以通过各种不同的方式将其挂接到您的代码中。最明显的是一个导入钩子,它为每个新模块GlobalsWrapper围绕其正常构建的globals. 虽然我不确定这将如何与 C 扩展模块交互,但我的猜测是它要么工作,要么被无害地忽略,其中任何一个都可能没问题。唯一的问题是这不会影响您的顶级脚本。如果这很重要,您可以编写一个包装器脚本,它execfile是带有 aGlobalsWrapper或类似内容的主脚本。

于 2013-05-23T18:53:00.123 回答
1

我一直在努力应对类似的挑战(尤其是在 Jupyter 笔记本中),并创建了一个小包来限制功能的范围。

>>> from localscope import localscope
>>> a = 'hello world'
>>> @localscope
... def print_a():
...     print(a)
Traceback (most recent call last):
  ...
ValueError: `a` is not a permitted global

@localscope装饰器使用 python 的反汇编器使用LOAD_GLOBAL(全局变量访问)或LOAD_DEREF(闭包访问)语句查找装饰函数的所有实例。如果要加载的变量是内置函数、被明确列为异常或满足谓词,则允许该变量。否则,会引发异常。

请注意,装饰器静态分析代码。因此,它无法访问闭包访问的变量的值。

于 2020-10-17T21:47:16.777 回答