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
上次修改: 2023-09-09 17:39:29 GMT