Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python增强提案

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

作者:
Mark Shannon <mark at hotpy.org>,Tian Gao <gaogaotiantian at hotmail.com>
讨论列表:
Discourse 线程
状态:
已接受
类型:
标准跟踪
创建:
2021年7月30日
Python版本:
3.13
历史记录:
2021年8月20日
决议:
Discourse 消息

目录

摘要

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

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

本PEP建议再次使这些命名空间保持一致。对frame.f_locals的修改将始终在底层变量中可见。对局部变量的修改将立即在frame.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_locals的实现返回一个字典,该字典是从局部变量数组动态创建的。这可能导致数组和字典彼此不同步。对f_locals的写入可能不会显示为对局部变量的修改。对局部变量的写入可能会丢失。

通过使frame.f_locals返回底层框架的视图,这些问题将消失。frame.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_locals映射的所有写入都将立即在底层变量中可见。对底层变量的所有更改都将立即在映射中可见。f_locals对象将是一个完整的映射,并且可以向其中添加任意键值对。

例如

def l():
    "Get the locals of caller"
    return sys._getframe(1).f_locals

def 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中,以上操作将失败并出现UnboundLocalError,因为l()['y'] = 4定义的y将丢失。

如果倒数第二行将y更改为z,这将是NameError,就像今天一样。添加到frame.f_locals的不是词法局部变量的键在frame.f_locals中仍然可见,但不会动态地成为局部变量。

C-API

API扩展

将添加三个新的C-API函数

PyObject *PyEval_GetFrameLocals(void)
PyObject *PyEval_GetFrameGlobals(void)
PyObject *PyEval_GetFrameBuiltins(void)

PyEval_GetFrameLocals()等效于:locals()PyEval_GetFrameGlobals()等效于:globals()

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

对现有API的更改

PyFrame_GetLocals(f)等效于f.f_locals,因此其返回值将根据上述访问f.f_locals的方式发生变化。

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

PyEval_GetLocals()
PyEval_GetGlobals()
PyEval_GetBuiltins()

应改用以下函数

PyEval_GetFrameLocals()
PyEval_GetFrameGlobals()
PyEval_GetFrameBuiltins()

它们返回新引用。

由于现在返回优化帧中帧局部变量的代理,而不是字典,因此PyEval_GetLocals()的语义已更改。

以下三个函数将变为空操作,并将被弃用

PyFrame_FastToLocalsWithError()
PyFrame_FastToLocals()
PyFrame_LocalsToFast()

优化函数的f_locals行为

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

但是f.f_locals == f.f_locals将为True,并且通过任何方式对底层变量的所有更改都将始终可见。

向后兼容性

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;
}

实现

每次读取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_impl

class 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 API

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内联推导的影响

对于函数内的内联推导,locals()当前在推导内部或外部的行为相同,并且不会改变。locals()在函数内部的行为通常会根据本PEP的其余部分进行更改。

对于模块或类作用域的内联推导,目前在内联推导中调用locals()会为每次调用返回一个新字典。本PEP将使函数内的locals()也始终为每次调用返回一个新字典,从而提高一致性;类或模块作用域的内联推导将表现得好像内联推导仍然是一个不同的函数。

与PEP 558的比较

本PEP和PEP 558具有共同的目标:使locals()frame.f_locals()的语义易于理解,并且操作可靠。

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

PEP 558没有准确指定何时更新内部副本,这使得PEP 558的行为无法推断。

实现

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


来源:https://github.com/python/peps/blob/main/peps/pep-0667.rst

上次修改:2024-07-03 17:01:00 GMT