Python笔记
Python 学习笔记后端视角学习笔记。
示例基于 Python 3.11+。
目录
一、环境与工程化
1.1 版本选择
直接装 3.11 或更高版本。
1 | |
1.2 虚拟环境
每个项目一个 venv,依赖互相隔离。
1 | |
.venv 目录记得加进 .gitignore。
1.3 依赖管理
| 方式 | 锁文件 | 说明 |
|---|---|---|
| pip + requirements.txt | 无 | pip freeze 会把间接依赖全展开,重装可能解析出不同版本 |
| uv | uv.lock | 新一代,解析速度最快,兼容 pip 生态 |
个人项目我用 uv,团队项目跟随现有约定。
1.4 工具链
| 工具 | 作用 |
|---|---|
| ruff | lint + 格式化,一套顶两套 |
| mypy | 静态类型检查 |
| pytest | 单元测试 |
1 | |
这三个建议在项目第一天就配上。
1.5 项目结构
1 | |
用 src 布局,能避免把当前目录加进导入路径,提前发现包结构问题。
二、语法基础
2.1 变量与动态类型
Python 的变量不是装值的盒子,是名字到对象的绑定。
1 | |
和静态类型语言不同,类型是运行期才确定的。类型注解只是提示,运行时不生效,由 mypy 单独检查。
2.2 内置容器
| 容器 | 字面量 | 有序 | 可变 | 查找 | 插入 |
|---|---|---|---|---|---|
| list | [1, 2] |
是 | 是 | O(n) | 尾部均摊 O(1) |
| tuple | (1, 2) |
是 | 否 | O(n) | — |
| set | {1, 2} |
否 | 是 | O(1) | O(1) |
| dict | {"a": 1} |
是(3.7+ 保证) | 是 | 平均 O(1) | 平均 O(1) |
几个容易踩的点:
- tuple 单元素必须带逗号:
(1,),写(1)的话就是 int 1 - set 元素和 dict 的 key 必须可哈希(也就是不可变),list / dict / set 不行,tuple 可以
- dict 查找平均 O(1),极端哈希冲突下会退化,别当成绝对保证
常用操作:
1 | |
分组统计是后端很常见的需求,setdefault 一行搞定:
1 | |
2.3 切片
1 | |
切片越界不报错,自动裁剪,返回的是新对象而不是视图。
2.4 推导式
1 | |
建议超过两层循环就拆成普通 for 写,可读性第一。
2.5 解包
1 | |
2.6 字符串
1 | |
- str 不可变,循环里别用
+=拼串,用"".join(parts) - 读写文件和网络流显式指定编码。Python 3 里 str 是 Unicode 码位序列,bytes 是字节序列,两者不能混用;不指定 encoding 时 Windows 默认走本地编码,换台机器就乱码
1 | |
2.7 循环
1 | |
for / while 可以带 else:循环正常结束(没遇到 break)才执行,适合写”没找到”的分支。
1 | |
遍历时删除元素是经典错误,两种正确写法:
1 | |
2.8 真值判断
以下值都为假:False、None、0、0.0、""、[]、{}、()、set()。其余为真。
1 | |
三、函数
3.1 参数
1 | |
* 之后的 c、d 是强制关键字参数,调用时必须写名字,比一堆重载清晰,可读性也好。
3.2 可变默认参数
有个坑:
1 | |
原因是默认值在函数定义时就求值好了,放在 __defaults__ 里。正确写法是用 None 占位,函数体内再创建:
1 | |
3.3 闭包与 lambda
1 | |
闭包捕获的是变量本身,循环里要留意:
1 | |
想按值捕获就加默认参数:lambda i=i: i。
3.4 装饰器
1 | |
装饰器的本质是用一个新函数替换掉名字绑定,是普通函数调用,不是编译期魔法,调试时看调用栈很直白。
functools.wraps 不写的话,call_api.__name__ 会变成 wrapper,日志和测试都会受影响。
3.5 生成器
1 | |
含 yield 的函数是生成器函数,调用它返回生成器,惰性求值。适合大文件逐行处理、无限序列、流式管道,内存占用 O(1)。
yield from sub_gen() 可以把子生成器的产出直接转发出去。
3.6 类型注解
1 | |
注解运行时不生效,靠 mypy 检查。至少给对外接口写上,能省掉大量翻源码确认类型的成本。
四、面向对象
4.1 基本结构
1 | |
每个方法的第一个参数显式写 self,相当于 this,但它是显式传递的,调用时不用写。
4.2 __init__ 与 __new__
__new__负责创建对象本身__init__负责初始化创建出来的对象
绝大多数情况只写 __init__。需要动 __new__ 的场景不多:继承不可变类型(tuple / str / int)、实现单例、配合元编程。
4.3 dataclass
写值对象首选,能少写一半样板代码:
1 | |
自动生成 __init__ / __repr__ / __eq__。要不可变就加 @dataclass(frozen=True),顺带获得可哈希能力。
4.4 继承与 MRO
1 | |
多继承的方法解析顺序由 C3 线性化确定,可以直接用 __mro__ 查看。实际项目里建议优先组合,多继承只在 mixin 场景用。
4.5 property / classmethod / staticmethod
1 | |
@property 相当于 getter,但调用处没有括号,改字段名不影响调用方。
4.6 魔术方法速查
| 方法 | 触发场景 |
|---|---|
__len__ |
len(obj) |
__iter__ |
for x in obj |
__getitem__ |
obj[k] |
__enter__ / __exit__ |
with obj: |
__eq__ |
== |
__hash__ |
放进 set / dict 当 key |
__call__ |
obj() |
__str__ |
print(obj) |
__slots__ |
限制实例属性,省内存 |
注意:定义了 __eq__ 却没定义 __hash__,Python 会把 __hash__ 置成 None,对象变得不可哈希,放进 set 或 dict 直接报错。
五、模块与包
1 | |
- 包就是含
__init__.py的目录(3.3+ 有命名空间包,可以省略) - 相对导入只能在包内用:
from .utils import parse if __name__ == "__main__":保证模块既能被 import 又能直接运行
1 | |
循环导入是新手可能遇到的报错(ImportError: cannot import name ...)。解法有两个:把 import 挪到函数体内延迟导入,或者把公共部分抽到第三个模块。
六、常见设计模式与惯用法
6.1 单例
最常见也最省事的写法是利用模块只初始化一次:
1 | |
需要类形式时重写 __new__:
1 | |
6.2 工厂方法
用 classmethod 做替代构造器,比一堆 if-else 拼参数清晰:
1 | |
6.3 策略模式
Python 里不需要为每种策略建一个类,函数本身就是策略:
1 | |
调用处传一个函数进来即可,替换策略不用改调用方。
6.4 依赖注入
框架层面 FastAPI 用 Depends,不依赖框架时手动装配也完全够用:
1 | |
把依赖写成构造参数,测试时换一个假实现就行,比在类内部 new 出来好测得多。
FastAPI 的写法:
1 | |
yield 形式的依赖,请求结束时会执行 finally 里的清理,资源不会漏。
6.5 配置对象
1 | |
frozen 之后不可篡改,也不会被误改。要支持环境变量覆盖可以用 pydantic-settings。
6.6 资源生命周期
文件、连接、锁这类申请和释放必须成对出现的操作,一律用 with,见 8.4。
七、常见陷阱清单
| # | 坑 | 正确做法 |
|---|---|---|
| 1 | 可变默认参数 | 用 None 占位,函数体内创建 |
| 2 | 用 is 比较值 |
值比较用 ==;只有 None 等单例句型用 is |
| 3 | 浅拷贝后改子对象 | 需要独立就用 copy.deepcopy |
| 4 | 遍历时按索引删元素 | 先重建列表,或倒序删 |
| 5 | 裸 except: |
至少写 except Exception:,别吞掉 KeyboardInterrupt |
| 6 | 循环里字符串 += |
"".join(parts) |
| 7 | 读文件不指定编码 | 显式 encoding="utf-8" |
| 8 | 定义 __eq__ 忘定义 __hash__ |
成对定义,或加 @dataclass(frozen=True) |
| 9 | 用 float 算金额 | decimal.Decimal |
| 10 | round(2.5) 期望得到 3 |
银行家进位得 2,需要精确控制用 Decimal |
7.1 拷贝语义
1 | |
普通赋值 b = a 连浅拷贝都不做,只是多一个名字指向同一个对象,这点比其他语言更需要留神。
7.2 异常
1 | |
raise ... from e 是异常链,排查问题时 __cause__ 里的原始异常往往才是关键。
八、进阶:并发与性能
8.1 GIL
全局解释器锁:同一进程内同一时刻只有一个线程在执行 Python 字节码。
- CPU 密集型:多线程拿不到并行收益,用
multiprocessing,或交给释放了 GIL 的 C 扩展(numpy 这类) - IO 密集型(网络、磁盘、数据库):多线程有效,线程在等待时会释放 GIL
8.2 三种并发模型怎么选
| 场景 | 选择 |
|---|---|
| CPU 密集 | multiprocessing |
| IO 密集、代码直白 | threading |
| 高并发连接、协程式 IO | asyncio(async/await) |
1 | |
threading 是一个任务一个线程;asyncio 是少量线程承载大量等待中的任务,靠事件循环调度,连接数上去之后内存和切换成本差别很大。
concurrent.futures 提供了统一 API,切换线程池和进程池只改一个类名:
1 | |
8.3 迭代器协议
1 | |
for 循环的底层就是反复调用 next() 直到 StopIteration。自己实现可迭代对象需要两个方法:__iter__ 返回迭代器,__next__ 产出下一个元素。
8.4 上下文管理器
1 | |
__exit__ 返回真值会吞掉异常,一般返回 None 让异常继续抛。标准库的 contextlib.contextmanager 可以用生成器快速写一个,常见的数据库 session 封装都是这么做的。
8.5 性能剖析
1 | |
先定位热点再优化,不要凭感觉。常见的加速手段,按收益排序:
- 换算法和数据结构
- 缓存:
functools.lru_cache - 向量化:numpy / pandas 替代 Python 层循环
- 上 C 扩展
九、标准库速查
| 模块 | 常用能力 |
|---|---|
pathlib |
Path("a/b.txt").read_text(encoding="utf-8"),替代 os.path |
json |
dumps / loads;输出中文加 ensure_ascii=False |
datetime |
datetime.now()、timedelta;naive 和 aware 时区对象别混用 |
collections |
defaultdict、Counter、deque、namedtuple |
itertools |
chain、groupby、product、islice |
functools |
lru_cache、partial、wraps |
logging |
logging.getLogger(__name__),别用 print 记日志 |
re |
match / search / findall / sub,复杂场景先 re.compile |
subprocess |
run(cmd, capture_output=True),别用 os.system |
typing |
Optional、Union、Callable、Protocol |
dataclasses |
@dataclass 写值对象 |
argparse / click |
CLI 参数解析,写运维脚本用 |
concurrent.futures |
线程池 / 进程池,API 统一 |
hashlib / hmac / secrets |
摘要、签名、安全随机数 |
decimal |
精确小数,金额计算 |
asyncio |
协程,见 8.2 |
几个高频写法:
1 | |
十、第三方生态
| 方向 | 常用库 |
|---|---|
| HTTP 客户端 | requests(同步)/ httpx(同步+异步) |
| Web 框架 | FastAPI(新项目首选)/ Django(全功能)/ Flask(轻量) |
| 数据校验 | pydantic |
| ORM | SQLAlchemy 2.x / Django ORM / Tortoise ORM |
| 异步任务 | Celery / arq |
| ASGI 服务器 | uvicorn / gunicorn |
| 数据处理 | pandas / numpy |
| 测试 | pytest + pytest-mock |
| 配置管理 | pydantic-settings / dynaconf |
十一、自测题
| 问题 | 答案要点 |
|---|---|
| list 和 tuple 的区别? | list 可变、tuple 不可变;tuple 可哈希所以能作 dict 的 key;tuple 更省内存、可安全共享 |
| 为什么 tuple 能作 dict 的 key? | 元素全部可哈希时,tuple 本身可哈希;list 可变所以不可哈希 |
| 什么是 GIL?多线程还有用吗? | 解释器级锁,同进程同时只有一个线程执行字节码;CPU 密改用多进程,IO 密集下多线程仍有效 |
装饰器的本质?不写 functools.wraps 会怎样? |
用新函数替换名字绑定;会丢失原函数的 __name__、__doc__,影响日志和测试 |
| 可变默认参数为什么是坑? | 默认值在函数定义时求值一次,存于 __defaults__,多次调用共享同一对象;用 None 占位 |
is 和 == 的区别? |
is 判同一对象(id 相等),== 调 __eq__ 判值;None 用 is None |
| 深拷贝和浅拷贝的区别? | copy() 只复制外层,内层仍共享;deepcopy 递归复制整棵树 |
| 生成器和普通函数的区别? | 含 yield 返回惰性迭代器,调用不立即执行;内存 O(1),适合流式处理 |
__init__ 和 __new__ 的区别? |
__new__ 创建实例,__init__ 初始化;改前者只在单例、不可变类型子类等场景 |
with 语句的原理? |
调用 __enter__ / __exit__,异常信息也传给 __exit__;返回真值才吞异常 |
| Python 的内存管理? | 引用计数为主 + 分代 gc 回收循环引用;可用 weakref 打破环,gc.collect() 手动触发 |
| 单例模式怎么写? | 模块级变量(模块天然单例,最常用)、重写 __new__、装饰器 |
| 列表去重保序怎么做? | list(dict.fromkeys(lst)),利用 dict 有序且 key 唯一 |
| 大文件统计词频,内存有限怎么做? | 逐行读入 + Counter 增量累加;或按哈希分片到多个临时文件再归并 |
*args 和 **kwargs 的顺序约束? |
位置参数 → *args → 强制关键字参数 → **kwargs |
/ 和 // 的区别? |
/ 真除法返回 float;// 整除向下取整 |
| 怎么实现 LRU? | OrderedDict.move_to_end + popitem,或直接用 @lru_cache |
| asyncio 的事件循环是什么? | 单线程调度协程的循环,await 让出控制权,用少量线程承载大量等待中的任务 |
| 为什么 dict 查找是 O(1)? | 哈希表,平均 O(1);开放寻址处理冲突,极端情况退化成 O(n) |
round(2.5) 等于几? |
2。银行家进位(round half to even),要精确控制用 Decimal |
十二、参考资料
- 官方文档:docs.python.org/zh-cn/3/,有中文版
- 《Python Cookbook(第 3 版)》:按问题查解决方案,适合有经验的开发者
- 《Fluent Python》:进阶首选,把数据模型和并发讲得很透
- PEP 8 和 ruff 文档:代码规范