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

16

我正在将一些Lisp代码翻译成Python。

在Lisp中,您可以使用let结构并将引入的变量声明为特殊的,从而具有动态作用域。 (参见http://en.wikipedia.org/wiki/Dynamic_scope#Dynamic_scoping

在Python中如何实现类似功能呢?看起来这种语言不直接支持,如果是这样,有什么好的仿真方法吗?

4个回答

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
< p > env.let的作用持续到with块结束。 < /p > < p > 请注意,如果您使用线程,您肯定希望为每个线程使用不同的_stack。 您可以使用threading.local来实现。 < /p >

4
这是在“不要那样做”和堆栈检查之间寻求妥协的一种方式(似乎会很慢而且难以验证)。 - Jason Orendorff
3
好的解决方案。它非常明确(即使它让某些人感到惊讶,也不会影响普通 Python 的语义)。我发现这种方法在科学绘图中非常有用,因为有很多设置需要在调用栈的某个点上设置,而且必须把它们一路携带到实际绘图发生的函数中是很痛苦的。 - Wynand Winterbach

14

我认为Justice在这里的推理是正确的。

另一方面 - 我无法抵制对Python“非自然”编程范例进行概念验证的实现 - 我只是喜欢这样做。:-)

因此,我创建了一个类,其对象的属性与您要求的方式相同(并且可以动态创建)。正如我所说,它仅处于概念验证状态 - 但我认为大多数常见错误(例如尝试访问未定义某个范围内的变量)应该引发错误,即使不是适当的错误(例如由于堆栈不足而导致的IndexError而不是AttributeError)

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年更新 - 另一个类似的问题出现了,我制作了一个不需要特殊命名空间对象的hack(但是需要使用cPython中的内部函数,例如将locals()更新为实际变量:https://stackoverflow.com/a/61015579/108205 (适用于Python 3.8)


恭喜!感谢您的辛勤工作,编程世界又有了一个解决方案,将深入到众多关键应用程序的核心! - yfeldblum
1
毕竟,Lisp的“特殊变量”并不可怕,对吧?它们就像bash中的环境变量。可怕的是默认使用动态作用域的语言。幸运的是,现在这样的语言已经不多了。 - Jason Orendorff
1
我很高兴大多数真实的编程语言不使用动态作用域...然而,我已经写了大量的Emacs Lisp,它使用动态作用域;但是对我来说,这感觉非常自然。(最近,Emacs Lisp提供了词法作用域作为一种选项,但我甚至从未尝试过使用它 :-) - offby1

7

对应于Lisp的“special”或动态作用域变量的Python习惯用语是“线程本地存储”。

这里有一个很好的讨论:什么是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()

3
对于需要与async代码(以及线程化代码)兼容的Python 3.7+,您应该使用contextvars而不是线程本地变量。这使您能够将上下文状态与任务相关联(所有任务在同一线程中运行),而不仅仅是线程。 - ShadowRanger

-8

动态作用域被认为是有害的。

不要使用它,也不要模拟它。

如果你需要模拟它,请定义一个dynamic_scope模块来模拟这种行为,并在所有源文件中导入该模块。该模块应该有begin方法,在使用动态作用域的函数的第一行调用该方法,还应该有endgetset方法。getset方法应该实现查找变量名的调用链,其中调用链由beginend实现。然后重构你的代码以消除动态作用域。


5
如果一种语言良好支持动态作用域,那么它可以成为一个极其有用的特性。我曾经对大型的Common Lisp程序进行微小的修改(3-4行),如果没有动态作用域就需要进行巨大但机械简单的修改。有时候,动态作用域是解决问题的自然方案。虽然如此,动态作用域在Python中并不自然,我不建议直接移植它,因为那可能会导致维护上的问题。 - Ken
4
动态作用域有好的使用方法,特别是在设置相对全局的设置时,你不想将其穿透到每个函数的参数中(例如输出stdout的位置)。当然,动态作用域变量应该被标记清楚,并且应该避免其一般性使用。Jason Orendorff提出的解决方案是Python的一个很好的折衷方案,它有助于简化我的一些代码。 - Wynand Winterbach

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接