跳至内容

Python 3 基础速查

面向复习与日常开发的 Python 3 基础速查,涵盖核心语法、对象与类型、容器、函数、异常、模块和工程常识。

  • Python 3
  • 基础语法
  • 工程实践
  • Python
  • 字符串
  • 集合

版本:示例使用 Python 3.13,按对应版本官方文档复核;实际项目以自身声明的 Python 版本为准。

1. Python、CPython 与交互环境

名称定位
Python语言及其规范
CPython最常用、由 Python 项目维护的实现
PyPy包含 JIT 的另一种实现,第三方扩展兼容性需要单独核对
IPython、Jupyter交互环境,通常运行在 CPython 之上

源码不一定被直接逐行解释执行。 以默认 CPython 为例,源码先编译为字节码,再由虚拟机执行,并可能缓存 .pyc。

具体执行方式取决于实现。Python 通常以一定运行时开销换取表达力、生态和开发效率,性能是否够用应以实测为准。

2. 安装、运行与环境隔离

尚未安装时,从 python.org 下载页 获取 Python。

  • 新项目:可以选择当前稳定版。
  • 已有项目:以项目声明的 Python 版本和依赖兼容范围为准。

安装完成后,先确认终端实际调用的是哪个版本。

2.1 确认解释器

macOS 或 Linux 通常使用 python3,Windows 可使用 py 启动器:

代码块 Shell
1
2
3
# macOS / Linux
python3 --version
# 输出格式:Python 3.13.11
代码块 Powershell
1
2
3
# Windows PowerShell
py --version
# 输出格式:Python 3.13.11

直接运行解释器会进入 REPL,适合验证表达式;把代码保存为 hello.py 后则可以运行脚本:

代码块 Python
1
2
3
4
name = "Python"
print(f"Hello, {name}!")

# 输出:Hello, Python!
代码块 Shell
1
2
3
# macOS / Linux
python3 hello.py
# 输出:Hello, Python!
代码块 Powershell
1
2
3
# Windows PowerShell
py hello.py
# 输出:Hello, Python!

2.2 用 venv 快速建立项目环境

已经安装 Python 后,可以先用标准库 venv 完成最小环境隔离实践。

  • 作用:为当前项目建立独立的第三方包目录,避免不同项目互相覆盖依赖。
  • 前提:基于现有 Python 创建环境,不会替你安装另一个 Python 版本。
  • 目录:.venv 只存放环境,不放项目源码,也不提交到 Git。
代码块 Shell
1
2
3
4
5
# macOS / Linux
# 用当前 python3 创建 .venv,再让当前 shell 优先使用其中的 python 和 pip
python3 -m venv .venv
source .venv/bin/activate
# 成功时通常无输出;当前 shell 的 Python 已切换到 .venv。
代码块 Powershell
1
2
3
4
# Windows PowerShell
py -m venv .venv
.venv\Scripts\Activate.ps1
# 成功时通常无输出;当前 PowerShell 的 Python 已切换到 .venv。

激活后统一使用当前环境中的 python。用 python -m pip 比直接使用 pip 更明确,因为它能保证安装目标就是当前解释器:

代码块 Shell
1
2
3
4
5
6
python --version
# 输出格式:Python 3.13.x
python -m pip --version
# 输出格式:pip <版本> from <.venv 路径>/site-packages/pip (python 3.13)
python -m pip install package_name
# 输出:安装日志取决于包、索引和缓存;package_name 需要替换成真实包名。

不要用 sudo pip install ... 修改系统 Python。

项目依赖应记录在 pyproject.toml 或锁文件中,虚拟环境本身可随时重建。

使用完毕后,退出当前 shell 的虚拟环境:

代码块 Shell
1
2
deactivate
# 成功时通常无输出;恢复激活前的解释器搜索路径,不会删除 .venv。

venv 与 Miniconda 怎么选?

  • venv:同样可用于生产项目,不能用“初学者 / 企业级”划分工具。
  • Miniconda:包含 conda、Python 及必要依赖的精简发行版。conda 可管理不同 Python 版本及非 Python 依赖。

选择取决于依赖和部署方式。Miniconda 的安装与环境管理另文介绍,本文先用 venv 跑通基础示例。

3. 语法、注释与命名

3.1 缩进是语法的一部分

Python 用缩进表示代码块。每层使用 4 个空格,不要混用 Tab 与空格。

为什么这样约定?

  • 4 个空格是 PEP 8 风格建议,不是解释器强制的唯一宽度。
  • 解析行首缩进时,Python 将 Tab 展开到下一个 8 列制表位,编辑器却可能按 4 列显示;视觉对齐不代表缩进层级相同。
  • 混用导致缩进含义依赖 Tab 宽度时,会触发 TabError,它属于 IndentationError。

编辑器建议:把 Tab 键设为插入 4 个空格。 按了 Tab 键,不等于文件里存了 Tab 字符。

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
temperature = 28

if temperature >= 30:
    print("炎热")
elif temperature >= 20:
    print("舒适")
else:
    print("偏冷")

# 输出:舒适

3.2 注释不等于文档字符串

  • 注释:# 从当前位置注释到行尾;多行注释需要每行都加 #。
  • 跨行字符串:'''...''' 和 """...""" 都可以使用,前后必须是同一种引号。三引号不是专用的“多行注释”语法。
  • 文档字符串:普通字符串字面量放在模块、类或函数体的第一条语句,可通过 __doc__ 或 help() 读取;惯例用三个双引号。

单独放在其他位置的三引号字符串通常没有可见效果,但内部转义和引号仍受字符串语法约束,不能把任意代码包进去当注释。

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def area(width: float, height: float) -> float:
    """返回矩形面积。"""
    return width * height  # 单位由调用方约定


print(area(3.0, 4.0))
print(area.__doc__)

message = '''第一行
第二行'''
print(message)

# 输出:
# 12.0
# 返回矩形面积。
# 第一行
# 第二行

注释应该解释“为什么”,而不是逐字复述代码。过时注释比没有注释更危险。

3.3 命名规则与惯例

标识符区分大小写,可以包含 Unicode 字母、数字和下划线,但不能以数字开头,也不能使用关键字。关键字应动态查看:

代码块 Python
1
2
3
4
5
6
7
import keyword

# kwlist 是保留关键字列表,不能用作普通变量名
print(keyword.kwlist)
# softkwlist 是软关键字列表:只在特定语法位置有特殊含义
# 例如 match、case 用于模式匹配,type 用于类型别名;其他位置仍可作为名称
print(keyword.softkwlist)

Python 3.13 输出:

代码块 Text
1
2
# ['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']
# ['_', 'case', 'match', 'type']

常用的 PEP 8 命名约定:

  • 变量、函数、模块:snake_case
  • 类:CapWords
  • 常量:UPPER_CASE
  • 内部实现细节:前导下划线,如 _cache

全大写只是“不要重新赋值”的协作约定,不会让对象真正不可变。

4. 名称、对象与类型

4.1 赋值是名称绑定

Python 变量更准确的说法是“名称绑定到对象”,而不是一个固定类型的内存盒子:

代码块 Python
1
2
3
4
5
6
7
8
9
value = 42
print(type(value))

value = "forty-two"
print(type(value))

# 输出:
# <class 'int'>
# <class 'str'>
  • 动态类型:名称可以重新绑定到不同类型的对象。
  • 强类型:通常不会偷偷把无关类型混合运算,例如整数与字符串不能直接相加。
代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
try:
    total = 1 + "2"
except TypeError as exc:
    print(f"TypeError: {exc}")

# 输出:TypeError: unsupported operand type(s) for +: 'int' and 'str'

total = 1 + int("2")
print(total)

# 输出:3

多个名称可以指向同一个可变对象:

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
first = [1, 2]
second = first
second.append(3)

print(first)
print(first is second)

# 输出:
# [1, 2, 3]
# True

需要独立的浅拷贝可使用 first.copy() 或 first[:];嵌套对象是否也要复制,需要根据数据结构决定是否使用 copy.deepcopy()。

4.2 常见内置类型

类别类型是否可变示例
空值NoneType否None
布尔bool否True、False
数值int、float、complex否42、3.14、1 + 2j
文本与字节str、bytes否"你好"、b"data"
序列list、tuple、range仅 list 可变[1, 2]、(1, 2)
映射dict是{"name": "Ada"}
集合set、frozenset仅 set 可变{1, 2}

“对象不可变”表示对象自身的值不能原地改变,不代表指向它的名称不能重新绑定。

complex:复数

  • 组成:a + bj 中,a 是实部,b 是虚部系数,j 表示平方为 -1 的虚数单位。
  • 写法:Python 写 1j;单独的 j 会被当成变量名。
  • 用途:支持加减乘除,常见于科学计算和信号处理;普通业务先认识表示法即可。
代码块 Python
1
2
3
4
5
6
7
8
9
number = complex(1, 2)  # 等价于 1 + 2j
print(number)
print(number.real, number.imag)
print(1j * 1j)

# 输出:
# (1+2j)
# 1.0 2.0
# (-1+0j)

set 与 frozenset:可变集合与不可变集合

二者都保存不重复的可哈希元素,支持交集、并集等运算。

类型能否原地增删自身能否作为字典键或集合元素
set可以不可以,自身不可哈希
frozenset不可以可以,自身可哈希

集合运算可以产生新对象,不等于修改原对象。

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
editable = {1, 2}
editable.add(3)
fixed = frozenset([1, 2, 2])

print(sorted(editable))
print(sorted(fixed))
print(fixed | {3} == {1, 2, 3})

# 输出:
# [1, 2, 3]
# [1, 2]
# True

4.3 真值规则

None、False、数值零以及空字符串和空容器通常为假,其余对象通常为真:

代码块 Python
1
2
3
4
5
6
items: list[str] = []

if not items:
    print("列表为空")

# 输出:列表为空

if value: 同时把 0、空值和空容器视为假。如果业务上必须区分这些情况,就应写出精确条件。

5. 数值与运算符

常用算术运算符包括 +、-、*、/、//、% 和 **。对内置 int、float,/ 执行除法并返回 float, // 则把商向负无穷取整:

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
print(7 / 2)
print(7 // 2)
print(-7 // 2)
print(2 ** 10)

# 输出:
# 3.5
# 3
# -4
# 1024

int() 把浮点数向零截断,不等于数学上的向下取整:

代码块 Python
1
2
3
4
5
6
7
8
import math

print(int(-1.8))
print(math.floor(-1.8))

# 输出:
# -1,int() 向零截断
# -2,math.floor() 向下取整

浮点数比较:math.isclose()

浮点数采用二进制表示,很多十进制小数不能精确存储。 math.isclose(a, b) 判断两数是否在允许误差内接近,返回布尔值。

  • rel_tol:相对误差。以两数绝对值中较大的一个为基准,默认 1e-9。
  • abs_tol:绝对误差。默认 0.0;比较接近零的数时,通常要指定正的 abs_tol。

误差阈值由业务确定。这个函数不会修改数值,也不等于先保留几位小数再比较。

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import math

print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(math.isclose(0.1 + 0.2, 0.3))
# 允许与零的差不超过 0.000000001
print(math.isclose(1e-10, 0.0, abs_tol=1e-9))

# 输出:
# 0.30000000000000004
# False
# True
# True

十进制计算:Decimal

Decimal 可精确表示 0.1 这类有限十进制小数,适合金额等需要明确十进制精度的计算。

  • 从字符串构造,如 Decimal("0.1"),避免带入 float 已有的二进制误差。
  • 运算仍有精度限制,例如不能把除法结果理解成无限精确。
  • 明确舍入规则,金额保留几位、如何取舍都应由业务规定。
代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from decimal import Decimal, ROUND_HALF_UP

print(Decimal("0.1") + Decimal("0.2"))
price = Decimal("19.90")
print(price * 3)
# quantize 按指定小数位舍入;这里明确采用 ROUND_HALF_UP
print(Decimal("19.905").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))

# 输出:
# 0.3
# 59.70
# 19.91

比较运算可以链式书写,逻辑运算使用 and、or、not:

代码块 Python
1
2
3
4
5
age = 24
if 18 <= age < 60:
    print("年龄在目标区间")

# 输出:年龄在目标区间

6. 字符串、字节与格式化输出

6.1 字符串是不可变的 Unicode 文本

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
language = "Python"

print(language[0])
print(language[-1])
print(language[1:4])
print(language.lower())

# 输出:
# P
# n
# yth
# python

切片遵循左闭右开区间 [start:stop:step]。字符串方法返回新对象,不会原地修改原字符串。

6.2 优先使用 f-string

% 格式化和 str.format() 仍然有效,但新代码通常优先使用可读性更好的 f-string:

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
name = "Ada"
score = 95.678
student_id = 7

print(f"{name} 的成绩是 {score:.2f}")
print(f"学号:{student_id:03d}")
print(f"调试值:{score=}")

# 输出:
# Ada 的成绩是 95.68
# 学号:007
# 调试值:score=95.678

转义字符 \n 表示换行,\t 表示水平制表符;制表符显示成多宽由终端或编辑器决定。

代码块 Python
1
2
3
print("第一行\n第二行")
# sep 是参数间的分隔符(默认空格);end 是输出结尾(默认换行)
print("A", "B", "C", sep=" | ", end="\n")

输出:

代码块 Text
1
2
3
# 第一行
# 第二行
# A | B | C

6.3 文本与字节要显式转换

网络、压缩和二进制文件通常处理 bytes;界面和业务文本通常处理 str。二者不能直接拼接:

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
text = "你好,Python"
payload = text.encode("utf-8")
restored = payload.decode("utf-8")

print(payload)
print(restored)

# 输出:
# b'\xe4\xbd\xa0\xe5\xa5\xbd\xef\xbc\x8cPython'
# 你好,Python
  • 编码:str → bytes,使用 .encode()。
  • 解码:bytes → str,使用 .decode(),按对应编码解释字节。

Python 3 的 str 已经是文本,没有 .decode() 方法,不能再次解码。

字节可以来自 .encode(),也可以直接从文件或网络读取,不要求先在当前程序里编码一次。 读写文本文件时应明确指定编码。

7. 输入与安全转换

input() 总是返回字符串。转换可能失败,应处理具体的 ValueError:

代码块 Python
1
2
3
4
5
6
7
8
raw_age = input("请输入年龄:").strip()

try:
    age = int(raw_age)
except ValueError:
    print("年龄必须是整数")
else:
    print(f"明年 {age + 1} 岁")

两种输入的执行结果:

代码块 Text
1
2
3
4
5
# 请输入年龄:20
# 明年 21 岁
#
# 请输入年龄:abc
# 年龄必须是整数

读取密码:getpass.getpass()

getpass.getpass() 是标准库中的密码输入函数,返回 str。

  • 与 input() 的区别:在支持的终端中不回显键入字符,通常连星号也不显示。
  • 职责边界:只负责输入,不负责加密或验证密码;读取后也不要打印密码。
代码块 Python
1
2
3
4
5
6
import getpass

password = getpass.getpass("请输入密码:")

# 支持隐藏输入的终端中,输出:请输入密码:
# 键入字符不回显,按回车后字符串保存到 password。

部分 IDE 控制台不支持隐藏输入,可能出现 GetPassWarning 并回显;遇到这种情况改在终端运行。

选择安全的转换方式

不要把 eval() 当成类型转换工具。 它会执行 Python 表达式,对不可信输入使用可能导致任意代码执行。

  • 已知目标类型:使用 int()、float()、str()。
  • JSON 数据:使用 json.loads()。它按 JSON 语法解析数据,不执行 Python 表达式。
  • 语法区别:JSON 用 true、null,Python 用 True、None。

两者的详细对比留到基础易混淆点文章。

代码块 Python
1
2
3
4
5
6
import json

settings = json.loads('{"theme": "dark", "page_size": 20}')
print(settings["theme"])

# 输出:dark

8. 核心容器

8.1 列表与元组

列表适合可变的有序数据,元组适合结构固定的记录或不可变序列:

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
tasks = ["read", "code"]
tasks.append("review")
tasks.extend(["test", "release"])

point = (10, 20)
x, y = point

print(tasks)
print(x, y)

# 输出:
# ['read', 'code', 'review', 'test', 'release']
# 10 20

元组本身不能增删或替换元素,但元素仍可能指向列表等可变对象,因此元组不保证“深层不可变”,也不一定可哈希。

避免用乘法创建共享的嵌套可变对象:

代码块 Python
1
2
3
4
5
6
7
8
# 错误:两行引用同一个内部列表
# matrix = [[0] * 3] * 2

matrix = [[0] * 3 for _ in range(2)]
matrix[0][0] = 1
print(matrix)

# 输出:[[1, 0, 0], [0, 0, 0]]

8.2 字典

字典保存键值映射,键必须可哈希。字典保留插入顺序,但业务需要排序时,仍应显式使用 sorted()。

为什么列表不能作为键?

  • 定位方式:字典借助哈希值定位键。可哈希对象的哈希值在生命周期中保持不变,相等对象具有相同哈希值。
  • 列表不可哈希:列表内容可变,Python 不给列表提供哈希能力,因此 {[1, 2, 3]: "abc"} 会报错。
  • 可用的键:整数、字符串,以及元素也全部可哈希的元组。
代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
try:
    mapping = {[1, 2, 3]: "abc"}
except TypeError as exc:
    print(f"TypeError: {exc}")

# 输出:TypeError: unhashable type: 'list'

mapping = {(1, 2, 3): "abc"}
print(mapping[(1, 2, 3)])

# 输出:abc

不可变不等于一定可哈希。 元组里含列表时仍不可哈希;自定义对象取决于它的哈希与相等性实现。

代码块 Python
1
2
3
4
5
6
7
8
user = {"name": "Ada", "age": 36}
user["city"] = "London"

for key, value in user.items():
    print(key, value)

email = user.get("email")  # 缺失时返回 None
print(email)

输出:

代码块 Text
1
2
3
4
# name Ada
# age 36
# city London
# None

读取键:下标还是 get()?

  • 必填字段用 mapping[key]:缺失时抛出 KeyError。例如姓名必填,user["name"] 能及时暴露缺字段问题。
  • 可选字段用 mapping.get(key):缺失时默认返回 None。例如用户可以不填邮箱,user.get("email") 就适合。

注意拼写:把 name 错写成 user.get("nmae"),也只会得到 None,错误可能拖到后续步骤才出现。

8.3 集合

集合保存不重复、可哈希的元素,适合去重和成员判断;集合本身没有可依赖的展示顺序。

代码块 Python
1
2
3
4
5
6
7
8
required = {"title", "content"}
provided = {"title", "content", "tags", "author"}

print(required <= provided)
print(provided - required)

# 输出:True
# 示例输出:{'tags', 'author'},元素顺序不保证

8.4 推导式

推导式适合简短、单一的映射或过滤;逻辑复杂时使用普通循环更清楚。

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
numbers = range(10)
squares = [number ** 2 for number in numbers if number % 2 == 0]
index = {name: position for position, name in enumerate(["Ada", "Guido"])}

print(squares)
print(index)

# 输出:
# [0, 4, 16, 36, 64]
# {'Ada': 0, 'Guido': 1}

生成器表达式使用圆括号,按需产生值,不会一次构造完整列表。

下面示例中的数字写法:

  • 1_000_000 就是 1000000:数字下划线是 Python 3.6 起支持的标准分隔写法,只提高可读性。
  • range(1_000_000):依次产生 0 到 999999。
代码块 Python
1
2
3
4
total = sum(number ** 2 for number in range(1_000_000))
print(total)

# 输出:333332833333500000

9. 分支、循环与模式匹配

9.1 if、for 与 while

Python 的 for 遍历可迭代对象,而不是只提供计数循环:

代码块 Python
1
2
3
4
names = ["Ada", "Guido", "Grace"]

for position, name in enumerate(names, start=1):
    print(position, name)

输出:

代码块 Text
1
2
3
# 1 Ada
# 2 Guido
# 3 Grace

并行遍历多个序列可使用 zip():

代码块 Python
1
2
3
4
5
names = ["Ada", "Guido"]
scores = [98, 95]

for name, score in zip(names, scores, strict=True):
    print(f"{name}: {score}")

输出:

代码块 Text
1
2
# Ada: 98
# Guido: 95

strict=True 会在迭代发现序列长度不一致时抛出 ValueError,适合长度必须相同的业务数据。

while 适合结束条件不是“遍历完某个集合”的情况。循环必须保证状态朝退出条件推进:

代码块 Python
1
2
3
4
remaining = 3
while remaining > 0:
    print(remaining)
    remaining -= 1

输出:

代码块 Text
1
2
3
# 3
# 2
# 1

9.2 match 是结构化模式匹配

match 是 Python 3.10 新增的模式匹配语句:按顺序比较各个 case 后的模式,只执行第一个匹配分支。

case 是软关键字,不是函数。 它既能匹配固定值,也能检查序列结构并取出元素。

下面把命令表示为元组。模式 ("move", direction, distance) 分三部分理解:

  • 结构:序列必须有三个元素。
  • 固定值:首元素必须为 "move"。
  • 捕获变量:后两个元素分别绑定给 direction、distance,不是与同名旧变量比较。
代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def describe(command: tuple[str, ...]) -> str:
    match command:  # 选择要匹配的数据
        case ("move", direction, distance):
            # 匹配三元素序列;例如 direction = "东",distance = "3"
            return f"向 {direction} 移动 {distance}"
        case ("quit",):
            # 匹配唯一元素为 "quit" 的序列;元组中的逗号不能省略
            return "退出"
        case _:
            # _ 匹配任何剩余情况,不保存匹配值,通常放在最后
            return "未知命令"


print(describe(("move", "东", "3")))
print(describe(("quit",)))
print(describe(("status",)))

# 输出:
# 向 东 移动 3
# 退出
# 未知命令

使用时记住:

  • 无需 break:匹配后不会自动继续执行下一个 case。
  • 没有匹配就跳过:没有分支匹配且没有 case _ 时,整个语句直接结束。
  • 序列模式也能匹配列表:参数类型标注本身不会限制运行时传入的类型。

简单条件继续用 if 即可。

10. 函数与作用域

10.1 参数与返回值

函数可以组合位置参数、默认参数和仅限关键字参数:

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from decimal import Decimal


def format_price(amount: Decimal, currency: str = "CNY", *, precision: int = 2) -> str:
    return f"{currency} {amount:.{precision}f}"


print(format_price(Decimal("19.90")))
print(format_price(Decimal("19.90"), precision=1))

# 输出:
# CNY 19.90
# CNY 19.9
  • * 后的参数:必须按关键字传递,避免布尔值、数字等位置参数含义不清。
  • *args:收集额外位置参数。
  • **kwargs:收集额外关键字参数。

但普通业务函数不应无理由接受任意参数。

函数没有显式 return 时返回 None。返回多个值实际上是返回一个元组:

代码块 Python
1
2
3
4
5
6
7
8
def bounds(values: list[int]) -> tuple[int, int]:
    return min(values), max(values)


lowest, highest = bounds([3, 1, 8])
print(lowest, highest)

# 输出:1 8

10.2 不要使用可变默认值

默认参数在函数定义时求值,只创建一次:

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# list[str] | None 是联合类型:items 可以是字符串列表,也可以是 None
# 这里的 | 表示“或”;末尾 = None 才是参数默认值,不是类型的一部分
def add_item(item: str, items: list[str] | None = None) -> list[str]:
    if items is None:
        items = []
    items.append(item)
    return items


print(add_item("read"))
print(add_item("code"))

# 输出:
# ['read']
# ['code']

这个规则同样适用于字典、集合以及自定义的可变对象。

10.3 作用域与闭包

名称查找通常遵循 LEGB:局部(Local)→ 外层函数(Enclosing)→ 全局(Global)→ 内置(Built-in)。

尽量通过参数和返回值传递数据,少依赖可变全局状态。

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def make_multiplier(factor: int):
    def multiply(value: int) -> int:
        return value * factor

    return multiply


double = make_multiplier(2)
print(double(5))

# 输出:10

lambda 适合短小的单表达式函数,例如排序键;一旦需要分支、异常处理或文档,就应使用 def。

代码块 Python
1
2
3
4
5
records = [{"name": "Ada", "score": 98}, {"name": "Guido", "score": 95}]
records.sort(key=lambda record: record["score"], reverse=True)
print(records)

# 输出:[{'name': 'Ada', 'score': 98}, {'name': 'Guido', 'score': 95}]

11. 异常与资源管理

异常用于报告当前层不能正常完成的操作。捕获异常时应尽可能具体,不要用裸 except: 隐藏退出、取消和程序错误。

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def parse_port(raw: str) -> int:
    try:
        port = int(raw)
    except ValueError as exc:
        raise ValueError("端口必须是整数") from exc

    if not 1 <= port <= 65535:
        raise ValueError("端口必须在 1 到 65535 之间")
    return port


for raw in ("8000", "abc", "70000"):
    try:
        print(parse_port(raw))
    except ValueError as exc:
        print(f"ValueError: {exc}")

# 输出(依次对应上面的三个输入):
# 8000
# ValueError: 端口必须是整数
# ValueError: 端口必须在 1 到 65535 之间

异常结构的职责:

  • try:只包围可能失败的最小范围。
  • except:处理明确知道如何恢复或转换的异常。
  • else:仅在没有异常时执行。
  • finally:无论是否异常都执行,适合必须进行的清理。

文件和锁等资源优先使用 with,由上下文管理器保证释放,通常无需手写 finally。

假设 notes.txt 包含“第一行”和“第二行”:

代码块 Python
1
2
3
4
5
6
from pathlib import Path

path = Path("notes.txt")
with path.open("r", encoding="utf-8") as file:
    for line in file:
        print(line.rstrip())

输出:

代码块 Text
1
2
# 第一行
# 第二行

数据库连接:核对 with 的具体职责

  • sqlite3.Connection 的 with 只管理事务提交或回滚,不会关闭连接。
  • 关闭连接仍需显式调用 close(),或另外使用能负责关闭的上下文管理器。

文件操作:区分状态检查与实际操作

  • 查看状态:Path.exists() 可判断此刻是否存在,但不能替代操作本身的错误处理。
  • 读写文件:使用明确的打开模式,捕获具体异常;不要用“读取失败就新建文件”的宽泛异常逻辑判断是否存在。
  • 保证约束:“先检查再操作”可能有并发竞态,真正的写入约束应由打开模式或底层存储保证。

12. 文件、路径与结构化数据

12.1 使用 pathlib 表达路径

pathlib.Path 把路径表示为对象,比手工拼接 / 或 \\ 更跨平台。

Path("data") 表示什么?

  • 位置:相对于当前工作目录的 data 路径。
  • 创建对象:不会创建目录,也不会自动变成绝对路径;调用 mkdir() 才会创建目录。
  • 系统差异:macOS / Linux 中是 PosixPath,Windows 中是 WindowsPath。

为什么会看到 PosixPath('data')?

查看方式(macOS / Linux)显示内容
交互环境直接查看对象,或使用 repr()PosixPath('data'),包含对象类型
使用 print()data,只显示路径文本

下面假设在 macOS 的 /Users/you/project 目录启动程序,且路径不涉及符号链接:

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pathlib import Path

data_dir = Path("data")
print(data_dir)
print(repr(data_dir))
print(data_dir.resolve())  # 返回解析后的绝对路径对象,不修改原来的 data_dir
print(data_dir)

# 示例输出(对象类型和绝对路径因系统、启动目录而异):
# data
# PosixPath('data')
# /Users/you/project/data
# data

路径对象之间的 / 表示拼接子路径。

下面创建目录并写入文件。data_dir / "report.txt" 在上述目录下实际指向 /Users/you/project/data/report.txt:

代码块 Python
1
2
3
4
5
6
7
8
9
from pathlib import Path

data_dir = Path("data")
data_dir.mkdir(parents=True, exist_ok=True)  # 补齐缺失的父目录;目标目录已存在时不报错

report_path = data_dir / "report.txt"
report_path.write_text("第一行\n第二行\n", encoding="utf-8")
content = report_path.read_text(encoding="utf-8")
print(content, end="")

输出如下,同时会生成 data/report.txt:

代码块 Text
1
2
# 第一行
# 第二行

read_text() 适合体积可控的文本;大文件应逐行读取,避免一次占用过多内存。

12.2 JSON 序列化与反序列化

  • 序列化:Python 对象 → 可存储、传输的 JSON 文本。
  • 反序列化:JSON 文本 → Python 对象。

函数名带 s 的版本主要处理字符串,不带 s 的版本处理已经打开的文件对象:

函数方向输入与结果
json.dumps(obj)序列化Python 对象 → JSON 字符串 str
json.dump(obj, file)序列化把 JSON 文本写入文件对象,返回 None
json.loads(text)反序列化JSON 字符串 → Python 对象,也支持 bytes、bytearray
json.load(file)反序列化从文件对象读取 JSON,返回 Python 对象

dump() / load() 接收文件对象,不是文件路径字符串。 两组方法分别这样使用:

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import json
from pathlib import Path

profile = {"name": "Ada", "skills": ["math", "programming"]}

# 字符串方式:对象 → JSON 字符串 → 对象
text = json.dumps(profile, ensure_ascii=False)
print(text)
print(json.loads(text))

# 文件方式:对象 → profile.json → 对象
path = Path("profile.json")
with path.open("w", encoding="utf-8") as file:
    json.dump(profile, file, ensure_ascii=False, indent=2)
with path.open("r", encoding="utf-8") as file:
    loaded = json.load(file)
print(loaded)

# 输出:
# {"name": "Ada", "skills": ["math", "programming"]}
# {'name': 'Ada', 'skills': ['math', 'programming']}
# {'name': 'Ada', 'skills': ['math', 'programming']}

类型限制:JSON 不会自动保存 datetime、Decimal、集合或自定义类,需要先转换为约定的数据结构。

不要对不可信数据使用 pickle。 反序列化恶意 pickle 可以执行代码。

13. 模块、包与程序入口

  • 模块:每个 .py 文件都是模块。
  • 常规包:目录通常包含 __init__.py。
  • 导入行为:会执行模块的顶层代码,因此顶层应以定义、常量和必要的轻量初始化为主。

下面只是项目结构示意;可安装包还需要在 pyproject.toml 中声明构建和包发现配置:

代码块 Text
1
2
3
4
5
6
7
8
price_tool/
├── pyproject.toml
├── src/
│   └── price_tool/
│       ├── __init__.py
│       ├── formatter.py
│       └── __main__.py
└── tests/

脚本入口使用显式的 main(),既方便测试,也避免模块被导入时直接执行业务:

代码块 Python
1
2
3
4
5
6
7
8
def main() -> None:
    print("程序开始")


if __name__ == "__main__":
    main()

# 作为脚本运行时输出:程序开始

Path.cwd():当前工作目录

cwd 是 current working directory。Path.cwd() 返回当前工作目录的绝对路径对象,通常由启动命令时所在的目录决定。

当前工作目录不保证等于脚本文件所在目录。 相对路径的读写以当前工作目录为基准。

常见导入形式与调用方式如下:

代码块 Python
1
2
3
4
5
6
7
8
import math
from pathlib import Path

print(math.sqrt(81))
print(Path.cwd())  # 查看当前工作目录

# 输出:9.0
# 示例输出:/Users/you/project,实际值取决于启动目录
  • 避免通配导入:from module import * 会让名称来源不清晰。
  • 避免模块重名:不要把文件命名为 random.py、json.py、typing.py 等标准库名称,否则可能遮蔽真正的模块。

14. 类与数据类

类把状态和行为组织在一起。不是所有数据都需要类:简单转换可用函数,固定的小型记录可考虑元组或数据类。

@dataclass 自动生成什么?

它是应用在类定义上的装饰器:类定义完成后,根据 name: str 等字段声明补齐常见方法。

自动生成的方法用途
__init__接收构造参数,保存为实例字段
__repr__以可读形式展示对象的字段内容
__eq__按字段比较同类型实例是否相等
代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from dataclasses import dataclass
from decimal import Decimal


@dataclass  # 自动生成初始化、展示和相等性比较方法
class Product:
    name: str
    price: Decimal
    stock: int = 0

    def reserve(self, quantity: int) -> None:
        if quantity <= 0:
            raise ValueError("数量必须大于零")
        if quantity > self.stock:
            raise ValueError("库存不足")
        self.stock -= quantity


product = Product("Keyboard", Decimal("399.00"), stock=10)  # 自动把参数保存为实例字段
product.reserve(2)
print(product.stock)
print(product)  # 使用自动生成的 __repr__ 展示字段
print(product == Product("Keyboard", Decimal("399.00"), stock=8))

# 输出:
# 8
# Product(name='Keyboard', price=Decimal('399.00'), stock=8)
# True

去掉 @dataclass 后有什么差异?

  • 构造参数要自己处理:只删装饰器,上面的 Product("Keyboard", ...) 会报 TypeError。需要手写 __init__,给 self.name、self.price、self.stock 赋值。
  • 展示与比较要自己实现:普通类不会默认按这些字段展示内容或比较相等。

它不替你完成什么?

  • reserve() 这类业务方法仍需手写。
  • 默认不生成大小排序方法。
  • 不会自动检查字段类型或业务合法性。

继承与组合:继承表达“是一个”,组合表达“拥有一个”。业务代码通常优先组合;确实需要共享接口和可替换行为时,再使用继承。

15. 迭代器与生成器

  • 可迭代对象:能产生迭代器。
  • 迭代器:通过 __next__() 逐个返回值,耗尽时抛出 StopIteration。

日常代码通常使用 for,无需手动调用这些方法。

包含 yield 的函数是生成器函数,调用后返回生成器对象,执行会在迭代时按需推进:

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 假设 notes.txt 依次包含“第一行”、空行和“第二行”
def read_non_empty_lines(path: str):
    with open(path, "r", encoding="utf-8") as file:
        for line in file:
            stripped = line.strip()
            if stripped:
                yield stripped


for line in read_non_empty_lines("notes.txt"):
    print(line)

输出:

代码块 Text
1
2
# 第一行
# 第二行

生成器适合流式处理,使用时注意:

  • 只能消费一次:需要重复遍历时,重新创建生成器,或在数据量允许时转成列表。
  • 资源生命周期:文件等资源可能在迭代期间一直保持打开,直到退出相应的上下文。

16. 类型标注

类型标注提高可读性,并帮助编辑器和静态类型检查器发现问题。Python 运行时不会根据类型注解强制检查类型。

type UserId = int:给类型起一个业务名称

  • 语法来源:Python 3.12 新增的类型别名声明。
  • 作用:给 int 起别名 UserId,让标注表达“这里的整数是用户编号”。
  • 边界:没有创建一种新的整数,也不会自动验证编号是否合法。
代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type UserId = int  # 类型别名:让标注中的用户编号更容易辨认


def active_names(users: dict[UserId, str], disabled: set[UserId]) -> list[str]:
    return [name for user_id, name in users.items() if user_id not in disabled]


user_id: UserId = 1001
print(type(user_id))
print(active_names({1: "Ada", 2: "Guido", 3: "Linus"}, {2}))

# 输出:
# <class 'int'>
# ['Ada', 'Linus']

区分 user_id 与 UserId

  • user_id 是变量:上例绑定的对象实际类型是 int。
  • UserId 是类型别名对象:不是普通赋值 UserId = int,不能当作 int() 那样调用。

常用标注写法

  • 字符串列表:list[str]。
  • 字符串键、整数值的字典:dict[str, int]。
  • 可空类型:User | None。

类型标注应表达真实契约,不要为了“全部有类型”而使用大量 Any 或复杂泛型。

静态类型检查不能替代外部输入的解析与校验。

17. 调试与测试

17.1 先读完整 traceback

  • 最后一行:异常类型和消息。
  • 上方调用栈:错误如何到达现场。

应修复根因,不要只在最外层加一个宽泛的 try/except。

常见异常包括:

  • SyntaxError:语法无法解析。
  • IndentationError:缩进不合法。
  • NameError:名称不存在。
  • TypeError:操作或调用不接受当前类型。
  • ValueError:类型可以接受,但值不合法。
  • KeyError、IndexError:映射键或序列位置不存在。
  • FileNotFoundError:目标文件不存在。

临时观察变量可以使用 print()。日志库的选择与配置不在本篇展开,先掌握如何读出异常位置和原因。

17.2 为行为写测试

标准库 unittest 可以直接起步,项目也可以选择第三方测试框架。测试应覆盖公开行为、边界和失败路径,避免绑定内部实现细节。

先认识示例中的三个名称:

  • unittest.TestCase:测试基类,提供一组断言方法;继承后即可使用。
  • test_:测试方法的名称前缀,测试运行器会调用这些方法。
  • self:当前测试实例,通过它调用继承的 assertEqual() 等方法。

把下面内容保存为 test_example.py:

代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import unittest


def add(left: int, right: int) -> int:
    return left + right


class AddTests(unittest.TestCase):  # 继承测试基类,获得 assertEqual 等方法
    def test_adds_two_integers(self) -> None:
        # 先算出实际值 add(2, 3),再与期望值 5 比较;不相等时报告测试失败
        self.assertEqual(add(2, 3), 5)


if __name__ == "__main__":
    unittest.main()  # 直接运行本文件时,发现并执行本文件的测试

# 结果:1 项测试通过,耗时因环境而异

self.assertEqual(actual, expected):比较实际值与期望值

  • 相等:断言通过。
  • 不相等:抛出断言失败,由测试运行器记录;它不是返回 True / False 的判断函数。

这里检查 2 + 3 是否为 5。把期望值改成 6,测试就会失败,并显示 5 != 6。

也可以通过命令行启动测试:-v 展示每项测试的名称,test_example 是去掉 .py 的模块名。

代码块 Shell
1
python -m unittest -v test_example

示例输出(unittest 默认写入 stderr,耗时因环境而异):

代码块 Text
1
2
3
4
5
6
# test_adds_two_integers (test_example.AddTests.test_adds_two_integers) ... ok
#
# ----------------------------------------------------------------------
# Ran 1 test in 0.000s
#
# OK

18. 单词统计小程序

下面的命令行程序综合了路径、函数、类型标注、容器、异常和标准库。

  • 输入:UTF-8 文本文件,逐行读取。
  • 统计规则:识别由英文字母和内部单引号组成的单词。
  • 适用边界:中文等没有空格边界的语言需要分词工具,不能直接套用这个正则表达式。
代码块 Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from collections import Counter
from pathlib import Path
import re
import sys


# 预先编译正则,供逐行匹配复用:
# [A-Za-z]+ 匹配连续英文字母;(?:...) 把内部规则作为一组,不单独返回捕获结果
# 后面的 ? 表示该组可出现零次或一次,因此可识别 don't 这样的单词
WORD_PATTERN = re.compile(r"[A-Za-z]+(?:'[A-Za-z]+)?")


def top_words(path: Path, limit: int = 10) -> list[tuple[str, int]]:
    # Counter 是计数字典:键为单词,值为出现次数
    counts: Counter[str] = Counter()
    with path.open("r", encoding="utf-8") as file:
        for line in file:
            # lower 统一小写,findall 返回本行单词列表,update 为每个单词累计次数
            counts.update(WORD_PATTERN.findall(line.lower()))
    # 返回最多 limit 组 (单词, 次数),按次数从高到低排列
    return counts.most_common(limit)


def main() -> int:
    # sys.argv[0] 是脚本路径,sys.argv[1] 是用户传入的文本文件路径
    # 共两个元素才说明恰好传入了一个参数
    if len(sys.argv) != 2:
        # .name 只取文件名;错误提示写到 stderr(标准错误流)
        print(f"用法:{Path(sys.argv[0]).name} <文本文件>", file=sys.stderr)
        return 2  # 约定退出码 2 表示参数用法错误

    path = Path(sys.argv[1])
    try:
        results = top_words(path)
    except (OSError, UnicodeError) as exc:  # 分别处理文件访问失败、文本解码失败
        print(f"读取失败:{exc}", file=sys.stderr)
        return 1  # 约定退出码 1 表示读取失败

    for word, count in results:
        # <20 表示左对齐、最小宽度 20;>6 表示右对齐、最小宽度 6
        print(f"{word:<20} {count:>6}")
    return 0  # 退出码 0 表示成功


if __name__ == "__main__":
    raise SystemExit(main())  # 执行 main,并把返回值交给操作系统作为进程退出码

将程序保存为 word_count.py,并令 sample.txt 内容为 Python is simple. Python is readable.:

代码块 Shell
1
python word_count.py sample.txt

输出:

代码块 Text
1
2
3
4
# python                    2
# is                        2
# simple                    1
# readable                  1

继续练习时可以逐步加入 argparse 参数、停用词、JSON 输出和单元测试。每次只增加一个可验证的能力,比一次堆入框架更容易理解。

19. 学完基础后检查什么

  • 能解释名称绑定、可变对象以及常见内置类型的用途。
  • 能为字符串、数值和结构化输入选择安全的转换方式,不对外部输入使用 eval()。
  • 能熟练使用列表、字典、集合、推导式、enumerate() 和 zip()。
  • 能写参数清晰的函数,避免可变默认值,并理解局部与外层作用域。
  • 能捕获具体异常,用 with 管理文件等资源。
  • 能创建虚拟环境,通过 python -m pip 管理当前项目依赖。
  • 能把代码拆成模块,使用 main() 入口,并为关键行为编写测试。

参考资料

评论

使用 GitHub 登录后参与讨论,评论将公开保存在 GitHub。

阅读到这里时加载评论。