Skip to content

颜色高亮与自动补全 ​

原生 REPL 的默认交互体验相当朴素:没有补全、没有高亮。但借助标准库 readline 与 rlcompleter,以及 3.13 新 REPL 内置的着色能力,可以把终端里的交互体验提升到接近现代编辑器的水平。

前置知识

阅读本节前,建议先了解:多行编辑与历史记录

基础概念 ​

要理解「补全」和「高亮」是怎么实现的,先要区分几个模块:

模块类型职责
readline标准库绑定 GNU Readline,提供行编辑、历史与补全钩子
rlcompleter标准库基于 readline 实现 Python 感知的补全器(补全对象属性)
site标准库启动时自动启用 rlcompleter,让补全开箱可用
3.13 新 REPL内置实现自带语法高亮、彩色提示符,不依赖 readline
pygments第三方语法高亮库,IPython 用它渲染输入

几个关键事实:

  • Python 默认已经开启补全:site 模块在交互模式下自动调用 rlcompleter,因此 Tab 键补全在多数系统上无需配置即可用。
  • 补全依赖 readline:若解释器未链接 readline(如某些精简构建),Tab 补全不可用。
  • 高亮在 3.13 才内置:3.12 及以前原生 REPL 完全没有输入高亮,只能靠 IPython。
  • 补全与高亮是两件独立的事:可以只有其一,也可以都没有。

补全能补全什么?

场景示例说明
模块名import jso<Tab> → json依赖 sys.path 扫描
已导入对象的属性json.du<Tab> → json.dumpsrlcompleter 的核心能力
关键字retu<Tab> → return补全器内置关键字表
变量名当前命名空间中的名字基于 globals()/locals()
文件路径在字符串中补全需显式配置补全器

需要注意:原生补全不做动态求值。rlcompleter 只查看 dir() 结果,不会执行表达式来推导更深的属性,这与 IPython 的补全有本质差异。

语法与用法 ​

检查当前环境的补全能力:

python
# readline 是否可用
import importlib.util
print("readline:", importlib.util.find_spec("readline") is not None)
print("rlcompleter:", importlib.util.find_spec("rlcompleter") is not None)
bash
python -c "import readline; print('readline 可用')"

最基础的补全体验(多数系统开箱即用):

python
>>> import js<Tab>
>>> import json             # 自动补全为 json
>>> json.lo<Tab>
>>> json.loads             # 补全为 json.loads
>>> def my_func(): pass
...
>>> my_f<Tab>
>>> my_func                # 补全为 my_func

显式配置补全器与绑定 Tab 键:

python
# ~/.pythonrc.py(片段)
import atexit
import readline
import rlcompleter

# 创建补全器,命名空间使用 globals()
completer = rlcompleter.Completer(namespace=None)

# 把 Tab 绑定到补全函数
readline.parse_and_bind("tab: complete")
readline.set_completer(completer.complete)

# 设置补全时的单词分隔符(默认会因包含点号而无法补全属性)
readline.set_completer_delims(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?")

让补全在属性访问时也生效的关键是从分隔符里移除 .(上面这行即为此目的)。默认分隔符包含 .,会导致 json.du 被切分成 json 和 du 两段,属性补全失效。

启用后,在 REPL 中:

text
>>> import pathlib
>>> p = pathlib.Path(".")
>>> p.exi<Tab>
>>> p.exists          # 补全成功

详细说明 ​

rlcompleter 的工作原理 ​

rlcompleter.Completer.complete 的签名是:

python
def complete(self, text: str, state: int) -> str | None:
    """返回第 state 个以 text 开头的候选,没有则返回 None。"""

readline 会反复调用它(state 从 0 递增),直到返回 None,从而得到全部候选。rlcompleter 的解析逻辑大致是:

  1. 按 . 切分 text,如 json.dum → ["json", "dum"];
  2. 用 eval() 求值除最后一段外的前缀(如 json),得到对象;
  3. 对该对象调用 dir(),筛选以最后一段开头的名字;
  4. 若前缀求值抛异常,则回退到当前命名空间中查找。

这解释了两点:一是为什么它能补全已导入对象的属性;二是为什么补全有副作用风险——eval() 确实会执行代码。

python
import rlcompleter

completer = rlcompleter.Completer()
completer.complete("json.du", 0)
# 'json.dumps'
completer.complete("json.du", 1)
# 'json.dump'
completer.complete("json.du", 2)
# None(没有更多候选)

为什么会看到多个候选 ​

当有多个匹配时,readline 的行为取决于绑定方式:

绑定行为
tab: complete循环切换候选,每次按 Tab 换下一个
tab: menu-complete直接替换为下一个候选

若希望按一次 Tab 列出全部候选、再按一次开始循环,可改用:

python
readline.parse_and_bind("tab: complete")

部分系统的默认绑定是 menu-complete,表现为按 Tab 直接把当前词替换成候选,容易让人困惑。

自定义补全:补全文件路径 ​

原生补全不处理文件路径,可以自定义一个补全器,在检测到 text 含路径分隔符时补全文件名。

python
# ~/.pythonrc.py(片段)
import os
import readline
from typing import Callable


def path_completer(text: str, state: int) -> str | None:
    """在含路径分隔符时补全文件路径,否则交给默认逻辑。"""
    if os.sep not in text:
        return None
    directory, prefix = os.path.split(text)
    base = directory or "."
    try:
        names = sorted(os.listdir(base))
    except OSError:
        return None
    matches = [name for name in names if name.startswith(prefix)]
    if state < len(matches):
        # 保留原有目录前缀
        return os.path.join(directory, matches[state]) if directory else matches[state]
    return None


readline.set_completer(path_completer)
readline.parse_and_bind("tab: complete")

这个补全器覆盖了默认行为,因此实际使用中常把 rlcompleter 与自定义逻辑组合:先尝试自定义,返回 None 时再交给 rlcompleter。

组合多个补全器 ​

更实用的写法是把自定义补全器与 rlcompleter 串联:

python
# ~/.pythonrc.py(片段)
import os
import readline
import rlcompleter

_completer = rlcompleter.Completer()


def combined_completer(text: str, state: int) -> str | None:
    """优先补全文件路径,否则交给 rlcompleter。"""
    if os.sep in text:
        directory, prefix = os.path.split(text)
        base = directory or "."
        try:
            matches = [n for n in sorted(os.listdir(base)) if n.startswith(prefix)]
        except OSError:
            matches = []
        if state < len(matches):
            return os.path.join(directory, matches[state]) if directory else matches[state]
        return None
    return _completer.complete(text, state)


readline.set_completer(combined_completer)
readline.parse_and_bind("tab: complete")
readline.set_completer_delims(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?")

先判断是否含路径分隔符,再决定走哪条分支,逻辑清晰且不互相干扰。

3.13 新 REPL 的高亮机制 ​

3.13 的新 REPL 内置语法高亮,无需 pygments。它利用 Python 自带的 tokenize 与 ast 对输入做增量着色:

元素颜色倾向
关键字一种强调色
字符串另一种颜色
注释通常为灰色/绿色
内置名与常量高亮
提示符可着色

控制高亮的环境变量与前面章节一致:PYTHON_COLORS、NO_COLOR、FORCE_COLOR。

bash
# 关闭高亮
NO_COLOR=1 python

# 明确开启(即使输出被重定向)
FORCE_COLOR=1 python

补全在高亮环境下是否冲突 ​

不冲突。高亮负责「渲染已输入的文本」,补全负责「补全时插入候选」,二者在实现层面是分开的。但要注意:3.13 的新 REPL 不使用 readline,因此通过 readline.set_completer() 配置的补全器在新 REPL 中可能不生效。

判断与选择:

需求3.12 及以前3.13+ 新 REPL
补全用 readline + rlcompleter内置基础补全
高亮无内置
自定义补全器readline.set_completer需按新 REPL 的接口扩展,或回退旧 REPL
最强体验IPythonIPython

若必须在新 REPL 中自定义补全,可设置 PYTHON_BASIC_REPL=1 回退到 readline 分支,再套用上面的配置。

引入 IPython 获得完整能力 ​

当内置能力不够用时,IPython 是最成熟的选择:

bash
# 安装
python -m pip install ipython
# 或用 uv 全局安装
uv tool install ipython

# 启动
ipython

IPython 提供的能力包括:

能力说明
富文本输出直接渲染图片、HTML、DataFrame
强大补全支持动态求值、模糊匹配
魔术命令%timeit、%run、%%bash 等
内联图表配合 matplotlib 直接显示
自动括号匹配输入 ( 自动补 )
多行编辑类似新 REPL,但更成熟

IPython 的补全同样是可扩展的:它通过 IPython.completer 提供统一接口,可以自动补全 pandas、numpy 等库的类型化属性。

实战示例 ​

示例一:一份完整的启动配置 ​

把补全、历史、别名的配置集中到一个启动文件,是常见的做法。

python
# ~/.pythonrc.py
"""Python 交互式解释器的启动配置。"""
from __future__ import annotations

import atexit
import os
import readline
import rlcompleter
from pathlib import Path

# ---------- 历史 ----------
HISTORY = Path(os.environ.get("PYTHON_HISTORY", Path.home() / ".python_history"))
if HISTORY.exists():
    readline.read_history_file(HISTORY)
readline.set_history_length(2000)
atexit.register(readline.write_history_file, HISTORY)

# ---------- 补全 ----------
_completer = rlcompleter.Completer()


def _completer_with_paths(text: str, state: int) -> str | None:
    """优先补全文件路径,否则交给 rlcompleter。"""
    if os.sep in text:
        directory, prefix = os.path.split(text)
        base = directory or "."
        try:
            matches = [n for n in sorted(os.listdir(base)) if n.startswith(prefix)]
        except OSError:
            return None
        if state < len(matches):
            return os.path.join(directory, matches[state]) if directory else matches[state]
        return None
    return _completer.complete(text, state)


readline.set_completer(_completer_with_paths)
readline.parse_and_bind("tab: complete")
# 移除 '.',使属性补全生效
readline.set_completer_delims(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?")

# ---------- 便捷提示 ----------
print("已加载启动配置:Tab 补全、历史记录已启用")

在 Shell 中通过 PYTHONSTARTUP 指向它:

bash
# 写入 ~/.zshrc 或 ~/.bashrc
export PYTHONSTARTUP="$HOME/.pythonrc.py"

验证是否生效:

bash
python -i -c "print('检查补全配置')"
# 应看到「已加载启动配置」提示

示例二:为特定对象注册补全 ​

有时希望为某个业务对象提供语义化补全(例如补全状态值)。下面演示如何注册专门的补全函数。

python
# repl_setup.py(在 PYTHONSTARTUP 中导入)
import readline

# 待补全的状态集合
STATUSES = ["pending", "running", "succeeded", "failed", "cancelled"]


def status_completer(text: str, state: int) -> str | None:
    """在输入 status_ 前缀时补齐状态名。"""
    if not text.startswith("status_"):
        return None
    prefix = text.removeprefix("status_")
    matches = [s for s in STATUSES if s.startswith(prefix)]
    if state < len(matches):
        return f"status_{matches[state]}"
    return None


_original = readline.get_completer()


def dispatch(text: str, state: int) -> str | None:
    """先尝试状态补全,再回退到原有补全器。"""
    own = status_completer(text, state)
    if own is not None:
        return own
    return _original(text, state) if _original else None


readline.set_completer(dispatch)
print("已注册 status_ 补全,输入 status_pe<Tab> 试试")

在 REPL 中:

text
>>> status_pe<Tab>
>>> status_pending
>>> status_ru<Tab>
>>> status_running

这种「用补全替代记忆」的方式,对经常需要输入枚举值的场景非常实用。

示例三:检测并报告终端交互能力 ​

写一个脚本,一次性报告当前环境的补全与高亮能力,便于排查「为什么我的 Tab 不工作」。

python
# repl_diagnostics.py
from __future__ import annotations

import importlib.util
import sys


def module_available(name: str) -> bool:
    """判断模块是否可导入。"""
    return importlib.util.find_spec(name) is not None


def report() -> dict[str, bool | str]:
    """汇总交互环境的各项能力。"""
    return {
        "python": sys.version.split()[0],
        "stdin_is_tty": sys.stdin.isatty(),
        "readline": module_available("readline"),
        "rlcompleter": module_available("rlcompleter"),
        "pyrepl_3_13": module_available("_pyrepl"),
        "ipython": module_available("IPython"),
        "term": __import__("os").environ.get("TERM", "<未设置>"),
    }


if __name__ == "__main__":
    for key, value in report().items():
        print(f"{key:16} {value}")

输出中的 readline 为 False 时,Tab 补全不可用;_pyrepl 为 True 表示可使用 3.13 的内置高亮。这份报告能快速定位问题所在。

示例四:在 IPython 中启用自动导入 ​

如果使用 IPython,可以通过配置让常用库自动可用并支持补全。

python
# ~/.ipython/profile_default/ipython_config.py(片段)
c = get_config()  # noqa: F821

# 启动时自动导入常用库,并支持 np./pd. 补全
c.InteractiveShellApp.exec_lines = [
    "import numpy as np",
    "import pandas as pd",
]

# 开启自动括号补全
c.TerminalInteractiveShell.auto_match = True

# 开启高亮(IPython 默认已开启,可显式确认)
c.TerminalInteractiveShell.highlighting_style = "monokai"

配置完成后,np.ar<Tab> 就能补全出 numpy 的属性,且输入时实时高亮。

注意事项 ​

  • . 必须在补全分隔符中移除:否则属性补全失效,json.du<Tab> 不会工作。
  • 补全器会 eval 前缀:rlcompleter 对前缀做求值,若前缀有副作用(如属性访问触发耗时计算),补全可能卡顿或产生副作用。
  • 3.13 新 REPL 未必响应 readline 配置:它不使用 readline,自定义补全需回退旧 REPL 或改用新接口。
  • 补全依赖解释器链接 readline:精简构建或部分环境中可能缺失。
  • Tab 绑定因系统而异:complete 与 menu-complete 行为不同,需按需设定。
  • 不要在生产脚本中依赖启动文件:PYTHONSTARTUP 只在交互模式下执行,脚本运行时不生效。
  • 高亮不影响代码语义:着色只是渲染层的事,与执行无关。
  • 自定义补全器的返回值必须匹配前缀:返回不含前缀的字符串会导致错误的补全结果。

常见问题 ​

Q:为什么 Tab 没有任何反应? 先检查 readline 是否可用,再确认 set_completer 与 parse_and_bind 都已执行。少数系统默认未绑定 Tab。

Q:为什么只能补全首段,属性补不出来? 多半是补全分隔符里包含 .。执行 readline.set_completer_delims(...) 移除点号即可。

Q:补全很慢怎么办?rlcompleter 会对前缀求值,若对象属性访问开销大,可缩小补全范围或改用更轻量的自定义补全器。

Q:3.13 中设置 set_completer 没效果? 新 REPL 不使用 readline。可设 PYTHON_BASIC_REPL=1 回退,或直接使用内置补全。

Q:能补全文件路径吗? 原生不支持,需要自定义补全器(见示例一)。

Q:要丢弃全部自定义配置怎么办? 执行 python -S 启动,跳过 site 模块,或临时取消 PYTHONSTARTUP 环境变量。

最佳实践 ​

  1. 把补全与历史配置集中放在 ~/.pythonrc.py,通过 PYTHONSTARTUP 加载,避免散落多处。
  2. 补全分隔符中务必移除 .,这是属性补全生效的前提。
  3. 用组合补全器串联路径补全与 rlcompleter,兼顾两类需求。
  4. 对经常输入的枚举值注册专用补全器,用补全代替记忆。
  5. 在 3.13+ 上享受内置高亮;需要更强补全时切换到 IPython。
  6. 用诊断脚本定期检查环境能力,尤其在换机器或换容器后。
  7. 不做重型 eval 的补全设计,避免补全时卡顿或触发副作用。

下一节

继续学习:启动文件与配置

参考链接 ​