【问题标题】:Implementing block scoping in Python在 Python 中实现块作用域
【发布时间】:2020-12-26 01:06:42
【问题描述】:

为了好玩,我正在尝试在 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:我想要某种构造,在退出时自动销毁绑定在其中的所有局部变量。

【问题讨论】:

  • @juanpa.arrivillaga 嗯,Python 名称解析不是在运行时有效吗?这算作词法作用域吗?在任何一种情况下,我想要做的基本上是创建“作用域”,在退出时破坏局部变量。
  • @juanpa.arrivillaga 谢谢,已编辑。
  • 无论如何,如果不认真破解运行时内部结构,这将很难。 del inspect.currentframe().f_back.f_locals[name] 不起作用,Python 不支持动态修改局部变量,即 locals()['x'] = 'foo' 实际上不会影响本地命名空间
  • 如果你得到这个工作,你一定要写一个答案
  • 不确定您当前的方法,因为它似乎首先尝试对 python 代码的解释方式进行一些非常深刻的更改,但是实现块作用域的一种更简单(但丑陋)的方法可能是使用元类来创建类,这些类在创建时执行 init 函数中的任何代码,然后删除它们自己。 (我的意思是这将是一个丑陋的黑客,但既然你只是为了好玩,你可能会从中学到很多东西哈哈)

标签: python python-internals


【解决方案1】:

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

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

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

这一直是 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'

(对于一个真正好的“生产质量”上下文类,您必须考虑到广告安全和异步任务安全 - 但以上是基本思想)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-05-27
    • 1970-01-01
    • 2019-06-17
    • 1970-01-01
    • 2017-01-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多