Python 教程

第 1 章:让程序说话

本章内容限 Python 3.12+ 用户

0. 认识 print 函数

本节目标:初步了解 print 函数
预计用时:5~10 分钟(含实操)
完成标志:能初步理解 print 函数的作用,并成功完成一次 print

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 的输出

本节目标:学习 print 函数的参数并尝试运用
预计用时:25~35 分钟(含实操)
完成标志:了解 print 的特性以及传入参数,并学会运用

很多教程会直接把一堆参数丢上来,让你去背,但是我们今天换一个方法——去 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 类型的基类,相当于是所有类型的父亲,可以画出这么一张图:

graph TD object --> int int --> bool object --> str object --> float object --> None object --> list object --> 其它

注意: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快递要不要加急送

核心收获

  1. 学会看源码:不再死记硬背参数,而是通过 Ctrl/Cmd + 左键点击 跳转到类型注解,获取最权威的一手信息。
  2. 理解设计模式:注意到 str | None = "默认值" 这种常见的 Python 参数设计范式,为后续学习其他标准库打下基础。
  3. 区分语法与参数:明确了逗号 , 只是语法,真正决定输出格式的是 sep 和 end。

避坑提醒

  • flush=True 不等于打印更快,它只是防止程序异常退出时丢失缓冲区内容。
  • file 参数接收的是文件对象而非文件名字符串,必须配合 open() 使用。
  • *values 的类型是 object,意味着你可以放心传入任何数据类型,无需提前转换。

掌握了 print 的参数控制后,我们已经能精准管理“怎么输出”。但参数只是调用的表象,真正决定输出内容的是 Python 对传入对象的底层转换机制——下一节我们将深入 print 对 object 的处理链路,带你理解 str()、__str__ 与 __repr__ 的调用优先级及触发时机,从而不仅学会“打印什么”,更明白“为什么打印出来是这样”,为后续调试自定义对象显示、日志序列化等问题建立正确的底层心智模型。

2. 了解 print 底层:Python 对字符串做了什么?(进阶)

本节目标:学习 print 函数的底层原理
预计用时: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 节的三步:

  1. PyObject_Str → 对象转字符串
  2. 拼接 sep 和 end
  3. file.write() → 输出

2.8 操作系统底层:两套实现

os.write 往下走,不同操作系统走不同的路:

Linux 调用路径

graph LR A[os.write] --> B[libc write] B --> C[内核 sys_write] C --> D[终端驱动] D --> E[屏幕]

可以用 strace 亲眼看到系统调用:

strace -e write python3 -c "print('hello')"
# 输出:write(1, "hello\n", 6) = 6

Windows 调用路径

graph LR A[os.write] --> B[_write C运行时] B --> C[WriteFile / WriteConsoleW] C --> D[控制台驱动] D --> E[屏幕]

2.9 流程总结

graph TD A["print('你好')"] --> B["① str() 转换 + sep/end 拼接"] B --> C["② sys.stdout.write()"] C --> D["编码:字符 → 字节"] D --> E["③ 缓冲区(攒批)"] E --> F{"刷新条件?"} F -->|遇到 换行| G["④ os.write() 系统调用"] F -->|缓冲区满| G F -->|flush=True| G G --> H{操作系统} H -->|Linux| I["sys_write → 终端"] H -->|Windows| J["WriteFile → 控制台"] I --> K["⑤ 屏幕显示"] J --> K

2.10 本节小结

至此,我们完成了一次从 Python 语法糖到操作系统内核的完整下潜。print 不再是一个黑盒函数,而是一条清晰可见的数据流水线:

  1. 对象层:通过 str() / __str__ / __repr__ 将任意对象翻译为人类可读的字符串表示;
  2. 流层:经由 TextIOWrapper 编码、BufferedWriter 攒批,最终由 FileIO 交付给操作系统;
  3. 系统层:通过 os.write 触发内核系统调用(Linux sys_write / Windows WriteFile),将字节送达终端或文件。

那么为什么要理解这条链路?不是为了平时写代码时手动 os.write(),而是在遇到问题是能第一时间判断问题所在。

难度提示回顾:本节涉及的 CPython 源码与系统调用属于进阶内容。如果你目前只需解决日常打印问题,掌握 2.1–2.4 已足够;2.5–2.8 可作为知识储备,在遇到深层问题时随时回来查阅。

3. 本章小结

至此,我们走完了 print 从“一句代码”到“系统调用”的完整路径。这一路的跨度确实不小:

graph LR A["第0节
认识 print"] --> B["第1节
参数控制"] B --> C["第2节
底层原理(进阶)"]

本章我们做的事情,本质上是一条 "从使用到底层" 的下潜路径:

节视角核心问题新手掌握程度
第0节使用者print 能干什么?怎么开始用?必须熟练
第1节调用者print 的参数怎么控制输出格式?必须熟练
第2节探索者print 内部到底发生了什么?了解即可,遇到bug再回查

如果要用一张图概括本章全部内容,可以这样梳理:

graph TD A["你写的代码
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__ 部分

底层知识是你的“调试工具箱”,不必提前背熟,但要知道工具在哪里。

一张图记住位置

graph TD object --> int int --> bool object --> str object --> float object --> None object --> list object --> 其它

这张图只需记住一件事:万物皆 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