主题
第 1 章 Python 是什么 & 为什么火
学习目标:能用一句话向产品经理解释「Python 是什么、为什么这么多人用」;能向面试官完整解释「一行 print() 背后发生了什么」;能用
python命令完成第一次 Hello World,并能用dis模块「反汇编」一段代码看字节码。
1.1 先搞懂「编程语言到底是什么」
1.1.1 一个生活类比:去星巴克点单
想象你走进星巴克点了一杯「冰美式 大杯 不加糖」,店员的处理方式有三种:
方式 A(提前批量做好) 方式 B(每次现场做) 方式 C(提前做半成品 + 现场组装)
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ 早上 6 点先把 │ │ 你说一句, │ │ 早上把咖啡液萃好 │
│ 100 杯做好放冰柜 │ │ 店员现场磨豆 → 萃取 │ │ 你点单时倒进杯子加冰 │
│ 你来直接拿走 │ │ → 加冰 → 递给你 │ │ → 递给你 │
│ │ │ │ │ │
│ ⚡ 取餐快 │ │ 🐢 取餐慢 │ │ 🚀 比 B 快 │
│ 🐢 不灵活 │ │ ⚡ 完全按你口味 │ │ 🌍 半成品可复用 │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
编译型语言 解释型语言 混合型语言(Python)
(C / Rust / Go) (Bash / 早期 JS) (Python / Java)- 编译型:把源码整体翻译成机器码保存为可执行文件,CPU 直接跑。代价是修改一行就要重编。
- 解释型:解释器逐行读源码,边读边执行。代价是每次运行都要重新解析。
- 混合型:源码先整体编译为字节码(不是机器码),运行时由虚拟机逐条解释执行字节码。兼顾启动速度和跨平台。
Python 属于混合型——这是理解 Python 一切性能特征的起点。
1.1.2 为什么不是直接编译成机器码?
如果直接编译成机器码(像 C 那样),Python 程序会更快,但要付出 3 个代价:
- 跨平台困难:x86 的二进制不能在 ARM 上跑,每个平台都要单独编译
- 修改即重编:你改一行 print() 就要等几十秒编译
- 失去动态性:编译时类型必须确定,不能在运行时随意
setattr、getattr、动态加 method
而字节码方案完美解决:字节码是「平台无关的中间语言」,写一次到处跑(这就是 Java 当年喊的口号「Write Once, Run Anywhere」,Python 也一样)。
💡 类比:字节码就像「乐高说明书」。说明书本身不是积木,但任何会拼乐高的人(虚拟机)拿到说明书都能拼出同样的成品。
1.2 一行 print() 背后:Python 代码的 5 步执行
python
# hello.py
x = 1 + 2
print(x)你敲下 python hello.py 回车后,CPython 解释器在毫秒之内做了这 5 件事:
1.2.1 五个阶段详解
| # | 阶段 | 输入 | 输出 | 工具 |
|---|---|---|---|---|
| 1 | 源码读取 | hello.py 文本 | UTF-8 字符流 | 文件系统 |
| 2 | 词法分析 (Lexer) | 字符流 | Token 列表 | tokenize 模块可看 |
| 3 | 语法分析 (Parser) | Token 列表 | AST 抽象语法树 | ast 模块可看 |
| 4 | 编译 (Compiler) | AST | 字节码 (code object) | compile() 内置函数 |
| 5 | 执行 (PVM) | 字节码 | 副作用(如打印) | Python 虚拟机循环 |
1.2.2 亲手验证:用 dis 模块看字节码
打开终端,复制这段代码到 REPL:
python
>>> import dis
>>> def add(a, b):
... return a + b
...
>>> dis.dis(add)
1 0 RESUME 0
2 2 LOAD_FAST 0 (a)
4 LOAD_FAST 1 (b)
6 BINARY_OP 0 (+)
10 RETURN_VALUE读懂这段输出:
2 2 LOAD_FAST 0 (a)
↑ ↑ ↑ ↑ ↑
源码行号 字节码偏移 指令名 参数 注释| 指令 | 含义 |
|---|---|
RESUME | 协程/生成器恢复点(3.11 引入,普通函数也带) |
LOAD_FAST 0 (a) | 把局部变量槽位 0(变量 a)压入操作数栈 |
LOAD_FAST 1 (b) | 把局部变量槽位 1(变量 b)压入操作数栈 |
BINARY_OP 0 (+) | 弹出栈顶两个值相加,结果压回栈 |
RETURN_VALUE | 弹出栈顶作为函数返回值 |
🎮 想看动画? 打开本章配套的
demo.html,「演示 2: PVM 栈式执行」可以一步步点击执行字节码,观察栈的变化。
1.2.3 字节码缓存:为什么第二次启动更快
第一次运行 python hello.py,Python 会:
bash
$ python hello.py
3
$ ls __pycache__/
hello.cpython-312.pyc # ← 自动生成!下次跳过编译hello.cpython-312.pyc 就是步骤 4 的产物。下次运行时,Python 比对源码 mtime,如果没改就直接读 .pyc 跳到步骤 5。
面试小坑:脚本入口(你 python 命令直接运行的那个文件)不会生成 .pyc,只有被 import 的模块才会。所以下面的 __pycache__ 只对 utils.py 生效:
bash
$ ls
main.py utils.py
$ python main.py # main 里 import utils
$ ls __pycache__/
utils.cpython-312.pyc # ← 只有 utils.pyc,没有 main.pyc1.3 Python 不是一个解释器,而是一份语言规范
很多人以为「Python = python 命令」,其实「Python」是一份语言规范(PEP, Python Enhancement Proposal),规范定义了:
- 语法长什么样(
if、for、def) - 内置类型有哪些(
int、list、dict) - 数据模型怎么工作(
__init__、__iter__)
按这份规范实现解释器,就有了5 大主流实现:
1.3.1 选型决策表
| 实现 | 用什么写的 | 适合场景 | 不适合 |
|---|---|---|---|
| CPython ⭐ | C 语言 | 99% 场景,所有第三方库都假设你用它 | 极致性能 |
| PyPy | RPython | 长时间运行的 CPU 密集脚本(数学计算、模拟) | 用大量 C 扩展(如老版本 numpy) |
| Jython | Java | 嵌入 Java 应用(调用 JVM 库) | 已停滞,仅支持 Python 2.7 |
| IronPython | C# | Windows 桌面、Excel 脚本 | 跨平台、社区小 |
| MicroPython | C(精简) | 单片机(ESP32 / Pyboard)、IoT | 完整数据处理 |
💡 本教程除非特别说明,全部以 CPython 3.10+ 为准。如果你装的是
python.org下载的、或者 Mac 自带的python3、或者 Anaconda 装的,都是 CPython。
1.3.2 怎么查我用的是哪个?
python
>>> import sys
>>> sys.implementation.name
'cpython'
>>> sys.version
'3.12.0 (main, ...) [GCC 11.4.0]'1.4 Python 为什么这么火?6 大应用领域
1.4.1 一个反直觉的事实:Python 很慢,但用的人最多
按 TIOBE 编程语言排行榜,Python 已经常年占据 Top 1。但同样的「100 万次空循环」,Python 比 C 慢 80~100 倍:
C (gcc -O2): ▎ 2 ms
Java (JIT): ▍ 5 ms
PyPy: █ 20 ms
Node.js (V8): █▌ 35 ms
CPython 3.12: █████████ 180 ms为什么慢还火?三个核心原因:
- 大部分程序的瓶颈不是 CPU:Web 后端、爬虫、运维脚本,瓶颈是网络/磁盘 IO,CPU 有 100 倍冗余。
- 开发速度抵消运行速度:写一个爬虫,Python 30 行,Java 200 行。机器多花 1 秒,程序员少花 1 小时。
- 真要算得快,调底层库:numpy / pandas / PyTorch 底层是 C++/CUDA,Python 只是「调度员」,跑不慢。
💡 经典比喻:Python 是胶水。每种胶水都不是性能最强的材料,但能把各种高性能材料粘起来用——这就是 Python 在 AI 领域的位置。
1.4.2 不同领域的核心库
| 领域 | 必学库 | 杀手锏项目 |
|---|---|---|
| Web 后端 | Django / Flask / FastAPI | Instagram、豆瓣早期 |
| 爬虫 | requests / Scrapy / Playwright | 搜索引擎数据采集 |
| AI / ML | PyTorch / TensorFlow / transformers | ChatGPT 训练栈 |
| 数据分析 | pandas / numpy / matplotlib | 量化投资、学术研究 |
| 运维 | Ansible / Fabric / paramiko | 替代 Shell 复杂脚本 |
| 教育 | Jupyter / IDLE | 全球少儿编程首选 |
1.5 第一次运行 Python:从 Hello World 到反汇编
1.5.1 三种运行方式
方式 1:交互式 REPL(最适合学习)
bash
$ python
Python 3.12.0 (main, ...) on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 1 + 2
3
>>> name = "Python"
>>> f"Hello, {name}!"
'Hello, Python!'
>>> exit() # 或按 Ctrl+DREPL = Read-Eval-Print-Loop(读取-求值-打印-循环),俗称「交互式解释器」。
🎮 没装 Python? 打开
demo.html→ 「演示 5:REPL 模拟器」,浏览器里就能玩简化版 REPL。
方式 2:脚本模式(生产代码)
bash
$ echo 'print("Hello, Python!")' > hello.py
$ python hello.py
Hello, Python!方式 3:模块模式(标准库工具)
bash
$ python -m http.server 8000 # 当前目录起个静态文件服务器
$ python -m json.tool data.json # 格式化 JSON 文件
$ python -m dis hello.py # 反汇编一个文件!-m 后面跟模块名,相当于把模块当成脚本运行。Python 标准库自带很多神器,不需要装第三方包。
1.5.2 完整实战:本章代码
完整可运行代码见 code/hello_python.py,包含 6 个演示:
hello_world()—— 最朴素的 Hello Worlddisassemble_demo()—— 用dis模块反汇编看字节码show_python_info()—— 打印当前解释器信息show_pyc_cache()—— 解释__pycache__缓存机制benchmark_print()—— 性能小实验:循环 print vs join 后单次 printthe_zen_of_python()—— 打印 Python 之禅
运行:
bash
$ cd 01_intro/code
$ python hello_python.py1.5.3 Python 之禅(Zen of Python)
每个 Python 程序员都该背下来的设计哲学:
python
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly. # 美比丑好
Explicit is better than implicit. # 显式比隐式好
Simple is better than complex. # 简单比复杂好
Complex is better than complicated. # 复杂比繁杂好
Flat is better than nested. # 扁平比嵌套好
Sparse is better than dense. # 稀疏比稠密好
Readability counts. # 可读性很重要
...
There should be one-- and preferably only one --obvious way to do it.重点:「显式优于隐式」「一种最显而易见的做法」是 Python 哲学的核心,影响了几乎所有语言设计决策(比如缩进强制、显式 self、显式 import)。
1.6 缩进哲学:为什么 Python 用空格?
python
# C 风格:用大括号分块
if (x > 0) {
print("positive");
} else {
print("non-positive");
}
# Python 风格:用缩进分块
if x > 0:
print("positive")
else:
print("non-positive")Guido van Rossum(Python 之父)的逻辑很简单:
「程序员本来就会用缩进对齐代码块——这是『风格惯例』。既然反正都要做,为什么不让它直接成为『语法规则』?这样就少打一对大括号、且团队风格永远一致。」
强制约定:
- 缩进必须统一:要么全用 4 空格,要么全用 tab,不能混用
- PEP 8 推荐 4 空格(Python 标准库就是这么用的)
- 编辑器一般都设置「按 Tab 自动转 4 空格」
踩坑警告:从其他人代码复制粘贴时,可能 tab 和空格混在一起肉眼看不出来。Python 3 会直接报错:
TabError: inconsistent use of tabs and spaces in indentation解决:用编辑器「显示空白字符」功能,或用 python -tt 强制检查。
1.7 本章小结
1.7.1 知识点速记
✓ Python 是「混合型」语言:源码 → 字节码 → PVM 解释执行
✓ 一行代码 5 步:源码 → Tokens → AST → 字节码 → PVM 执行
✓ Python 是规范,CPython 是默认实现,还有 PyPy / Jython 等
✓ 用 dis 模块可以「反汇编」看字节码
✓ __pycache__ 缓存字节码,加速二次启动
✓ Python 慢但火:胶水语言、IO 瓶颈、开发效率高
✓ 缩进是语法的一部分,PEP 8 推荐 4 空格1.7.2 下一步
- ✅ 跑一遍
code/hello_python.py - ✅ 打开
demo.html体验 5 个交互演示 - ✅ 默写本章末尾的 6 道面试题
- ➡️ 进入 第 2 章:基础语法与数据类型
1.8 面试高频题
Q1. Python 是解释型还是编译型语言?⭐⭐
考察点:对 Python 执行模型的理解。这道题答错的人,对 .pyc 文件、字节码、PVM 都会一无所知。
标准答案:
Python 是混合型语言,准确地说是「先编译后解释」:
- 编译阶段:源码
.py先被编译成字节码(不是机器码),缓存到__pycache__/*.pyc - 解释阶段:Python 虚拟机(PVM)逐条解释执行字节码
所以 Python 不是纯解释型(纯解释型每次都重新解析源码),也不是纯编译型(纯编译型生成的是机器码,不需要虚拟机)。
加分项:
- 提到 Java 也是同样的混合模式(源码 →
.class→ JVM) - 用
dis.dis()演示一个函数的字节码 - 提到
python -O可以生成.pyo(优化字节码,去掉 assert)
易错点:
- 别说「Python 是解释型」(不严谨)
- 别说「Python 没有编译过程」(错的)
Q2. CPython / Jython / PyPy 有什么区别?⭐⭐⭐
考察点:是否真的理解「Python 是规范不是实现」。
标准答案:
「Python」是一份语言规范,下面这些都是符合规范的不同实现:
| 实现 | 用什么写 | 核心特点 |
|---|---|---|
| CPython | C 语言 | 官方默认、事实标准、生态最完整、有 GIL |
| PyPy | RPython | 带 JIT 即时编译,长时间运行的 CPU 密集任务快 4-10 倍 |
| Jython | Java | 编译为 JVM 字节码,无 GIL,可调用 Java 库(停滞在 2.7) |
| IronPython | C# | 运行在 .NET CLR 上,访问 .NET 类库 |
| MicroPython | C(精简) | 仅需 256KB 内存,用于单片机 / IoT |
加分项:
- 解释 RPython 不是 Python:是 Python 的一个静态子集,最终编译为 C 再编译为本地代码
- 提到 PyPy 的 JIT 是「meta-tracing JIT」(追踪频繁执行的循环编译为机器码)
- 提到 Python 3.13 的 No-GIL 实验(PEP 703)
易错点:
- 别把 PyPy 说成「Python 写的 Python」(不严谨)
- 别把 Cython 和 CPython 混淆(Cython 是「Python 转 C 的编译器」,不是 Python 实现)
Q3. .pyc 文件是什么?什么时候生成?⭐⭐
考察点:对模块加载机制和性能优化的理解。
标准答案:
.pyc 是 Python 编译后的字节码缓存文件,存在 __pycache__/ 目录下。
何时生成:
- 模块被
import时,如果对应的.pyc不存在或已过期(源文件 mtime 比 .pyc 新),重新编译生成 - 直接
python xxx.py跑的入口文件不会生成 .pyc(只缓存被 import 的模块)
何时使用:
- 下次 import 时,比对源码 mtime
- 没改:直接读 .pyc 跳过编译,加快启动
- 改了:重新编译并覆盖 .pyc
文件名格式:<模块名>.cpython-<版本>.pyc,比如 utils.cpython-312.pyc。版本号区分不同 Python 版本,避免冲突。
加分项:
- 提到
.pyc不是机器码,仍然需要 PVM 解释,所以加速有限(主要省词法+语法+编译三步,不省执行) - 提到
python -O生成的.pyo(去掉 assert 和__debug__块的优化版本) - 提到
compileall模块可以批量预编译整个目录
易错点:
- 别以为
.pyc是机器码(不是!还是字节码) - 别以为删了
.pyc程序就会变慢(只影响首次启动,毫秒级)
Q4. 一行 print('hi') 背后发生了什么?⭐⭐⭐⭐
考察点:对解释器整个执行流程的系统理解。
标准答案(按 5 步说):
词法分析(Lexer):源码字符流 → Token 列表
NAME 'print', OP '(', STRING "'hi'", OP ')', NEWLINE语法分析(Parser):Token 列表 → AST 抽象语法树
Expr(Call(func=Name('print'), args=[Constant('hi')]))编译(Compiler):AST → 字节码
LOAD_NAME print LOAD_CONST 'hi' CALL 1 POP_TOPPVM 执行:栈式虚拟机循环执行每条字节码
LOAD_NAME print→ 在命名空间查找print,压栈LOAD_CONST 'hi'→ 把常量压栈CALL 1→ 弹出 1 个参数和函数对象,调用print('hi')POP_TOP→ 弹出返回值(None)
系统调用:
print内部调用sys.stdout.write(),底层是write()系统调用,把字节写入文件描述符 1(标准输出),最终显示在终端。
加分项:
- 提到第 2-3 步可用
ast模块查看 - 提到第 4 步可用
dis模块查看 - 提到 print 是个
builtin_function_or_method,有 C 实现 - 提到 PVM 是「ceval.c 里的一个巨大 switch 循环」(CPython 源码细节)
易错点:
- 别跳过编译阶段直接说「解释执行」
- 别忘了系统调用(show off 加分)
Q5. Python 2 和 Python 3 最关键的区别?⭐
考察点:对 Python 历史和向后不兼容的认知。
标准答案(5 大核心区别):
| 特性 | Python 2 | Python 3 |
|---|---|---|
print | 语句 print "hi" | 函数 print("hi") |
| 字符串 | str=字节, unicode=文本 | str=Unicode 文本, bytes=字节 |
| 整除 | 5/2 == 2(整数相除截断) | 5/2 == 2.5(真除法),5//2==2(整除) |
| range | range() 返回 list(占内存) | range() 返回惰性迭代器 |
| input | raw_input 返字符串,input 会 eval | 只剩 input,返字符串 |
加分项:
- 提到 Python 2 已于 2020 年 1 月 1 日 EOL(End of Life),停止维护
- 提到
from __future__ import print_function让 Python 2 提前用 3 的语法 - 提到
2to3工具可以自动转换部分代码
易错点:
- 别在 2026 年还说「我用 Python 2」(已经死了)
- 别忘了字符串变化是 Python 3 最痛的迁移点
Q6. 为什么 Python 用缩进做语法?⭐
考察点:对设计哲学的理解(送分题,但答好能体现品味)。
标准答案:
Guido 的设计逻辑是:「程序员本来就会用缩进对齐代码块表达层次(这是写代码的好习惯),既然反正都要做,为什么不让它直接成为语法?」
好处:
- 强制统一风格:团队协作不会出现「我用花括号他用缩进」的争论
- 代码更简洁:少打一对大括号
- 可读性最好:视觉上的层次 = 逻辑上的层次
代价:
- 复制粘贴代码时容易缩进对不齐
- tab 和空格混用会报错
- 不能写一行
if x: pass之类的紧凑写法(不过其实可以,但不推荐)
加分项:
- 提到 PEP 8 推荐 4 空格
- 提到
python -tt严格检查 tab/空格混用 - 提到 Guido 在 ABC 语言(Python 灵感来源)就用缩进语法
易错点:
- 别说「缩进是缺点」(这是 Python 的核心特性)
- 别用 tab,永远用 4 空格
📚 延伸阅读
- PEP 8 - Style Guide for Python Code
- PEP 20 - The Zen of Python
- CPython 源码导读
- 《CPython Internals》— Anthony Shaw(深入 CPython 源码)
- Python 官方教程
🎬 可视化演示
演示加载缓慢或样式异常?点此在新标签页打开 ↗
💻 示例代码
python
"""第 1 章实战代码:从 Hello World 到字节码反汇编
运行方式:
python hello_python.py
依赖:仅需 Python 3.10+ 标准库,无第三方包。
"""
import dis
import sys
import time
def hello_world() -> None:
"""最朴素的 Hello World:感受 Python 的简洁。"""
print("Hello, Python!")
def disassemble_demo() -> None:
"""演示用 dis 模块查看 Python 字节码。"""
print("\n" + "=" * 60)
print("演示 1:dis 模块反汇编一段代码")
print("=" * 60)
def add(a: int, b: int) -> int:
return a + b
print("\n源代码:")
print(" def add(a, b):")
print(" return a + b")
print("\ndis.dis(add) 的输出:")
print("-" * 60)
dis.dis(add)
print("-" * 60)
print("\n字节码指令解读:")
print(" LOAD_FAST a 把局部变量 a 推入操作数栈")
print(" LOAD_FAST b 把局部变量 b 推入操作数栈")
print(" BINARY_OP + 弹出栈顶两个值相加,结果推回栈")
print(" RETURN_VALUE 弹出栈顶作为返回值")
def show_python_info() -> None:
"""打印当前 Python 解释器的关键信息。"""
print("\n" + "=" * 60)
print("演示 2:当前 Python 环境信息")
print("=" * 60)
print(f" Python 版本 : {sys.version.split()[0]}")
print(f" 实现名称 : {sys.implementation.name} (CPython / PyPy / Jython 等)")
print(f" 字节码版本 : magic = {sys.implementation.cache_tag}")
print(f" 可执行文件 : {sys.executable}")
print(f" 操作系统平台 : {sys.platform}")
print(f" 最大递归深度 : {sys.getrecursionlimit()}")
def show_pyc_cache() -> None:
"""演示首次运行后会生成 __pycache__ 目录。"""
print("\n" + "=" * 60)
print("演示 3:__pycache__ 字节码缓存")
print("=" * 60)
print("""
第一次运行模块时,Python 会把编译好的字节码缓存到:
__pycache__/<模块名>.cpython-<版本>.pyc
下次运行时,如果源文件没改(比对 mtime),直接读 .pyc 跳过编译。
这就是 Python 启动比第一次"略快一点"的秘密。
试试看:
$ python -c "import dis; print('hi')"
$ ls __pycache__/
""")
def benchmark_print(n: int = 100_000) -> None:
"""对比 print 多次调用 vs 一次构造大字符串的性能差异。
这是一个从一开始就培养"性能感觉"的小练习。
"""
print("\n" + "=" * 60)
print(f"演示 4:性能小实验(n={n:,} 次输出)")
print("=" * 60)
# 写入临时缓冲,避免刷屏
import io
buf = io.StringIO()
t0 = time.perf_counter()
for i in range(n):
print(i, file=buf, end="")
t1 = time.perf_counter()
print(f" 方式 A: 循环 {n:,} 次 print 耗时 {(t1 - t0) * 1000:.2f} ms")
buf2 = io.StringIO()
t0 = time.perf_counter()
chunk = "".join(str(i) for i in range(n))
print(chunk, file=buf2, end="")
t1 = time.perf_counter()
print(f" 方式 B: join 后 1 次 print 耗时 {(t1 - t0) * 1000:.2f} ms")
print("\n 结论:Python 函数调用本身有开销,能批量做就别循环做。")
def the_zen_of_python() -> None:
"""打印 Python 之禅 —— 这是 Python 的设计哲学。"""
print("\n" + "=" * 60)
print("演示 5:Python 之禅 (彩蛋)")
print("=" * 60)
import this # noqa: F401 导入即触发打印
def main() -> None:
print("=" * 60)
print(" Python 第 1 章 · Hello World & 字节码探秘")
print("=" * 60)
hello_world()
disassemble_demo()
show_python_info()
show_pyc_cache()
benchmark_print()
the_zen_of_python()
print("\n" + "=" * 60)
print(" 跑完了!下一步建议:")
print(" 1) 用 python -m dis hello_python.py 反汇编整个文件")
print(" 2) 修改 add 函数为乘法,再看字节码变化")
print(" 3) 打开同目录下的 demo.html,体验可视化执行流程")
print("=" * 60)
if __name__ == "__main__":
main()