Skip to content

动态 import 优化指南

动态导入常用于实现延迟解析(lazy resolution)与规避模块间的循环导入问题。本文以此为出发点,既讨论动态导入在设计上的用途与风险,也评估其在性能敏感场景(如游戏 Mod 开发)中的开销与优化策略。

模块导入流程

从用户态代码角度看,import 语句或 __import__() 函数调用会经历以下步骤:

  1. 首先检查 sys.modules 缓存(已加载模块)。
  2. 若未命中缓存,按查找规则定位模块文件并加载。
  3. 执行模块顶层代码、初始化并写入 sys.modules。

重复导入同一模块通常开销很小,但深层导入或在热路径反复触发导入逻辑可能带来可感知的延迟。

性能问题分析

下面的基准脚本用于演示重复导入不同层级模块在不同 Python 版本上的差异:

python
# -*- coding: utf-8 -*-
import sys
from time import time

if sys.version_info < (3, 0):
    RANGE = xrange
else:
    RANGE = range

print("Python version: %s" % sys.version)

COUNT = 10000000

# 一级导入测试
t = time()
for _ in RANGE(COUNT):
    # 反复导入同一模块
    import xml
print("Time taken for repeated imports1: %fs" % (time() - t))

# 二级导入测试
t = time()
for _ in RANGE(COUNT):
    import xml.etree
print("Time taken for repeated imports2: %fs" % (time() - t))

# 三级导入测试
t = time()
for _ in RANGE(COUNT):
    import xml.etree.cElementTree
print("Time taken for repeated imports3: %fs" % (time() - t))

运行示例

以下示例输出展示了在不同 Python 版本下的典型结果(仅供参考):

text
Python version: 3.12.10 (tags/v3.12.10:0cc8128, Apr  8 2025, 12:21:36) [MSC v.1943 64 bit (AMD64)]
Time taken for repeated imports1: 0.675504s
Time taken for repeated imports2: 1.321499s (多级导入开销增加)
Time taken for repeated imports3: 1.321020s (深层导入开销不再增加)

Python version: 2.7.13 (v2.7.13:a06454b1afa1, Dec 17 2016, 20:53:40) [MSC v.1500 64 bit (AMD64)]
Time taken for repeated imports1: 2.127000s
Time taken for repeated imports2: 2.478000s (多级导入开销增加)
Time taken for repeated imports3: 3.105000s (深层导入开销继续增加)

通过上述数据可以看出,频繁导入多级模块的开销在 Python2 下随着层级增加而线性增长。

性能优化方案

以下示例展示了针对 Py2/3 的导入缓存方式:

python
# -*- coding: utf-8 -*-
import sys
from time import time

if sys.version_info < (3, 0):
    RANGE = xrange
    _CELEMENTTREE_CACHE = None

    # py2手动缓存导入结果 避免重复导入多级解析开销
    def IMPORT_CELEMENTTREE():
        global _CELEMENTTREE_CACHE
        if _CELEMENTTREE_CACHE is None:
            import xml.etree.cElementTree
            _CELEMENTTREE_CACHE = xml.etree.cElementTree
        return _CELEMENTTREE_CACHE
else:
    RANGE = range
    # py3可以使用 lru_cache 缓存导入结果 避免重复导入开销
    from functools import lru_cache

    @lru_cache(maxsize=None)
    def IMPORT_CELEMENTTREE():
        import xml.etree.cElementTree
        return xml.etree.cElementTree

t = time()
for _ in RANGE(10000000):
    IMPORT_CELEMENTTREE()
print("Time taken for cached imports: %fs" % (time() - t))

运行结果

使用缓存优化后,重复导入多级模块的开销显著降低:

text
Python3:Time taken for cached imports: 0.391149s
Python2:Time taken for cached imports: 0.578000s

底层实现解析

通过分析 CPython 源码,我们可以看到不同版本在处理导入时的差异:

Python3 下的实现

import会优先尝试以完整路径从缓存中读取模块,若存在缓存则开销较小;对于多级导入,CPython 通常只在必要时做一次路径切割与处理,因此深度不会线性放大开销。

c
// CPython 3.12
PyObject*
PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
                                 PyObject *locals, PyObject *fromlist,
                                 int level)
{
    PyThreadState *tstate = _PyThreadState_GET();
    PyObject *abs_name = NULL;
    PyObject *final_mod = NULL;
    PyObject *mod = NULL;
    PyObject *package = NULL;
    PyInterpreterState *interp = tstate->interp;
    int has_from;

    // ... 省略校验逻辑
    // 尝试以完整full路径搜索缓存 例如a.b.c
    mod = import_get_module(tstate, abs_name);
    if (mod == NULL && _PyErr_Occurred(tstate)) {
        goto error;
    }

    if (mod != NULL && mod != Py_None) {/* ... */}
    else {
        Py_XDECREF(mod);
        // 不存在缓存时,调用真实的加载逻辑初始化模块并缓存
        mod = import_find_and_load(tstate, abs_name);
        if (mod == NULL) {
            goto error;
        }
    }

    has_from = 0;
    if (fromlist != NULL && fromlist != Py_None) {
        has_from = PyObject_IsTrue(fromlist);
        if (has_from < 0)
            goto error;
    }
    if (!has_from) {
        // import a.b.c 时 仅返回第一级的a模块对象 通常搭配as重新绑定
        Py_ssize_t len = PyUnicode_GET_LENGTH(name);
        if (level == 0 || len > 0) {
            Py_ssize_t dot;
            dot = PyUnicode_FindChar(name, '.', 0, len, 1);
            if (dot == -2) {/* ... */}

            if (dot == -1) {
                // 不存在.视为单级导入 直接返回模块对象
                final_mod = Py_NewRef(mod);
                goto error;
            }

            if (level == 0) {
                // 当存在.时将完成至多一次切割提取首级模块(性能开销点)但随着层级增加并不会线性增加其开销
                PyObject *front = PyUnicode_Substring(name, 0, dot);
                if (front == NULL) {
                    goto error;
                }
                // 返回首级模块对象
                final_mod = PyImport_ImportModuleLevelObject(front, NULL, NULL, NULL, 0);
                Py_DECREF(front);
            }
            else {/* ... */}
        }
        else {/* ... */}
    }
    else {
        // ...
    }

  error:
    Py_XDECREF(abs_name);
    Py_XDECREF(mod);
    Py_XDECREF(package);
    if (final_mod == NULL) {
        remove_importlib_frames(tstate);
    }
    return final_mod;
}

Python2 下的实现

相比之下,Python2 在每次导入多级模块时,都会逐级调用加载逻辑检查缓存,导致随着模块路径深度增加,导入开销线性增加:

c
// CPython 2.7
static PyObject*
import_module_level(char *name, PyObject *globals, PyObject *locals,
                    PyObject *fromlist, int level)
{
    char *buf;
    Py_ssize_t buflen = 0;
    PyObject *parent, *head, *next, *tail;

    // ... 省略校验逻辑

    buf = PyMem_MALLOC(MAXPATHLEN+1);
    if (buf == NULL) {/* ... */}

    parent = get_parent(globals, buf, &buflen, level);
    if (parent == NULL)
        goto error_exit;

    Py_INCREF(parent);
    head = load_next(parent, level < 0 ? Py_None : parent, &name, buf,
                        &buflen);
    Py_DECREF(parent);
    if (head == NULL)
        goto error_exit;

    tail = head;
    Py_INCREF(tail);
    // py2中每次import都会分析完整的模块路径 导致频繁调用load_next检查缓存
    // 相较于py3只使用full路径一次性检查,其性能开销会随着模块路径深度线性增加
    while (name) {
        // 通过循环逐级加载/校验子模块
        next = load_next(tail, tail, &name, buf, &buflen);
        Py_DECREF(tail);
        if (next == NULL) {
            Py_DECREF(head);
            goto error_exit;
        }
        tail = next;
    }
    if (tail == Py_None) {/* ... */}
    if (fromlist != NULL) {/* ... */}
    if (fromlist == NULL) {/* ... */}

    Py_DECREF(head);
    if (!ensure_fromlist(tail, fromlist, buf, buflen, 0)) {
        Py_DECREF(tail);
        goto error_exit;
    }

    PyMem_FREE(buf);
    return tail;

error_exit:
    PyMem_FREE(buf);
    return NULL;
}

综上所述,Python3 在导入多级模块时的优化设计显著降低了深层导入的性能开销,而 Python2 则因逐级检查缓存导致开销线性增加。

实用建议

  • 避免深层导入(如 a.b.c.d)频繁出现在热路径:在模块顶层统一导入一次并复用。
  • 对于大型或延迟只在某些事件中用到的模块,采用局部导入(函数内 import)或懒加载,以减少启动和常驻内存开销。
  • 在 Python2 环境下对深层模块特别小心,考虑显式缓存导入结果;在 Python3 中,使用 functools.lru_cache 是简洁且安全的选择。
  • 在高频 tick 性能敏感的场景,优先在程序初始化阶段做好一次性导入或缓存,尽可能避免在循环中调用 import。
  • 当无法避免复杂导入路径时,使用简单的封装函数(如上例 IMPORT_CELEMENTTREE)来隐藏兼容性与缓存细节,便于维护。
  • 如果可能,评估升级至 Python3 带来的长期运行时优化收益(这真的可能吗?)。

总结

  • 重复导入单一模块通常成本低,但深层导入在不同 Python 版本下表现不同。
  • 将导入移出热路径、统一缓存与使用局部/延迟导入,是最直接且有效的优化手段。
  • 针对 MC Mod 场景,设计上尽可能避免循环导入,在程序初始化阶段做好导入,能显著降低运行时导入的性能开销。

Released under the BSD3 License