颜色高亮与自动补全
原生 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.dumps | rlcompleter 的核心能力 |
| 关键字 | retu<Tab> → return | 补全器内置关键字表 |
| 变量名 | 当前命名空间中的名字 | 基于 globals()/locals() |
| 文件路径 | 在字符串中补全 | 需显式配置补全器 |
需要注意:原生补全不做动态求值。rlcompleter 只查看 dir() 结果,不会执行表达式来推导更深的属性,这与 IPython 的补全有本质差异。
语法与用法
检查当前环境的补全能力:
# readline 是否可用
import importlib.util
print("readline:", importlib.util.find_spec("readline") is not None)
print("rlcompleter:", importlib.util.find_spec("rlcompleter") is not None)python -c "import readline; print('readline 可用')"最基础的补全体验(多数系统开箱即用):
>>> 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 键:
# ~/.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 中:
>>> import pathlib
>>> p = pathlib.Path(".")
>>> p.exi<Tab>
>>> p.exists # 补全成功详细说明
rlcompleter 的工作原理
rlcompleter.Completer.complete 的签名是:
def complete(self, text: str, state: int) -> str | None:
"""返回第 state 个以 text 开头的候选,没有则返回 None。"""readline 会反复调用它(state 从 0 递增),直到返回 None,从而得到全部候选。rlcompleter 的解析逻辑大致是:
- 按
.切分text,如json.dum→["json", "dum"]; - 用
eval()求值除最后一段外的前缀(如json),得到对象; - 对该对象调用
dir(),筛选以最后一段开头的名字; - 若前缀求值抛异常,则回退到当前命名空间中查找。
这解释了两点:一是为什么它能补全已导入对象的属性;二是为什么补全有副作用风险——eval() 确实会执行代码。
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 列出全部候选、再按一次开始循环,可改用:
readline.parse_and_bind("tab: complete")部分系统的默认绑定是 menu-complete,表现为按 Tab 直接把当前词替换成候选,容易让人困惑。
自定义补全:补全文件路径
原生补全不处理文件路径,可以自定义一个补全器,在检测到 text 含路径分隔符时补全文件名。
# ~/.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 串联:
# ~/.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。
# 关闭高亮
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 |
| 最强体验 | IPython | IPython |
若必须在新 REPL 中自定义补全,可设置 PYTHON_BASIC_REPL=1 回退到 readline 分支,再套用上面的配置。
引入 IPython 获得完整能力
当内置能力不够用时,IPython 是最成熟的选择:
# 安装
python -m pip install ipython
# 或用 uv 全局安装
uv tool install ipython
# 启动
ipythonIPython 提供的能力包括:
| 能力 | 说明 |
|---|---|
| 富文本输出 | 直接渲染图片、HTML、DataFrame |
| 强大补全 | 支持动态求值、模糊匹配 |
| 魔术命令 | %timeit、%run、%%bash 等 |
| 内联图表 | 配合 matplotlib 直接显示 |
| 自动括号匹配 | 输入 ( 自动补 ) |
| 多行编辑 | 类似新 REPL,但更成熟 |
IPython 的补全同样是可扩展的:它通过 IPython.completer 提供统一接口,可以自动补全 pandas、numpy 等库的类型化属性。
实战示例
示例一:一份完整的启动配置
把补全、历史、别名的配置集中到一个启动文件,是常见的做法。
# ~/.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 指向它:
# 写入 ~/.zshrc 或 ~/.bashrc
export PYTHONSTARTUP="$HOME/.pythonrc.py"验证是否生效:
python -i -c "print('检查补全配置')"
# 应看到「已加载启动配置」提示示例二:为特定对象注册补全
有时希望为某个业务对象提供语义化补全(例如补全状态值)。下面演示如何注册专门的补全函数。
# 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 中:
>>> status_pe<Tab>
>>> status_pending
>>> status_ru<Tab>
>>> status_running这种「用补全替代记忆」的方式,对经常需要输入枚举值的场景非常实用。
示例三:检测并报告终端交互能力
写一个脚本,一次性报告当前环境的补全与高亮能力,便于排查「为什么我的 Tab 不工作」。
# 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,可以通过配置让常用库自动可用并支持补全。
# ~/.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 环境变量。
最佳实践
- 把补全与历史配置集中放在
~/.pythonrc.py,通过PYTHONSTARTUP加载,避免散落多处。 - 补全分隔符中务必移除
.,这是属性补全生效的前提。 - 用组合补全器串联路径补全与
rlcompleter,兼顾两类需求。 - 对经常输入的枚举值注册专用补全器,用补全代替记忆。
- 在 3.13+ 上享受内置高亮;需要更强补全时切换到 IPython。
- 用诊断脚本定期检查环境能力,尤其在换机器或换容器后。
- 不做重型
eval的补全设计,避免补全时卡顿或触发副作用。
下一节
继续学习:启动文件与配置