如何在非封闭作用域中访问调用者的变量(即实现动态作用域)?

26 投票
4 回答
10822 浏览
提问于 2025-04-17 20:12

4 个回答

7

我们可以更调皮一点。

这是对“有没有更优雅/简短的方法来实现 reach() 函数?”这个问题的一部分的回答。

  1. 我们可以给用户更好的语法:可以用 outer.foo 代替 reach("foo")

    这样输入起来更方便,而且语言本身会立刻告诉你,如果你用了一个不合法的变量名(属性名和变量名有相同的限制)。

  2. 我们可以抛出错误,以便更好地区分“这个不存在”和“这个被设置为 None”。

    如果我们真的想把这两种情况混在一起,可以用 getattr 加上默认参数,或者用 try-except AttributeError

  3. 我们可以优化:不需要悲观地一次性构建一个足够大的列表来容纳所有的调用帧。

    在大多数情况下,我们可能不需要一直追溯到调用栈的根部。

  4. 虽然我们在不恰当地访问调用栈的帧,违反了编程中一个非常重要的规则——不要让远处的东西隐形地影响行为,但这并不意味着我们不能保持文明。

    如果有人试图在没有调用栈帧检查支持的 Python 上使用这个严肃的 API 来进行实际工作,我们应该友好地提醒他们。

import inspect


class OuterScopeGetter(object):
    def __getattribute__(self, name):
        frame = inspect.currentframe()
        if frame is None:
            raise RuntimeError('cannot inspect stack frames')
        sentinel = object()
        frame = frame.f_back
        while frame is not None:
            value = frame.f_locals.get(name, sentinel)
            if value is not sentinel:
                return value
            frame = frame.f_back
        raise AttributeError(repr(name) + ' not found in any outer scope')


outer = OuterScopeGetter()

太棒了。现在我们可以直接这样做:

>>> def f():
...    return outer.x
... 
>>> f()
Traceback (most recent call last):
    ...
AttributeError: 'x' not found in any outer scope
>>> 
>>> x = 1
>>> f()
1
>>> x = 2
>>> f()
2
>>> 
>>> def do_something():
...     print(outer.y)
...     print(outer.z)
... 
>>> def g():
...     y = 3
...     def h():
...         z = 4
...         do_something()
...     h()
... 
>>> g()
3
4

调皮的事情优雅地实现了。

(顺便说一下,这是我在 dynamicscope 库中更完整实现的一个简化只读版本。)

9

在我看来,reach的实现没有优雅的方法,也不应该有。因为这会引入一种新的非标准的间接方式,这种方式真的很难理解、调试、测试和维护。正如Python的座右铭(可以试着输入import this)所说:

明确比隐含要好。

所以,直接传递参数吧。未来的你会非常感谢今天的你。

7

我最后做的事情是

scope = locals()

并让 scope 可以从 do_something 访问。这样我就不需要去找,但仍然可以访问调用者的局部变量字典。这跟我自己建立一个字典然后传递出去是很相似的。

撰写回答