PEP 267 – 优化模块命名空间访问
- 作者:
- Jeremy Hylton <jeremy at alum.mit.edu>
- 状态:
- 推迟
- 类型:
- 标准跟踪
- 创建日期:
- 2001年5月23日
- Python 版本:
- 2.2
- 发布历史:
推迟
虽然这个 PEP 是一个好主意,但尚未有人着手解决这个 PEP、PEP 266 和 PEP 280 之间的差异。因此,它被推迟了。
摘要
本 PEP 提出了一个全局模块命名空间和内置命名空间的新实现,以加快名称解析。该实现将使用一个对象指针数组来处理这些命名空间中的大多数操作。编译器将在编译时为全局变量和模块属性分配索引。
当前的实现将这些命名空间表示为字典。每次使用全局名称都会导致一次字典查找;内置名称会导致两次字典查找,一次在全局命名空间中失败的查找,以及第二次在内置命名空间中的查找。
此实现应能加快使用模块级函数和变量的 Python 代码。它还应消除为了加快这些名称的访问而演变出的笨拙的编码风格。
该实现很复杂,因为全局和内置命名空间可以动态修改,而编译器无法检测到这些修改。(例如:模块的命名空间在模块导入后被脚本修改。)因此,实现必须维护几个辅助数据结构以保留这些动态特性。
引言
本 PEP 提出了模块对象属性访问的新实现,该实现优化了对编译时已知的模块变量的访问。模块将这些变量存储在一个数组中,并提供一个使用数组偏移量查找属性的接口。对于全局变量、内置变量和导入模块的属性,编译器将生成使用数组偏移量进行快速访问的代码。
[描述设计的关键部分:dlict、编译器支持、笨拙的名称技巧变通方法、优化其他模块的全局变量]
该实现将保留模块命名空间的现有语义,包括在运行时修改模块命名空间以影响内置名称可见性的能力。
DLict 设计
命名空间使用一种有时被称为 dlict 的数据结构实现。它是一个字典,其中一些字典条目具有编号的槽位。该类型必须用 C 实现才能达到可接受的性能。新的类型类统一工作应该使这相当容易。DLict 可能是一个字典的子类,它为某些键提供了替代存储模块。
此处包含一个 Python 实现以说明基本设计
"""A dictionary-list hybrid"""
import types
class DLict:
def __init__(self, names):
assert isinstance(names, types.DictType)
self.names = {}
self.list = [None] * size
self.empty = [1] * size
self.dict = {}
self.size = 0
def __getitem__(self, name):
i = self.names.get(name)
if i is None:
return self.dict[name]
if self.empty[i] is not None:
raise KeyError, name
return self.list[i]
def __setitem__(self, name, val):
i = self.names.get(name)
if i is None:
self.dict[name] = val
else:
self.empty[i] = None
self.list[i] = val
self.size += 1
def __delitem__(self, name):
i = self.names.get(name)
if i is None:
del self.dict[name]
else:
if self.empty[i] is not None:
raise KeyError, name
self.empty[i] = 1
self.list[i] = None
self.size -= 1
def keys(self):
if self.dict:
return self.names.keys() + self.dict.keys()
else:
return self.names.keys()
def values(self):
if self.dict:
return self.names.values() + self.dict.values()
else:
return self.names.values()
def items(self):
if self.dict:
return self.names.items()
else:
return self.names.items() + self.dict.items()
def __len__(self):
return self.size + len(self.dict)
def __cmp__(self, dlict):
c = cmp(self.names, dlict.names)
if c != 0:
return c
c = cmp(self.size, dlict.size)
if c != 0:
return c
for i in range(len(self.names)):
c = cmp(self.empty[i], dlict.empty[i])
if c != 0:
return c
if self.empty[i] is None:
c = cmp(self.list[i], dlict.empty[i])
if c != 0:
return c
return cmp(self.dict, dlict.dict)
def clear(self):
self.dict.clear()
for i in range(len(self.names)):
if self.empty[i] is None:
self.empty[i] = 1
self.list[i] = None
def update(self):
pass
def load(self, index):
"""dlict-special method to support indexed access"""
if self.empty[index] is None:
return self.list[index]
else:
raise KeyError, index # XXX might want reverse mapping
def store(self, index, val):
"""dlict-special method to support indexed access"""
self.empty[index] = None
self.list[index] = val
def delete(self, index):
"""dlict-special method to support indexed access"""
self.empty[index] = 1
self.list[index] = None
编译器问题
编译器目前收集模块中所有全局变量的名称。这些是在模块级别绑定或在声明它们为全局的类或函数体中绑定的名称。
编译器将为每个全局名称分配索引,并将全局名称和索引添加到模块的代码对象中。然后,每个代码对象将不可撤销地绑定到其定义所在的模块。(不确定这是否存在一些微妙的问题。)
对于导入模块的属性,模块将存储一个间接记录。在内部,模块将存储一个指向定义模块的指针,以及属性在定义模块全局变量数组中的偏移量。偏移量将在第一次查找名称时初始化。
运行时模型
PythonVM 将通过新的操作码进行扩展,以通过模块级数组访问全局变量和模块属性。
函数对象需要指向定义它的模块,以便提供对模块级全局数组的访问。
对于存储在 dlict 中的模块属性(称之为静态属性),get/delattr 实现需要使用旧的按名称接口跟踪对这些属性的访问。如果静态属性被动态更新,例如
mod.__dict__["foo"] = 2
实现需要更新数组槽位而不是备份字典。
向后兼容性
dlict 将需要维护关于槽位当前是否使用的元信息。它还需要维护一个指向内置命名空间的指针。当全局命名空间中当前没有使用某个名称时,查找将必须回退到内置命名空间。
在相反的情况下,每个模块可能需要一个用于内置命名空间的特殊访问器函数,该函数检查是否已动态添加了一个覆盖内置名称的全局名称。此检查仅在模块的 dlict 发生动态更改时发生,即当绑定了一个在编译时未发现的名称时。
对于模块的全局命名空间在运行时未以奇怪方式修改的常见情况,这些机制几乎没有成本。它们会为那些对全局名称进行异常操作的模块增加开销,但这是一种不常见的做法,可能值得劝阻。
在未来的 Python 版本中,可能希望禁用对全局命名空间的动态添加。如果是这样,新实现可以提供警告。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0267.rst