Python 速成 Day 1:用 JS/TS 经验理解数据结构与函数参数
这篇文章面向有 JS/TS 和 Node.js 经验的开发者。目标是在约 150 分钟内,能编写常见数据处理函数,并准确判断函数是否会修改原始数据。重点是迁移语言经验,不重复解释变量、条件和循环。
学习顺序是:容器与字段 → 名称与对象 → 拷贝与默认值 → 数据转换与函数参数。先完成讲解与输出预测,再做三个练习,最后用面试 Q&A 检查理解。
建议准备 day1.py,先预测示例输出再运行。示例语法使用 Python 3.12;文章不涉及具体项目源码。
| 时间 | 内容 |
|---|---|
| 30 分钟 | 容器与 JS/TS 对照 |
| 25 分钟 | 对象、参数传递与拷贝 |
| 25 分钟 | 函数参数与输出预测 |
| 45 分钟 | 三个编码练习 |
| 25 分钟 | 面试自测与复盘 |
1. 先认识 Python 的数据容器
Section titled “1. 先认识 Python 的数据容器”按数据的用途选择,而不是机械翻译 JS 类型。
| Python | JS 中的相近用途 | 关键区别与使用场景 |
|---|---|---|
list |
Array | 有序、可变;适合文档列表,append() 返回 None |
tuple |
固定结构的数组 | 不能替换元素;适合 (id, status),内部对象仍可能可变 |
dict |
Map / Object | 键值映射,保留插入顺序;键需要可哈希 |
set |
Set | 去重和成员判断;元素需要可哈希,不承诺遍历顺序 |
str |
string | 不可变文本,适合文本内容与标识符 |
bytes |
Buffer / Uint8Array 的部分用途 | 不可变字节序列,用于文件和网络数据;与文本之间需要编码解码 |
可哈希指对象能提供符合相等规则的稳定哈希值,供字典和集合定位。常见字符串、整数可以作为键,列表和字典不可以。dict/set 查找平均为 O(1),list 成员查找通常为 O(n),不是所有输入下的绝对保证。
names = ["Alice", "Bob"]assert names.append("Carol") is Nonepoint = (10, 20)user = {"id": 1, "name": "Alice"}tags = {"python", "agent", "python"}assert len(tags) == 2assert isinstance({}, dict)assert isinstance(set(), set)
text = "你好"encoded = text.encode("utf-8")assert isinstance(encoded, bytes)assert encoded.decode("utf-8") == text空集合写成 set(),{} 是空字典。先掌握 UTF-8 转换,今天跳过完整 Unicode 规范和容器底层实现。
2. 从字典访问理解缺失字段
Section titled “2. 从字典访问理解缺失字段”方括号访问缺失键会抛 KeyError,不是 JS 的 undefined。 字典键也不能直接用属性语法访问。
user = {"name": "Alice"}assert user["name"] == "Alice"assert user.get("age") is Noneassert user.get("age", 18) == 18
try: user["age"]except KeyError: passelse: raise AssertionError("缺失键应该抛出 KeyError")
user["age"] = Noneassert user.get("age", 18) is Noneget() 的默认值仅处理键不存在,不会替换已有的 None。必填字段应明确校验,不要只是为了避免异常而改成 get()。
对字典使用 in 默认检查键:"name" in user。对元组使用 outcome in ("created", "updated") 则检查是否等于其中一个元素,类似 JS 数组的 includes()。
3. 真假值与默认值的区别
Section titled “3. 真假值与默认值的区别”读取字典时,字段缺失和值为空已经需要分别处理;设置默认值时,还要进一步区分空值与假值。Python 空列表、空字典和空集合都是假值,与 JS 的空数组、空对象不同。or 会处理所有假值。
assert bool([]) is Falseassert bool({}) is Falseassert bool(set()) is Falselimit = 0assert (limit or 10) == 10assert (10 if limit is None else limit) == 0当 0 表示合法配置,或空列表表示明确不选择任何项时,不能把它们随意替换为默认值。
4. 名称绑定、可变性与参数传递
Section titled “4. 名称绑定、可变性与参数传递”能够访问和筛选数据后,下一个问题是函数会不会改变这些数据。这里先建立“名称指向对象”的理解,再看拷贝与默认参数。
对普通对象参数而言,两者有相近直觉:修改对象和重新绑定局部参数是两件事。 Python 参数名称绑定到传入的对象,修改可变对象会被调用者观察到,重新绑定局部参数不会改变调用者的变量绑定。
def update(items): items.append(1) # 修改共享对象 items = [2] # 只重新绑定局部名称
values = []update(values)assert values == [1]这常称为对象共享传递,比单独背“引用传递”准确。
常见可变对象是 list、dict、set;常见不可变对象是数字、bool、str、tuple、bytes。不可变不表示名称不能重新赋值,也不表示递归冻结内部对象:
name = "Alice"name = "Bob" # 重新绑定合法record = (1, ["python"])record[1].append("agent")assert record == (1, ["python", "agent"])同样,列表 append(item) 保存的是对象引用,不会复制字典。字典 result[outcome] += 1 则读取整数、计算新值,再更新字典条目;不是把整数对象本身修改了。
5. 对象身份与值相等
Section titled “5. 对象身份与值相等”is 比较对象身份,== 按类型定义的规则比较相等。 Python 的列表可以按内容相等,而 JS 的数组 === 比较身份。
a = [1, 2]b = [1, 2]c = aassert a == bassert a is not bassert a is c判断空值一般用 is None,状态字符串用 ==。不要依赖数字或字符串缓存来使用 is。
6. 从对象共享理解浅拷贝与深拷贝
Section titled “6. 从对象共享理解浅拷贝与深拷贝”现在已经能区分同一个对象与内容相等的不同对象,就可以准确判断复制到底隔离了哪一层。
浅拷贝只复制外层容器,嵌套对象仍可能共享。 与 JS { ...original } 的思路相近。
from copy import deepcopy
original = {"timeout": 10, "tags": ["python"]}copied = original.copy()isolated = deepcopy(original)copied["timeout"] = 20copied["tags"].append("agent")
assert original == {"timeout": 10, "tags": ["python", "agent"]}assert isolated == {"timeout": 10, "tags": ["python"]}深拷贝递归复制支持复制的嵌套对象,但连接、文件、模型等资源不能随意复制,大型结构也有成本。先明确修改范围,再选择复制哪些数据。
热身题:下面输出什么?
a = [{"tags": []}]b = a.copy()b[0]["tags"].append("python")print(a)print(a is b)print(a[0] is b[0])print(bool([]))答案依次为 [{'tags': ['python']}]、False、True、False。外层列表不同,内部字典相同;Python 布尔值首字母大写。
7. 默认参数的求值时机
Section titled “7. 默认参数的求值时机”对象共享不仅发生在赋值和拷贝中,也可能藏在函数定义里。这是 JS 开发者需要特别调整的一处直觉。
默认参数表达式在函数定义执行时求值,后续省略参数的调用会复用它。 JS 默认参数表达式在调用时求值,不能直接迁移这个直觉。
def add_tag_bad(tag, tags=[]): tags.append(tag) return tags
assert add_tag_bad("python") == ["python"]assert add_tag_bad("agent") == ["python", "agent"]使用 None 在函数内创建列表,可以解决默认对象共享。若还要求不修改显式传入的数据,可以返回新列表:
def add_tag(tag, tags=None): current = [] if tags is None else tags return [*current, tag]
existing = ["python"]assert add_tag("agent", existing) == ["python", "agent"]assert existing == ["python"]解决默认对象共享,与保证不修改输入,是两个要求。
8. 用推导式、切片和解包转换数据
Section titled “8. 用推导式、切片和解包转换数据”推导式按“遍历 → 筛选 → 产出”理解,切片不含终点,解包用于分配元素。
records = [{"id": "a", "active": True}, {"id": "b", "active": False}]ids = [record["id"] for record in records if record["active"]]by_id = {record["id"]: record for record in records}assert ids == ["a"]assert by_id["a"] is records[0]
items = [10, 20, 30, 40]assert items[1:3] == [20, 30]assert items[-1] == 40first, *rest = itemsassert first == 10assert rest == [20, 30, 40]字典推导式遇到重复键,后面的值覆盖前面的值;它不会复制每个记录。列表切片也只是浅拷贝。
排序区别也要记住:sorted(items) 返回新列表,items.sort() 原地排序且返回 None。不要写 items = items.sort()。
9. 位置参数、关键字参数与展开
Section titled “9. 位置参数、关键字参数与展开”前面的解包发生在赋值或调用时。函数定义里的星号则控制参数怎样接收,不能把所有星号都理解成对象解构。独立 * 要求后面的参数只能按名称传入。
| 写法 | 定义处的作用 |
|---|---|
* |
后续参数必须按名称传入 |
*args |
收集额外位置参数为 tuple |
**kwargs |
收集额外关键字参数为 dict |
def call_model(prompt, *args, timeout=10, **kwargs): return prompt, args, timeout, kwargs
assert call_model("hello", "extra", timeout=5, temperature=0.2) == ( "hello", ("extra",), 5, {"temperature": 0.2})
def search(query, *, limit=10): return query, limit
assert search("python", limit=5) == ("python", 5)# search("python", 5) 会抛 TypeError。JS 的 function search(query, { limit = 10 } = {}) 使用对象解构;Python 的关键字参数没有传入一个待解构的 options 对象。
调用位置的 *values、**options 则表示展开,与定义位置的收集方向相反。
三个编码练习
Section titled “三个编码练习”每题约 15 分钟,先独立实现,再查看后面的参考答案。
A. 清洗接口数据
Section titled “A. 清洗接口数据”实现 normalize_records(records):缺失 id 跳过;重复 id 保留最后一条;缺失 type 默认 unknown;缺失或为 None 的 size 默认 0;不修改输入。用重复 ID、零值和空输入验证。
B. 合并配置
Section titled “B. 合并配置”实现 merge_config(defaults, overrides):后者覆盖前者,保留 timeout=0;tags 约定为字符串列表,返回列表不与输入共享。不写通用深度合并。
C. 构造请求参数
Section titled “C. 构造请求参数”实现 build_request(query, *, limit=10, tags=None, **options):收集额外选项,复制标签列表,并说明重复参数如何处理。每题至少写三个断言。
练习参考答案
Section titled “练习参考答案”A. 清洗接口数据
Section titled “A. 清洗接口数据”要求:缺失 id 跳过,重复 id 保留最后一条;type 缺失默认 unknown;size 缺失或为 None 默认 0;不修改输入。本题不扩展到 ID 合法性校验。
from copy import deepcopy
def normalize_records(records): by_id = {} for record in records: if "id" not in record: continue normalized = record.copy() normalized.setdefault("type", "unknown") if normalized.get("size") is None: normalized["size"] = 0 by_id[normalized["id"]] = normalized return list(by_id.values())
records = [ {"id": "a", "type": "image", "size": 100}, {"id": "b", "type": "video", "size": 0}, {"id": "a", "type": "image", "size": 200}, {"type": "audio", "size": 50}, {"id": "c", "size": None},]before = deepcopy(records)assert normalize_records(records) == [ {"id": "a", "type": "image", "size": 200}, {"id": "b", "type": "video", "size": 0}, {"id": "c", "type": "unknown", "size": 0},]assert records == beforeassert normalize_records([]) == []setdefault 只处理缺失键。更新已有字典键不改变其位置,因此输出按 ID 首次出现排序,但内容取最后一条。浅拷贝满足本次不修改输入的要求,不承诺返回结果与所有嵌套数据彻底隔离。
B. 合并配置
Section titled “B. 合并配置”要求:overrides 覆盖 defaults,保留 timeout=0,不修改输入;返回结果的 tags 不与输入共享。只考虑 tags 为字符串列表的已知结构。
def merge_config(defaults, overrides): merged = {**defaults, **overrides} merged["tags"] = merged["tags"].copy() return merged
defaults = {"timeout": 10, "tags": ["base"]}overrides = {"timeout": 0}result = merge_config(defaults, overrides)assert result["timeout"] == 0assert result["tags"] is not defaults["tags"]result["tags"].append("python")assert defaults == {"timeout": 10, "tags": ["base"]}assert overrides == {"timeout": 0}
overrides = {"tags": ["custom"]}result = merge_config(defaults, overrides)result["tags"].append("agent")assert overrides["tags"] == ["custom"]字典展开类似 JS 对象展开,后者覆盖前者。单独复制已知可变字段,不需要引入通用深度合并;没有使用 or,所以合法的 0 得到保留。
C. 构造请求参数
Section titled “C. 构造请求参数”要求:返回 query、limit、tags 和额外选项;默认列表独立;显式传入的标签列表也要复制;额外选项不能覆盖核心字段。本题约定 tags 为列表或 None。
def build_request(query, *, limit=10, tags=None, **options): return { **options, "query": query, "limit": limit, "tags": [] if tags is None else list(tags), }
first = build_request("python")second = build_request("agent")first["tags"].append("backend")assert second["tags"] == []
original_tags = ["rag"]request = build_request("python", tags=original_tags)request["tags"].append("agent")assert original_tags == ["rag"]assert request["tags"] == ["rag", "agent"]
request = build_request("python", limit=0, temperature=0.2)assert request["limit"] == 0assert request["temperature"] == 0.2
try: build_request("python", limit=10, **{"limit": 5})except TypeError: passelse: raise AssertionError("重复传参应该失败")这个签名已经将核心字段交给显式参数接收。build_request("python", **{"limit": 5}) 中,limit 进入显式参数,不进入 options;这是合法传参。重复提供 limit 时,Python 在进入函数体前抛 TypeError,所以不必再检查 "limit" in options。
六道面试题与简短答案
Section titled “六道面试题与简短答案”- Python 参数如何传递? 参数绑定到传入对象;修改对象可以影响调用者,重新绑定局部名称不会。
- 四种容器怎么选? list 保存有序可变集合;tuple 表达固定组合;dict 按键查找;set 去重和做集合运算。
- is 与 == 有何不同? 前者比较身份,后者比较相等。不同对象可以相等。
- 可变默认参数为何共享? 默认表达式在函数定义执行时求值,后续调用复用;用 None 在内部创建独立容器。
- 浅拷贝与深拷贝隔离什么? 浅拷贝只复制外层,深拷贝递归处理嵌套对象;资源对象和成本需要另外考虑。
- tuple 一定可哈希吗? 不一定,所有元素都可哈希才可以;
(1, ["python"])不能作为字典键。
合上参考答案,重新完成三个函数,并用断言验证零值、缺失字段、重复 ID 和对象隔离。能解释独立 *、浅拷贝和默认参数的行为,即完成今天的核心目标。