Python 语法、对象与容器

# Python 语法、对象与容器

本篇目标

掌握 Python 的基本语法、常用容器和控制流程,并理解变量绑定、可变对象与不可变对象。学完后,你应该能够读写常见的数据处理代码。

理解变量绑定掌握常用容器写清控制流程避免可变性陷阱

# 先记住一句话

Python 变量不是装数据的盒子,而是指向对象的名字;赋值默认不会复制对象。

# names 和 backup 最初指向同一个列表对象。
names = ["Ada", "Lin"]
backup = names

# 列表是可变对象,通过任意一个名字修改,另一个名字都能观察到变化。
names.append("Grace")
print(backup)  # ['Ada', 'Lin', 'Grace']

# 浅复制会创建新的外层列表;如果元素本身也是可变对象,仍可能共享内层对象。
snapshot = names.copy()
1
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("还没有结果")
1
2
3
4
5

# 字符串与格式化

name = "Ada"
score = 96.5

# f-string 会把表达式结果放入字符串,格式说明 .1f 表示保留一位小数。
message = f"{name} 的得分是 {score:.1f}"
print(message)

# 字符串不可变,replace 会返回新字符串,不会修改原值。
normalized = message.replace("得分", "成绩")
1
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"]
1
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: "未找到"}
1
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")
1
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))  # []
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

可以先记住两种基本结构:

[结果表达式 for 元素 in 可迭代对象 if 条件]  → 列表推导式
(结果表达式 for 元素 in 可迭代对象 if 条件)  → 生成器表达式
1
2
对比 列表推导式 生成器表达式
外层符号 [] ()
计算时机 立即计算全部结果 被循环、next() 或 list() 消费时逐项计算
返回对象 list 生成器,也是迭代器
内存特点 保存全部结果 通常只保存当前迭代状态
重复读取 可以反复遍历和按索引读取 通常只能向前消费一次
更适合 数据较少,结果需要重复使用 数据较多,结果只需顺序处理一次

不要为了追求 Pythonic 把多层业务规则塞进一条推导式,可读性比少写几行更重要。Pythonic 指符合 Python 常用习惯、表达自然并且容易阅读的写法,但不等于代码越短越好。

# 真值与短路逻辑

空字符串、空容器、数字 0、None 和 False 在条件中都被视为假值。

user_input = ""

# or 会返回第一个真值操作数,不一定返回 bool。
query = user_input or "默认问题"

# 如果业务上需要区分空字符串和 None,就必须显式判断,不能只写 if value。
1
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)
1
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. 列表推导式和生成器表达式怎样选择?参考答案

列表推导式会立即生成完整列表,适合结果需要重复访问或数据量较小的场景;生成器表达式按需产生值,更节省峰值内存,但通常只能顺序消费一次。是否更快要结合消费方式测量,不能只因为写法短就默认使用。

# 接下来学什么

下一篇学习 函数、模块与异常,把零散代码组织成可复用、可失败也可测试的程序。

# 参考资料

上次更新时间: 2026年09月10日 00:03:52