Python 语法、对象与容器
# Python 语法、对象与容器
掌握 Python 的基本语法、常用容器和控制流程,并理解变量绑定、可变对象与不可变对象。学完后,你应该能够读写常见的数据处理代码。
# 先记住一句话
Python 变量不是装数据的盒子,而是指向对象的名字;赋值默认不会复制对象。
# names 和 backup 最初指向同一个列表对象。
names = ["Ada", "Lin"]
backup = names
# 列表是可变对象,通过任意一个名字修改,另一个名字都能观察到变化。
names.append("Grace")
print(backup) # ['Ada', 'Lin', 'Grace']
# 浅复制会创建新的外层列表;如果元素本身也是可变对象,仍可能共享内层对象。
snapshot = names.copy()
2
3
4
5
6
7
8
9
10
# 基本类型与空值
| 类型 | 示例 | 是否可变 | 常见用途 |
|---|---|---|---|
int、float | 42、3.14 | 否 | 数值计算 |
bool | True、False | 否 | 条件判断 |
str | "hello" | 否 | 文本 |
None | None | 否 | 表示没有值或尚未设置 |
list | [1, 2] | 是 | 有序、可重复数据 |
tuple | (1, 2) | 否 | 不希望改变的有序数据 |
dict | {"id": 1} | 是 | 键值映射 |
set | {"a", "b"} | 是 | 去重和集合运算 |
None 不是空字符串,也不是数字 0。判断是否为 None 时使用 is None,因为这里关心的是同一个单例对象,而不是普通的值相等。
result = None
# is 用于身份判断;不要写 result == None。
if result is None:
print("还没有结果")
2
3
4
5
# 字符串与格式化
name = "Ada"
score = 96.5
# f-string 会把表达式结果放入字符串,格式说明 .1f 表示保留一位小数。
message = f"{name} 的得分是 {score:.1f}"
print(message)
# 字符串不可变,replace 会返回新字符串,不会修改原值。
normalized = message.replace("得分", "成绩")
2
3
4
5
6
7
8
9
# 列表、字典和集合
notes = [
{"title": "Python", "tags": {"backend", "ai"}},
{"title": "Go", "tags": {"backend", "concurrency"}},
]
# 列表推导式适合简单映射或过滤;逻辑复杂时改用普通循环更清楚。
backend_titles = [note["title"] for note in notes if "backend" in note["tags"]]
# get 在键不存在时返回默认值,适合可选字段;必需字段仍应直接索引并尽早暴露错误。
first_summary = notes[0].get("summary", "暂无摘要")
# 集合天然去重,并支持交集、并集等运算。
common_tags = notes[0]["tags"] & notes[1]["tags"]
2
3
4
5
6
7
8
9
10
11
12
13
字典键名为什么有时带引号
字典字面量中的 "title" 是字符串键,必须用单引号或双引号包住,两种写法没有语义区别。没有引号的名称会被当作变量表达式,而不会像 JavaScript 对象那样自动转换成同名字符串。
first = {"title": "Python"} # 双引号字符串键。
second = {'title': 'Python'} # 单引号写法与上面等价。
key = "title"
third = {key: "Python"} # 先读取变量 key 的值,最终仍得到字符串键 "title"。
# dict() 会把合法的关键字参数名转换成字符串键。
fourth = dict(title="Python", level="basic")
# 字典键不局限于字符串,只要对象可哈希即可。
status_text = {200: "成功", 404: "未找到"}
2
3
4
5
6
7
8
9
10
11
Python 字典允许单引号、双引号和非字符串键;JSON 的键只能是使用双引号包裹的字符串;JavaScript 对象中符合标识符规则的键可以省略引号。dict(title="Python") 比较简洁,但无法直接表示 "user-name" 这类不符合 Python 标识符规则的键。
浅复制和深复制
list.copy()、切片和 dict.copy() 只复制外层容器。嵌套对象仍被共享。确实需要递归复制时可以使用 copy.deepcopy(),但大型对象深复制成本高,优先重新审视数据所有权。
# 条件、循环与模式匹配
def classify_score(score: int) -> str:
# Python 使用缩进定义代码块,同一层级必须保持一致。
if score >= 90:
return "优秀"
if score >= 60:
return "通过"
return "需要复习"
tasks = ["读取文档", "生成摘要", "保存结果"]
# enumerate 会同时产生序号和元素;start=1 表示序号从 1 而不是默认的 0 开始。
for index, task in enumerate(tasks, start=1):
print(index, task)
# zip 按位置组合多个可迭代对象;默认在最短输入结束时停止。
for task, seconds in zip(tasks, [2, 5, 1]):
print(f"{task}: {seconds}s")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 列表推导式和生成器表达式
两者都可以把 “遍历、筛选、转换” 写在一个表达式里。最直观的区别是:列表推导式使用方括号并立即生成完整列表,生成器表达式使用圆括号并在消费时逐个计算。
numbers = [1, 2, 3, 4]
# 普通循环:先创建空列表,再把符合条件的结果逐个加入列表。
result: list[int] = []
for number in numbers:
if number % 2 == 0:
result.append(number * 2)
# 列表推导式:与上面的循环结果相同,执行到这里时立即得到 [4, 8]。
list_result = [number * 2 for number in numbers if number % 2 == 0]
# 生成器表达式:圆括号只创建按需计算的生成器,不会立即得到完整结果。
generator_result = (number * 2 for number in numbers if number % 2 == 0)
# list() 会逐项消费生成器并收集结果;生成器耗尽后不能从头重复读取。
print(list(generator_result)) # [4, 8]
print(list(generator_result)) # []
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
可以先记住两种基本结构:
[结果表达式 for 元素 in 可迭代对象 if 条件] → 列表推导式
(结果表达式 for 元素 in 可迭代对象 if 条件) → 生成器表达式
2
| 对比 | 列表推导式 | 生成器表达式 |
|---|---|---|
| 外层符号 | [] | () |
| 计算时机 | 立即计算全部结果 | 被循环、next() 或 list() 消费时逐项计算 |
| 返回对象 | list | 生成器,也是迭代器 |
| 内存特点 | 保存全部结果 | 通常只保存当前迭代状态 |
| 重复读取 | 可以反复遍历和按索引读取 | 通常只能向前消费一次 |
| 更适合 | 数据较少,结果需要重复使用 | 数据较多,结果只需顺序处理一次 |
不要为了追求 Pythonic 把多层业务规则塞进一条推导式,可读性比少写几行更重要。Pythonic 指符合 Python 常用习惯、表达自然并且容易阅读的写法,但不等于代码越短越好。
# 真值与短路逻辑
空字符串、空容器、数字 0、None 和 False 在条件中都被视为假值。
user_input = ""
# or 会返回第一个真值操作数,不一定返回 bool。
query = user_input or "默认问题"
# 如果业务上需要区分空字符串和 None,就必须显式判断,不能只写 if value。
2
3
4
5
6
# 数据处理小例子
from collections import Counter
# records 是列表;列表中的每条记录都是字典。
# 字典键是字符串,值暂时允许是任意 Python 对象,因此后面仍要检查 tags 的真实类型。
# -> dict[str, int] 表示函数预计返回 “字符串键、整数值” 的字典。
def count_tags(
records: list[dict[str, object]],
) -> dict[str, int]:
"""统计记录中的标签数量;缺少 tags 的记录按空列表处理。"""
counter: Counter[str] = Counter()
for record in records:
# 这里假设 tags 来自已校验的数据;外部输入应先做 Schema 校验。
tags = record.get("tags", [])
# isinstance() 在运行时检查类型;只有 tags 确实是列表才继续处理。
if isinstance(tags, list):
# 再过滤列表元素,只把字符串标签交给 Counter 统计。
counter.update(tag for tag in tags if isinstance(tag, str))
return dict(counter)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
函数参数使用 参数名: 类型 标注,返回类型固定写在参数列表后面的 -> 类型 中。这里的 -> 是 Python 类型标注语法,不是函数执行方向,也不能换成 JavaScript 箭头函数使用的 =>。类型标注主要供阅读、IDE 和静态检查工具使用,不会自动校验运行时传入的数据。
# 高频面试题与回答
1. `is` 和 `==` 有什么区别?参考答案
== 比较值是否相等,is 比较是否为同一个对象。业务数据通常用 ==,判断 None 使用 is None。不要依赖小整数或字符串驻留等实现细节。
2. 列表和元组怎样选择?参考答案
需要增删改的有序集合使用列表;表达固定结构或不希望被修改的序列可以使用元组。不可变不等于绝对安全:元组内部如果包含列表,内层列表仍然可以改变。
3. Python 中可变对象和不可变对象有什么区别?参考答案
可变对象可以在身份不变时修改内部状态,例如 list、dict 和 set;不可变对象修改时会得到新对象,例如 int、str 和 tuple。多个变量引用同一个可变对象时,一处原地修改会被其他引用观察到,这是参数传递和缓存代码中的常见陷阱。
4. 浅复制和深复制有什么区别?参考答案
浅复制只创建新的外层容器,内部元素仍与原对象共享;深复制会递归复制可复制的子对象。深复制成本更高,也不一定符合连接、锁或外部资源的语义,工程中更推荐先明确哪些层需要独立,而不是默认复制整张对象图。
5. 为什么字典的键必须可哈希?参考答案
字典通过键的哈希值定位存储位置,因此键在作为索引期间必须保持稳定,并且相等对象应具有相同哈希值。list 和 dict 会原地变化,所以不能直接作为键;str、int 和只包含可哈希元素的 tuple 通常可以。
6. 列表推导式和生成器表达式怎样选择?参考答案
列表推导式会立即生成完整列表,适合结果需要重复访问或数据量较小的场景;生成器表达式按需产生值,更节省峰值内存,但通常只能顺序消费一次。是否更快要结合消费方式测量,不能只因为写法短就默认使用。
# 接下来学什么
下一篇学习 函数、模块与异常,把零散代码组织成可复用、可失败也可测试的程序。