我正在将一些Lisp代码翻译成Python。
在Lisp中,您可以使用let结构并将引入的变量声明为特殊的,从而具有动态作用域。 (参见http://en.wikipedia.org/wiki/Dynamic_scope#Dynamic_scoping)
在Python中如何实现类似功能呢?看起来这种语言不直接支持,如果是这样,有什么好的仿真方法吗?
我正在将一些Lisp代码翻译成Python。
在Lisp中,您可以使用let结构并将引入的变量声明为特殊的,从而具有动态作用域。 (参见http://en.wikipedia.org/wiki/Dynamic_scope#Dynamic_scoping)
在Python中如何实现类似功能呢?看起来这种语言不直接支持,如果是这样,有什么好的仿真方法吗?
以下是类似于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 >
我认为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)
对应于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()
async
代码(以及线程化代码)兼容的Python 3.7+,您应该使用contextvars
而不是线程本地变量。这使您能够将上下文状态与任务相关联(所有任务在同一线程中运行),而不仅仅是线程。。 - ShadowRanger动态作用域被认为是有害的。
不要使用它,也不要模拟它。
如果你需要模拟它,请定义一个dynamic_scope
模块来模拟这种行为,并在所有源文件中导入该模块。该模块应该有begin
方法,在使用动态作用域的函数的第一行调用该方法,还应该有end
、get
和set
方法。get
和set
方法应该实现查找变量名的调用链,其中调用链由begin
和end
实现。然后重构你的代码以消除动态作用域。