开封市网站建设_网站建设公司_Vue_seo优化
2025/12/25 7:26:11 网站建设 项目流程

一 阶段学习

阶段 1:Pythonic 思维 & 数据模型(打好“世界观”)

对应书里:数据模型、序列、映射、切片等章节。

重点不是语法,而是:

  • 认清几类“协议”(可以类比成“接口”/“约定”):
    • 容器协议:__len____getitem__
    • 可迭代协议:__iter____next__
    • 上下文协议:__enter____exit__
  • 理解:为什么“实现几个魔法方法,一个类就能无缝融入整个 Python 生态”。

这个阶段的目标:以后你看到任意一个“高级特性”,都能从“数据模型/协议”角度去理解,而不是死记写法。

阶段 2:函数式风格 & 函数高级用法

对应:函数、闭包、装饰器、functools 等章节。

从“能写装饰器”升级到“知道什么时候用、高级用法怎么写”:

  • 函数是“一等公民”:可以像数据一样传来传去、嵌套、返回
  • 闭包:记住外层环境的函数(非常实用)
  • 装饰器:横切逻辑(日志、缓存、鉴权)的一致写法
  • 偏函数、operatorfunctools:让代码更“函数式”和简洁

阶段 3:面向对象 & 元编程(包括你头疼的“元类”)

对应:类、抽象基类、描述符、__getattr__、元类相关章节。

这里我会用“类比 + 分层”来讲,不直接把元类当黑魔法丢给你:

  1. 先稳固普通 OOP + 协议/抽象基类
  2. 再讲“描述符”——理解属性访问是怎么被拦截/定制的
  3. 最后才到“元类”:类也是对象,谁来“制造类”?

记忆类比:

  • 实例 对象 的“工厂”是
  • 类 的“工厂”就是 元类
  • 元类就相当于“开模具的工厂”,决定类长什么样、出生时做哪些事

阶段 4:并发 & 异步(重点帮你啃下来)

对应:并发、asyncio、协程章节。

重点不是 API 背下来,而是:

  • 异步的核心类比:
    • 线程:开多个“人”一起干活
    • 异步:一个人干活,但在等待 I/O 时不发呆,去干别的
  • 搞清楚3个关键概念:
    • 协程对象(async def 返回的东西)
    • 事件循环(调度中心)
    • await(“这件事我等结果,但空档时间我先让出 CPU”)
  • 带你把 async/awaitasyncio 这一块串起来,至少做到“看得懂+敢用”。

二、具体学习节奏(符合你要“快”的要求)

因为你是 C 档,我们不按“从零讲起”,而是这样:

  1. 每次选一个小主题(可跨1~3 小节)
    • 例:Python 数据模型总览序列和切片的 Pythonic 写法装饰器进阶
  2. 我先给你一份Typora 友好的小笔记:
    • 结构会是:
      • 核心结论
      • 一图/一类比帮你记
      • 1~2 个关键代码片段
      • 常见坑 & 工作中能用上的点
  3. 如果你愿意,我给一个很小的练习(5~10 分钟)
    • 你写完贴上来
    • 我只针对“Pythonic 程度”和“设计思路”给你反馈
  4. 你觉得这个主题 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 档水平,下面是立即能用上的几点:

  1. 给自己的核心类补上 __repr__
    • 以后 log / 调试时,打印对象不再是 <User object at 0x...>,而是有信息的字符串
    • 对排查线上问题非常有价值
  2. 写库/框架代码时,用“协议思维”设计接口:
    • 比如某函数参数声明为“任何可迭代对象即可”(支持 list、tuple、生成器、你自定义的类),而不是死写 list
    • 这就是《流畅的 Python》强调的“鸭子类型”和“协议”
  3. 读源码时,看见一堆__xxx__不慌
    • 你先判断:这是在实现什么“能力”?
    • 再去翻对应的数据模型说明(你以后基本能靠记忆/经验判断)

注释:

  1. !r

    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__。
  2. __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(同类型数字,节省内存)
  • 通用不可变序列:

    • tuplestrbytes
  • 通用可变序列:

    • listbytearraycollections.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]

记忆小点:

  • 读顺序:左边结果表达式forif 条件(可选)
  • 当逻辑很简单时,可读性反而更好(因为“全部逻辑都在一行可见”)

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:起始索引(含),默认 0
  • end:结束索引(不含),默认 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 分钟,选做)

假设你在写一个简单的日志过滤工具:

  1. 有一个 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"},]
  1. 请你写两段代码(尽量用列表推导式 + 切片):
  • 取出所有 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)

四、这一节的“联想记忆”总结

可以用两个比喻把这一块挂在脑子里:

  1. dict = 带标签的盒子

    • list:只能按编号拿
    • dict:写清楚标签再装,后面你就不用记“0 是 name、1 是 age”这种脑力消耗大的东西
  2. 拆包 = 拆快递

    • 函数返回多个值 → 装在一个盒子里(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 的参数 x
  • make_multiplier 执行完返回 multiplier 时,x 已经“超出生命周期”了
  • 但 Python 会把 x 封在 multiplier 里面,形成一个“闭包”
  • 所以 doubletriple 各自记住了自己的 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 "成功"

记忆法:

  • 普通装饰器:@decoratordecorator(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 用装饰器来实现。


五、一个综合练习(可选,加深理解)

你可以试着写一个“日志装饰器”,它有这些功能:

  1. 记录函数名、参数、返回值
  2. 记录函数执行时间
  3. 可以通过参数控制是否记录(比如 @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)

六、本节的“联想记忆”总结

  1. 闭包 = 函数 + 环境

    • 函数可以“记住”它创建时的变量
    • 适合做“带配置的函数生成器”
  2. 装饰器 = 函数的包装纸

    • 一个函数套另一个函数
    • @wraps 记得加,不然原函数信息就丢了
  3. 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

七、本节“联想记忆”总结

  1. 协议 = 约定的行为

    • 不用继承,只要实现对应方法就行
    • “像鸭子就当鸭子”是核心思想
  2. 抽象基类 = 强制的协议

    • @abstractmethod 标记必须实现的方法
    • 常用 collections.abc 里的基类做类型检查
  3. 描述符 = 属性的“代理”

    • 控制 obj.attr 的读写行为
    • 适合做验证、延迟加载、缓存等
  4. 元类 = 类的“工厂”

    • 很少用,通常框架才需要
    • 记住:类是对象,元类是“类的类”就行

    你以后看到源码里有 __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 密集型任务
  • 进程间不共享内存,需要用 QueuePipe 等通信

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"

八、本节“联想记忆”总结

  1. GIL 是“厨房只有一把刀”

    • 多线程不能真正并行执行 Python 代码
    • 但不影响 I/O 操作(I/O 时刀可以借给别人)
  2. 多线程 = 多个人干 I/O 活

    • 适合网络请求、文件读写
    • 记得加锁保护共享数据
  3. 多进程 = 多个厨房干 CPU 活

    • 绕过 GIL,真正并行
    • 适合计算密集型任务
  4. 协程 = 一个人聪明地干 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》学习路线图(完整版)

我们已经走完了这本书的核心路线:

  1. 数据模型 → 理解 __repr__, __len__, __getitem__ 等基础
  2. 序列 → 列表推导式、切片、各种序列类型选择
  3. 映射与拆包 → 字典、**kwargs、解包等数据处理技巧
  4. 函数进阶 → 闭包、装饰器、functools
  5. 面向对象进阶 → 协议、抽象基类、描述符、元类
  6. 并发 → 多线程、多进程、协程的适用场景
  7. 上下文管理器与属性访问 → 让代码更安全、更 Pythonic

八、后续学习建议

  1. 深入阅读源码:找一些你常用的库(如 requests、flask、django),看看它们是如何利用这些特性的

  2. 写一些小工具

    :比如:

    • 自己写一个 ORM 框架
    • 写一个异步爬虫
    • 写一个配置管理库
  3. 关注类型提示:Python 3.5+ 的 typing 模块,让你的 Pythonic 代码也更安全

  4. 了解其他 Python 实现:Jython、PyPy 等,看看 GIL 只是 CPython 的特性


恭喜你完成了《流畅的 Python》的核心内容学习!🎉

你现在已经具备了:

  • 理解 Python 特殊方法和协议的能力
  • 写 Pythonic 代码的思维方式
  • 选择合适并发模型的经验
  • 用装饰器、上下文管理器等高级特性的技能

你可以说,已经真正“流畅”地使用 Python 了!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询