抽象
在早期版本的 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-通用许可证,以更宽松的许可证为准。
文章声明:以上内容(如有图片或视频在内)除非注明,否则均为腾龙猫勺儿原创文章,转载或复制请以超链接形式并注明出处。
本文作者:猫勺本文链接:https://www.jo6.cn/post/61.html