如何在Python中创建动态作用域变量?

16 投票
4 回答
6171 浏览
提问于 2025-04-15 17:37

我正在把一些代码从Lisp语言翻译成Python。

在Lisp中,你可以使用一个叫做let的结构,在这个结构中引入的变量可以被声明为特殊变量,从而具有动态作用域。 (可以查看这个链接了解更多:http://en.wikipedia.org/wiki/Dynamic_scope#Dynamic_scoping

那么在Python中,我该如何做到这一点呢?看起来这个语言并不直接支持这种特性,如果真是这样,有什么好的方法可以模拟它呢?

4 个回答

7

在Python中,与Lisp里的“特殊”或动态作用域变量对应的概念是“线程本地存储”。

这里有个不错的讨论:什么是Python中的“线程本地存储”,我为什么需要它?

如果你想完全模拟Lisp的特殊变量,包括let语句,可以使用上下文管理器:

from __future__ import with_statement # if Python 2.5
from contextlib import contextmanager
import threading

dyn = threading.local()

@contextmanager
def dyn_vars(**new):
    old = {}
    for name, value in new.items():
        old[name] = getattr(dyn, name, None)
        setattr(dyn, name, value)
    yield
    for name, value in old.items():
        setattr(dyn, name, value)

示例(虽然有点傻,但它展示了可重入的特性):

def greet_self():
    print 'Hi', dyn.who_am_I

def greet_selves():
    with dyn_vars(who_am_I='Evil Twin'):
        greet_self()
    greet_self()

with dyn_vars(who_am_I='Tobia'):
    greet_selves()
15

这里有一个东西,它的工作方式有点像Lisp中的特殊变量,但更适合Python。

_stack = []

class _EnvBlock(object):
    def __init__(self, kwargs):
        self.kwargs = kwargs
    def __enter__(self):
        _stack.append(self.kwargs)
    def __exit__(self, t, v, tb):
        _stack.pop()

class _Env(object):
    def __getattr__(self, name):
        for scope in reversed(_stack):
            if name in scope:
                return scope[name]
        raise AttributeError("no such variable in environment")
    def let(self, **kwargs):
        return _EnvBlock(kwargs)
    def __setattr__(self, name, value):
        raise AttributeError("env variables can only be set using `with env.let()`")

env = _Env()

你可以这样使用它:

with env.let(bufsize=8192, encoding="ascii"):
    print env.bufsize  # prints 8192
    a()  # call a function that uses env.bufsize or env.encoding

env.let的效果会持续到with块的结束。

注意,如果你使用线程的话,每个线程肯定需要一个不同的_stack。你可以使用threading.local来实现这一点。

14

我觉得Justice在这里的推理是非常正确的。

另一方面——我忍不住想要实现一个概念验证,尝试另一种对Python来说“非自然”的编程范式——我就是喜欢这样做。:-)

所以,我创建了一个类,它的对象属性的作用域正如你所需要的(而且可以动态创建)。正如我所说的,这只是一个概念验证的状态——但我认为大多数常见的错误(比如试图访问一个在当前作用域中根本没有定义的变量)应该会引发错误,即使不是最合适的错误(例如,应该是AttributeError,但却引发了由于栈下溢而导致的IndexError)。

import inspect


class DynamicVars(object):
    def __init__(self):
        object.__setattr__(self, "variables", {})

    def normalize(self, stackframe):
        return [hash(tpl[0]) for tpl in stackframe[1:]]

    def __setattr__(self, attr, value):
        stack = self.normalize(inspect.stack())
        d = {"value": value, "stack": stack}
        if not attr in self.variables:
            self.variables[attr] = []
            self.variables[attr].append(d)
        else:
            our_value = self.variables[attr]
            if our_value[-1]["stack"] == stack:
                our_value[-1]["value"] = value
            elif len(stack) <= len(our_value):
                while our_value and stack !=  our_value["stack"]:
                    our_value.pop()
                our_value.append(d)
            else: #len(stack) > len(our_value):
                our_value.append(d)
    def __getattr__(self, attr):
        if not attr in self.variables:
            raise AttributeError
        stack = self.normalize(inspect.stack())
        while self.variables[attr]:
            our_stack = self.variables[attr][-1]["stack"]
            if our_stack == stack[-len(our_stack):]:
                break
            self.variables[attr].pop()
        else:
            raise AttributeError
        return self.variables[attr][-1]["value"]


# for testing:
def c():
    D = DynamicVars()
    D.c = "old"
    print D.c
    def a():
        print D.c
    a()
    def b():
        D.c = "new"
        a()
    b()
    a()
    def c():
        D.c = "newest"
        a()
        b()
        a()
    c()
    a()

c()

2020年更新 - 又出现了一个类似的问题,我做了一个黑科技,不需要特殊的命名空间对象(但使用了cPython中的内部东西,比如将locals()更新为实际变量:https://stackoverflow.com/a/61015579/108205(适用于Python 3.8)

撰写回答