《Effective Python》深度笔记:90 个有效方法与工程实践
- 约 19 分钟读完 - 全文 7476 字《Effective Python》深度笔记:90 个有效方法与工程实践
这份笔记基于 Brett Slatkin 的《Effective Python: 90 Specific Ways to Write Better Python》(第 2 版),并结合日常工程实践做了补充。与简单罗列条目不同,我尝试为每条建议补充可运行的代码片段、容易踩的坑以及落地时的判断标准,让这份笔记既能速查,也能作为团队代码评审的参考。
前置补充:一些高频但容易忽略的基础
在正式进入原著的 90 条之前,先把日常写 Python 时反复出现、但原著不会专门展开的基础点列出来,方便后续章节引用。
1. Literal 类型提示
Literal 用于限定变量只能取某些固定的字面量值,适合状态码、枚举字符串等场景。
from typing import Literal
def move(direction: Literal["up", "down", "left", "right"]) -> None:
...
move("up") # OK
move("forward") # 类型检查器会报错2. Pydantic 的 Field
用 Field 可以在数据模型里同时做校验和文档描述,比裸的 BaseModel 更适合生产接口。
from pydantic import BaseModel, Field
class User(BaseModel):
name: str = Field(
...,
description="用户的真实姓名",
min_length=1,
max_length=50,
)
age: int = Field(
default=18,
ge=0,
le=150,
description="用户年龄",
)
user = User(name="Alice", age=25)
print(user.model_dump()) # {'name': 'Alice', 'age': 25}3. f-string
Python 3.6+ 推荐用 f-string 做字符串插值,可读性比 % 和 .format() 都好。
author = "Alice"
to = "Bob"
result = f"你好,{author}!这封邮件是发送给 {to} 的。"4. range 与切片
lista = list(range(1, 7, 2)) # [1, 3, 5]
listb = sorted(lista, reverse=True) # [5, 3, 1]
listb[-1] # 1,倒数第一个
listb[0:1] # [5],切片左闭右开5. 字典操作
字典支持解包合并(Python 3.9+):
a = {"x": 1, "y": 2}
b = {"y": 3, "z": 4}
c = {**a, **b} # {'x': 1, 'y': 3, 'z': 4}
del c["x"] # 删除键6. 函数的变长参数
def sum_all(*numbers: int) -> int:
return sum(numbers)
def build_user(**kwargs: str) -> dict:
return kwargs
sum_all(1, 2, 3) # 6
build_user(name="Alice", age="25") # {'name': 'Alice', 'age': '25'}7. 模块引用
# ppp.py 中定义了 def abc(): ...
from ppp import abc8. 类的构造方法
Python 不支持方法重载,多个 __init__ 会互相覆盖,只保留最后一个。
class Demo:
def __init__(self, x):
self.x = x
def __init__(self, x, y): # 上面的定义被覆盖
self.x = x
self.y = y9. JSON 读写
import json
with open("data.json", "r", encoding="utf-8") as f:
data = json.load(f)
with open("out.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)10. unittest 入口
if __name__ == "__main__":
unittest.main()被导入时 __name__ 是模块名,只有直接执行时才等于 "__main__"。
11. matplotlib 快速绘图
import matplotlib.pyplot as plt
ss = [1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144]
fig, ax = plt.subplots()
ax.plot(ss)
ax.set_title("Square Numbers")
plt.show()12. CSV 处理
import csv
with open("data.csv", newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
print(row["name"], row["age"])13. 建立虚拟环境
python -m venv ll_env
source ll_env/bin/activate # Linux/macOS
ll_env\Scripts\activate # Windows一、Pythonic 思维
Python 的“风格”不是语法规则,而是一种让代码更简洁、可读、可维护的思维方式。本章的建议大多不依赖某个库,而是写代码时的默认选择。
14. 检查 Python 版本
不同版本差异很大。写脚本前确认目标版本,避免用了高版本特性却跑在低版本环境。
import sys
print(sys.version_info)15. 遵循 PEP 8 风格指南
- 缩进 4 个空格
- 行宽 79 字符(现代项目常放宽到 100 或 120)
- 函数名、变量名用
snake_case - 类名用
CamelCase - 常量用
UPPER_SNAKE_CASE
工具链:black、ruff、flake8。
16. 区分 bytes 与 str
bytes 是原始二进制数据,str 是 Unicode 文本。混用是最常见的编码 bug 来源。
b = b"hello" # bytes
s = "hello" # str
# 显式编解码
b.decode("utf-8") # -> str
s.encode("utf-8") # -> bytes落地建议:文件读取、网络请求、数据库返回的边界处,统一做编码转换,内部逻辑全部用 str。
17. 优先使用 f-string
name = "Alice"
age = 30
# 不推荐
print("Name: %s, Age: %d" % (name, age))
print("Name: {}, Age: {}".format(name, age))
# 推荐
print(f"Name: {name}, Age: {age}")
print(f"Name: {name!r}, Age: {age:.2f}") # !r 用 repr,:.2f 格式化数字18. 用辅助函数替代复杂表达式
复杂表达式一行写完很爽,但调试和复用都痛苦。
# 不推荐
result = (x + y) * 2 if x > 0 and y > 0 else (x - y) * 2
# 推荐
def compute_adjustment(x: int, y: int) -> int:
if x > 0 and y > 0:
return (x + y) * 2
return (x - y) * 2
result = compute_adjustment(x, y)19. 用解包替代索引取值
pair = ("Alice", 30)
# 不推荐
name = pair[0]
age = pair[1]
# 推荐
name, age = pair20. 用 enumerate 替代 range(len(...))
fruits = ["apple", "banana", "cherry"]
# 不推荐
for i in range(len(fruits)):
print(i, fruits[i])
# 推荐
for i, fruit in enumerate(fruits):
print(i, fruit)
# 还可以指定起始索引
for i, fruit in enumerate(fruits, start=1):
print(i, fruit)21. 用 zip 并行处理多个迭代器
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
for name, age in zip(names, ages):
print(f"{name}: {age}")当长度不一致时,zip 会以最短为准。需要补齐时用 itertools.zip_longest。
22. 避免 for/while ... else
else 块只在循环没有因 break 退出时执行,语义非常容易误解。
# 容易误读
for x in items:
if x < 0:
break
else:
print("all positive")
# 更清晰的写法
found_negative = False
for x in items:
if x < 0:
found_negative = True
break
if not found_negative:
print("all positive")23. 用赋值表达式减少重复
# 不推荐
count = get_count()
if count > 0:
process(count)
# 推荐
if (count := get_count()) > 0:
process(count)章节小结:Pythonic 思维的核心是“可读性优先”。多利用解包、enumerate、zip、f-string、赋值表达式等语法糖,但不要把代码写成一行炫技。当表达式复杂时,果断拆成辅助函数。
二、列表和字典
列表和字典是 Python 最常用的两种数据结构。本章的建议集中在如何高效、安全、可维护地操作它们。
24. 了解序列切片
切片支持越界、负索引和省略参数。
a = [1, 2, 3, 4, 5]
a[:3] # [1, 2, 3]
a[2:] # [3, 4, 5]
a[::2] # [1, 3, 5]
a[::-1] # [5, 4, 3, 2, 1]
a[1:100] # [2, 3, 4, 5],不报错25. 不要在单次切片中同时指定起止和步进
# 不推荐:stride 和 start/end 同时出现,可读性差
x = a[1:-1:2]
# 推荐:拆成两步
b = a[::2]
x = b[1:-1]26. 用星号解包捕获多个元素
first, *middle, last = [1, 2, 3, 4, 5]
# first = 1, middle = [2, 3, 4], last = 5
a, *b = [1] # a=1, b=[]27. 用 sort 的 key 表达复杂排序
students = [
{"name": "Alice", "age": 20},
{"name": "Bob", "age": 18},
{"name": "Charlie", "age": 20},
]
# age 降序,name 升序
sorted_students = sorted(students, key=lambda x: (-x["age"], x["name"]))28. 不要过分依赖字典的插入顺序
虽然 Python 3.7+ 保证字典插入顺序,但如果代码需要跨语言或兼容旧版本,仍应显式排序。
from collections import OrderedDict
# 当顺序是业务逻辑的一部分时,显式表达
ordered = OrderedDict([("a", 1), ("b", 2)])29. 用 get 处理缺失键
# 不推荐
counts = {}
if "apple" in counts:
counts["apple"] += 1
else:
counts["apple"] = 1
# 推荐
counts["apple"] = counts.get("apple", 0) + 130. 用 defaultdict 替代 setdefault
from collections import defaultdict
# 不推荐
visits = {}
visits.setdefault("China", set()).add("Beijing")
# 推荐
visits = defaultdict(set)
visits["China"].add("Beijing")31. 用 __missing__ 构建依赖键的默认值
class MyDict(dict):
def __missing__(self, key):
value = self[key] = f"computed-{key}"
return value
d = MyDict()
print(d["foo"]) # computed-foo
print(d) # {'foo': 'computed-foo'}章节小结:操作列表和字典时,优先使用切片、解包、get、defaultdict 等 Python 提供的简洁写法。需要表达复杂逻辑时,用 key 函数或自定义 __missing__,而不是写一堆 if key in dict。
三、函数
函数是 Python 中组织逻辑的核心单元。本章讨论如何设计清晰、健壮、易扩展的函数接口。
32. 函数返回值不要超过 3 个
返回多个未命名值会让调用端极易出错。
from typing import NamedTuple
# 不推荐
def get_user():
return "Alice", 25, "Beijing"
name, age, city = get_user() # 顺序一旦变化就崩
# 推荐
class User(NamedTuple):
name: str
age: int
city: str
def get_user() -> User:
return User("Alice", 25, "Beijing")
u = get_user()
print(u.name, u.age)33. 异常优于返回 None
# 不推荐:None 容易与 0、False、空字符串混淆
def divide(a, b):
if b == 0:
return None
return a / b
# 推荐
def divide(a, b):
if b == 0:
raise ValueError("b cannot be zero")
return a / b34. 闭包与变量作用域
闭包可以引用外层变量,但赋值时需要 nonlocal。
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c()) # 1
print(c()) # 235. 用可变位置参数让接口更简洁
def log(message, *args):
print(message.format(*args))
log("Hello, {}", "Alice")
log("{} + {} = {}", 1, 2, 3)注意:不要把无限生成器用 * 展开传给函数,会耗尽内存。
36. 用关键字参数表示可选行为
def connect(host, port=3306, timeout=30):
...
# 调用意图清晰
connect("localhost", timeout=10)37. 用 None 和 docstring 处理动态默认值
from datetime import datetime
# 错误:默认值只在定义时计算一次
def log_event(msg, when=datetime.now()):
...
# 正确
def log_event(msg, when=None):
"""Log a message with optional timestamp.
Args:
msg: Message to log.
when: Timestamp; defaults to current time.
"""
if when is None:
when = datetime.now()
print(f"{when}: {msg}")38. 用仅限关键字和仅限位置参数增强接口清晰度
def greet(name, /, greeting="Hello", *, excited=False):
"""
name: 只能位置传参
greeting: 位置或关键字均可
excited: 只能关键字传参
"""
tail = "!" if excited else "."
print(f"{greeting}, {name}{tail}")
greet("Alice") # OK
greet("Alice", "Hi") # OK
greet("Alice", excited=True) # OK
# greet(name="Alice") # 错误:name 是位置-only39. 用 functools.wraps 保留装饰器元信息
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("before")
return func(*args, **kwargs)
return wrapper
@my_decorator
def add(a, b):
"""Return sum of a and b."""
return a + b
print(add.__name__) # add
print(add.__doc__) # Return sum of a and b.章节小结:设计函数接口时,优先让调用方“一看就懂”。用关键字参数、位置/关键字限定、命名元组返回值来处理可选行为和复杂返回;用 None 处理动态默认值;用 @wraps 保证装饰器透明。
四、推导与生成器
推导式和生成器是 Python 表达数据转换的利器。本章的重点是可读性边界和内存效率。
40. 用列表推导替代 map/filter
a = [1, 2, 3, 4, 5]
# 不推荐
squares = list(map(lambda x: x**2, filter(lambda x: x % 2 == 0, a)))
# 推荐
squares = [x**2 for x in a if x % 2 == 0]41. 控制推导式的复杂度
条件、嵌套、转换超过两个子表达式时,建议改写成普通循环。
# 不推荐:难以理解
matrix = [[...]]
flat = [x for row in matrix for cell in row for x in cell if x > 0]
# 推荐
flat = []
for row in matrix:
for cell in row:
for x in cell:
if x > 0:
flat.append(x)42. 用赋值表达式消除推导中的重复
def get_batches(count, size):
return count // size
stocks = {"apple": 10, "banana": 3}
order = ["apple", "banana", "orange"]
found = {
name: batches
for name in order
if (batches := get_batches(stocks.get(name, 0), 4))
}
# {'apple': 2}43. 让函数用 yield 逐个生成值
def read_large_file(path):
with open(path, "r") as f:
for line in f:
yield line.strip()
# 内存友好,每次只处理一行
for line in read_large_file("huge.log"):
process(line)44. 谨慎迭代函数收到的参数
迭代器只能遍历一次。
def normalize(numbers):
total = sum(numbers)
for n in numbers: # 如果 numbers 是迭代器,这里已经空了
yield 100 * n / total
it = iter([1, 2, 3])
print(list(normalize(it))) # []解决方案:把参数转换成列表,或要求调用方传入可重复遍历的集合。
def normalize(numbers):
numbers = list(numbers) # 显式物化
total = sum(numbers)
for n in numbers:
yield 100 * n / total45. 用生成器表达式改写大列表推导
# 列表推导:一次性占用大量内存
lengths = [len(x) for x in open("file.txt")]
# 生成器表达式:惰性求值
lengths = (len(x) for x in open("file.txt"))
print(next(lengths))46. 用 yield from 连接多个生成器
def gen_a():
yield 1
yield 2
def gen_b():
yield 3
yield 4
def gen_all():
yield from gen_a()
yield from gen_b()
print(list(gen_all())) # [1, 2, 3, 4]47. 不要用 send 给生成器注入数据
send 会让 yield 表达式返回新值,但与 yield from 组合时行为复杂,团队代码里容易成为“谜之代码”。
48. 不要用 throw 变换生成器状态
throw 可向生成器发送异常,同样难以理解且难以调试。
49. 用 itertools 拼装迭代器
from itertools import chain, cycle, islice, repeat, tee
list(chain([1, 2], [3, 4])) # [1, 2, 3, 4]
list(repeat("x", 3)) # ['x', 'x', 'x']
list(islice(cycle([1, 2]), 5)) # [1, 2, 1, 2, 1]
it = iter([1, 2, 3])
a, b = tee(it, 2)章节小结:推导式适合简单转换,生成器适合大数据和惰性计算。不要为了一点代码量牺牲可读性;当推导式超过两个子表达式时,改写成循环更稳妥。同时记住迭代器只能遍历一次,必要时显式物化。
五、类与接口
Python 的面向对象非常灵活,本章讨论如何用类构建清晰、可组合、可测试的设计。
50. 类使用组合,避免嵌套内置类型
复杂结构应该封装成类,而不是到处传递 dict 或 tuple。
# 不推荐
def process(data: dict):
name = data["name"]
address = data["address"]["city"]
# 推荐
from dataclasses import dataclass
@dataclass
class Address:
city: str
@dataclass
class Person:
name: str
address: Address
def process(person: Person):
print(person.address.city)51. 让简单接口接受函数,而不是实例
names = ["Alice", "Bob", "Charlie"]
names.sort(key=len) # 传入函数即可defaultdict 的工厂函数、sorted 的 key 都是这种思想的体现。
52. 通过 @classmethod 多态构造对象
class InputData:
def read(self):
raise NotImplementedError
class PathInputData(InputData):
def __init__(self, path):
self.path = path
def read(self):
with open(self.path) as f:
return f.read()
class Worker:
def __init__(self, input_data):
self.input_data = input_data
@classmethod
def create_workers(cls, input_class, config):
workers = []
for item in config:
workers.append(cls(input_class(item)))
return workers53. 通过 super() 初始化超类
class Base:
def __init__(self, value):
self.value = value
class Derived(Base):
def __init__(self, value, extra):
super().__init__(value)
self.extra = extra多重继承时,super() 配合 MRO 能保证父类初始化顺序正确。
54. 用 Mixin 表示可组合的功能
class JSONSerializableMixin:
def to_json(self):
import json
return json.dumps(self.__dict__)
class User(JSONSerializableMixin):
def __init__(self, name):
self.name = name
u = User("Alice")
print(u.to_json()) # {"name": "Alice"}55. 优先使用 public 属性
Python 没有真正的私有。_ 开头是约定上的 protected,__ 开头只是名字改写(name mangling),仍然可以访问。
class Demo:
def __init__(self):
self._internal = 1
self.__mangled = 2
d = Demo()
print(d._internal) # 可以访问
print(d._Demo__mangled) # 也可以访问落地建议:与其强行私有,不如把内部状态用 _ 标记,文档中说明不稳定的 API。
56. 自定义容器类继承 collections.abc
from collections.abc import Sequence
class MyList(Sequence):
def __init__(self, items):
self._items = list(items)
def __getitem__(self, index):
return self._items[index]
def __len__(self):
return len(self._items)继承 collections.abc 的抽象基类可以自动帮你补齐 __iter__、__contains__ 等方法。
章节小结:类的设计原则是“组合优于嵌套、接口优于实现”。用 @dataclass 做数据封装,用 @classmethod 做多态构造,用 Mixin 组合横切功能,用 collections.abc 保证容器接口完整。
六、元类与属性
本章进入 Python 的动态特性:属性访问控制、描述符、元类与类装饰器。这些特性威力大,但也容易过度使用。
57. 用 @property 取代 getter/setter
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Too cold")
self._celsius = value58. 不要急着用 @property 重构旧代码
只有当访问逻辑变得复杂、且多处重复时,才值得引入 @property。简单字段直接公开即可。
59. 用描述符改写需要复用的 property
class Validated:
def __set_name__(self, owner, name):
self.name = name
self.private_name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name, None)
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.private_name, value)
def validate(self, value):
raise NotImplementedError
class NonNegative(Validated):
def validate(self, value):
if value < 0:
raise ValueError(f"{self.name} must be >= 0")
class Person:
age = NonNegative()
p = Person()
p.age = 30
# p.age = -1 # ValueError60. 针对惰性属性使用 __getattr__、__getattribute__、__setattr__
class LazyRecord:
def __init__(self):
self.exists = 5
def __getattr__(self, name):
value = f"Value for {name}"
setattr(self, name, value)
return value
data = LazyRecord()
print(data.exists) # 5,普通属性
print(data.foo) # Value for foo,触发 __getattr__
print(data.foo) # Value for foo,已缓存61. 用 __init_subclass__ 验证子类
class Base:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if not hasattr(cls, "required_method"):
raise TypeError(f"{cls.__name__} must define required_method")
class Good(Base):
def required_method(self):
pass
# class Bad(Base): pass # TypeError62. 用 __init_subclass__ 记录子类
class PluginBase:
plugins = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.plugins.append(cls)
class PluginA(PluginBase):
pass
class PluginB(PluginBase):
pass
print(PluginBase.plugins) # [<class PluginA>, <class PluginB>]63. 用 __set_name__ 给类属性加注解
如第 59 条示例,__set_name__ 让描述符在类创建时自动拿到属性名。
64. 优先考虑类装饰器,而不是元类
类装饰器通常比元类更易读、更易调试。
def add_repr(cls):
def __repr__(self):
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{cls.__name__}({attrs})"
cls.__repr__ = __repr__
return cls
@add_repr
class Person:
def __init__(self, name):
self.name = name
p = Person("Alice")
print(p) # Person(name='Alice')章节小结:属性访问控制、描述符、类装饰器、元类都是 Python 提供的元编程工具。日常开发中,@property 和类装饰器已经能解决 90% 的问题;描述符和元类适合框架级代码,业务代码里应谨慎使用。
七、并发与并行
Python 的并发模型有三条路:多线程、多进程、协程。本章的核心是根据场景选对工具。
65. subprocess 管理子进程
import subprocess
# 同步等待
result = subprocess.run(["ls", "-l"], capture_output=True, text=True)
print(result.stdout)
# 异步启动
proc = subprocess.Popen(["sleep", "5"])
print("doing other things...")
proc.communicate() # 等待完成66. 不要用线程做 CPU 密集型并行计算
GIL 限制了同一时刻只有一个线程执行 Python 字节码。多线程适合 I/O 等待,不适合纯计算。
import threading
# 这个例子不会因为多线程而加速
def count(n):
while n > 0:
n -= 1
threads = [threading.Thread(target=count, args=(100_000_000,)) for _ in range(2)]
for t in threads:
t.start()
for t in threads:
t.join()67. 用 Lock 防止多线程竞争
from threading import Lock, Thread
counter = 0
lock = Lock()
def increment():
global counter
for _ in range(100_000):
with lock:
counter += 1
threads = [Thread(target=increment) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter) # 1_000_00068. 用 Queue 协调线程进度
from queue import Queue
from threading import Thread
q = Queue(maxsize=1) # 容量为 1,形成背压
def producer():
for i in range(5):
q.put(i)
print(f"produced {i}")
q.put(None) # 结束信号
def consumer():
while True:
item = q.get()
if item is None:
break
print(f"consumed {item}")
Thread(target=producer).start()
Thread(target=consumer).start()69. 用 ThreadPoolExecutor 做线程池
from concurrent.futures import ThreadPoolExecutor, as_completed
def fetch(url):
import urllib.request
with urllib.request.urlopen(url, timeout=5) as resp:
return len(resp.read())
urls = ["https://example.com"] * 5
with ThreadPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(fetch, url): url for url in urls}
for future in as_completed(futures):
print(future.result())70. 协程实现高并发 I/O
import asyncio
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
await asyncio.gather(
say_after(1, "hello"),
say_after(2, "world"),
)
asyncio.run(main())71. asyncio.run_in_executor 包裹同步阻塞调用
import asyncio
from concurrent.futures import ThreadPoolExecutor
def blocking_io():
import time
time.sleep(1)
return "done"
async def main():
loop = asyncio.get_running_loop()
with ThreadPoolExecutor() as pool:
result = await loop.run_in_executor(pool, blocking_io)
print(result)
asyncio.run(main())72. 用 concurrent.futures 实现多进程
from concurrent.futures import ProcessPoolExecutor
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(fib, [30, 31, 32, 33]))
print(results)章节小结:并发工具选择的核心是 GIL。I/O 密集型优先协程,次选线程;CPU 密集型用多进程。线程间共享状态要加锁,协程里不要阻塞事件循环,必要时用 run_in_executor。
八、稳定与性能
本章关注如何让程序更可靠、更可预测、更快。但前提永远是:先分析,再优化。
73. 合理利用 try/except/else/finally
def read_file(path):
f = open(path, "r")
try:
data = f.read()
except UnicodeDecodeError as e:
print(f"decode error: {e}")
raise
else:
print(f"read {len(data)} bytes")
return data
finally:
f.close()74. 用 contextlib 和 with 改写复用 try/finally
from contextlib import contextmanager
@contextmanager
def managed_resource(name):
print(f"acquiring {name}")
yield name
print(f"releasing {name}")
with managed_resource("db") as r:
print(f"using {r}")75. 用 datetime 处理本地时间
from datetime import datetime, timezone
# 不推荐:依赖系统时区的 naive datetime
now = datetime.now()
# 推荐:显式带时区
utc_now = datetime.now(timezone.utc)
print(utc_now)76. 用 copyreg 实现可靠的 pickle 操作
import copyreg
import pickle
class GameState:
def __init__(self, level, health):
self.level = level
self.health = health
def pickle_game_state(state):
kwargs = {"level": state.level, "health": state.health}
return unpickle_game_state, (kwargs,)
def unpickle_game_state(kwargs):
return GameState(**kwargs)
copyreg.pickle(GameState, pickle_game_state)
state = GameState(1, 100)
data = pickle.dumps(state)
restored = pickle.loads(data)77. 用 Decimal 做精确计算
from decimal import Decimal, ROUND_HALF_UP
# 浮点数的精度问题
print(0.1 + 0.2) # 0.30000000000000004
# Decimal 精确控制
price = Decimal("0.1") + Decimal("0.2")
print(price) # 0.3
# 控制舍入
fee = Decimal("10.005").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
print(fee) # 10.0178. 先分析性能,再优化
python -m cProfile -s cumtime my_script.py不要凭感觉优化。用 cProfile 找到真正的热点函数。
79. 优先考虑 deque 实现生产者-消费者队列
from collections import deque
d = deque()
d.append(1)
d.appendleft(0)
d.pop() # 1
d.popleft() # 0deque 两端操作都是 O(1),list 的 pop(0) 是 O(n)。
80. 用 bisect 搜索已排序序列
import bisect
values = [1, 3, 5, 7, 9]
index = bisect.bisect_left(values, 4)
print(index) # 2,4 应该插入的位置81. 用 heapq 制作优先级队列
import heapq
from functools import total_ordering
@total_ordering
class Task:
def __init__(self, priority, name):
self.priority = priority
self.name = name
def __lt__(self, other):
return self.priority < other.priority
def __eq__(self, other):
return self.priority == other.priority
heap = []
heapq.heappush(heap, Task(2, "write tests"))
heapq.heappush(heap, Task(1, "fix bug"))
while heap:
task = heapq.heappop(heap)
print(task.priority, task.name)82. 用 memoryview 与 bytearray 实现零拷贝 bytes 操作
data = bytearray(b"hello world")
mv = memoryview(data)
sub = mv[6:11]
sub[0] = ord("W") # 修改会影响原始 data
print(data) # bytearray(b'hello World')章节小结:稳定性的关键是边界处理和资源管理,性能的关键是选对数据结构和避免过早优化。Decimal 处理钱、deque 处理队列、bisect/heapq 处理有序和优先数据、cProfile 指导优化方向。
九、测试与调试
测试不是写完代码后的“补充工作”,而是代码的一部分。本章讨论如何写出可维护、可定位问题的测试与调试策略。
83. 通过 repr 字符串输出调试信息
x = {"a": 1, "b": [2, 3]}
print(repr(x)) # {'a': 1, 'b': [2, 3]}
# 自定义类也应实现 __repr__
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x!r}, {self.y!r})"
p = Point(1, 2)
print(repr(p)) # Point(1, 2)84. 在 TestCase 子类里验证相关行为
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual("foo".upper(), "FOO")
def test_isupper(self):
self.assertTrue("FOO".isupper())
self.assertFalse("Foo".isupper())
if __name__ == "__main__":
unittest.main()85. 测试前后准备与清理放在 setUp/tearDown
class TestDatabase(unittest.TestCase):
def setUp(self):
self.conn = create_test_db()
def tearDown(self):
self.conn.close()
cleanup_test_db()
def test_insert(self):
self.conn.execute("INSERT INTO users VALUES ('Alice')")
row = self.conn.execute("SELECT * FROM users").fetchone()
self.assertEqual(row[0], "Alice")86. 用 Mock 模拟复杂依赖
from unittest.mock import Mock, patch
def get_user_name(user_id):
import requests
resp = requests.get(f"/users/{user_id}")
return resp.json()["name"]
with patch("requests.get") as mock_get:
mock_get.return_value.json.return_value = {"name": "Alice"}
assert get_user_name(1) == "Alice"87. 把依赖系统封装起来,便于模拟和测试
from abc import ABC, abstractmethod
class UserRepository(ABC):
@abstractmethod
def get(self, user_id):
...
class HttpUserRepository(UserRepository):
def get(self, user_id):
... # 真实 HTTP 调用
class FakeUserRepository(UserRepository):
def __init__(self, users):
self.users = users
def get(self, user_id):
return self.users.get(user_id)
# 测试时使用 FakeUserRepository88. 考虑用 pdb 做交互调试
def buggy_func(x):
import pdb; pdb.set_trace()
return x / 0
buggy_func(10)常用命令:n(下一步)、s(进入函数)、c(继续)、p var(打印变量)、q(退出)。
89. 用 tracemalloc 掌握内存使用与泄漏
import tracemalloc
tracemalloc.start()
# 运行你的代码
large = [bytearray(1024 * 1024) for _ in range(100)]
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")[:10]
for stat in top_stats:
print(stat)章节小结:好的测试应贴近真实使用场景,同时把外部依赖隔离。用 setUp/tearDown 管理状态,用抽象接口和 Mock 隔离外部系统,用 pdb 和 tracemalloc 定位 bug 和内存问题。
十、协作开发
代码最终要被人读、被人改。本章的建议关乎如何让代码在团队中稳定地演进。
90. 学会寻找社区模块
遇到通用问题先查 PyPI:https://pypi.org。不要重复造轮子,但也不要盲目引入依赖。
91. 用虚拟环境隔离项目,并重建依赖关系
# 生成依赖
pip freeze > requirements.txt
# 重建环境
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt生产环境建议用 pip-tools 或 poetry 管理依赖树。
92. 每个函数、类与模块都要写 docstring
def calculate_area(radius):
"""Calculate the area of a circle.
Args:
radius: The radius of the circle.
Returns:
The area as a float.
Raises:
ValueError: If radius is negative.
"""
if radius < 0:
raise ValueError("radius must be non-negative")
return 3.14159 * radius ** 293. 用包来安排模块,提供稳固 API
myproject/
__init__.py
core.py
utils.py# __init__.py
from .core import process
from .utils import helper
__all__ = ["process", "helper"]94. 用模块级代码配置不同部署环境
import os
ENV = os.getenv("APP_ENV", "development")
if ENV == "production":
DATABASE_URL = os.getenv("DATABASE_URL")
else:
DATABASE_URL = "sqlite:///dev.db"95. 为自编模块定义根异常
class MyLibError(Exception):
"""Base exception for mylib."""
class ConfigError(MyLibError):
"""Raised when configuration is invalid."""
# 调用方可以统一捕获 MyLibError
try:
do_something()
except MyLibError as e:
logger.error(f"mylib error: {e}")96. 用适当方式打破循环依赖
最佳方案是把公共代码下沉到依赖最底层:
# 不推荐:A -> B -> A
# 推荐:A -> common <- B如果重构成本高,可以用动态导入:
def do_something():
from module_b import helper
helper()97. 重构时用 warnings 提醒 API 变更
import warnings
def old_function():
warnings.warn(
"old_function is deprecated, use new_function instead",
DeprecationWarning,
stacklevel=2,
)
return new_function()98. 考虑用 typing 做静态分析
from typing import Optional, List
def find_user(user_id: int) -> Optional[dict]:
...
def list_users(ids: List[int]) -> List[dict]:
...配合 mypy、pyright 等类型检查工具,能在运行前发现一类常见 bug。
章节小结:协作开发的核心是“让别人能安全地改你的代码”。用虚拟环境隔离依赖、用 docstring 和类型注解降低理解成本、用根异常和 warnings 提供稳定的 API 演进路径、用包结构控制对外暴露的接口。
全文总结
《Effective Python》的 90 条建议并不是孤立的技巧,而是一套从代码风格到系统设计的完整思维框架。通读下来,可以提炼出几条贯穿全书的主线:
-
可读性优先。Python 的设计哲学是“有一种明显的方式去做一件事”。f-string、解包、
enumerate、zip、with、列表推导等语法特性,都是为了让意图更直接。不要为了少写几行而牺牲可读性。 -
显式优于隐式。无论是时区、编码、动态默认值,还是循环依赖、API 废弃,都应该在代码中明确表达,而不是依赖隐式行为或口头约定。
-
根据场景选择工具。并发选线程、协程还是进程?排序用
sort、二分还是堆?精度用 float 还是 Decimal?没有银弹,只有合适的工具。 -
测试和文档是代码的一部分。Mock、抽象接口、docstring、类型注解、
warnings,都是为了让代码在团队中可持续演进。 -
先分析,再优化。性能问题要用
cProfile验证,内存问题要用tracemalloc观察,避免凭感觉做“优化”。
最后,这些建议的价值不在于一次性背下来,而在于形成写代码时的默认习惯。每次写完一段代码,都可以回头问一句:这里有没有更 Pythonic、更可维护、更高效的写法?坚持这个习惯,90 条建议自然会内化为直觉。
我在 ftmao.com/blog 持续输出 AI 工程、Python 实践与多模态生成相关内容,欢迎关注和交流。