Python笔记

Python 学习笔记后端视角学习笔记。

示例基于 Python 3.11+。

目录

  1. 环境与工程化
  2. 语法基础
  3. 函数
  4. 面向对象
  5. 模块与包
  6. 常见设计模式与惯用法
  7. 常见陷阱清单
  8. 进阶:并发与性能
  9. 标准库速查
  10. 第三方生态
  11. 自测题
  12. 参考资料

一、环境与工程化

1.1 版本选择

直接装 3.11 或更高版本。

1
2
python --version
python -m pip --version

1.2 虚拟环境

每个项目一个 venv,依赖互相隔离。

1
2
3
4
5
6
7
8
9
python -m venv .venv

# Windows
.venv\Scripts\activate
# macOS / Linux
source .venv/bin/activate

pip install requests
pip freeze > requirements.txt # 导出当前环境全部依赖

.venv 目录记得加进 .gitignore。

1.3 依赖管理

方式 锁文件 说明
pip + requirements.txt 无 pip freeze 会把间接依赖全展开,重装可能解析出不同版本
uv uv.lock 新一代,解析速度最快,兼容 pip 生态

个人项目我用 uv,团队项目跟随现有约定。

1.4 工具链

工具 作用
ruff lint + 格式化,一套顶两套
mypy 静态类型检查
pytest 单元测试
1
2
3
ruff check . && ruff format .
mypy .
pytest -v

这三个建议在项目第一天就配上。

1.5 项目结构

1
2
3
4
5
6
myproject/
├── pyproject.toml
├── src/myproject/
│ ├── __init__.py
│ └── service.py
└── tests/test_service.py

用 src 布局,能避免把当前目录加进导入路径,提前发现包结构问题。


二、语法基础

2.1 变量与动态类型

Python 的变量不是装值的盒子,是名字到对象的绑定。

1
2
x = 1          # 名字 x 绑定到 int 对象
x = "hello" # 同一个名字现在绑定到 str 对象

和静态类型语言不同,类型是运行期才确定的。类型注解只是提示,运行时不生效,由 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
2
3
4
5
6
d = {"a": 1, "b": 2}
d.get("c", 0) # 不存在返回默认值,不抛 KeyError
d.setdefault("c", 0) # 不存在则写入后返回
for k, v in d.items():
print(k, v)
"a" in d # 判断 key 是否存在,O(1)

分组统计是后端很常见的需求,setdefault 一行搞定:

1
2
3
groups = {}
for item in items:
groups.setdefault(item.type, []).append(item)

2.3 切片

1
2
3
4
5
6
7
8
9
s = "abcdef"
s[1:4] # 'bcd' 左闭右开
s[:3] # 'abc'
s[3:] # 'def'
s[-2:] # 'ef' 负索引,从尾部数
s[::2] # 'ace' 步长 2
s[::-1] # 'fedcba' 反转

lst[1:3] = [7, 8, 9] # 切片赋值,可以改变长度

切片越界不报错,自动裁剪,返回的是新对象而不是视图。

2.4 推导式

1
2
3
4
5
6
squares = [x * x for x in range(10)]
evens = [x for x in range(10) if x % 2 == 0]
pairs = [(x, y) for x in "ab" for y in "12"] # 支持多层
unique = {x % 3 for x in range(10)} # set 推导
mapping = {x: x * x for x in range(5)} # dict 推导
lazy = (x * x for x in range(10)) # 生成器表达式,惰性求值

建议超过两层循环就拆成普通 for 写,可读性第一。

2.5 解包

1
2
3
4
5
6
7
8
a, b = 1, 2
a, b = b, a # 交换,不需要临时变量

first, *rest = [1, 2, 3, 4] # first=1, rest=[2,3,4]
*init, last = [1, 2, 3, 4] # init=[1,2,3], last=4

for k, v in d.items(): # 遍历解包
...

2.6 字符串

1
2
3
4
5
name, age = "Tom", 30
f"{name} is {age}" # f-string,3.6+,首选
f"{age:.2f}" # 30.00,两位小数
f"{name!r}" # 加引号的 repr
f"{age=}" # age=30,调试时好用
  1. str 不可变,循环里别用 += 拼串,用 "".join(parts)
  2. 读写文件和网络流显式指定编码。Python 3 里 str 是 Unicode 码位序列,bytes 是字节序列,两者不能混用;不指定 encoding 时 Windows 默认走本地编码,换台机器就乱码
1
2
3
text = open("a.txt", encoding="utf-8").read()
data = "中文".encode("utf-8") # bytes
"中文".encode("utf-8").decode("utf-8") # 回到 str

2.7 循环

1
2
3
4
5
6
for i, v in enumerate(lst):        # 带下标,别用 range(len(lst))
...
for a, b in zip(l1, l2): # 并行遍历,短的那个先结束
...
for x in reversed(lst): # 逆序,不修改原列表
...

for / while 可以带 else:循环正常结束(没遇到 break)才执行,适合写”没找到”的分支。

1
2
3
4
5
for x in data:
if match(x):
break
else:
raise ValueError("not found")

遍历时删除元素是经典错误,两种正确写法:

1
2
lst = [x for x in lst if keep(x)]          # 建新列表
lst[:] = [x for x in lst if keep(x)] # 原地替换内容

2.8 真值判断

以下值都为假:False、None、0、0.0、""、[]、{}、()、set()。其余为真。

1
2
3
4
5
6
if not lst:            # 空列表,不用 len(lst) == 0
...
if x is None: # 判 None 用 is,不用 ==
...
if name and age: # 逻辑运算
...

三、函数

3.1 参数

1
2
3
4
def f(a, b=2, *args, c, d=4, **kwargs):
...

f(1, c=3) # a=1, b=2, args=(), c=3, d=4, kwargs={}

* 之后的 c、d 是强制关键字参数,调用时必须写名字,比一堆重载清晰,可读性也好。

3.2 可变默认参数

有个坑:

1
2
3
4
5
6
def append(x, lst=[]):        # 错:默认值只创建一次,多次调用共享同一个 list
lst.append(x)
return lst

append(1) # [1]
append(2) # [1, 2],不是 [2]

原因是默认值在函数定义时就求值好了,放在 __defaults__ 里。正确写法是用 None 占位,函数体内再创建:

1
2
3
4
5
def append(x, lst=None):
if lst is None:
lst = []
lst.append(x)
return lst

3.3 闭包与 lambda

1
2
3
4
5
6
7
8
9
10
def counter():
n = 0
def inc():
nonlocal n # 声明修改外层变量,否则会被当成新建局部变量
n += 1
return n
return inc

double = lambda x: x * 2
sorted(users, key=lambda u: (u.age, u.name)) # lambda 最常用于 key=

闭包捕获的是变量本身,循环里要留意:

1
2
fns = [lambda: i for i in range(3)]
[f() for f in fns] # [2, 2, 2],不是 [0, 1, 2]

想按值捕获就加默认参数:lambda i=i: i。

3.4 装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import functools

def retry(times):
def deco(fn):
@functools.wraps(fn) # 保留原函数名和文档字符串,别省
def wrapper(*args, **kwargs):
for i in range(times):
try:
return fn(*args, **kwargs)
except Exception:
if i == times - 1:
raise
return wrapper
return deco

@retry(3)
def call_api():
...

装饰器的本质是用一个新函数替换掉名字绑定,是普通函数调用,不是编译期魔法,调试时看调用栈很直白。

functools.wraps 不写的话,call_api.__name__ 会变成 wrapper,日志和测试都会受影响。

3.5 生成器

1
2
3
4
5
def fib():
a, b = 0, 1
while True:
yield a
a, b = b, a + b

含 yield 的函数是生成器函数,调用它返回生成器,惰性求值。适合大文件逐行处理、无限序列、流式管道,内存占用 O(1)。

yield from sub_gen() 可以把子生成器的产出直接转发出去。

3.6 类型注解

1
2
3
4
5
6
7
from typing import Optional

def find(user_id: int) -> Optional[dict]: # 3.10+ 可写 dict | None
...

def f(items: list[str], mapping: dict[str, int]) -> None: # 3.9+ 可用内置泛型
...

注解运行时不生效,靠 mypy 检查。至少给对外接口写上,能省掉大量翻源码确认类型的成本。


四、面向对象

4.1 基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User:
count = 0 # 类属性,所有实例共享

def __init__(self, name: str, age: int = 0):
self.name = name # 实例属性
self.age = age
User.count += 1

def greet(self) -> str:
return f"hi {self.name}"

def __repr__(self) -> str:
return f"User(name={self.name!r}, age={self.age})"

u = User("Tom")

每个方法的第一个参数显式写 self,相当于 this,但它是显式传递的,调用时不用写。

4.2 __init__ 与 __new__

  • __new__ 负责创建对象本身
  • __init__ 负责初始化创建出来的对象

绝大多数情况只写 __init__。需要动 __new__ 的场景不多:继承不可变类型(tuple / str / int)、实现单例、配合元编程。

4.3 dataclass

写值对象首选,能少写一半样板代码:

1
2
3
4
5
6
7
8
9
10
11
from dataclasses import dataclass, field

@dataclass
class Point:
x: int
y: int = 0
tags: list[str] = field(default_factory=list) # 可变默认值必须这么写

p1 = Point(1, 2)
p2 = Point(1, 2)
p1 == p2 # True,自动生成了 __eq__

自动生成 __init__ / __repr__ / __eq__。要不可变就加 @dataclass(frozen=True),顺带获得可哈希能力。

4.4 继承与 MRO

1
2
3
4
5
6
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

D.__mro__ # D -> B -> C -> A -> object

多继承的方法解析顺序由 C3 线性化确定,可以直接用 __mro__ 查看。实际项目里建议优先组合,多继承只在 mixin 场景用。

4.5 property / classmethod / staticmethod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Circle:
def __init__(self, r: float):
self._r = r

@property
def area(self) -> float: # 像属性一样访问,实际是方法调用
return 3.14 * self._r ** 2

@classmethod
def unit(cls) -> "Circle": # 替代构造器,cls 是类本身
return cls(1)

@staticmethod
def help() -> str: # 挂在类上的普通函数,收不到 self
return "..."

@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
2
3
4
5
6
7
8
# myproject/service.py
def run():
...

# 使用方,三种等价写法
from myproject.service import run
from myproject import service
import myproject.service as svc
  • 包就是含 __init__.py 的目录(3.3+ 有命名空间包,可以省略)
  • 相对导入只能在包内用:from .utils import parse
  • if __name__ == "__main__": 保证模块既能被 import 又能直接运行
1
2
3
# myproject/service.py
if __name__ == "__main__":
run()

循环导入是新手可能遇到的报错(ImportError: cannot import name ...)。解法有两个:把 import 挪到函数体内延迟导入,或者把公共部分抽到第三个模块。


六、常见设计模式与惯用法

6.1 单例

最常见也最省事的写法是利用模块只初始化一次:

1
2
3
4
5
6
# settings.py
class _Settings:
def __init__(self):
self.debug = False

settings = _Settings() # 模块被导入时才创建,天然单例

需要类形式时重写 __new__:

1
2
3
4
5
6
7
8
9
class Config:
_instance = None

def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

Config() is Config() # True

6.2 工厂方法

用 classmethod 做替代构造器,比一堆 if-else 拼参数清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
from dataclasses import dataclass

@dataclass
class Notifier:
target: str

@classmethod
def email(cls, to: str) -> "Notifier":
return cls(target=f"email:{to}")

@classmethod
def sms(cls, phone: str) -> "Notifier":
return cls(target=f"sms:{phone}")

6.3 策略模式

Python 里不需要为每种策略建一个类,函数本身就是策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import time

def fixed_backoff(n: int) -> float:
return 1.0

def exp_backoff(n: int) -> float:
return 0.5 * (2 ** n)

def call_with_retry(fn, backoff=exp_backoff, times=3):
for i in range(times):
try:
return fn()
except Exception:
if i == times - 1:
raise
time.sleep(backoff(i))

调用处传一个函数进来即可,替换策略不用改调用方。

6.4 依赖注入

框架层面 FastAPI 用 Depends,不依赖框架时手动装配也完全够用:

1
2
3
4
5
6
7
class UserService:
def __init__(self, repo, cache):
self.repo = repo
self.cache = cache

# 组装在入口处完成
service = UserService(repo=SqlUserRepo(dsn), cache=RedisCache(url))

把依赖写成构造参数,测试时换一个假实现就行,比在类内部 new 出来好测得多。

FastAPI 的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from fastapi import Depends, FastAPI

app = FastAPI()

def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

@app.get("/users/{user_id}")
def read_user(user_id: int, db=Depends(get_db)):
return db.get(user_id)

yield 形式的依赖,请求结束时会执行 finally 里的清理,资源不会漏。

6.5 配置对象

1
2
3
4
5
6
7
8
9
from dataclasses import dataclass

@dataclass(frozen=True)
class Settings:
dsn: str
pool_size: int = 10
timeout: float = 3.0

settings = Settings(dsn="postgresql://...")

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
2
3
4
5
6
7
8
import copy

a = [[1, 2], [3, 4]]
b = a.copy() # 浅拷贝:外层是新的,内层还是共享的
b[0][0] = 99
a[0][0] # 99,被改到了

c = copy.deepcopy(a) # 深拷贝:完全独立

普通赋值 b = a 连浅拷贝都不做,只是多一个名字指向同一个对象,这点比其他语言更需要留神。

7.2 异常

1
2
3
4
5
6
7
8
9
try:
risky()
except ValueError as e:
logger.error("bad value: %s", e)
raise RuntimeError("more context") from e # 保留异常链,别丢现场
else:
... # 没抛异常才执行
finally:
cleanup() # 无论如何都执行

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
2
3
4
5
6
7
8
9
10
11
12
import asyncio
import aiohttp

async def fetch(session, url):
async with session.get(url) as resp:
return await resp.text()

async def main():
async with aiohttp.ClientSession() as session:
results = await asyncio.gather(*(fetch(session, u) for u in urls))

asyncio.run(main())

threading 是一个任务一个线程;asyncio 是少量线程承载大量等待中的任务,靠事件循环调度,连接数上去之后内存和切换成本差别很大。

concurrent.futures 提供了统一 API,切换线程池和进程池只改一个类名:

1
2
3
4
from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor(max_workers=8) as pool:
results = list(pool.map(process, items))

8.3 迭代器协议

1
2
3
4
5
it = iter([1, 2, 3])
next(it) # 1
next(it) # 2
next(it) # 3
next(it) # StopIteration

for 循环的底层就是反复调用 next() 直到 StopIteration。自己实现可迭代对象需要两个方法:__iter__ 返回迭代器,__next__ 产出下一个元素。

8.4 上下文管理器

1
2
3
4
5
6
7
8
9
10
11
12
import time

class Timer:
def __enter__(self):
self.t0 = time.perf_counter()
return self

def __exit__(self, *exc): # 异常信息也会传进来
print(f"elapsed: {time.perf_counter() - self.t0:.3f}s")

with Timer():
do_something()

__exit__ 返回真值会吞掉异常,一般返回 None 让异常继续抛。标准库的 contextlib.contextmanager 可以用生成器快速写一个,常见的数据库 session 封装都是这么做的。

8.5 性能剖析

1
2
3
import cProfile

cProfile.run("main()", sort="cumtime")

先定位热点再优化,不要凭感觉。常见的加速手段,按收益排序:

  1. 换算法和数据结构
  2. 缓存:functools.lru_cache
  3. 向量化:numpy / pandas 替代 Python 层循环
  4. 上 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
2
3
4
5
6
7
8
9
10
11
12
13
from collections import Counter
from functools import lru_cache
from pathlib import Path

Counter("mississippi").most_common(3) # [('i', 4), ('s', 4), ('p', 2)]

@lru_cache(maxsize=None)
def fib(n: int) -> int:
return n if n < 2 else fib(n - 1) + fib(n - 2)

Path("logs").mkdir(exist_ok=True)
for p in Path(".").glob("**/*.py"):
print(p)

十、第三方生态

方向 常用库
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 文档:代码规范

Python笔记
https://zyue2022.github.io/2026/10/18/Python笔记/
作者
ZYUE
发布于
2026年10月18日
更新于
2026年10月18日
许可协议