在 Python 中实现块作用域

Implementing block scoping in Python

为了好玩,我正在尝试在 Python 中实现类似于 block scoping 的东西。这就是我的结论:

import inspect


class Scope:
    def __init__(self, *args):
        self._start = inspect.currentframe().f_back.f_locals.copy()
        print(self._start)

    def __enter__(self, *args):
        pass

    def __exit__(self, *args):
        for name in inspect.currentframe().f_back.f_locals.copy():
            if name not in self._start:
                print(f"deleting {name}")
                # print(globals())
                # del globals()[name]
                del inspect.currentframe().f_back.f_locals[name] # this doesn't do anything
                print(inspect.currentframe().f_back.f_locals) # no change


def compute(foo):
    with Scope():
        bar = foo ** 2
        print(bar)
    return bar  # this should fail


compute(5)

很遗憾,当前帧以外的帧似乎无法修改。 (可能有充分的理由?:P)。我也试过修改 globals dict 并且它有效,但仅适用于创建的词法范围在模块级别的情况。我希望我的解决方案在函数内部工作,等等。

我试过使用 with API 来模仿其他语言的作用域。我很高兴听到使用可能有效的不同技术的解决方案。

TLDR:我想要某种构造,在退出时自动销毁绑定在其中的所有局部变量。

帧中的局部变量不能写入 - 但可以读取。 这样做的主要原因是,尽管任何范围内的变量都可以表示为字典,但为了提高效率,某些时候变量开始以不同的方式存储在与框架本身相关联的插槽中,但在 Python 代码中不可见.这样,读取和写入变量的操作就不会通过字典间接进行,并且使用了一个简单的局部变量线性索引。

每当调用 locals() 或框架的 .f_locals 成员时,cPython 会透明地将局部变量中的当前值复制到生成的字典中。

此外,局部变量的另一个存储称为“快速存储”意味着 space 在编译时为代码对象的所有局部变量保留 - 并且函数被编译为单个代码对象.这意味着即使变量仅在块内使用,它也会在函数范围内存在。

这是 Python 很长一段时间以来的行为,但最近在 PEP 558 中有所记录:https://www.python.org/dev/peps/pep-0558/

对于全局变量没有这样的机制,影响globalsframe.f_globals返回的字典会改变相应的变量——这就是为什么你的想法适用于全局变量。

语言中最接近的机制是在 except 块的末尾 - 在那个点 Python 将删除异常变量(以避免引用异常中的对象本身,通常不会在 except 块之后重用。

如果你想要一些真正有用的东西,而不仅仅是玩具,我用过 2 或 3 次的一件事是拥有一个可以在 with 块内更改的上下文对象,并且将在退出时恢复其值 - 以便恢复以前的值。这个对象通常是一个“上下文”的东西——例如,带有前景和线宽等参数的绘图 API,或者小数位数和小数的舍入策略。

from collections import ChainMap

class Context:
    def __init__(self):
        self._stack = [{}]
        
    def __enter__(self):
        self._stack.append({})
    
    def __exit__(self, *args, **kw):
        self._stack.pop()
        
    def __getattr__(self, name):
        if name.startswith("_"):
            return super().__getattr__(self, name)
        return ChainMap(*reversed(self._stack))[name]
    
    def __setattr__(self, name, value):
        if name.startswith("_"):
            return super().__setattr__(name, value)
        self._stack[-1][name] = value
        
    def __delattr__(self, name):
        if name.startswith("_"):
            return super().__deltattr__(name)
        del self._stack[1][name]

这是在 ipython 控制台中运行的代码:


In [137]: ctx = Context() 

In [138]: ctx.color = "red"                          

In [139]: ctx.color       
Out[139]: 'red'

In [140]: with ctx: 
     ...:     ctx.color = "blue" 
     ...:     print(ctx.color) 
     ...:                 
blue

In [141]: ctx.color       
Out[141]: 'red'

(对于真正好的“生产质量”上下文 class,您必须考虑 thead 安全和 asyncio 任务安全 - 但以上是基本思想)