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()
的语义易于理解,并且操作可靠。
实现
该实现正在开发中,作为GitHub上的草稿拉取请求。
版权
本文档置于公共领域或根据CC0-1.0-通用许可证,以更宽松的许可证为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0667.rst