一 阶段学习
阶段 1:Pythonic 思维 & 数据模型(打好“世界观”)
对应书里:数据模型、序列、映射、切片等章节。
重点不是语法,而是:
- 认清几类“协议”(可以类比成“接口”/“约定”):
- 容器协议:
__len__、__getitem__ - 可迭代协议:
__iter__、__next__ - 上下文协议:
__enter__、__exit__
- 容器协议:
- 理解:为什么“实现几个魔法方法,一个类就能无缝融入整个 Python 生态”。
这个阶段的目标:以后你看到任意一个“高级特性”,都能从“数据模型/协议”角度去理解,而不是死记写法。
阶段 2:函数式风格 & 函数高级用法
对应:函数、闭包、装饰器、functools 等章节。
从“能写装饰器”升级到“知道什么时候用、高级用法怎么写”:
- 函数是“一等公民”:可以像数据一样传来传去、嵌套、返回
- 闭包:记住外层环境的函数(非常实用)
- 装饰器:横切逻辑(日志、缓存、鉴权)的一致写法
- 偏函数、
operator、functools:让代码更“函数式”和简洁
阶段 3:面向对象 & 元编程(包括你头疼的“元类”)
对应:类、抽象基类、描述符、__getattr__、元类相关章节。
这里我会用“类比 + 分层”来讲,不直接把元类当黑魔法丢给你:
- 先稳固普通 OOP + 协议/抽象基类
- 再讲“描述符”——理解属性访问是怎么被拦截/定制的
- 最后才到“元类”:类也是对象,谁来“制造类”?
记忆类比:
- 实例 对象 的“工厂”是 类
- 类 的“工厂”就是 元类
- 元类就相当于“开模具的工厂”,决定类长什么样、出生时做哪些事
阶段 4:并发 & 异步(重点帮你啃下来)
对应:并发、asyncio、协程章节。
重点不是 API 背下来,而是:
- 异步的核心类比:
- 线程:开多个“人”一起干活
- 异步:一个人干活,但在等待 I/O 时不发呆,去干别的
- 搞清楚3个关键概念:
- 协程对象(
async def返回的东西) - 事件循环(调度中心)
await(“这件事我等结果,但空档时间我先让出 CPU”)
- 协程对象(
- 带你把
async/await、asyncio这一块串起来,至少做到“看得懂+敢用”。
二、具体学习节奏(符合你要“快”的要求)
因为你是 C 档,我们不按“从零讲起”,而是这样:
- 每次选一个小主题(可跨1~3 小节)
- 例:
Python 数据模型总览、序列和切片的 Pythonic 写法、装饰器进阶等
- 例:
- 我先给你一份Typora 友好的小笔记:
- 结构会是:
- 核心结论
- 一图/一类比帮你记
- 1~2 个关键代码片段
- 常见坑 & 工作中能用上的点
- 结构会是:
- 如果你愿意,我给一个很小的练习(5~10 分钟)
- 你写完贴上来
- 我只针对“Pythonic 程度”和“设计思路”给你反馈
- 你觉得这个主题 OK之后,直接跟我说:“开始下一个”
- 我就自动切到下一个主题,不再问你“要不要继续”。
马上开始:第一站——Python 数据模型(总览)
对应《流畅的 Python》开头章节的“灵魂”:
全书后面的魔法方法、高级特性,都是围绕“数据模型”转的。
1. 数据模型是啥?——一个联想记忆
可以把 Python 数据模型 想象成一本“接口说明书”:
- “如果你的对象想被
len()用,就实现__len__” - “想被
for x in obj遍历,就实现__iter__或__getitem__” - “想支持
obj[a:b]切片,就实现__getitem__,懂得识别slice对象”
也就是说:
不是你去改 Python,而是按 Python 预先约定好的“钩子函数”来写类,让解释器自动帮你干活。
常见的几类魔法方法可以按“能力”分类记忆(你以后看到就知道哪一挂的):
- 表示 & 调试:
__repr__,__str__ - 容器协议:
__len__,__getitem__,__setitem__,__contains__ - 可迭代协议:
__iter__,__next__ - 上下文管理:
__enter__,__exit__ - 运算符重载:
__add__,__mul__,__eq__,__lt__等
2. 一个最小例子:写一个“迷你容器类”
类比:你自己写一个“清单对象”,让它看起来就像 list 一样能用
len()、能for、能用索引访问。
class TodoList:def __init__(self, tasks):self._tasks = list(tasks)def __len__(self):return len(self._tasks)def __getitem__(self, index):return self._tasks[index]def __repr__(self):return f"TodoList({self._tasks!r})"todos = TodoList(["写日报", "review MR", "修 bug"])print(len(todos)) # 3 -> 因为有 __len__
for t in todos: # for in 能用 -> 因为 __getitem__print(t)
print(todos[1]) # 支持索引
print(todos) # 有可读的 repr
关键点不是这个类多高级,而是:
- 你没继承 list,也没实现 for 循环
- 只是实现了
__len__和__getitem__,解释器就自动把它当“序列”看待
以后你看到别人写的一些看起来很“魔法”的类,本质都在这里:
实现合适的数据模型方法,就能骗过(准确说:说服)解释器把它当某种对象类型对待。
3. 这个对你实际工作有什么用?
结合你现在 C 档水平,下面是立即能用上的几点:
- 给自己的核心类补上
__repr__- 以后 log / 调试时,打印对象不再是
<User object at 0x...>,而是有信息的字符串 - 对排查线上问题非常有价值
- 以后 log / 调试时,打印对象不再是
- 写库/框架代码时,用“协议思维”设计接口:
- 比如某函数参数声明为“任何可迭代对象即可”(支持 list、tuple、生成器、你自定义的类),而不是死写 list
- 这就是《流畅的 Python》强调的“鸭子类型”和“协议”
- 读源码时,看见一堆
__xxx__不慌- 你先判断:这是在实现什么“能力”?
- 再去翻对应的数据模型说明(你以后基本能靠记忆/经验判断)
注释:
-
!rPython 对象的字符串表示:`__repr__`与`__str__`__repr__ 和 __str__ 是 Python 中控制对象“字符串表示”的两个核心魔术方法,核心差异在于设计目标(面向开发者 vs 面向用户),而 logger.info() 和 print() 对二者的调用逻辑不同,下面分维度讲清楚:一、__repr__ vs __str__ 核心区别先看一张表快速对比:特性__repr____str__设计目标面向开发者(调试/日志/还原对象)面向普通用户(友好展示/可读性优先)核心要求尽可能精准、无歧义,最好能 eval(repr(obj)) == obj简洁、易懂,无需还原对象调用触发(默认)所有未指定 str 的场景(如终端直接输对象)print(obj)/str(obj) 主动触发兜底逻辑若未定义,返回 <类名 object at 内存地址>若未定义,会自动调用 __repr__典型场景调试、日志、REPL 交互、异常栈用户展示、控制台打印、报告输出直观示例(最易理解)import datetimedt = datetime.datetime(2025, 12, 24, 10, 30)# __str__:用户友好的格式 print(str(dt)) # 输出:2025-12-24 10:30:00 print(dt) # 等价于 str(dt),输出同上# __repr__:开发者精准的格式(可还原对象) print(repr(dt)) # 输出:datetime.datetime(2025, 12, 24, 10, 30, 0) # 终端直接输 dt,触发 __repr__: # >>> dt # datetime.datetime(2025, 12, 24, 10, 30, 0)自定义类的示例(更贴近你的场景):class TodoList:def __init__(self, tasks):self.tasks = tasks# 定义 __repr__:面向开发者,精准展示属性def __repr__(self):return f"TodoList(tasks={self.tasks!r})" # !r 保证属性也是 repr 格式# 定义 __str__:面向用户,简化展示def __str__(self):return f"待办清单:{', '.join(self.tasks)}"tl = TodoList(["写代码", "吃饭\n睡觉"])# 触发 __str__ print(tl) # 输出:待办清单:写代码, 吃饭# 睡觉(\n 被解析,符合用户阅读习惯)# 触发 __repr__ print(repr(tl)) # 输出:TodoList(tasks=['写代码', '吃饭\n睡觉'])(保留 \n,精准) >>> tl # 终端直接输对象,输出同上二、print 场景:优先用 __str__,兜底 __repr__print(object) 的底层逻辑是:1. 尝试调用对象的 __str__ 方法;2. 若对象未定义 __str__,则自动调用 __repr__ 兜底。两种情况示例:# 情况1:只定义 __repr__,未定义 __str__ class TodoList:def __init__(self, tasks):self.tasks = tasksdef __repr__(self):return f"TodoList(tasks={self.tasks!r})"tl = TodoList(["写代码"]) print(tl) # 输出:TodoList(tasks=['写代码'])(兜底调用 __repr__)# 情况2:同时定义 __str__ 和 __repr__ class TodoList:def __init__(self, tasks):self.tasks = tasksdef __repr__(self):return f"TodoList(tasks={self.tasks!r})"def __str__(self):return f"待办:{self.tasks}"tl = TodoList(["写代码"]) print(tl) # 输出:待办:['写代码'](优先调用 __str__)三、日志场景(logger.info(object)):默认用 __repr__Python 日志模块(logging)的核心逻辑:- 当你直接传对象(如 logger.info(tl)),日志系统会调用 repr(object)(即 __repr__);- 若你传字符串拼接(如 logger.info(f"当前待办:{tl}")),则调用 str(object)(即 __str__)。关键示例(必看):import logginglogging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__)class TodoList:def __init__(self, tasks):self.tasks = tasksdef __repr__(self):return f"TodoList(tasks={self.tasks!r})" # 含 \n 等特殊字符def __str__(self):return f"待办清单:{', '.join(self.tasks)}"tl = TodoList(["写代码", "吃饭\n睡觉"])# 场景1:直接传对象 → 调用 __repr__(日志推荐!) logger.info(tl) # 日志输出:INFO:__main__:TodoList(tasks=['写代码', '吃饭\n睡觉']) # 优点:保留 \n 等特殊字符,调试时能看到真实数据# 场景2:字符串拼接 → 调用 __str__ logger.info(f"当前待办:{tl}") # 日志输出:INFO:__main__:当前待办:待办清单:写代码, 吃饭 # 睡觉(\n 被解析,日志换行,易造成格式混乱)四、日志/打印场景的最佳实践1. 打印场景(print)- 优先定义 __str__:给用户/终端展示简洁、易读的内容;- 无需过度追求精准,重点是“看得懂”。2. 日志场景(logger)- 核心原则:日志是给开发者/运维看的,要精准、无歧义,因此:- 推荐直接传对象(logger.info(obj)),依赖 __repr__;- 务必定义 __repr__,且用 !r 处理属性(如 self.tasks!r),保留特殊字符(\n、\t、引号等);- 避免用 f"xxx{obj}" 拼接日志(易丢失关键信息,或造成日志换行混乱);- 若日志需要“用户友好”的格式,可主动调用 str(obj):logger.info(f"待办清单(用户视角):{str(tl)}")3. 类的定义建议- 至少定义 __repr__:保证调试/日志有精准的字符串表示;- 按需定义 __str__:仅当需要给普通用户展示时才定义,否则用 __repr__ 兜底即可;- __repr__ 尽量满足“可还原对象”(如 TodoList(tasks=['写代码'])),方便调试时直接复制重建对象。总结场景触发的方法核心建议print(obj)优先 __str__,兜底 __repr__定义 __str__ 保证可读性logger.info(obj)__repr__定义 __repr__ 保证精准性,用!rlogger.info(f"{obj}")__str__日志尽量避免这种写法(易丢信息)终端直接输对象__repr__无(调试用,依赖 __repr__)简单记:__repr__ 是“给开发者看的调试字符串”,__str__ 是“给用户看的友好字符串”;日志用 __repr__,打印用 __str__。 -
__str__和__repr__:Python 对象的字符串表示:`__repr__`与`__str__`__repr__ 和 __str__ 是 Python 中控制对象“字符串表示”的两个核心魔术方法,核心差异在于设计目标(面向开发者 vs 面向用户),而 logger.info() 和 print() 对二者的调用逻辑不同,下面分维度讲清楚:一、__repr__ vs __str__ 核心区别先看一张表快速对比:特性__repr____str__设计目标面向开发者(调试/日志/还原对象)面向普通用户(友好展示/可读性优先)核心要求尽可能精准、无歧义,最好能 eval(repr(obj)) == obj简洁、易懂,无需还原对象调用触发(默认)所有未指定 str 的场景(如终端直接输对象)print(obj)/str(obj) 主动触发兜底逻辑若未定义,返回 <类名 object at 内存地址>若未定义,会自动调用 __repr__典型场景调试、日志、REPL 交互、异常栈用户展示、控制台打印、报告输出直观示例(最易理解)import datetimedt = datetime.datetime(2025, 12, 24, 10, 30)# __str__:用户友好的格式 print(str(dt)) # 输出:2025-12-24 10:30:00 print(dt) # 等价于 str(dt),输出同上# __repr__:开发者精准的格式(可还原对象) print(repr(dt)) # 输出:datetime.datetime(2025, 12, 24, 10, 30, 0) # 终端直接输 dt,触发 __repr__: # >>> dt # datetime.datetime(2025, 12, 24, 10, 30, 0)自定义类的示例(更贴近你的场景):class TodoList:def __init__(self, tasks):self.tasks = tasks# 定义 __repr__:面向开发者,精准展示属性def __repr__(self):return f"TodoList(tasks={self.tasks!r})" # !r 保证属性也是 repr 格式# 定义 __str__:面向用户,简化展示def __str__(self):return f"待办清单:{', '.join(self.tasks)}"tl = TodoList(["写代码", "吃饭\n睡觉"])# 触发 __str__ print(tl) # 输出:待办清单:写代码, 吃饭# 睡觉(\n 被解析,符合用户阅读习惯)# 触发 __repr__ print(repr(tl)) # 输出:TodoList(tasks=['写代码', '吃饭\n睡觉'])(保留 \n,精准) >>> tl # 终端直接输对象,输出同上二、print 场景:优先用 __str__,兜底 __repr__print(object) 的底层逻辑是:1. 尝试调用对象的 __str__ 方法;2. 若对象未定义 __str__,则自动调用 __repr__ 兜底。两种情况示例:# 情况1:只定义 __repr__,未定义 __str__ class TodoList:def __init__(self, tasks):self.tasks = tasksdef __repr__(self):return f"TodoList(tasks={self.tasks!r})"tl = TodoList(["写代码"]) print(tl) # 输出:TodoList(tasks=['写代码'])(兜底调用 __repr__)# 情况2:同时定义 __str__ 和 __repr__ class TodoList:def __init__(self, tasks):self.tasks = tasksdef __repr__(self):return f"TodoList(tasks={self.tasks!r})"def __str__(self):return f"待办:{self.tasks}"tl = TodoList(["写代码"]) print(tl) # 输出:待办:['写代码'](优先调用 __str__)三、日志场景(logger.info(object)):默认用 __repr__Python 日志模块(logging)的核心逻辑:- 当你直接传对象(如 logger.info(tl)),日志系统会调用 repr(object)(即 __repr__);- 若你传字符串拼接(如 logger.info(f"当前待办:{tl}")),则调用 str(object)(即 __str__)。关键示例(必看):import logginglogging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__)class TodoList:def __init__(self, tasks):self.tasks = tasksdef __repr__(self):return f"TodoList(tasks={self.tasks!r})" # 含 \n 等特殊字符def __str__(self):return f"待办清单:{', '.join(self.tasks)}"tl = TodoList(["写代码", "吃饭\n睡觉"])# 场景1:直接传对象 → 调用 __repr__(日志推荐!) logger.info(tl) # 日志输出:INFO:__main__:TodoList(tasks=['写代码', '吃饭\n睡觉']) # 优点:保留 \n 等特殊字符,调试时能看到真实数据# 场景2:字符串拼接 → 调用 __str__ logger.info(f"当前待办:{tl}") # 日志输出:INFO:__main__:当前待办:待办清单:写代码, 吃饭 # 睡觉(\n 被解析,日志换行,易造成格式混乱)四、日志/打印场景的最佳实践1. 打印场景(print)- 优先定义 __str__:给用户/终端展示简洁、易读的内容;- 无需过度追求精准,重点是“看得懂”。2. 日志场景(logger)- 核心原则:日志是给开发者/运维看的,要精准、无歧义,因此:- 推荐直接传对象(logger.info(obj)),依赖 __repr__;- 务必定义 __repr__,且用 !r 处理属性(如 self.tasks!r),保留特殊字符(\n、\t、引号等);- 避免用 f"xxx{obj}" 拼接日志(易丢失关键信息,或造成日志换行混乱);- 若日志需要“用户友好”的格式,可主动调用 str(obj):logger.info(f"待办清单(用户视角):{str(tl)}")3. 类的定义建议- 至少定义 __repr__:保证调试/日志有精准的字符串表示;- 按需定义 __str__:仅当需要给普通用户展示时才定义,否则用 __repr__ 兜底即可;- __repr__ 尽量满足“可还原对象”(如 TodoList(tasks=['写代码'])),方便调试时直接复制重建对象。总结场景触发的方法核心建议print(obj)优先 __str__,兜底 __repr__定义 __str__ 保证可读性logger.info(obj)__repr__定义 __repr__ 保证精准性,用!rlogger.info(f"{obj}")__str__日志尽量避免这种写法(易丢信息)终端直接输对象__repr__无(调试用,依赖 __repr__)简单记:__repr__ 是“给开发者看的调试字符串”,__str__ 是“给用户看的友好字符串”;日志用 __repr__,打印用 __str__。
序列与切片的 Pythonic 用法速通
那我们就按计划,进入:序列与切片的 Pythonic 用法速通。
一、本节目标(你学完应该能做到什么)
- 写循环时,优先想到:列表推导式 / 生成器表达式
- 面对索引、切片相关代码,看一眼就知道有没有写“土味”
- 知道在什么场景下选
list/tuple/deque等
二、先把“序列大家族”分个类(一个简单脑图)
用一个记忆法:两条线划分——“是不是数字专用”、“是不是可变”。
-
数字专用:
array.array(同类型数字,节省内存)
-
通用不可变序列:
tuple、str、bytes
-
通用可变序列:
list、bytearray、collections.deque
够你日常工作 99% 场景的是:
-
一般顺序数据:
list -
不会变、用来当“记录”:
tuple -
两端频繁增删:
collections.deque
类比记忆:
list:普通货架tuple:封箱打包好的箱子(不再改)deque:推车,两头都方便装卸
三、列表推导式 & 生成器表达式:Pythonic 的基本气质
1. 列表推导式:for 循环的“压缩写法”,但更清晰
典型土味写法:
squares = []
for i in range(10):squares.append(i * i)
更 Pythonic 的写法:
squares = [i * i for i in range(10)]
带条件过滤:
even_squares = [i * i for i in range(10) if i % 2 == 0]
记忆小点:
- 读顺序:左边结果表达式 ← for ← if 条件(可选)
- 当逻辑很简单时,可读性反而更好(因为“全部逻辑都在一行可见”)
2. 生成器表达式:节省内存的“懒序列”
列表推导式会一次性生成完整列表;
生成器表达式只在需要时算下一项。
# 列表推导式
squares_list = [i * i for i in range(10)]# 生成器表达式(注意括号)
squares_gen = (i * i for i in range(10))for x in squares_gen:print(x)
使用场景的直觉记忆:
- 结果要重复用很多次 → 列表推导式(直接拿 list 比较方便)
- 只迭代一遍就完了/数据量可能很大 → 生成器表达式(省内存)
四、切片:不只是 list[1:3] 那么简单
1. 基本形式:seq[start:end:step]
start:起始索引(含),默认 0end:结束索引(不含),默认 len(seq)step:步长,默认 1
典型例子:
a = [0, 1, 2, 3, 4, 5]print(a[1:4]) # [1, 2, 3]
print(a[:3]) # [0, 1, 2]
print(a[3:]) # [3, 4, 5]
print(a[::2]) # [0, 2, 4]
2. 负索引 & 负步长:从右边、逆序看世界
a = [0, 1, 2, 3, 4, 5]print(a[-1]) # 5
print(a[-2:]) # [4, 5]
print(a[::-1]) # [5, 4, 3, 2, 1, 0] 逆序
print(a[-3::-1]) # [3, 2, 1, 0]
记忆类比:
- 负索引:从右往左数
- 负步长:倒着走
3. 切片其实是个对象:slice
在需要复用一段切片逻辑时,slice 很方便:
s = 'Hello, Python'first_word = slice(0, 5) # 相当于 [0:5]
print(s[first_word]) # 'Hello'
你也可以把 slice 存在类里、配置里,而不是写死在代码中。
4. 切片赋值:list 的一大特技
这一点是很多人忽略的 Pythonic 写法:
a = [1, 2, 3, 4, 5]a[1:3] = [20, 30] # 替换某一段
print(a) # [1, 20, 30, 4, 5]a[1:3] = [7, 8, 9, 10] # 长度可以变化
print(a) # [1, 7, 8, 9, 10, 4, 5]# 删除一段(赋空列表)
a[1:4] = []
print(a) # [1, 10, 4, 5]
和 del 联合使用也很常见:
del a[1:3]
类比:你可以一下子“换掉这一段”,而不是对每个元素单独操作。
5. 一个工作中非常常见的坑:浅拷贝 vs 引用
很多人会写:
b = a # 这不是 copy,只是引用同一个对象
正确的浅拷贝方法之一:全切片:
b = a[:] # 浅拷贝
或者更直白:
b = list(a)
差异示例:
a = [1, 2, 3]
b = a
c = a[:]a.append(4)
print(b) # [1, 2, 3, 4] b 跟着变
print(c) # [1, 2, 3] c 不变
五、常见“不 Pythonic 写法”对比(帮你自查项目)
你在项目里可以留意以下几种写法,看到就可以考虑重构。
1. 冗长循环 + append 可以考虑用列表推导
# 不太 Pythonic
result = []
for user in users:if user.is_active:result.append(user.id)# 更推荐
result = [user.id for user in users if user.is_active]
前提:逻辑简单,读起来一眼就够;
如果 if/for 里面逻辑很复杂,还是拆成多行更清晰。
2. 手写逆序 / 拷贝
# 逆序
rev = list(reversed(a)) # 👍
rev = a[::-1] # 👍
# 用 range(len(a)-1, -1, -1) 再 index 的就比较土# 拷贝
copy1 = a[:] # 👍
copy2 = list(a) # 👍
六、给你一个很小的练习(5~10 分钟,选做)
假设你在写一个简单的日志过滤工具:
- 有一个
logs列表,每个元素是这样的 dict:
logs = [{"level": "INFO", "message": "start", "user": "alice"},{"level": "ERROR", "message": "failed", "user": "bob"},{"level": "WARNING", "message": "slow", "user": "alice"},{"level": "ERROR", "message": "timeout", "user": "alice"},]
- 请你写两段代码(尽量用列表推导式 + 切片):
- 取出所有
ERROR级别日志的message列表(比如["failed", "timeout"]) - 只保留前 n 条(用切片完成),比如前 1 条
映射(dict)、拆包与 Pythonic 数据处理风格
那我们就进入下一块:映射(dict)和拆包:Pythonic 数据处理风格。
一、本节你要掌握的东西
- 把
dict当成“默认数据结构”来用,而不是只会d[key] = value - 会用 字典推导式、
get/setdefault等更优雅地处理数据 - 熟悉各种 拆包 写法:
a, b = ...、*rest、**config,减少大量啰嗦代码
你以后在写业务逻辑、适配接口、做数据清洗时,会非常常用。
二、映射(dict)的核心思维
1. dict 是“带标签的盒子”
类比:
- list:一排格子,只能用“编号”找:
arr[0] - dict:一堆盒子,每个盒子贴了标签:
d['user_id']
典型使用:
user = {"id": 123,"name": "Alice","is_active": True,
}
常用访问方式:
python
user["id"] # 不存在会报 KeyError user.get("id") # 不存在返回 None user.get("id", 0) # 不存在返回默认值 0
记忆点:
凡是“字段不一定有”的场景,优先考虑 get,而不是 try/except 或 in 判断一大堆。
2. 字典推导式:处理结构化数据最顺手的工具之一
例子:把一堆 (key, value) 对变成 dict,只取满足条件的。
user["id"] # 不存在会报 KeyError
user.get("id") # 不存在返回 None
user.get("id", 0) # 不存在返回默认值 0
再比如:把所有 value 做一个简单变换:
pairs = [("a", 1), ("b", 2), ("c", 3)]d = {k: v for (k, v) in pairs if v % 2 == 1}
# d -> {"a": 1, "c": 3}
一眼就能看清“输入 + 变换 + 条件”,比 for 循环拼起来清爽。
3. dict 的几个常用“视图”
scores = {"alice": 80, "bob": 90}
curved = {name: score + 5 for name, score in scores.items()}
# {"alice": 85, "bob": 95}
典型用法:
d = {"a": 1, "b": 2}d.keys() # dict_keys(['a', 'b'])
d.values() # dict_values([1, 2])
d.items() # dict_items([('a', 1), ('b', 2)])
比 for k in d: v = d[k] 更清晰,也少一次查找。
4. 合并和更新多个 dict(非常常用)
Python 3.9+:用 | 和 |=
a = {"x": 1, "y": 2}
b = {"y": 20, "z": 3}c = a | b # {"x": 1, "y": 20, "z": 3} b 覆盖 a
a |= b # a 变成 {"x": 1, "y": 20, "z": 3}
兼容老版本:用解包
c = {**a, **b}
类比:
{**a, **b}就像“先把 a 全倒进去,再倒 b(冲掉同名)”。
5. setdefault:统计 / 分组的利器
土味写法:
groups = {}
for user in users:dept = user.departmentif dept not in groups:groups[dept] = []groups[dept].append(user)
更 Pythonic:
groups = {}
for user in users:groups.setdefault(user.department, []).append(user)
setdefault(key, default) 的行为:
- 如果 key 不在 dict 中,就先设为 default,然后返回这个值
- 如果已存在,就直接返回原有的值
非常适合“按 key 分组、累积列表/计数”的场景。
三、拆包:把“结构”拆成变量 & 把变量打包成参数
拆包是 Fluent Python 里反复出现的一个“底层动作”,把它用顺了很多写法自然就 Pythonic 了。
1. 元组拆包:平行赋值
point = (10, 20)
x, y = point # x=10, y=20
用于函数返回多个值时非常舒服:
def get_bounds(data):return min(data), max(data)low, high = get_bounds([3, 1, 5])
类比:函数把结果打包成“盒子”(tuple),调用方拆盒子拿东西。
2. 带 * 的拆包:保留一头,吃掉中间一串
head, *middle, tail = [1, 2, 3, 4, 5]
# head = 1, middle = [2, 3, 4], tail = 5first, *_, last = [10, 20, 30, 40]
# _ 表示“我不在乎中间那些东西”
非常适合日志解析、路径拆分等“只要头尾,不在乎中间”的场景。
3. 函数调用时的 :把序列拆成参数
有一个函数:
def add(x, y, z):return x + y + z
现在有个列表:
params = [1, 2, 3]
add(*params) # 等价于 add(1, 2, 3)
类比:list/tuple 是一包参数,
*是“把这包拆开顺序塞进去”。
到你工作里,非常常见的场景是把“配好的参数”传给库函数,比如 requests、数据库驱动等。
4. 函数调用时的 :用 dict 传“带名字的参数”
config = {"host": "localhost","port": 6379,"db": 0,
}def connect(host, port, db):...connect(**config) # 等价于 connect(host="localhost", port=6379, db=0)
这在实际业务中用得非常多:
- 配置从文件/环境读出来 → dict
- 函数/类构造函数需要一堆 named 参数
**config一把灌进去
也可以自由组合:
default = {"host": "localhost", "port": 6379}
override = {"port": 6380}final = {**default, **override}
connect(**final)
四、这一节的“联想记忆”总结
可以用两个比喻把这一块挂在脑子里:
-
dict = 带标签的盒子
- list:只能按编号拿
- dict:写清楚标签再装,后面你就不用记“0 是 name、1 是 age”这种脑力消耗大的东西
-
拆包 = 拆快递
- 函数返回多个值 → 装在一个盒子里(tuple)
a, b = .../*rest/**config→ 拆盒子,按名字、位置分给不同人、不同函数
你以后看到「大量 if key in dict」、「手写一堆
params["x"] = x」这种代码,就可以思考是不是可以用:
get/setdefault/ 字典推导*args/**kwargs/ 返回 tuple + 拆包
来简化。
函数进阶:闭包、装饰器和 functools
好的,我们进入下一块:函数进阶:闭包、装饰器和 functools。
一、本节目标(你学完应该掌握什么)
- 理解闭包的“环境记忆”机制(知道它为什么能记住外层变量)
- 能自己写装饰器(包括带参数的装饰器)
- 熟悉
functools模块几个常用的工具(wraps,lru_cache,partial) - 能看懂源码里的高级装饰器实现
二、闭包:函数记住它“出生时”的环境
1. 一个最简单的闭包例子
def make_multiplier(x):def multiplier(y):return x * yreturn multiplierdouble = make_multiplier(2)
triple = make_multiplier(3)print(double(5)) # 10
print(triple(5)) # 15
关键点:
multiplier函数用到了外层函数make_multiplier的参数xmake_multiplier执行完返回multiplier时,x已经“超出生命周期”了- 但 Python 会把
x封在multiplier里面,形成一个“闭包” - 所以
double和triple各自记住了自己的x(2 和 3)
2. 闭包的本质记忆法
可以这样联想:
- 普通函数:一张“菜谱”(输入 → 输出)
- 闭包:一个“带调料包的菜谱”(输入 → 输出 + 环境状态)
multiplier 函数不只是一个函数,它还“随身携带”了它创建时的环境变量 x。
3. 一个工作中常见的闭包用法:配置生成器
def make_validator(min_val, max_val):def validate(value):return min_val <= value <= max_valreturn validateage_validator = make_validator(0, 150)
score_validator = make_validator(0, 100)print(age_validator(25)) # True
print(score_validator(105)) # False
比写成 class 更轻量,比写成全局函数更灵活。
三、装饰器:给函数“穿衣服”
1. 装饰器的本质:函数接收函数,返回函数
def my_decorator(func):def wrapper(*args, **kwargs):print("函数调用前")result = func(*args, **kwargs)print("函数调用后")return resultreturn wrapper@my_decorator
def greet(name):print(f"Hello, {name}!")greet("Alice")
# 输出:
# 函数调用前
# Hello, Alice!
# 函数调用后
@my_decorator 等价于:
greet = my_decorator(greet)
记忆点:
- 装饰器 = 一个接收函数、返回函数的函数
wrapper里用*args, **kwargs保证能适配任何函数result = func(...)执行原函数,然后可以对结果做处理
2. 带参数的装饰器:装饰器的装饰器
def retry(max_times):def decorator(func):def wrapper(*args, **kwargs):for i in range(max_times):try:return func(*args, **kwargs)except Exception as e:if i == max_times - 1:raise eprint(f"第 {i+1} 次尝试失败")return wrapperreturn decorator@retry(max_times=3)
def unreliable_api():import randomif random.random() < 0.7:raise Exception("网络错误")return "成功"
记忆法:
- 普通装饰器:
@decorator→decorator(func) - 带参数装饰器:
@decorator(arg)→decorator(arg)(func)
所以结构是:三层函数
3. 装饰器的“坑”:丢失原函数信息
@my_decorator
def greet(name):"""Greet someone."""print(f"Hello, {name}!")print(greet.__name__) # wrapper,不是 greet
print(greet.__doc__) # None,不是 "Greet someone."
解决:用 functools.wraps
from functools import wrapsdef my_decorator(func):@wraps(func) # 复制原函数的 __name__, __doc__ 等def wrapper(*args, **kwargs):print("函数调用前")result = func(*args, **kwargs)print("函数调用后")return resultreturn wrapper
四、functools 模块:让你的函数更“智能”
1. lru_cache:简单的记忆化
from functools import lru_cache@lru_cache(maxsize=128)
def fibonacci(n):if n < 2:return nreturn fibonacci(n-1) + fibonacci(n-2)print(fibonacci(100)) # 非常快,因为中间结果被缓存了
适用场景:
- 递归算法(如斐波那契、动态规划)
- 重复计算某个值(比如根据 id 查用户信息)
2. partial:函数“预配置”
from functools import partialdef power(base, exponent):return base ** exponentsquare = partial(power, exponent=2)
cube = partial(power, exponent=3)print(square(5)) # 25
print(cube(3)) # 27
类比:
partial 就像“给一个通用工具配好默认参数,变成专用工具”。
在工作中常见用法:
# 把一个复杂函数,变成几个简单函数
from urllib.parse import urlencodemake_api_url = partial(urlencode, doseq=True)# 或者配合 map/filter
from operator import addadd_to_ten = partial(add, 10)
list(map(add_to_ten, [1, 2, 3])) # [11, 12, 13]
3. singledispatch:Python 的“多态”模拟
from functools import singledispatch@singledispatch
def format_data(obj):return str(obj)@format_data.register
def _(obj: int):return f"整数: {obj}"@format_data.register
def _(obj: str):return f"字符串: {obj.upper()}"print(format_data(42)) # 整数: 42
print(format_data("hello")) # 字符串: HELLO
记忆法:
类似 Java/C++ 里的“函数重载”,但 Python 用装饰器来实现。
五、一个综合练习(可选,加深理解)
你可以试着写一个“日志装饰器”,它有这些功能:
- 记录函数名、参数、返回值
- 记录函数执行时间
- 可以通过参数控制是否记录(比如
@log(enabled=True))
import time
from functools import wrapsdef log(enabled=True):def decorator(func):@wraps(func)def wrapper(*args, **kwargs):if not enabled:return func(*args, **kwargs)start = time.time()print(f"调用 {func.__name__}({args}, {kwargs})")result = func(*args, **kwargs)elapsed = time.time() - startprint(f"返回 {result},耗时 {elapsed:.3f}s")return resultreturn wrapperreturn decorator@log(enabled=True)
def add(a, b):time.sleep(0.1) # 模拟耗时return a + badd(3, 5)
六、本节的“联想记忆”总结
-
闭包 = 函数 + 环境
- 函数可以“记住”它创建时的变量
- 适合做“带配置的函数生成器”
-
装饰器 = 函数的包装纸
- 一个函数套另一个函数
@wraps记得加,不然原函数信息就丢了
-
functools = 函数工具箱
lru_cache:给函数加个“记忆”partial:函数预配置singledispatch:模拟重载
你以后看到别人写的装饰器,可以按“三层函数”去拆解;看到
functools的用法,知道它是“让函数更聪明”的标准库。
面向对象进阶:抽象基类、协议、描述符、元类
好的,我们进入下一块:面向对象进阶:抽象基类、协议、描述符、元类。
一、本节目标(你学完应该掌握什么)
- 理解 Python 的“协议”思想(鸭子类型)和抽象基类
- 能用描述符控制属性访问(比如验证、延迟加载等)
- 理解元类的本质(类的“工厂”),并能看懂源码中的元类用法
这一块是《流畅的 Python》里比较“硬核”的部分,但也是理解 Python 面向对象精髓的关键。
二、协议(Protocol):Python 的“接口”思想
1. 什么是协议?
协议就是“约定好的一套行为规范”,Python 不需要显式声明,只要对象实现了对应的方法,就认为它遵循了某个协议。
比如:
- 可迭代协议:实现了
__iter__或__getitem__ - 上下文管理协议:实现了
__enter__和__exit__ - 序列协议:实现了
__len__和__getitem__
class MyList:def __init__(self, items):self.items = itemsdef __len__(self):return len(self.items)def __getitem__(self, index):return self.items[index]my_list = MyList([1, 2, 3])
print(len(my_list)) # 3
for item in my_list: # for 能用,因为实现了 __getitem__print(item)
2. “鸭子类型”:像鸭子就当它是鸭子
“If it walks like a duck and it quacks like a duck, then it must be a duck.”
类比:Python 不关心“你是什么类型”,只关心“你能干什么”。
def process_sequence(seq):for item in seq:print(item)# 可以传 list, tuple, 自定义类, 生成器... 只要是可迭代的就行
process_sequence([1, 2, 3])
process_sequence((4, 5, 6))
三、抽象基类(ABC):强制实现协议
1. 为什么需要抽象基类?
协议是“约定”,但不强制。如果你想强制别人实现某些方法,就需要抽象基类。
from abc import ABC, abstractmethodclass Animal(ABC):@abstractmethoddef make_sound(self):passclass Dog(Animal):def make_sound(self):return "Woof!"# Animal() # 会报错,不能实例化抽象类
dog = Dog() # OK
2. 常用的内置抽象基类
from collections.abc import Iterable, Mapping, MutableMappingisinstance([], Iterable) # True
isinstance({}, Mapping) # True
isinstance({}, MutableMapping) # True
工作中可以用它来“类型检查”:
def process_data(data):if not isinstance(data, Mapping):raise TypeError("data must be a mapping")# ...
四、描述符(Descriptor):控制属性访问
描述符让你可以自定义属性的“获取/设置/删除”行为。
1. 描述符协议
实现以下任意一个方法的类就是描述符:
__get__:获取属性时调用__set__:设置属性时调用__delete__:删除属性时调用
2. 一个简单的验证描述符
class ValidatedAttribute:def __init__(self, validator=None):self.validator = validatordef __set_name__(self, owner, name):self.name = namedef __get__(self, obj, objtype=None):if obj is None:return selfreturn obj.__dict__.get(self.name)def __set__(self, obj, value):if self.validator and not self.validator(value):raise ValueError(f"Invalid value for {self.name}: {value}")obj.__dict__[self.name] = valueclass Person:name = ValidatedAttribute(lambda x: len(x) > 0)age = ValidatedAttribute(lambda x: 0 <= x <= 150)p = Person()
p.name = "Alice" # OK
p.age = 25 # OK
# p.age = -1 # 会抛出 ValueError
3. 描述符的“坑”:实例字典 vs 类字典
class Quantity:def __init__(self, storage_name):self.storage_name = storage_namedef __set__(self, obj, value):if value <= 0:raise ValueError('value must be > 0')obj.__dict__[self.storage_name] = valuedef __get__(self, obj, objtype=None):if obj is None:return selfreturn obj.__dict__[self.storage_name]class LineItem:weight = Quantity('weight')price = Quantity('price')item = LineItem()
item.weight = 10
item.price = 5
记忆点:
描述符是“类属性”,所以所有实例共享同一个描述符对象,但数据存在各自的 __dict__ 中。
五、元类(Metaclass):类的“工厂”
1. 元类的本质
- 对象 ← 类(实例化)
- 类 ← 元类(实例化)
class MyClass:x = 1# 等价于
MyClass = type('MyClass', (object,), {'x': 1})
type 就是默认的元类。
2. 自定义元类:给所有类加个“注册”功能
class RegisterMeta(type):registry = {}def __new__(cls, name, bases, attrs):new_cls = super().__new__(cls, name, bases, attrs)cls.registry[name] = new_clsreturn new_clsclass Model(metaclass=RegisterMeta):passclass User(Model):passclass Order(Model):passprint(RegisterMeta.registry) # {'Model': <class '__main__.Model'>, 'User': <class '__main__.User'>, ...}
3. 元类的“记忆法”
- 类是“创建对象的模板”
- 元类是“创建类的模板”
- 类比:
- 工厂(类) → 生产零件(对象)
- 工厂设计图(元类) → 生产工厂(类)
4. 什么时候用元类?
元类比较少用,通常只在以下场景:
- 框架开发(比如 ORM 框架自动添加属性)
- 代码自动生成
- 强制所有子类实现某些特性
工作中 99% 的场景,用装饰器、描述符、ABC 就够了。
六、一个综合例子:用协议 + 描述符实现一个 ORM 字段
from abc import ABC, abstractmethodclass Field(ABC):def __init__(self, name=None):self.name = namedef __set_name__(self, owner, name):if self.name is None:self.name = namedef __get__(self, obj, objtype=None):if obj is None:return selfreturn obj.__dict__.get(self.name)def __set__(self, obj, value):if not self.validate(value):raise ValueError(f"Invalid value for {self.name}: {value}")obj.__dict__[self.name] = value@abstractmethoddef validate(self, value):passclass IntegerField(Field):def validate(self, value):return isinstance(value, int) and value >= 0class StringField(Field):def validate(self, value):return isinstance(value, str) and len(value) <= 100class Model:def __init__(self, **kwargs):for name, value in kwargs.items():setattr(self, name, value)class User(Model):name = StringField()age = IntegerField()user = User(name="Alice", age=25)
print(user.name) # Alice
print(user.age) # 25
七、本节“联想记忆”总结
-
协议 = 约定的行为
- 不用继承,只要实现对应方法就行
- “像鸭子就当鸭子”是核心思想
-
抽象基类 = 强制的协议
- 用
@abstractmethod标记必须实现的方法 - 常用
collections.abc里的基类做类型检查
- 用
-
描述符 = 属性的“代理”
- 控制
obj.attr的读写行为 - 适合做验证、延迟加载、缓存等
- 控制
-
元类 = 类的“工厂”
- 很少用,通常框架才需要
- 记住:类是对象,元类是“类的类”就行
你以后看到源码里有
__get__/__set__就知道是描述符;看到metaclass=就知道是元类在作怪。
并发与并行:多线程、多进程、协程
好的,我们进入下一块:并发与并行:多线程、多进程、协程。
这一块是你之前说“比较难”的部分,我会用大量类比和图示帮你理解,让你从“知道 API”升级到“知道什么时候用、为什么用”。
一、本节目标(你学完应该掌握什么)
- 理解 GIL(全局解释器锁)对 Python 并发的影响
- 知道多线程、多进程、协程的适用场景
- 能用
asyncio写异步代码 - 理解事件循环和
await的本质
二、先搞清楚几个概念(类比法记忆)
1. 并发 vs 并行
- 并发(Concurrency):一个人交替做多件事(比如做饭时,煮饭的同时切菜)
- 并行(Parallelism):多个人同时做多件事(比如一家四口一人做一道菜)
在 Python 里:
- 多线程:适合 I/O 密集型(网络请求、文件读写)
- 多进程:适合 CPU 密集型(计算、加密)
- 协程:适合高并发 I/O(比如爬虫、Web 服务)
2. GIL(全局解释器锁):Python 的“单线程锁”
GIL 是 CPython 解释器的特性,保证同一时刻只有一个线程在执行 Python 字节码。
关键影响:
- 多线程无法利用多核 CPU(Python 代码层面)
- 但对 I/O 操作不影响(I/O 时会释放 GIL)
类比:
一个厨房(CPU),但只有一把刀(GIL),所以一个人在用刀时,其他人只能等着。
三、多线程:适合 I/O 密集型任务
1. 基本用法
import threading
import timedef worker(name):print(f"Worker {name} started")time.sleep(2) # 模拟 I/O 等待print(f"Worker {name} finished")# 创建线程
threads = []
for i in range(3):t = threading.Thread(target=worker, args=(f"Thread-{i}",))threads.append(t)t.start()# 等待所有线程结束
for t in threads:t.join()
2. 线程安全:共享数据的问题
import threadingcounter = 0
lock = threading.Lock()def increment():global counterfor _ in range(100000):with lock: # 加锁保证线程安全counter += 1# 多线程执行
threads = []
for _ in range(2):t = threading.Thread(target=increment)threads.append(t)t.start()for t in threads:t.join()print(counter) # 200000
记忆点:
- 用
threading.Lock()或with lock:保证数据安全 - 适合网络请求、文件操作等 I/O 密集型任务
四、多进程:绕过 GIL,真正并行
1. 基本用法
import multiprocessing
import timedef cpu_bound_task(n):# CPU 密集型任务result = 0for i in range(n):result += i * ireturn resultif __name__ == "__main__":processes = []start = time.time()# 串行执行for _ in range(4):cpu_bound_task(1000000)print(f"串行耗时: {time.time() - start:.2f}s")# 并行执行start = time.time()with multiprocessing.Pool(processes=4) as pool:results = pool.map(cpu_bound_task, [1000000] * 4)print(f"并行耗时: {time.time() - start:.2f}s")
记忆点:
- 多进程有启动开销,适合 CPU 密集型任务
- 进程间不共享内存,需要用
Queue、Pipe等通信
2. 进程间通信
import multiprocessingdef worker(queue):queue.put("Hello from process")if __name__ == "__main__":queue = multiprocessing.Queue()p = multiprocessing.Process(target=worker, args=(queue,))p.start()p.join()print(queue.get()) # "Hello from process"
五、协程(asyncio):高并发 I/O 的终极武器
1. 协程的核心类比
多线程: 开多个“人”一起干活(开销大)
协程: 一个人干活,但 I/O 时主动让出 CPU,去干别的(开销小)
import asyncioasync def fetch_data(url):print(f"开始获取 {url}")await asyncio.sleep(1) # 模拟网络请求print(f"完成获取 {url}")return f"data from {url}"async def main():tasks = [fetch_data("url1"),fetch_data("url2"),fetch_data("url3"),]results = await asyncio.gather(*tasks)print(results)# 运行协程
asyncio.run(main())
2. async / await 的本质
async def:定义协程函数(返回协程对象)await:暂停当前协程,把控制权交给事件循环,等 I/O 完成再回来
类比:
async def:写一个“可以暂停的任务”await:说“这个事我要等结果,但等的时候我可以先干别的”
3. 事件循环:协程的“调度中心”
import asyncioasync def task1():print("Task 1 start")await asyncio.sleep(1)print("Task 1 end")async def task2():print("Task 2 start")await asyncio.sleep(1)print("Task 2 end")async def main():await asyncio.gather(task1(), task2())asyncio.run(main())
4. 实际工作中的异步爬虫例子
import asyncio
import aiohttpasync def fetch_url(session, url):async with session.get(url) as response:return await response.text()async def fetch_all_urls(urls):async with aiohttp.ClientSession() as session:tasks = [fetch_url(session, url) for url in urls]results = await asyncio.gather(*tasks)return results# 使用
urls = ["http://example.com", "http://google.com", "http://github.com"]
results = asyncio.run(fetch_all_urls(urls))
优势:
- 比多线程更轻量(内存占用少)
- 比多线程更高效(I/O 并发度更高)
- 适合高并发网络请求场景
六、选择指南:什么时候用什么?
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| CPU 密集型(计算、加密) | 多进程 | 绕过 GIL,真正并行 |
| I/O 密集型(网络请求、文件读写) | 协程 > 多线程 | 协程开销更小,并发度更高 |
| 简单并发(少量任务) | 多线程 | 上手简单,代码直观 |
| 需要共享状态 | 多进程 + Queue | 避免线程安全问题 |
七、asyncio 的几个关键概念
1. 协程对象 vs 任务
async def hello():await asyncio.sleep(1)return "Hello"coro = hello() # 协程对象,还没执行
task = asyncio.create_task(coro) # 任务,会被调度执行
2. asyncio.gather vs asyncio.wait
# gather:等待所有任务完成,返回结果列表
results = await asyncio.gather(task1(), task2())# wait:更灵活,可以指定完成条件
done, pending = await asyncio.wait([task1(), task2()], return_when=asyncio.FIRST_COMPLETED)
3. 在同步代码中调用异步函数
import asynciodef sync_function():# 在同步函数中运行异步代码return asyncio.run(async_function())async def async_function():await asyncio.sleep(1)return "done"
八、本节“联想记忆”总结
-
GIL 是“厨房只有一把刀”
- 多线程不能真正并行执行 Python 代码
- 但不影响 I/O 操作(I/O 时刀可以借给别人)
-
多线程 = 多个人干 I/O 活
- 适合网络请求、文件读写
- 记得加锁保护共享数据
-
多进程 = 多个厨房干 CPU 活
- 绕过 GIL,真正并行
- 适合计算密集型任务
-
协程 = 一个人聪明地干 I/O 活
- I/O 时主动让出 CPU 去干别的
- 最适合高并发网络请求
你以后看到并发需求,可以先问自己:
- 是 CPU 密集型还是 I/O 密集型?
- 并发量有多大?
- 是否需要共享状态?
然后按上面的选择指南决定用什么。
上下文管理器、属性访问、特殊方法的综合应用
好的,我们进入最后一块:上下文管理器、属性访问、特殊方法的综合应用。
这一块会把前面学的所有零散知识点串起来,让你真正理解《流畅的 Python》想传达的核心思想:Pythonic 的本质是利用语言特性让代码更清晰、更安全、更符合直觉。
一、本节目标(你学完应该掌握什么)
- 能写出自己的上下文管理器(
with语句) - 理解
__getattr__/__setattr__等属性访问魔法方法 - 能把特殊方法、协议、鸭子类型等概念融会贯通
- 会用
@contextmanager简化上下文管理器编写
二、上下文管理器:自动清理的“安全网”
1. with 语句的本质
with open("file.txt") as f:content = f.read()
# f 自动关闭,即使出异常也关闭
等价于:
f = open("file.txt")
try:content = f.read()
finally:f.close()
核心思想:
确保“进入”和“退出”时的配对操作,不管中间发生什么。
2. 实现上下文管理器的两种方式
方式一:实现 __enter__ 和 __exit__
class DatabaseConnection:def __init__(self, host, port):self.host = hostself.port = portself.connected = Falsedef connect(self):print(f"Connecting to {self.host}:{self.port}")self.connected = Truedef close(self):if self.connected:print("Closing connection")self.connected = Falsedef __enter__(self):self.connect()return selfdef __exit__(self, exc_type, exc_val, exc_tb):self.close()# 如果返回 True,会抑制异常return False# 使用
with DatabaseConnection("localhost", 5432) as conn:print("Do something with connection")# 自动关闭连接
方式二:用 contextlib.contextmanager 装饰器
from contextlib import contextmanager@contextmanager
def database_connection(host, port):print(f"Connecting to {host}:{port}")try:yield f"connection_to_{host}:{port}" # 相当于 __enter__finally:print("Closing connection") # 相当于 __exit__# 使用
with database_connection("localhost", 5432) as conn:print(f"Using {conn}")
记忆点:
yield前是进入逻辑yield后是退出逻辑yield的值就是as后面的变量
3. 实际工作中的常见用法
import time
from contextlib import contextmanager@contextmanager
def timer():start = time.time()try:yieldfinally:print(f"耗时: {time.time() - start:.3f}s")@contextmanager
def suppress_errors(*exceptions):try:yieldexcept exceptions:pass # 忽略指定异常# 使用
with timer():time.sleep(1)with suppress_errors(ValueError, TypeError):risky_operation()
三、属性访问魔法方法:控制 obj.attr 的行为
1. __getattr__:访问不存在的属性时调用
class FlexibleDict:def __init__(self):self.data = {}def __getattr__(self, name):# 访问不存在的属性时,从 data 中查找if name in self.data:return self.data[name]raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")obj = FlexibleDict()
obj.data['name'] = 'Alice'
print(obj.name) # Alice,通过 __getattr__ 访问
2. __setattr__:设置属性时调用
class ValidatedPerson:def __setattr__(self, name, value):if name == 'age':if not isinstance(value, int) or value < 0:raise ValueError("Age must be a positive integer")super().__setattr__(name, value)person = ValidatedPerson()
person.age = 25 # OK
# person.age = -1 # 会抛出 ValueError
3. __getattribute__:访问任何属性时调用(慎用)
class LoggingProxy:def __init__(self, target):super().__setattr__('_target', target)def __getattribute__(self, name):# 避免无限递归if name.startswith('_'):return super().__getattribute__(name)target = super().__getattribute__('_target')value = getattr(target, name)print(f"访问属性: {name}")return valueclass User:def __init__(self, name):self.name = nameuser = User("Alice")
proxy = LoggingProxy(user)
print(proxy.name) # 会打印 "访问属性: name"
注意: __getattribute__ 会影响所有属性访问,容易造成无限递归。
四、综合应用:用特殊方法实现一个“链式调用”库
class Chain:def __init__(self, value=None):self._value = valuedef __getattr__(self, name):# 模拟 API 调用def method(*args, **kwargs):print(f"调用 {name}({args}, {kwargs})")# 这里可以实际调用 API 或处理逻辑return Chain(f"result_of_{name}")return methoddef __call__(self, *args, **kwargs):# 让对象可调用print(f"调用链式对象: {args}, {kwargs}")return selfdef __repr__(self):return f"Chain(value={self._value!r})"# 使用
api = Chain()
result = api.users.get(id=123).posts.create(title="Hello") # 链式调用
五、协议的综合应用:实现一个“可查询”的数据结构
class QueryableList:def __init__(self, items):self._items = list(items)def __iter__(self):return iter(self._items)def __len__(self):return len(self._items)def __getitem__(self, index):return self._items[index]def where(self, predicate):"""过滤"""return QueryableList(item for item in self if predicate(item))def select(self, selector):"""映射"""return QueryableList(selector(item) for item in self)def first(self, default=None):"""取第一个"""for item in self:return itemreturn default# 使用
data = QueryableList([1, 2, 3, 4, 5, 6])
result = data.where(lambda x: x % 2 == 0).select(lambda x: x * 2).first()
print(result) # 4 (第一个偶数的2倍)
六、Pythonic 的核心思想总结
1. 利用协议,而不是继承
# 不好的方式
class MyList(list): # 继承pass# 更 Pythonic
class MyList: # 实现序列协议def __len__(self): ...def __getitem__(self): ...
2. 用特殊方法让对象“融入”Python 生态
# 让你的对象可以被 len() 用、for 循环、切片...
class MyContainer:def __len__(self): ...def __getitem__(self): ...def __setitem__(self): ...
3. 用上下文管理器确保资源安全
# 任何需要“开始-结束”配对操作的场景
with database_transaction():# 操作数据库,自动提交或回滚with file_lock():# 操作文件,自动释放锁
4. 用属性访问控制让 API 更友好
# 让用户感觉像在访问普通属性,但背后有逻辑
user.age = 25 # 实际调用了验证逻辑
七、《流畅的 Python》学习路线图(完整版)
我们已经走完了这本书的核心路线:
- 数据模型 → 理解
__repr__,__len__,__getitem__等基础 - 序列 → 列表推导式、切片、各种序列类型选择
- 映射与拆包 → 字典、
**kwargs、解包等数据处理技巧 - 函数进阶 → 闭包、装饰器、
functools - 面向对象进阶 → 协议、抽象基类、描述符、元类
- 并发 → 多线程、多进程、协程的适用场景
- 上下文管理器与属性访问 → 让代码更安全、更 Pythonic
八、后续学习建议
-
深入阅读源码:找一些你常用的库(如 requests、flask、django),看看它们是如何利用这些特性的
-
写一些小工具
:比如:
- 自己写一个 ORM 框架
- 写一个异步爬虫
- 写一个配置管理库
-
关注类型提示:Python 3.5+ 的
typing模块,让你的 Pythonic 代码也更安全 -
了解其他 Python 实现:Jython、PyPy 等,看看 GIL 只是 CPython 的特性
恭喜你完成了《流畅的 Python》的核心内容学习!🎉
你现在已经具备了:
- 理解 Python 特殊方法和协议的能力
- 写 Pythonic 代码的思维方式
- 选择合适并发模型的经验
- 用装饰器、上下文管理器等高级特性的技能
你可以说,已经真正“流畅”地使用 Python 了!