PEP 667 – 命名空间的一致视图

猫勺猫勺 03-15 122 阅读 0 评论

抽象

在早期版本的 Python 中,所有命名空间,无论是在函数中, 类或模块都以相同的方式实现:作为字典。

出于性能原因,函数命名空间的实现是 改变。不幸的是,这意味着通过访问这些命名空间并不再一致,并且一些 多年来,奇怪的错误随着线程、生成器和协程而悄然出现 被添加。locals()frame.f_locals

PEP 建议再次使这些命名空间保持一致。 修改将始终可见 基础变量。对局部变量的修改将 立即在 中可见,它们将是 无论线程或协程如何,都保持一致。frame.f_localsframe.f_locals

该函数的行为将与现在对类的作用相同 和模块范围。对于函数范围,它将返回一个瞬时 底层 .locals()frame.f_locals

赋予动机

目前实施和缓慢, 不一致且有缺陷。 我们希望让它更快、更一致,最重要的是修复错误。locals()frame.f_locals

例如:

class C:
    x = 1
    sys._getframe().f_locals['x'] = 2
    print(x)

指纹2

但:

def f():
    x = 1
    sys._getframe().f_locals['x'] = 2
    print(x)f()

指纹1

这是不一致的,令人困惑的。 使用此 PEP 时,两个示例都将打印 .2

更糟糕的是,当前的行为可能会导致奇怪的错误。

当前行为没有补偿优势; 它不可靠且速度慢。

理由

返回字典的当前实现 这是从局部变量数组动态创建的。 这可能导致数组和字典与 彼此。写入可能不会显示为 对局部变量的修改。对局部变量的写入可能会丢失。frame.f_localsf_locals

通过使 return 成为 底层框架,这些问题就消失了。 始终在 与框架同步,因为它是框架的视图,而不是框架的副本。frame.f_localsframe.f_locals

规范

Python

frame.f_locals将返回帧上的视图对象,该对象 实现接口。collections.abc.Mapping

对于模块和类范围将是一个字典, 对于函数范围,它将是一个自定义类。frame.f_locals

locals()将被定义为:

def locals():
    frame = sys._getframe(1)
    f_locals = frame.f_locals
    if frame.is_function():
        f_locals = dict(f_locals)
    return f_locals

对映射的所有写入都将立即可见 在基础变量中。对基础变量的所有更改 将立即在映射中可见。对象将 是一个完整的映射,并且可以向其添加任意键值对。f_localsf_locals

例如:

def l():
    "Get the locals of caller"
    return sys._getframe(1).f_localsdef test():
    if 0: y = 1 # Make 'y' a local variable
    x = 1
    l()['x'] = 2
    l()['y'] = 4
    l()['z'] = 5
    y
    print(locals(), x)

test()将打印 。{'x': 2, 'y': 4, 'z': 5} 2

在 Python 3.10 中,上述操作将失败,并显示 , 因为 by 的定义丢失了。UnboundLocalErroryl()['y'] = 4

如果将倒数第二行从 改为 ,这将是 ,就像今天一样。添加到其中的键不是 从词法上讲,局部变量在 中仍然可见,但不可见 动态地成为局部变量。yzNameErrorframe.f_localsframe.f_locals

C-API接口

API 的扩展

将添加四个新的 C-API 函数:

PyObject *PyEval_GetFrameLocals(void)PyObject *PyEval_GetFrameGlobals(void)PyObject *PyEval_GetFrameBuiltins(void)PyObject *PyFrame_GetLocals(PyFrameObject *f)

PyEval_GetFrameLocals()相当于:。 相当于:。locals()PyEval_GetFrameGlobals()globals()

PyFrame_GetLocals(f)相当于:。f.f_locals

所有这些函数都将返回一个新引用。

对现有 API 的更改

以下 C-API 函数将被弃用,因为它们返回借用的引用:

PyEval_GetLocals()PyEval_GetGlobals()PyEval_GetBuiltins()

它们将在 3.15 中删除。

应改用以下函数:

PyEval_GetFrameLocals()PyEval_GetFrameGlobals()PyEval_GetFrameBuiltins()

返回新的引用。

的语义发生了变化,因为它现在返回一个 帧局部变量的视图,而不是字典。PyEval_GetLocals()

以下三个函数将成为 no-ops,并且将被弃用:

PyFrame_FastToLocalsWithError()PyFrame_FastToLocals()PyFrame_LocalsToFast()

它们将在 3.15 中删除。

优化功能的f_locals行为

尽管行为就像函数的命名空间一样, 会有一些可观察到的差异。 例如,可能是 .f.f_localsf.f_locals is f.f_localsFalse

但是,将是 和 无论如何,对基础变量的所有更改都将是 始终可见。f.f_locals == f.f_localsTrue

向后兼容

Python

目前的实现有许多极端情况和奇怪之处。 可能需要更改解决这些问题的代码。 用于简单模板或打印调试的代码, 将继续正常工作。用于修改局部变量的调试器和其他工具现在可以正常工作, 即使存在线程代码、协程和生成器。locals()f_locals

C-API接口

PyEval_GetLocals

因为返回一个借来的引用,所以它需要 要缓存在帧上的字典,延长其生存期和 创建一个循环。 应该改用。PyEval_GetLocals()PyEval_GetFrameLocals()

此代码:

locals = PyEval_GetLocals();if (locals == NULL) {
    goto error_handler;}Py_INCREF(locals);

应替换为:

locals = PyEval_GetFrameLocals();if (locals == NULL) {
    goto error_handler;}

PyFrame_FastToLocals等

这些函数旨在转换内部“快速”表示 函数的局部变量到字典,反之亦然。

不再需要给他们打电话。直接访问帧字段的 C 代码应修改为改为调用:f_localsPyFrame_GetLocals()

PyFrame_FastToLocals(frame);PyObject *locals = frame.f_locals;Py_INCREF(locals);

成为:

PyObject *locals = PyFrame_GetLocals(frame);if (frame == NULL)
    goto error_handler;

实现

每次读取都会创建一个新的代理对象,该对象提供 作为本地映射(包括cell和free)的外观 变量名称替换为这些局部变量的值。frame.f_locals

下面概述了可能的实现。 所有以下划线开头的属性都是不可见的,并且 无法直接访问。 它们仅用于说明拟议的设计。

NULL: Object # NULL is a singleton representing the absence of a value.class CodeType:

    _name_to_offset_mapping_impl: dict | NULL
    _cells: frozenset # Set of indexes of cell and free variables
    ...

    def __init__(self, ...):
        self._name_to_offset_mapping_impl = NULL
        self._variable_names = deduplicate(
            self.co_varnames + self.co_cellvars + self.co_freevars
        )
        ...

    @property
    def _name_to_offset_mapping(self):
        "Mapping of names to offsets in local variable array."
        if self._name_to_offset_mapping_impl is NULL:
            self._name_to_offset_mapping_impl = {
                name: index for (index, name) in enumerate(self._variable_names)
            }
        return self._name_to_offset_mapping_implclass FrameType:

    _locals : array[Object] # The values of the local variables, items may be NULL.
    _extra_locals: dict | NULL # Dictionary for storing extra locals not in _locals.
    _locals_cache: FrameLocalsProxy | NULL # required to support PyEval_GetLocals()

    def __init__(self, ...):
        self._extra_locals = NULL
        self._locals_cache = NULL
        ...

    @property
    def f_locals(self):
        return FrameLocalsProxy(self)class FrameLocalsProxy:
    "Implements collections.MutableMapping."

    __slots__ "_frame"

    def __init__(self, frame:FrameType):
        self._frame = frame

    def __getitem__(self, name):
        f = self._frame
        co = f.f_code
        if name in co._name_to_offset_mapping:
            index = co._name_to_offset_mapping[name]
            val = f._locals[index]
            if val is NULL:
                raise KeyError(name)
            if index in co._cells
                val = val.cell_contents
                if val is NULL:
                    raise KeyError(name)
            return val
        else:
            if f._extra_locals is NULL:
                raise KeyError(name)
            return f._extra_locals[name]

    def __setitem__(self, name, value):
        f = self._frame
        co = f.f_code
        if name in co._name_to_offset_mapping:
            index = co._name_to_offset_mapping[name]
            kind = co._local_kinds[index]
            if index in co._cells
                cell = f._locals[index]
                cell.cell_contents = val
            else:
                f._locals[index] = val
        else:
            if f._extra_locals is NULL:
                f._extra_locals = {}
            f._extra_locals[name] = val

    def __iter__(self):
        f = self._frame
        co = f.f_code
        yield from iter(f._extra_locals)
        for index, name in enumerate(co._variable_names):
            val = f._locals[index]
            if val is NULL:
                continue
            if index in co._cells:
                val = val.cell_contents
                if val is NULL:
                    continue
            yield name

    def __contains__(self, item):
        f = self._frame
        if item in f._extra_locals:
            return True
        return item in co._variable_names

    def __len__(self):
        f = self._frame
        co = f.f_code
        res = 0
        for index, _ in enumerate(co._variable_names):
            val = f._locals[index]
            if val is NULL:
                continue
            if index in co._cells:
                if val.cell_contents is NULL:
                    continue
            res += 1
        return len(self._extra_locals) + res

C 接口

PyEval_GetLocals()将大致实现如下:

PyObject *PyEval_GetLocals(void) {
    PyFrameObject * = ...; // Get the current frame.
    if (frame->_locals_cache == NULL) {
        frame->_locals_cache = PyEval_GetFrameLocals();
    }
    return frame->_locals_cache;}

与返回借用引用的所有函数一样,必须注意 确保引用的使用时间不会超过对象的生存期。

对 PEP 709 内联推导的影响

对于函数中的内联推导,当前行为为 理解的内在或外都一样,这一点不会改变。这 内部函数的行为通常会按照 这个 PEP 的其余部分。locals()locals()

对于模块或类范围内的内联推导式,当前在内联推式内调用会为每个推导式返回一个新字典 叫。这个 PEP 将使函数内也总是返回一个新的 每个调用的字典,提高一致性;内联类或模块范围 理解将表现得好像内联理解仍然是 不同的功能。locals()locals()

与 PEP 558 的比较

这个 PEP 和 PEP 558 有一个共同的目标: 使语义和可理解,并且它们的操作可靠。locals()frame.f_locals()

此 PEP 和 PEP 558 之间的主要区别在于 PEP 558 保留了局部变量的内部副本, 而这个 PEP 没有。

PEP 558 没有确切指定内部副本的确切时间 更新,使 PEP 558 的行为无法推理。

未解决的问题

让 locals() 返回一个映射代理

另一种定义方式是:locals()

def locals():
    return sys._getframe(1).f_locals

这将更简单,更容易理解。然而 分配时会出现向后兼容性问题 到局部变量或传递给 或 。locals()evalexec

映射代理的生存期

每次读取属性都会创建一个新的映射代理。 这样做是为了避免创建参考循环。f_locals

另一种方法是在帧上缓存代理,这样就可以了。 这样做的缺点是参考周期会延迟收集 帧和映射代理,直到下一个循环集合。frame.f_locals is frame.f_locals

PyEval_GetLocals()已经创建了一个循环,因为它返回了一个借来的引用。

实现

该实现正在开发中,作为 Github 上的草稿拉取请求。

版权

本文档被置于公共领域或位于 CC0-1.0-通用许可证,以更宽松的许可证为准。

The End 微信扫一扫

文章声明:以上内容(如有图片或视频在内)除非注明,否则均为腾龙猫勺儿原创文章,转载或复制请以超链接形式并注明出处。

本文作者:猫勺本文链接:https://www.jo6.cn/post/61.html

上一篇 下一篇

相关阅读

发表评论

访客 访客
快捷回复: 表情:
评论列表 (暂无评论,122人围观)

还没有评论,来说两句吧...

取消
微信二维码
微信二维码
支付宝二维码