第 1 章:让程序说话
本章内容限 Python 3.12+ 用户
0. 认识 print 函数
本节目标:初步了解
预计用时:5~10 分钟(含实操)
完成标志:能初步理解
0.1 print() 是什么?
print 译为打印,可见其作用就是在控制台打印你想要的东西。
在你写下任何复杂的程序之前,你需要一种方式确认:“我的代码真的在按我预期的方式运行吗?”
print 就是你与程序对话的第一个窗口。
0.2 写下第一个 print()
现在请你打开编辑器,新建一个文件 hello.py,并输入以下内容:
print("Hello, world!")
print(42)
print("年龄:", 18)
保存后,请在已激活虚拟环境的终端执行:
python hello.py
不出意外你会看到:
Hello, world!
42
年龄: 18
如果看到了这三行输出,恭喜你——你已经完成了和 Python 的第一次成功交互。
0.3 为什么这很重要?
不要小看这三行输出。它们代表了一个完整的闭环:
- 你写下了意图(代码)
- Python 执行了逻辑(解释器)
- 你验证了结果(终端反馈)
从现在开始,每当你不确定某段代码是否正确时,先用 print 看一眼。这比任何猜测、搜索都更可靠。
实用主义原则:不要等到“完全理解”才开始用。先用起来,获得手感,再回头搞懂原理。
0.4 本节小结
print是你和程序的对话窗口,用于验证代码、观察状态- 它能处理字符串、数字、多参数等多种输入
- 现在就用起来,把
print当作你的“代码听诊器”
你已经迈出了第一步。下一节,我们将揭开 print “翻译”过程背后的机制,让你从“能用”走向“精准表达”。
1. 精准控制 print 的输出
本节目标:学习
预计用时:25~35 分钟(含实操)
完成标志:了解
很多教程会直接把一堆参数丢上来,让你去背,但是我们今天换一个方法——去 print 的源代码里找答案。
打开编辑器,输入 print(),然后:
- VS Code:按住
Ctrl(Mac 为Cmd)点击print,或右键选择 “转到定义”。 - PyCharm:按住
Ctrl(Mac 为Cmd)点击print,或右键选择 “Go to Definition”。
你应该会看到类似这样的类型注解:
def print(
*values: object,
sep: str | None = " ",
end: str | None = "\n",
file: SupportsWrite[str] | None = None,
flush: Literal[False] = False,
) -> None: ...
不要慌,这是 print 最权威的文档
1.1 参数 *values:为什么可以写 print(1,2,3,4,5,6,7) ?
当你右键跳转到 print 的定义时,第一个映入眼帘的参数就是它。
各位可能会卡在这里,这个 * 是什么意思?
其实,它是 Python 的 可变位置参数(variadic positional arguments),规则很简单:
- 匹配关键字:如果你传入的不是
sep,end这样的控制关键字,就会被values收下。 - 数量不限:你可以传入 1 个,10 个,100 个甚至不传。
这就是为什么以下调用都可行:
print() # 空着
print("Hello,world!") # 一个字符串
print(11, 45, 14) # 一堆数字
print(9527, "Hello", 67) # 混合
需要注意:*values 不是 print 的超能力,而是 Python 函数的通用机制(什么是函数呢?将会在续章中提到)。未来也会频繁用到它来设计灵活的接口。
为什么类型是 object ?
注意 *values: object 中的类型标注是 object,而不是其他,这是为什么呢?
object 译为 对象,是所有 Python 类型的基类,相当于是所有类型的父亲,可以画出这么一张图:
注意:bool 继承自 int,所以 bool 是 int 的儿子
这就意味着 print() 可以传入任何类型。
解包 * 将在 列表 一章展开讲解
1.2 参数 end:精准控制结束字符
如果你使用过 print,可能会好奇,为什么两行 print 会自动分成两行输出?
print("Hello")
print("World!")
# Hello
# World!
这其实是 print 函数 end 参数的作用。
end 译为 结束,可以用该参数指定结束字符。
请再次打开源码,找到这一行:
end: str | None = "\n",
不难发现,end 的类型注解是 str | None = "\n",这意味着我们可以选择传入字符串,也可以选择空着。
设计模式识别:
str | None = "\n"是 Python 中非常常见的参数设计。它允许通过end=None来显式表达 “我需要默认值”,也可以直接为空使用默认值。
那么如果我们不希望自带换行呢?可以使用 end 参数指定结束字符:
示例:
print("Hello", end="")
print("World!")
输出:
HelloWorld!
示例:
print("Hello", end=", ") # 指定结束字符为逗号
print("World!")
输出:
Hello, World!
1.3 参数 sep:控制分隔字符
sep 是 separator(分隔符)的缩写。
正如 end 控制“结尾用什么”,sep 控制的是“多个值之间用什么隔开”。
回到源码查看签名:
sep: str | None = " ",
结构与 end 完全对称:默认是一个空格,这就是为什么 print("a", "b") 输出 a b 而不是 ab。
sep 通常用于 列表或元组解包时的分隔符(将会在后续章中讲解)。目前只需要知道,需要同时打印多个值用 , 分割,, 是语法,不输出;sep 才决定输出中间隔用什么。
1.4 参数 file:本地保存输出内容
file 决定打印的内容去哪里。默认输出到终端,但你可以让它写入本地文件。
file 的传入值不能是字符串,由于该参数涉及类与对象,将在后续章节进行讲解。
现阶段可以先学会如何使用,意义可以暂且不管
with open("output.txt", "w") as f:
print("text to file", file=f)
该代码会将 text to file 输出到 output.txt。
1.5 参数 flush:立即写入还是攒着再写
flush 控制打印内容是否立即写入目标设备(默为 False)。默认情况下,会先攒着,等程序结束或手动调用 flush() / 缓冲区满 / 遇到异常 时才会强制刷新。
如何理解?
把 print() 想象成寄快递:
flush=False(默认):包裹先放到驿站(缓冲区),等驿站满了 / 程序结束 / 遇到换行符\n才会派送到目的地。flush=True:强制要求快递员立刻将包裹派送到目的地,跳过驿站的等待环节。
说明
flush=True 并不会加快输出速度,而是防止内容丢失
print("hello", flush=True) # 立即写入,不怕程序崩溃丢数据
print("hello", flush=False) # 正常情况不会丢失,仅程序异常退出时可能丢失
什么时候需要立即刷新
- 实时显示进度条避免卡顿
- 程序崩溃/异常时保留已输出的内容
- 交互式程序中及时看到输出结果
1.6 本节小结
恭喜你完成了 print 函数的源码级拆解!让我们用一张表快速回顾本节学到的核心参数:
| 参数 | 作用 | 默认值 | 记忆口诀 |
|---|---|---|---|
*values | 接收任意数量、任意类型的打印内容 | 无 | 万物皆可 print |
sep | 控制多个值之间的分隔符 | " " (空格) | 中间隔开用什么 |
end | 控制每次打印结束后的字符 | "\n" (换行) | 结尾追加什么 |
file | 指定输出目标(终端或文件) | None (终端) | 内容送到哪里去 |
flush | 控制是否立即写入,跳过缓冲等待 | False | 快递要不要加急送 |
核心收获
- 学会看源码:不再死记硬背参数,而是通过
Ctrl/Cmd + 左键点击跳转到类型注解,获取最权威的一手信息。 - 理解设计模式:注意到
str | None = "默认值"这种常见的 Python 参数设计范式,为后续学习其他标准库打下基础。 - 区分语法与参数:明确了逗号
,只是语法,真正决定输出格式的是sep和end。
避坑提醒
flush=True不等于打印更快,它只是防止程序异常退出时丢失缓冲区内容。file参数接收的是文件对象而非文件名字符串,必须配合open()使用。*values的类型是object,意味着你可以放心传入任何数据类型,无需提前转换。
掌握了 print 的参数控制后,我们已经能精准管理“怎么输出”。但参数只是调用的表象,真正决定输出内容的是 Python 对传入对象的底层转换机制——下一节我们将深入 print 对 object 的处理链路,带你理解 str()、__str__ 与 __repr__ 的调用优先级及触发时机,从而不仅学会“打印什么”,更明白“为什么打印出来是这样”,为后续调试自定义对象显示、日志序列化等问题建立正确的底层心智模型。
2. 了解 print 底层:Python 对字符串做了什么?(进阶)
本节目标:学习
预计用时:25 分钟+
完成标志:初步了解 Python 字符流底层逻辑
本节难度较大,请酌情阅读
2.1 现象反差:为什么 print 和直接输出不一样?
打开 Python 交互式环境(Win+R 输入 python 按回车),输入下面两行代码,对比输出:
>>> 'hello\nworld'
'hello\nworld'
>>> print('hello\nworld')
hello
world
同样一个字符串,直接回车和交给 print 的结果完全不一样。
- 直接回车:Python 显示的是对象的字面量表示(repr),
\n以转义形式呈现,告诉你“这里有个换行符”。 print输出:\n被解释执行了,变成了真正的换行。
结论:变量存的是原始数据,print 负责把它翻译成人类可读的格式,并发送到输出设备。print 是一个“翻译官”。
2.2 拆解动作:print 在幕后执行的三个步骤
第一步:str() 强制转换
print 可以接收任何对象,但最终输出的必须是字符串。它会先调 str() 把对象转成字符串。
print(123) # 123 → "123"
print([1, 2, 3]) # [1, 2, 3] → "[1, 2, 3]"
print(None) # None → "None"
自定义对象靠 __str__ 方法决定打印内容:
class Person:
def __init__(self, name):
self.name = name
def __str__(self):
return f"姓名:{self.name}"
p = Person("小明")
print(p) # 输出:姓名:小明
没有 __str__ 就退而求其次用 __repr__,多数情况你不需要操心这一步。
第二步:拼接 sep 与 end
print 的完整签名:
print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)
多个参数时,sep 连接它们,end 收尾。
print("A", "B", "C", sep=" | ", end="!\n")
# 内部拼接结果:"A | B | C!\n"
第三步:写入 sys.stdout
拼完的字符串交给 sys.stdout.write()。sys.stdout 是标准输出流,像一根管道,一头连程序,一头连终端或文件。
import sys
print("Hello")
sys.stdout.write("Hello\n") # 等效,但 write 不自动加换行
到文件:
with open("log.txt", "w") as f:
print("这条写入文件了", file=f)
2.3 编码:字符串的“翻译官”
print 拼出来的字符串是给人看的,但操作系统只认字节(0 和 1)。所以 print 在交出数据前,必须把字符翻译成字节——这个过程叫编码。
"你好".encode("utf-8") # b'\xe4\xbd\xa0\xe5\xa5\xbd'
print 用哪个编码?看 sys.stdout.encoding:
import sys
print(sys.stdout.encoding) # Linux 下 utf-8,Windows 中文系统 gbk
这个值是终端环境给的,不是 Python 定的。所以同一段代码在不同机器上可能一个正常、一个乱码。
编码不匹配的后果:
- 乱码:数据用 UTF-8 编码,终端用 GBK 解码 → 显示乱码
- 报错:字符超出编码范围(比如 GBK 不支持 emoji)→
UnicodeEncodeError
解决办法:设环境变量 PYTHONIOENCODING=utf-8,或启动时加 -X utf8。
2.4 缓冲区:为什么 print 有时候“赖着不写”?
你很可能遇到过这种情况:
import time
print("加载中", end="")
time.sleep(3)
print("完成!")
“加载中”没立刻显示,等了 3 秒才和“完成!”一起蹦出来。
Python 不会每写一个字符就塞给操作系统,而是先攒在缓冲区里,等攒够一批再送。就像寄快递,你不会写一个字就发一次,而是攒一箱再发。
三种缓冲模式:
| 模式 | 触发条件 | 典型场景 |
|---|---|---|
| 行缓冲 | 遇到 \n 才输出 | 终端交互 |
| 全缓冲 | 缓冲区满了才输出(通常几 KB) | 写入文件 |
| 无缓冲 | 立刻输出 | sys.stderr |
强制刷新:
print("加载中", end="", flush=True) # 立刻刷新
sys.stdout.flush() # 手动刷新
python -u script.py # 启动时禁用缓冲
2.5 sys.stdout 的层次结构
sys.stdout 不是一块铁板,它包了三层。打个比方:
- 顶层:翻译官(
TextIOWrapper),把字符翻译成字节 - 中间层:仓库(
BufferedWriter),负责攒货 - 底层:送货员(
FileIO),直接敲门找操作系统
用代码验证:
import sys
print(type(sys.stdout)) # TextIOWrapper 翻译官
print(type(sys.stdout.buffer)) # BufferedWriter 仓库
print(type(sys.stdout.buffer.raw)) # FileIO 送货员
各层用法:
# 翻译官:接收字符串,自动编码
sys.stdout.write("你好\n")
# 仓库:接收字节,不编码但攒批
sys.stdout.buffer.write(b"\xe4\xbd\xa0\xe5\xa5\xbd\n")
# 送货员:接收字节,不攒批,直接敲门
sys.stdout.buffer.raw.write(b"\xe4\xbd\xa0\xe5\xa5\xbd\n")
2.6 os.write:跳过所有中间人
print 绕了一大圈,最后干的就是这事:
import os
os.write(1, b"Hello World\n") # 1 是标准输出的文件描述符
os.write 是那个“送货员”本人:
- 不帮你翻译(必须传
bytes) - 不帮你攒批(调一次写一次)
- 不帮你加换行
日常几乎用不到,但它是理解 print 底层的终点。
2.7 CPython 源码里的 print
提示:以下内容涉及 C 语言,不喜欢看源码的可以跳过。
CPython 是 Python 的官方实现——就是你平时装的 Python 解释器。C 表示它是用 C 语言写的。print 的实现在 Python/bltinmodule.c,函数名叫 builtin_print_impl。
下面是 CPython 3.12 中 print 的核心代码:
static PyObject *
builtin_print_impl(PyObject *module, PyObject *args, PyObject *kwargs)
{
PyObject *sep = NULL; // 分隔符,默认空格
PyObject *end = NULL; // 结尾符,默认换行
PyObject *file = NULL; // 输出目标,默认 sys.stdout
int flush = 0; // 是否强制刷新
// 1. 解析参数
static char *kwlist[] = {"sep", "end", "file", "flush", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|OOOO:print", kwlist,
&sep, &end, &file, &flush))
return NULL;
// 2. 设置默认值
if (sep == NULL) sep = PyUnicode_FromString(" ");
if (end == NULL) end = PyUnicode_FromString("\n");
if (file == NULL) file = PySys_GetObject("stdout");
// 3. 逐个处理要打印的对象
PyObject *output = PyUnicode_FromString("");
for (int i = 0; i < PyTuple_Size(args); i++) {
PyObject *obj = PyTuple_GetItem(args, i);
PyObject *str_obj = PyObject_Str(obj); // 转字符串
if (str_obj == NULL) return NULL;
if (i > 0) {
PyUnicode_Append(&output, sep); // 加分隔符
}
PyUnicode_Append(&output, str_obj);
Py_DECREF(str_obj);
}
// 4. 追加结尾符
PyUnicode_Append(&output, end);
// 5. 调用 file.write()
PyObject *write = PyObject_GetAttrString(file, "write");
PyObject *result = PyObject_CallOneArg(write, output);
// 6. 如果 flush=True,调用 file.flush()
if (flush) {
PyObject *flush_method = PyObject_GetAttrString(file, "flush");
if (flush_method != NULL) {
PyObject_CallNoArgs(flush_method);
}
}
return Py_None;
}
代码完美对应 2.2 节的三步:
PyObject_Str→ 对象转字符串- 拼接
sep和end file.write()→ 输出
2.8 操作系统底层:两套实现
os.write 往下走,不同操作系统走不同的路:
Linux 调用路径
可以用 strace 亲眼看到系统调用:
strace -e write python3 -c "print('hello')"
# 输出:write(1, "hello\n", 6) = 6
Windows 调用路径
2.9 流程总结
2.10 本节小结
至此,我们完成了一次从 Python 语法糖到操作系统内核的完整下潜。print 不再是一个黑盒函数,而是一条清晰可见的数据流水线:
- 对象层:通过
str()/__str__/__repr__将任意对象翻译为人类可读的字符串表示; - 流层:经由
TextIOWrapper编码、BufferedWriter攒批,最终由FileIO交付给操作系统; - 系统层:通过
os.write触发内核系统调用(Linuxsys_write/ WindowsWriteFile),将字节送达终端或文件。
那么为什么要理解这条链路?不是为了平时写代码时手动 os.write(),而是在遇到问题是能第一时间判断问题所在。
难度提示回顾:本节涉及的 CPython 源码与系统调用属于进阶内容。如果你目前只需解决日常打印问题,掌握 2.1–2.4 已足够;2.5–2.8 可作为知识储备,在遇到深层问题时随时回来查阅。
3. 本章小结
至此,我们走完了 print 从“一句代码”到“系统调用”的完整路径。这一路的跨度确实不小:
认识 print"] --> B["第1节
参数控制"] B --> C["第2节
底层原理(进阶)"]
本章我们做的事情,本质上是一条 "从使用到底层" 的下潜路径:
| 节 | 视角 | 核心问题 | 新手掌握程度 |
|---|---|---|---|
| 第0节 | 使用者 | print 能干什么?怎么开始用? | 必须熟练 |
| 第1节 | 调用者 | print 的参数怎么控制输出格式? | 必须熟练 |
| 第2节 | 探索者 | print 内部到底发生了什么? | 了解即可,遇到bug再回查 |
如果要用一张图概括本章全部内容,可以这样梳理:
print('Hello')"] --> B["第1节: 参数控制"] B --> B1["*values: 万物皆可打印"] B --> B2["sep/end: 控制格式"] B --> B3["file/flush: 控制去向与时机"] B --> C["第2节: 底层链路(选读)"] C --> C1["① str() 转换"] C1 --> C2["② 编码与缓冲"] C2 --> C3["③ 系统调用"]
3.1 核心收获
从"能用"到"精准"
print 远不止 print("Hello World")。通过 sep、end、file、flush,你可以精准控制输出的格式、目的地和时机。这是你日常开发中最常用的部分,建议反复练习直到形成肌肉记忆。
从"黑盒"到"可排查"
第2节的底层链路不需要你现在全部记住。它的真正价值是:当你以后遇到乱码、输出延迟、内容丢失等“诡异问题”时,知道该去哪里找答案。比如:
- 输出乱码 → 回看 2.3 编码部分
- 打印不及时 → 回看 2.4 缓冲区部分
- 自定义对象打印不对 → 回看 2.2 第一步
__str__部分
底层知识是你的“调试工具箱”,不必提前背熟,但要知道工具在哪里。
一张图记住位置
这张图只需记住一件事:万物皆 object。这就是为什么 print 能接收任何类型——它最终都会调用 str() 去转换,而所有类型都继承自 object,所以永远不会报错。
3.2 本章小测
1. 动手实践:精准控制输出
请编写一个简单的 Python 程序,使其在控制台严格输出以下四行内容:
你好,Python
1|2|3
123
123
约束条件:
- 第二行必须使用
print的sep参数实现。 - 第三行与第四行必须合并在同一个
print()调用中实现(提示:利用end或字符串拼接)。 - 不允许出现多余的换行或空格。
2. 回顾反思
本章中你印象最深的一个“原来如此”的瞬间是什么?
3. 进阶思考(选做)
为什么 print 不直接把字符贴到屏幕上,而要经过编码、缓冲这么多层?试着用自己的话解释这种设计的合理性。
3.3 附:本章概念速查表
使用建议:不必背诵此表。遇到问题时,按关键词定位到对应小节复习即可。
| 概念 | 首次出现 | 一句话说明 | 优先级 |
|---|---|---|---|
print | 第0节 | 输出函数 | 高 |
sep / end | 第1节 | 控制分隔符与结尾符 | 高 |
file / flush | 第1节 | 控制输出目标与刷新时机 | 高 |
str() | 第1、2节 | 将任意对象转为字符串 | 高 |
__str__ / __repr__ | 第2节 | 自定义对象的打印表现 | 中 |
sys.stdout | 第2节 | 标准输出流对象 | 中 |
| 编码 | 第2节 | 字符到字节的转换规则 | 中 |
| 缓冲区 | 第2节 | 攒批发送,减少系统开销 | 中 |
os.write / 系统调用 | 第2节 | 最底层的写入操作 | 低 |
| CPython | 第2节 | Python 的 C 语言官方实现 | 低 |
by 澜湾
by LanWan