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

Python 增强提案

PEP 329 – 将内置函数视为常量在标准库中使用

作者:
Raymond Hettinger <python at rcn.com>
状态:
已拒绝
类型:
标准跟踪
创建日期:
2004 年 4 月 18 日
Python 版本:
2.4
发布历史:
2004 年 4 月 18 日

目录

摘要

该提案旨在添加一个函数,用于将内置函数引用视为常量,并在整个标准库中应用该函数。

状态

该 PEP 已被作者自行拒绝。尽管 ASPN 上的食谱受到了好评,但人们对将其纳入核心发行版的意愿较低。

Jython 实现不使用字节码,因此如果移除当前的 _len=len 优化,其性能会受到影响。

此外,改变字节码是提高性能和实现更清晰代码的最不干净的方法之一。更健壮的解决方案可能需要编译器 pragma 指令或指示哪些内容可以被优化的元变量(类似于 const/volatile 声明)。

动机

该库包含诸如 _len=len 之类的代码,旨在创建快速的局部引用,而不是较慢的全局查找。虽然这是性能所必需的,但这些构造会使代码混乱,并且通常不完整(错失了许多机会)。

如果该提案被采纳,这些构造就可以从代码库中消除,同时在性能方面也能改进其结果。

库中目前有超过一百处 while 1 的用法。它们没有被替换为更易读的 while True,因为性能原因(编译器无法消除测试,因为 True 不被认为是始终为常量)。将 True 转换为常量将使代码更清晰,同时保持性能。

许多其他基本的 Python 操作由于全局查找而运行得慢得多。在 try/except 语句中,在测试是否匹配之前,会动态查找捕获的异常。类似地,简单的身份测试,如 while x is not None 需要在每次迭代中重新查找 None 变量。内置函数的查找尤其糟糕,因为必须首先检查封闭的全局作用域。这些查找链吞噬了缓存空间,而这些空间最好用在其他地方。

总之,如果该提案被采纳,代码将变得更清晰,并且整体性能将得到提高。

提案

添加一个名为 codetweaks.py 的模块,其中包含两个函数:bind_constants()bind_all()。第一个函数执行常量绑定,第二个函数递归地将其应用于目标模块中的每个函数和类。

对于标准库中的大多数模块,在脚本末尾附近添加一对行。

import codetweaks, sys
codetweaks.bind_all(sys.modules[__name__])

除了绑定内置函数外,还有一些模块(如 sre_compile)也适合将模块变量和内置函数绑定为常量。

问答

  1. 这会迫使每个人都关注优化问题吗?

    因为它是在自动完成的,所以减少了思考优化的需求。

  2. 简而言之,它是如何工作的?

    每个函数都有其字节码(Python 虚拟机语言)的属性和一个常量表。bind 函数扫描字节码中的 LOAD_GLOBAL 指令,并检查值是否已知。如果是,它会将该值添加到常量表中,并将操作码替换为 LOAD_CONSTANT

  3. 它何时起作用?

    当一个模块首次导入时,Python 会编译字节码并运行绑定优化。后续的导入将重用之前的工作。每个会话都会重复此过程(结果不会保存在 pyc 文件中)。

  4. 你怎么知道它有效?

    我实现了它,将其应用于库中的每个模块,并且测试套件无异常运行。

  5. 如果模块定义了一个变量来隐藏(shadowing)一个内置函数怎么办?

    这确实会发生。例如,True 可以在模块级别重新定义为 True = (1==1)。下面的示例实现会检测到隐藏,并保持全局查找不变。

  6. 你是否第一个认识到大多数全局查找都是针对永不改变的值?

    不,这一点早已为人所知。Skip Montanaro 在 PEP 266 中给出了一个清晰的解释。

  7. 如果我想替换 builtins 模块并提供我自己的实现怎么办?

    要么在导入模块之前执行此操作,要么重新加载模块,或者禁用 codetweaks.py(它会有一个禁用标志)。

  8. 该模块对 Python 字节码的变化有多敏感?

    它导入 opcode.py 以防止重新编号。此外,它使用 LOAD_CONSTLOAD_GLOBAL,这些是基础且一直存在的。尽管如此,编码方案可能会改变,并且此实现以及像 dis 这样依赖于当前编码方案的模块也必须随之改变。

  9. 对启动时间有什么影响?

    我无法测量到差异。除 warnings.py 外,没有启动模块被绑定。此外,绑定函数非常快,只需对代码字符串进行一次扫描以查找 LOAD_GLOBAL 操作码。

示例实现

这是 codetweaks.py 的一个示例实现。

from types import ClassType, FunctionType
from opcode import opmap, HAVE_ARGUMENT, EXTENDED_ARG
LOAD_GLOBAL, LOAD_CONST = opmap['LOAD_GLOBAL'], opmap['LOAD_CONST']
ABORT_CODES = (EXTENDED_ARG, opmap['STORE_GLOBAL'])

def bind_constants(f, builtin_only=False, stoplist=[], verbose=False):
    """ Return a new function with optimized global references.

    Replaces global references with their currently defined values.
    If not defined, the dynamic (runtime) global lookup is left undisturbed.
    If builtin_only is True, then only builtins are optimized.
    Variable names in the stoplist are also left undisturbed.
    If verbose is True, prints each substitution as is occurs.

    """
    import __builtin__
    env = vars(__builtin__).copy()
    stoplist = dict.fromkeys(stoplist)
    if builtin_only:
        stoplist.update(f.func_globals)
    else:
        env.update(f.func_globals)

    co = f.func_code
    newcode = map(ord, co.co_code)
    newconsts = list(co.co_consts)
    codelen = len(newcode)

    i = 0
    while i < codelen:
        opcode = newcode[i]
        if opcode in ABORT_CODES:
            return f    # for simplicity, only optimize common cases
        if opcode == LOAD_GLOBAL:
            oparg = newcode[i+1] + (newcode[i+2] << 8)
            name = co.co_names[oparg]
            if name in env and name not in stoplist:
                value = env[name]
                try:
                    pos = newconsts.index(value)
                except ValueError:
                    pos = len(newconsts)
                    newconsts.append(value)
                newcode[i] = LOAD_CONST
                newcode[i+1] = pos & 0xFF
                newcode[i+2] = pos >> 8
                if verbose:
                    print name, '-->', value
        i += 1
        if opcode >= HAVE_ARGUMENT:
            i += 2

    codestr = ''.join(map(chr, newcode))
    codeobj = type(co)(co.co_argcount, co.co_nlocals, co.co_stacksize,
                    co.co_flags, codestr, tuple(newconsts), co.co_names,
                    co.co_varnames, co.co_filename, co.co_name,
                    co.co_firstlineno, co.co_lnotab, co.co_freevars,
                    co.co_cellvars)
    return type(f)(codeobj, f.func_globals, f.func_name, f.func_defaults,
                    f.func_closure)


def bind_all(mc, builtin_only=False, stoplist=[], verbose=False):
    """Recursively apply bind_constants() to functions in a module or class.

    Use as the last line of the module (after everything is defined, but
    before test code).

    In modules that need modifiable globals, set builtin_only to True.

    """
    for k, v in vars(mc).items():
        if type(v) is FunctionType:
            newv = bind_constants(v, builtin_only, stoplist, verbose)
            setattr(mc, k, newv)
        elif type(v) in (type, ClassType):
            bind_all(v, builtin_only, stoplist, verbose)


def f(): pass
try:
    f.func_code.code
except AttributeError:                  # detect non-CPython environments
    bind_all = lambda *args, **kwds: 0
del f

import sys
bind_all(sys.modules[__name__])         # Optimizer, optimize thyself!

注意自动检测非 CPython 环境(该环境没有字节码)[2]。在这种情况下,bind 函数只会返回原始函数不变。这确保了添加到库模块的两行代码不会影响其他实现。

最终代码应添加一个标志,以便于禁用绑定。

参考资料

[1] ASPN 的非私有实现食谱 https://code.activestate.com/recipes/277940/


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

最后修改:2025-02-01 08:59:27 GMT