Skip to content

第 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 个代价:

  1. 跨平台困难:x86 的二进制不能在 ARM 上跑,每个平台都要单独编译
  2. 修改即重编:你改一行 print() 就要等几十秒编译
  3. 失去动态性:编译时类型必须确定,不能在运行时随意 setattrgetattr、动态加 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 件事:

Python 代码执行流程

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.pyc

1.3 Python 不是一个解释器,而是一份语言规范

很多人以为「Python = python 命令」,其实「Python」是一份语言规范(PEP, Python Enhancement Proposal),规范定义了:

  • 语法长什么样(iffordef
  • 内置类型有哪些(intlistdict
  • 数据模型怎么工作(__init____iter__

按这份规范实现解释器,就有了5 大主流实现

Python 实现家谱

1.3.1 选型决策表

实现用什么写的适合场景不适合
CPythonC 语言99% 场景,所有第三方库都假设你用它极致性能
PyPyRPython长时间运行的 CPU 密集脚本(数学计算、模拟)用大量 C 扩展(如老版本 numpy)
JythonJava嵌入 Java 应用(调用 JVM 库)已停滞,仅支持 Python 2.7
IronPythonC#Windows 桌面、Excel 脚本跨平台、社区小
MicroPythonC(精简)单片机(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 大应用领域

Python 应用领域

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

为什么慢还火?三个核心原因:

  1. 大部分程序的瓶颈不是 CPU:Web 后端、爬虫、运维脚本,瓶颈是网络/磁盘 IO,CPU 有 100 倍冗余。
  2. 开发速度抵消运行速度:写一个爬虫,Python 30 行,Java 200 行。机器多花 1 秒,程序员少花 1 小时。
  3. 真要算得快,调底层库:numpy / pandas / PyTorch 底层是 C++/CUDA,Python 只是「调度员」,跑不慢。

💡 经典比喻:Python 是胶水。每种胶水都不是性能最强的材料,但能把各种高性能材料粘起来用——这就是 Python 在 AI 领域的位置。

1.4.2 不同领域的核心库

领域必学库杀手锏项目
Web 后端Django / Flask / FastAPIInstagram、豆瓣早期
爬虫requests / Scrapy / Playwright搜索引擎数据采集
AI / MLPyTorch / TensorFlow / transformersChatGPT 训练栈
数据分析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+D

REPL = 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 个演示:

  1. hello_world() —— 最朴素的 Hello World
  2. disassemble_demo() —— 用 dis 模块反汇编看字节码
  3. show_python_info() —— 打印当前解释器信息
  4. show_pyc_cache() —— 解释 __pycache__ 缓存机制
  5. benchmark_print() —— 性能小实验:循环 print vs join 后单次 print
  6. the_zen_of_python() —— 打印 Python 之禅

运行:

bash
$ cd 01_intro/code
$ python hello_python.py

1.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 下一步


1.8 面试高频题

Q1. Python 是解释型还是编译型语言?⭐⭐

考察点:对 Python 执行模型的理解。这道题答错的人,对 .pyc 文件、字节码、PVM 都会一无所知。

标准答案

Python 是混合型语言,准确地说是「先编译后解释」:

  1. 编译阶段:源码 .py 先被编译成字节码(不是机器码),缓存到 __pycache__/*.pyc
  2. 解释阶段:Python 虚拟机(PVM)逐条解释执行字节码

所以 Python 不是纯解释型(纯解释型每次都重新解析源码),也不是纯编译型(纯编译型生成的是机器码,不需要虚拟机)。

加分项

  • 提到 Java 也是同样的混合模式(源码 → .class → JVM)
  • dis.dis() 演示一个函数的字节码
  • 提到 python -O 可以生成 .pyo(优化字节码,去掉 assert)

易错点

  • 别说「Python 是解释型」(不严谨)
  • 别说「Python 没有编译过程」(错的)

Q2. CPython / Jython / PyPy 有什么区别?⭐⭐⭐

考察点:是否真的理解「Python 是规范不是实现」。

标准答案

「Python」是一份语言规范,下面这些都是符合规范的不同实现:

实现用什么写核心特点
CPythonC 语言官方默认、事实标准、生态最完整、有 GIL
PyPyRPython带 JIT 即时编译,长时间运行的 CPU 密集任务快 4-10 倍
JythonJava编译为 JVM 字节码,无 GIL,可调用 Java 库(停滞在 2.7)
IronPythonC#运行在 .NET CLR 上,访问 .NET 类库
MicroPythonC(精简)仅需 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 步说):

  1. 词法分析(Lexer):源码字符流 → Token 列表

    NAME 'print', OP '(', STRING "'hi'", OP ')', NEWLINE
  2. 语法分析(Parser):Token 列表 → AST 抽象语法树

    Expr(Call(func=Name('print'), args=[Constant('hi')]))
  3. 编译(Compiler):AST → 字节码

    LOAD_NAME      print
    LOAD_CONST     'hi'
    CALL           1
    POP_TOP
  4. PVM 执行:栈式虚拟机循环执行每条字节码

    • LOAD_NAME print → 在命名空间查找 print,压栈
    • LOAD_CONST 'hi' → 把常量压栈
    • CALL 1 → 弹出 1 个参数和函数对象,调用 print('hi')
    • POP_TOP → 弹出返回值(None
  5. 系统调用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 2Python 3
print语句 print "hi"函数 print("hi")
字符串str=字节, unicode=文本str=Unicode 文本, bytes=字节
整除5/2 == 2(整数相除截断)5/2 == 2.5(真除法),5//2==2(整除)
rangerange() 返回 list(占内存)range() 返回惰性迭代器
inputraw_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 的设计逻辑是:「程序员本来就会用缩进对齐代码块表达层次(这是写代码的好习惯),既然反正都要做,为什么不让它直接成为语法?」

好处

  1. 强制统一风格:团队协作不会出现「我用花括号他用缩进」的争论
  2. 代码更简洁:少打一对大括号
  3. 可读性最好:视觉上的层次 = 逻辑上的层次

代价

  • 复制粘贴代码时容易缩进对不齐
  • tab 和空格混用会报错
  • 不能写一行 if x: pass 之类的紧凑写法(不过其实可以,但不推荐)

加分项

  • 提到 PEP 8 推荐 4 空格
  • 提到 python -tt 严格检查 tab/空格混用
  • 提到 Guido 在 ABC 语言(Python 灵感来源)就用缩进语法

易错点

  • 别说「缩进是缺点」(这是 Python 的核心特性)
  • 别用 tab,永远用 4 空格

📚 延伸阅读

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

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()

hello_python.py ↗