如何在非封闭作用域中访问调用者的变量(即实现动态作用域)?
考虑这个例子:
def outer():
s_outer = "outer\n"
def inner():
s_inner = "inner\n"
do_something()
inner()
我希望在 do_something
这个函数里,能够访问到调用它的函数中的变量,比如 s_outer
和 s_inner
。更一般来说,我想从其他不同的函数调用它,但每次都能在它们各自的环境中执行,并访问它们各自的变量(也就是实现 动态作用域)。
我知道在 Python 3.x 中,nonlocal
关键字可以让你在 inner
函数里访问 s_outer
。可惜的是,这个方法只对在 inner
函数内部定义的 do_something
有用。否则,inner
就不是一个 词法上 包含的作用域(同样的,outer
也不是,除非 do_something
是在 outer
内部定义的)。
我找到了用标准库 inspect
来检查调用栈的方法,并做了一个小工具,可以在 do_something()
里这样调用:
def reach(name):
for f in inspect.stack():
if name in f[0].f_locals:
return f[0].f_locals[name]
return None
然后
def do_something():
print( reach("s_outer"), reach("s_inner") )
运行得很好。
那么 reach
能不能更简单地实现呢?还有其他方法可以解决这个问题吗?
4 个回答
我们可以更调皮一点。
这是对“有没有更优雅/简短的方法来实现 reach()
函数?”这个问题的一部分的回答。
我们可以给用户更好的语法:可以用
outer.foo
代替reach("foo")
。这样输入起来更方便,而且语言本身会立刻告诉你,如果你用了一个不合法的变量名(属性名和变量名有相同的限制)。
我们可以抛出错误,以便更好地区分“这个不存在”和“这个被设置为
None
”。如果我们真的想把这两种情况混在一起,可以用
getattr
加上默认参数,或者用try
-except AttributeError
。我们可以优化:不需要悲观地一次性构建一个足够大的列表来容纳所有的调用帧。
在大多数情况下,我们可能不需要一直追溯到调用栈的根部。
虽然我们在不恰当地访问调用栈的帧,违反了编程中一个非常重要的规则——不要让远处的东西隐形地影响行为,但这并不意味着我们不能保持文明。
如果有人试图在没有调用栈帧检查支持的 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
库中更完整实现的一个简化只读版本。)
在我看来,reach
的实现没有优雅的方法,也不应该有。因为这会引入一种新的非标准的间接方式,这种方式真的很难理解、调试、测试和维护。正如Python的座右铭(可以试着输入import this
)所说:
明确比隐含要好。
所以,直接传递参数吧。未来的你会非常感谢今天的你。
我最后做的事情是
scope = locals()
并让 scope
可以从 do_something
访问。这样我就不需要去找,但仍然可以访问调用者的局部变量字典。这跟我自己建立一个字典然后传递出去是很相似的。