高级技巧
掌握变量、数据结构、函数和类以后,下一阶段主要学习:
更简洁地处理数据
更合理地组织程序
更高效地处理大量任务
更安全地管理资源
更准确地表达类型
更方便地测试和维护
高级 Python 的核心不是把代码写得复杂,而是:
用更合适的语言特性,把复杂问题写得更简单。
主要内容包括:
- 解包和参数传递
- 推导式和生成器
- 迭代器
- 装饰器
- 闭包和高阶函数
- 上下文管理器
- 特殊方法
- 数据类
- 模式匹配
- 高级类型提示
- 异常组
- 并发编程
- 异步编程
- 缓存
- 性能分析
- 日志
- 测试
- 项目结构
Python 可以把列表、元组等序列中的元素直接分配给多个变量。
stock = ("600519", "贵州茅台", 1500.0)
code, name, price = stock
print(code)
print(name)
print(price)
变量数量必须与元素数量一致。
使用 * 接收多个元素:
numbers = [1, 2, 3, 4, 5]
first, *middle, last = numbers
print(first)
print(middle)
print(last)
结果:
1
[2, 3, 4]
5
注意,星号变量得到的是列表。
stock = (
"600519",
"贵州茅台",
1500.0,
"白酒",
)
code, name, *_ = stock
通常使用 _ 表示该值不会使用。
只取首尾:
first, *_, last = numbers
stock = (
"600519",
"贵州茅台",
(1500.0, 1520.0),
)
code, name, (open_price, close_price) = stock
list_a = [1, 2]
list_b = [3, 4]
result = [*list_a, *list_b, 5]
结果:
[1, 2, 3, 4, 5]
base = {
"code": "600519",
"name": "贵州茅台",
}
price_info = {
"price": 1500.0,
"change": 1.5,
}
stock = {
**base,
**price_info,
}
也可以使用 Python 3.9 以上支持的 |:
stock = base | price_info
如果键重复,后面的值覆盖前面的值:
a = {"price": 100}
b = {"price": 120}
result = a | b
结果:
{"price": 120}
原地更新:
a |= b
numbers = [
number ** 2
for number in range(1, 11)
]
带条件:
even_squares = [
number ** 2
for number in range(1, 11)
if number % 2 == 0
]
labels = [
"偶数" if number % 2 == 0 else "奇数"
for number in range(1, 6)
]
注意两种条件位置的区别。
过滤:
[
number
for number in numbers
if number > 0
]
二选一转换:
[
number if number > 0 else 0
for number in numbers
]
matrix = [
[1, 2, 3],
[4, 5, 6],
]
展开二维列表:
flattened = [
number
for row in matrix
for number in row
]
结果:
[1, 2, 3, 4, 5, 6]
复杂推导式可读性较差时,应改成普通循环。
stocks = [
{
"code": "600519",
"name": "贵州茅台",
},
{
"code": "000001",
"name": "平安银行",
},
]
stock_map = {
stock["code"]: stock["name"]
for stock in stocks
}
结果:
{
"600519": "贵州茅台",
"000001": "平安银行",
}
prefixes = {
code[:2]
for code in [
"600519",
"603259",
"000001",
"002594",
]
}
Python 3.8 以上支持 :=,称为赋值表达式或海象运算符。
普通写法:
text = input("请输入内容:")
while text != "quit":
print(text)
text = input("请输入内容:")
使用海象运算符:
while (
text := input("请输入内容:")
) != "quit":
print(text)
普通写法:
data = get_data()
length = len(data)
if length > 10:
print(length)
简化:
if (
length := len(get_data())
) > 10:
print(length)
texts = [
"Python",
"",
"Java",
" ",
"Go",
]
clean_texts = [
clean
for text in texts
if (clean := text.strip())
]
结果:
["Python", "Java", "Go"]
不要为了少写一行而滥用 :=。
stocks = [
{
"name": "贵州茅台",
"price": 1500,
},
{
"name": "平安银行",
"price": 11,
},
{
"name": "宁德时代",
"price": 300,
},
]
result = sorted(
stocks,
key=lambda stock: stock["price"],
)
先按行业排序,再按价格降序:
stocks = [
{
"name": "A公司",
"industry": "银行",
"price": 10,
},
{
"name": "B公司",
"industry": "白酒",
"price": 100,
},
{
"name": "C公司",
"industry": "银行",
"price": 20,
},
]
result = sorted(
stocks,
key=lambda stock: (
stock["industry"],
-stock["price"],
),
)
from operator import itemgetter
按照字典字段排序:
result = sorted(
stocks,
key=itemgetter("price"),
)
多字段:
result = sorted(
stocks,
key=itemgetter(
"industry",
"price",
),
)
对象列表:
from dataclasses import dataclass
from operator import attrgetter
@dataclass
class Stock:
code: str
price: float
排序:
stocks = [
Stock("600519", 1500),
Stock("000001", 11),
]
result = sorted(
stocks,
key=attrgetter("price"),
)
collections 提供了许多实用数据结构。
from collections import Counter
industries = [
"银行",
"白酒",
"银行",
"半导体",
"银行",
]
counts = Counter(industries)
print(counts)
结果:
Counter({
"银行": 3,
"白酒": 1,
"半导体": 1,
})
查看出现次数最多的元素:
print(counts.most_common(2))
结果:
[
("银行", 3),
("白酒", 1),
]
更新计数:
counts.update([
"银行",
"白酒",
])
普通字典分组:
groups = {}
for stock in stocks:
industry = stock["industry"]
if industry not in groups:
groups[industry] = []
groups[industry].append(stock)
使用 defaultdict:
from collections import defaultdict
groups: defaultdict[
str,
list[dict[str, object]],
] = defaultdict(list)
for stock in stocks:
groups[stock["industry"]].append(stock)
统计:
counts = defaultdict(int)
for industry in industries:
counts[industry] += 1
from collections import deque
queue = deque()
右侧添加:
queue.append("A")
左侧添加:
queue.appendleft("B")
右侧弹出:
queue.pop()
左侧弹出:
queue.popleft()
deque 从左侧删除元素的效率高于列表。
限制最大长度:
recent_prices = deque(
maxlen=5
)
for price in [
10,
11,
12,
13,
14,
15,
]:
recent_prices.append(price)
最终只保留最近五个值。
适合:
- 队列
- 滑动窗口
- 最近记录
- 广度优先搜索
将多个字典组合成一个逻辑视图:
from collections import ChainMap
default_config = {
"timeout": 10,
"retries": 3,
}
user_config = {
"timeout": 30,
}
config = ChainMap(
user_config,
default_config,
)
print(config["timeout"])
print(config["retries"])
优先使用前面的字典。
能够被 for 遍历的对象称为可迭代对象。
例如:
list
tuple
str
dict
set
range
for 循环内部会先调用:
iter(object)
得到迭代器,再不断调用:
next(iterator)
numbers = [10, 20, 30]
iterator = iter(numbers)
print(next(iterator))
print(next(iterator))
print(next(iterator))
再次调用:
next(iterator)
会抛出:
StopIteration
class Countdown:
def __init__(
self,
start: int,
) -> None:
self.current = start
def __iter__(self):
return self
def __next__(self) -> int:
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
使用:
for number in Countdown(5):
print(number)
可迭代对象:
可以生成迭代器
迭代器:
可以通过 next() 逐个返回数据
迭代器通常只能消费一次。
生成器是创建迭代器的简单方式。
def countdown(start: int):
while start > 0:
yield start
start -= 1
使用:
for number in countdown(5):
print(number)
yield 会:
- 返回一个值
- 暂停函数
- 保存当前状态
- 下次继续执行
列表推导式:
squares = [
number ** 2
for number in range(1_000_000)
]
生成器表达式:
squares = (
number ** 2
for number in range(1_000_000)
)
列表会一次性保存全部结果。
生成器按需产生数据,更节省内存。
普通写法:
def flatten(groups):
for group in groups:
for item in group:
yield item
使用 yield from:
def flatten(groups):
for group in groups:
yield from group
调用:
groups = [
[1, 2],
[3, 4],
[5],
]
print(list(flatten(groups)))
def batch[
T
](
items: list[T],
size: int,
):
if size <= 0:
raise ValueError(
"size 必须大于 0"
)
for index in range(
0,
len(items),
size,
):
yield items[
index:index + size
]
使用:
codes = [
"600519",
"000001",
"300750",
"002594",
"603259",
]
for group in batch(
codes,
size=2,
):
print(group)
结果:
["600519", "000001"]
["300750", "002594"]
["603259"]
不推荐:
content = file.read()
大文件可能占用大量内存。
推荐逐行处理:
def read_lines(
file_path: str,
):
with open(
file_path,
"r",
encoding="utf-8",
) as file:
for line in file:
yield line.rstrip("\n")
itertools 提供高效的迭代工具。
合并多个可迭代对象:
from itertools import chain
result = chain(
[1, 2],
[3, 4],
[5, 6],
)
print(list(result))
对迭代器进行切片:
from itertools import islice
numbers = (
number
for number in range(100)
)
result = islice(
numbers,
10,
20,
)
print(list(result))
按照连续相同键分组。
使用前通常需要先排序:
from itertools import groupby
from operator import itemgetter
stocks = sorted(
stocks,
key=itemgetter("industry"),
)
for industry, group in groupby(
stocks,
key=itemgetter("industry"),
):
print(
industry,
list(group),
)
生成组合:
from itertools import combinations
codes = [
"600519",
"000001",
"300750",
]
for pair in combinations(
codes,
2,
):
print(pair)
笛卡尔积:
from itertools import product
markets = ["上海", "深圳"]
directions = ["买入", "卖出"]
for item in product(
markets,
directions,
):
print(item)
累计计算:
from itertools import accumulate
changes = [
10,
-5,
20,
-3,
]
result = list(
accumulate(changes)
)
print(result)
结果:
[10, 5, 25, 22]
闭包是内部函数记住外层函数变量的机制。
def create_multiplier(
factor: float,
):
def multiply(
value: float,
) -> float:
return value * factor
return multiply
使用:
double = create_multiplier(2)
triple = create_multiplier(3)
print(double(10))
print(triple(10))
闭包适合:
- 保存配置
- 创建函数工厂
- 装饰器
- 保存简单状态
def create_counter():
count = 0
def increase() -> int:
nonlocal count
count += 1
return count
return increase
使用:
counter = create_counter()
print(counter())
print(counter())
print(counter())
接收函数作为参数,或者返回函数的函数,称为高阶函数。
def apply_operation(
a: float,
b: float,
operation,
):
return operation(a, b)
使用:
def add(
a: float,
b: float,
) -> float:
return a + b
result = apply_operation(
10,
20,
add,
)
固定函数的一部分参数:
from functools import partial
def calculate_tax(
amount: float,
rate: float,
) -> float:
return amount * rate
创建固定税率函数:
calculate_vat = partial(
calculate_tax,
rate=0.13,
)
print(
calculate_vat(1000)
)
from functools import reduce
numbers = [1, 2, 3, 4]
result = reduce(
lambda a, b: a * b,
numbers,
)
print(result)
结果:
24
很多场景中,普通循环比 reduce() 更清晰。
装饰器用于在不修改原函数代码的情况下增加功能。
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(
*args,
**kwargs,
):
print(
f"开始调用:"
f"{func.__name__}"
)
result = func(
*args,
**kwargs,
)
print(
f"调用结束:"
f"{func.__name__}"
)
return result
return wrapper
使用:
@log_call
def add(
a: int,
b: int,
) -> int:
return a + b
from functools import wraps
def repeat(times: int):
if times <= 0:
raise ValueError(
"times 必须大于 0"
)
def decorator(func):
@wraps(func)
def wrapper(
*args,
**kwargs,
):
result = None
for _ in range(times):
result = func(
*args,
**kwargs,
)
return result
return wrapper
return decorator
使用:
@repeat(3)
def greet(name: str) -> None:
print(f"你好,{name}")
from functools import wraps
from time import perf_counter
def measure_time(func):
@wraps(func)
def wrapper(
*args,
**kwargs,
):
start = perf_counter()
try:
return func(
*args,
**kwargs,
)
finally:
elapsed = (
perf_counter()
- start
)
print(
f"{func.__name__} "
f"耗时:"
f"{elapsed:.6f} 秒"
)
return wrapper
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(
self,
*args,
**kwargs,
):
self.count += 1
print(
f"调用次数:"
f"{self.count}"
)
return self.func(
*args,
**kwargs,
)
使用:
@CountCalls
def say_hello() -> None:
print("Hello")
上下文管理器负责自动获取和释放资源。
最常见的例子:
with open(
"data.txt",
"r",
encoding="utf-8",
) as file:
content = file.read()
退出 with 后,文件自动关闭。
class Timer:
def __enter__(self):
from time import perf_counter
self._start = perf_counter()
return self
def __exit__(
self,
exc_type,
exc_value,
traceback,
):
from time import perf_counter
self.elapsed = (
perf_counter()
- self._start
)
print(
f"耗时:"
f"{self.elapsed:.6f} 秒"
)
return False
使用:
with Timer():
total = sum(
range(1_000_000)
)
from contextlib import contextmanager
from time import perf_counter
@contextmanager
def timer():
start = perf_counter()
try:
yield
finally:
elapsed = (
perf_counter()
- start
)
print(
f"耗时:"
f"{elapsed:.6f} 秒"
)
使用:
with timer():
total = sum(
range(1_000_000)
)
忽略指定异常:
from contextlib import suppress
with suppress(
FileNotFoundError
):
file_path.unlink()
等价于:
try:
file_path.unlink()
except FileNotFoundError:
pass
不要忽略不明确的异常。
动态管理多个上下文:
from contextlib import ExitStack
file_names = [
"a.txt",
"b.txt",
"c.txt",
]
with ExitStack() as stack:
files = [
stack.enter_context(
open(
name,
"r",
encoding="utf-8",
)
)
for name in file_names
]
contents = [
file.read()
for file in files
]
特殊方法让自定义对象支持 Python 内置语法。
class Stock:
def __init__(
self,
code: str,
price: float,
) -> None:
self.code = code
self.price = price
def __repr__(self) -> str:
return (
"Stock("
f"code={self.code!r}, "
f"price={self.price!r}"
")"
)
class Stock:
def __init__(
self,
code: str,
price: float,
) -> None:
self.code = code
self.price = price
def __lt__(
self,
other: "Stock",
) -> bool:
return self.price < other.price
现在可以:
stocks.sort()
让对象支持索引:
class StockCollection:
def __init__(
self,
stocks: list,
) -> None:
self._stocks = stocks
def __getitem__(
self,
index,
):
return self._stocks[index]
使用:
collection[0]
collection[1:3]
让对象可以像函数一样调用:
class PriceFilter:
def __init__(
self,
min_price: float,
) -> None:
self.min_price = min_price
def __call__(
self,
stock: dict[str, object],
) -> bool:
return (
float(stock["price"])
>= self.min_price
)
使用:
price_filter = PriceFilter(
min_price=100,
)
result = [
stock
for stock in stocks
if price_filter(stock)
]
from dataclasses import dataclass
@dataclass(slots=True)
class Stock:
code: str
name: str
price: float
特点:
- 限制动态添加属性
- 减少内存占用
- 适合大量对象
@dataclass(
frozen=True,
slots=True,
)
class StockCode:
value: str
创建后不能修改:
code.value = "000001"
会报错。
@dataclass(order=True)
class Stock:
price: float
code: str
name: str
会自动生成比较方法。
字段顺序决定比较顺序。
如果只想按价格比较:
from dataclasses import (
dataclass,
field,
)
@dataclass(order=True)
class Stock:
price: float
code: str = field(
compare=False
)
name: str = field(
compare=False
)
@dataclass(kw_only=True)
class Order:
code: str
price: float
quantity: int
创建对象时必须使用关键字:
order = Order(
code="600519",
price=1500,
quantity=100,
)
Python 3.10 以上支持 match。
def handle_status(
status: int,
) -> str:
match status:
case 200:
return "成功"
case 404:
return "不存在"
case 500:
return "服务器错误"
case _:
return "未知状态"
match status:
case 200 | 201 | 204:
print("请求成功")
case 400 | 404:
print("客户端错误")
case _:
print("其他状态")
command = [
"buy",
"600519",
"100",
]
match command:
case [
"buy",
code,
quantity,
]:
print(
f"买入 {code} "
f"{quantity} 股"
)
case [
"sell",
code,
quantity,
]:
print(
f"卖出 {code} "
f"{quantity} 股"
)
case _:
print("无效命令")
event = {
"type": "price",
"code": "600519",
"price": 1500.0,
}
match event:
case {
"type": "price",
"code": code,
"price": price,
}:
print(
code,
price,
)
case {
"type": "news",
"title": title,
}:
print(title)
case _:
print("未知事件")
match stock:
case {
"code": code,
"change": change,
} if change >= 9.5:
print(
f"{code} 接近涨停"
)
case {
"code": code,
"change": change,
} if change <= -9.5:
print(
f"{code} 接近跌停"
)
case _:
print("普通波动")
from dataclasses import dataclass
@dataclass
class BuyOrder:
code: str
quantity: int
@dataclass
class SellOrder:
code: str
quantity: int
def handle_order(order) -> None:
match order:
case BuyOrder(
code=code,
quantity=quantity,
):
print(
f"买入 {code} "
f"{quantity} 股"
)
case SellOrder(
code=code,
quantity=quantity,
):
print(
f"卖出 {code} "
f"{quantity} 股"
)
类型提示可以提高大型项目的可维护性。
Python 3.12 支持:
type StockCode = str
type Price = float
type StockData = dict[
str,
object,
]
使用:
def normalize_code(
code: StockCode,
) -> StockCode:
return code.strip()
Python 3.12 新语法:
def first[T](
items: list[T],
) -> T | None:
if not items:
return None
return items[0]
class Repository[T]:
def __init__(self) -> None:
self._items: list[T] = []
def add(
self,
item: T,
) -> None:
self._items.append(item)
def all(self) -> list[T]:
return self._items.copy()
使用:
stock_repository = (
Repository[Stock]()
)
用于描述固定结构的字典:
from typing import TypedDict
class StockDict(TypedDict):
code: str
name: str
price: float
函数:
def show_stock(
stock: StockDict,
) -> None:
print(
stock["code"],
stock["name"],
stock["price"],
)
可选字段:
from typing import NotRequired
class StockDict(TypedDict):
code: str
name: str
price: float
industry: NotRequired[str]
限制参数只能取指定值:
from typing import Literal
type Direction = Literal[
"buy",
"sell",
]
def create_order(
code: str,
direction: Direction,
) -> None:
pass
描述对象需要具备的方法,而不要求继承指定父类。
from typing import Protocol
class Executable(Protocol):
def execute(self) -> None:
...
函数:
def run_task(
task: Executable,
) -> None:
task.execute()
任何拥有 execute() 方法的对象都可以传入。
描述函数类型:
from collections.abc import Callable
def apply_filter(
stocks: list[Stock],
condition: Callable[
[Stock],
bool,
],
) -> list[Stock]:
return [
stock
for stock in stocks
if condition(stock)
]
为同一函数描述多个调用方式:
from typing import overload
@overload
def parse_value(
value: str,
target: type[int],
) -> int:
...
@overload
def parse_value(
value: str,
target: type[float],
) -> float:
...
def parse_value(
value: str,
target: type[int] | type[float],
) -> int | float:
return target(value)
不要让所有错误都使用 ValueError。
class StockError(Exception):
"""股票业务异常。"""
class InvalidStockCodeError(
StockError
):
"""股票代码无效。"""
class InvalidPriceError(
StockError
):
"""股票价格无效。"""
使用:
def validate_code(
code: str,
) -> str:
if (
len(code) != 6
or not code.isdigit()
):
raise InvalidStockCodeError(
f"无效股票代码:{code}"
)
return code
捕获:
try:
validate_code("ABC")
except InvalidStockCodeError as error:
print(error)
在处理底层异常时,可以保留原始原因。
def parse_price(
value: str,
) -> float:
try:
return float(value)
except ValueError as error:
raise InvalidPriceError(
f"价格格式错误:{value}"
) from error
异常信息会显示完整调用链。
不希望保留底层异常:
raise InvalidPriceError(
"价格无效"
) from None
Python 3.11 以上支持 ExceptionGroup 和 except*。
errors = [
ValueError("价格错误"),
TypeError("类型错误"),
]
raise ExceptionGroup(
"数据处理失败",
errors,
)
分别捕获:
try:
raise ExceptionGroup(
"多个错误",
[
ValueError("值错误"),
TypeError("类型错误"),
],
)
except* ValueError as error:
print(
"捕获值错误:",
error,
)
except* TypeError as error:
print(
"捕获类型错误:",
error,
)
异常组在并发任务中尤其有用。
from functools import lru_cache
@lru_cache(maxsize=128)
def calculate(
number: int,
) -> int:
print(
f"实际计算:{number}"
)
return number ** 2
重复调用:
print(calculate(10))
print(calculate(10))
第二次直接使用缓存。
查看缓存信息:
print(
calculate.cache_info()
)
清空缓存:
calculate.cache_clear()
参数必须是可哈希对象。
无限缓存:
from functools import cache
@cache
def fibonacci(
number: int,
) -> int:
if number < 2:
return number
return (
fibonacci(number - 1)
+ fibonacci(number - 2)
)
from functools import cached_property
class Report:
def __init__(
self,
values: list[float],
) -> None:
self.values = values
@cached_property
def average(self) -> float:
print("执行平均值计算")
return (
sum(self.values)
/ len(self.values)
)
第一次访问时计算,后续直接返回缓存结果:
report.average
report.average
如果底层数据变化,需要手动删除缓存:
del report.average
并发主要有三种方式:
多线程
多进程
异步编程
选择原则:
网络请求、文件等待 → 多线程或异步
CPU 密集计算 → 多进程
大量并发网络任务 → asyncio
from concurrent.futures import (
ThreadPoolExecutor,
)
from time import sleep
def fetch_data(
code: str,
) -> str:
sleep(1)
return f"{code} 数据"
并发执行:
codes = [
"600519",
"000001",
"300750",
]
with ThreadPoolExecutor(
max_workers=3,
) as executor:
results = list(
executor.map(
fetch_data,
codes,
)
)
print(results)
顺序与输入顺序一致。
from concurrent.futures import (
ThreadPoolExecutor,
as_completed,
)
with ThreadPoolExecutor(
max_workers=3,
) as executor:
future_map = {
executor.submit(
fetch_data,
code,
): code
for code in codes
}
for future in as_completed(
future_map
):
code = future_map[future]
try:
result = future.result()
except Exception as error:
print(
code,
error,
)
else:
print(
code,
result,
)
多个线程修改共享数据可能出现竞争条件。
from threading import Lock
lock = Lock()
total = 0
def increase() -> None:
global total
with lock:
total += 1
更好的方式是尽量减少共享可变状态。
from concurrent.futures import (
ProcessPoolExecutor,
)
def calculate_square(
number: int,
) -> int:
return number ** 2
def main() -> None:
numbers = list(
range(1_000_000)
)
with ProcessPoolExecutor() as executor:
results = list(
executor.map(
calculate_square,
numbers,
chunksize=1000,
)
)
if __name__ == "__main__":
main()
多进程需要注意:
- 启动进程有成本
- 数据需要序列化
- 小任务不一定更快
- Windows 和 macOS 应保留主入口判断
异步适合大量等待型任务。
import asyncio
定义异步函数:
async def fetch_data(
code: str,
) -> str:
await asyncio.sleep(1)
return f"{code} 数据"
运行:
async def main() -> None:
result = await fetch_data(
"600519"
)
print(result)
asyncio.run(main())
async def main() -> None:
codes = [
"600519",
"000001",
"300750",
]
results = await asyncio.gather(
*[
fetch_data(code)
for code in codes
]
)
print(results)
如果顺序执行,需要约三秒。
并发执行通常只需要约一秒。
Python 3.11 以上支持结构化并发:
async def main() -> None:
tasks = []
async with asyncio.TaskGroup() as group:
for code in [
"600519",
"000001",
"300750",
]:
task = group.create_task(
fetch_data(code)
)
tasks.append(task)
results = [
task.result()
for task in tasks
]
print(results)
如果其中一个任务失败,其他任务会被统一管理。
async def main() -> None:
try:
async with asyncio.timeout(2):
result = await fetch_data(
"600519"
)
print(result)
except TimeoutError:
print("任务超时")
async def limited_fetch(
code: str,
semaphore: asyncio.Semaphore,
) -> str:
async with semaphore:
return await fetch_data(code)
async def main() -> None:
semaphore = asyncio.Semaphore(5)
results = await asyncio.gather(
*[
limited_fetch(
code,
semaphore,
)
for code in codes
]
)
避免一次发起过多请求。
同步:
def function():
pass
异步:
async def function():
pass
异步不是自动变快。
只有存在大量等待时才有明显优势,例如:
- 网络请求
- 数据库查询
- 文件或消息队列等待
- WebSocket
- 大量 API 调用
纯计算任务应优先考虑多进程。
不要在正式项目中到处使用 print()。
import logging
基础配置:
logging.basicConfig(
level=logging.INFO,
format=(
"%(asctime)s "
"%(levelname)s "
"%(name)s "
"%(message)s"
),
)
获取日志器:
logger = logging.getLogger(
__name__
)
记录日志:
logger.debug("调试信息")
logger.info("程序开始")
logger.warning("数据不完整")
logger.error("操作失败")
logger.critical("严重错误")
try:
result = 10 / 0
except ZeroDivisionError:
logger.exception(
"计算失败"
)
logger.exception() 会自动记录异常堆栈。
推荐:
logger.info(
"正在处理股票:%s",
code,
)
不推荐:
logger.info(
f"正在处理股票:{code}"
)
参数化日志在日志级别关闭时更高效。
from time import perf_counter
start = perf_counter()
result = sum(
range(1_000_000)
)
elapsed = (
perf_counter()
- start
)
print(elapsed)
from timeit import timeit
elapsed = timeit(
"sum(range(1000))",
number=10_000,
)
print(elapsed)
比较两种写法:
list_time = timeit(
"[x * x for x in range(1000)]",
number=10_000,
)
loop_time = timeit(
"""
result = []
for x in range(1000):
result.append(x * x)
""",
number=10_000,
)
import cProfile
def main() -> None:
result = sum(
number ** 2
for number in range(
1_000_000
)
)
cProfile.run(
"main()"
)
也可以在终端运行:
python -m cProfile script.py
按照累计耗时排序:
python -m cProfile -s cumulative script.py
优化前应先分析,不要凭感觉优化。
通常优先级:
选择更合理的算法
选择更合适的数据结构
减少不必要的重复计算
使用生成器减少内存
使用内置函数
使用缓存
最后才考虑细节优化
例如,成员判断:
code in code_list
列表平均需要线性查找。
集合:
code in code_set
通常更快。
total = sum(
number ** 2
for number in range(
10_000_000
)
)
生成器不会一次性创建巨大列表。
@dataclass(slots=True)
class Stock:
code: str
price: float
大量对象时可以减少内存。
new_list = old_list[:]
会复制整个列表。
只读使用时,不一定需要复制。
del large_data
通常 Python 会自动管理内存,但大型临时数据不再使用时可以主动删除引用。
现代 Python 推荐使用 pathlib。
from pathlib import Path
当前目录:
current_dir = Path.cwd()
用户目录:
home_dir = Path.home()
组合路径:
file_path = (
Path("data")
/ "stocks.json"
)
判断:
file_path.exists()
file_path.is_file()
file_path.is_dir()
创建目录:
file_path.parent.mkdir(
parents=True,
exist_ok=True,
)
查找文件:
for file_path in Path(
"data"
).glob("*.json"):
print(file_path)
递归查找:
for file_path in Path(
"data"
).rglob("*.json"):
print(file_path)
import json
格式化输出:
text = json.dumps(
data,
ensure_ascii=False,
indent=2,
)
自定义对象序列化:
from dataclasses import (
asdict,
is_dataclass,
)
def json_default(value):
if is_dataclass(value):
return asdict(value)
if isinstance(value, Path):
return str(value)
raise TypeError(
f"无法序列化:"
f"{type(value).__name__}"
)
使用:
text = json.dumps(
data,
ensure_ascii=False,
indent=2,
default=json_default,
)
def add(
a: int,
b: int,
) -> int:
return a + b
assert add(1, 2) == 3
assert 适合开发测试,不应代替业务参数验证。
import unittest
class TestAdd(
unittest.TestCase
):
def test_positive_numbers(
self,
) -> None:
self.assertEqual(
add(1, 2),
3,
)
def test_negative_numbers(
self,
) -> None:
self.assertEqual(
add(-1, -2),
-3,
)
if __name__ == "__main__":
unittest.main()
class TestStockCode(
unittest.TestCase
):
def test_invalid_code(
self,
) -> None:
with self.assertRaises(
InvalidStockCodeError
):
validate_code("ABC")
重点测试:
正常输入
边界输入
空数据
错误类型
异常情况
极端数据
函数越纯粹,越容易测试。
不推荐函数内部直接创建所有依赖:
class StockService:
def get_stock(self):
database = Database()
return database.query()
推荐从外部传入:
class StockService:
def __init__(
self,
repository,
) -> None:
self.repository = repository
def get_stock(
self,
code: str,
):
return self.repository.find(
code
)
优点:
- 更容易测试
- 更容易替换数据库
- 减少模块耦合
- 更容易模拟依赖
根据第一个参数的类型选择不同实现。
from functools import singledispatch
@singledispatch
def serialize(value) -> str:
raise TypeError(
f"不支持类型:"
f"{type(value).__name__}"
)
注册整数:
@serialize.register
def _(
value: int,
) -> str:
return str(value)
注册列表:
@serialize.register
def _(
value: list,
) -> str:
return ",".join(
map(str, value)
)
使用:
print(serialize(100))
print(serialize([1, 2, 3]))
固定选项适合使用 Enum。
from enum import Enum
class Direction(Enum):
BUY = "buy"
SELL = "sell"
使用:
direction = Direction.BUY
print(direction)
print(direction.value)
判断:
if direction is Direction.BUY:
print("买入")
Python 3.11 以上支持:
from enum import StrEnum
class Direction(StrEnum):
BUY = "buy"
SELL = "sell"
StrEnum 的成员可以更自然地作为字符串使用。
from enum import (
Enum,
auto,
)
class Status(Enum):
PENDING = auto()
RUNNING = auto()
FINISHED = auto()
描述符是实现属性管理的底层机制。
class PositiveNumber:
def __set_name__(
self,
owner,
name,
) -> None:
self.private_name = (
f"_{name}"
)
def __get__(
self,
instance,
owner,
):
if instance is None:
return self
return getattr(
instance,
self.private_name,
)
def __set__(
self,
instance,
value,
) -> None:
if value < 0:
raise ValueError(
"数值不能小于零"
)
setattr(
instance,
self.private_name,
value,
)
使用:
class Stock:
price = PositiveNumber()
def __init__(
self,
price: float,
) -> None:
self.price = price
描述符适合:
- 属性校验
- ORM 字段
- 框架设计
- 属性代理
普通业务代码通常使用 property 更简单。
推荐结构:
stock_project/
├── pyproject.toml
├── README.md
├── src/
│ └── stock_project/
│ ├── __init__.py
│ ├── models.py
│ ├── services.py
│ ├── repositories.py
│ ├── exceptions.py
│ └── main.py
└── tests/
├── test_models.py
└── test_services.py
职责示例:
models.py 数据模型
services.py 业务逻辑
repositories.py 数据读写
exceptions.py 自定义异常
main.py 程序入口
tests/ 自动化测试
现代 Python 项目通常使用 pyproject.toml。
[project]
name = "stock-project"
version = "0.1.0"
description = "股票数据处理项目"
requires-python = ">=3.12"
dependencies = []
[project.optional-dependencies]
dev = [
"pytest",
"mypy",
"ruff",
]
安装开发依赖:
python -m pip install -e ".[dev]"
定义数据模型:
from dataclasses import dataclass
@dataclass(
slots=True,
frozen=True,
)
class Stock:
code: str
name: str
price: float
industry: str
def __post_init__(self) -> None:
clean_code = (
self.code.strip()
)
if (
len(clean_code) != 6
or not clean_code.isdigit()
):
raise InvalidStockCodeError(
self.code
)
if self.price < 0:
raise InvalidPriceError(
str(self.price)
)
object.__setattr__(
self,
"code",
clean_code,
)
object.__setattr__(
self,
"name",
self.name.strip(),
)
解析数据:
def parse_stock(
data: dict[str, object],
) -> Stock:
try:
return Stock(
code=str(data["code"]),
name=str(data["name"]),
price=float(data["price"]),
industry=str(
data.get(
"industry",
"未知",
)
),
)
except KeyError as error:
raise StockError(
f"缺少字段:"
f"{error.args[0]}"
) from error
批量解析:
def parse_stocks(
records: list[
dict[str, object]
],
):
for index, record in enumerate(
records,
start=1,
):
try:
yield parse_stock(record)
except StockError as error:
logger.warning(
"第 %s 条数据无效:%s",
index,
error,
)
按行业分组:
from collections import defaultdict
def group_by_industry(
stocks,
) -> dict[str, list[Stock]]:
groups: defaultdict[
str,
list[Stock],
] = defaultdict(list)
for stock in stocks:
groups[
stock.industry
].append(stock)
return dict(groups)
统计:
from collections import Counter
def count_industries(
stocks,
) -> Counter[str]:
return Counter(
stock.industry
for stock in stocks
)
筛选器:
from collections.abc import (
Callable,
)
type StockFilter = Callable[
[Stock],
bool,
]
def filter_stocks(
stocks,
condition: StockFilter,
):
for stock in stocks:
if condition(stock):
yield stock
主板筛选条件:
def is_main_board(
stock: Stock,
) -> bool:
return stock.code.startswith(
("00", "60")
)
价格筛选器工厂:
def create_price_filter(
min_price: float,
max_price: float | None = None,
) -> StockFilter:
def condition(
stock: Stock,
) -> bool:
if stock.price < min_price:
return False
if (
max_price is not None
and stock.price > max_price
):
return False
return True
return condition
完整处理:
def process_records(
records: list[
dict[str, object]
],
) -> list[Stock]:
stocks = parse_stocks(records)
main_board_stocks = filter_stocks(
stocks,
is_main_board,
)
price_filter = (
create_price_filter(
min_price=10,
max_price=1000,
)
)
filtered_stocks = filter_stocks(
main_board_stocks,
price_filter,
)
return sorted(
filtered_stocks,
key=lambda stock: (
stock.price
),
reverse=True,
)
这个案例综合使用了:
数据类
不可变对象
生成器
自定义异常
异常链
日志
类型别名
高阶函数
闭包
惰性处理
排序
不推荐:
result = [
transform(item)
for group in groups
if group
for item in group
if validate(item)
if another_condition(item)
]
逻辑复杂时改用普通循环。
不推荐:
lambda x: (
complicated_expression_1(x)
if condition(x)
else complicated_expression_2(x)
)
应改成命名函数。
为了复用几行代码建立多层继承,通常得不偿失。
优先考虑组合。
装饰器层数太多,会让函数实际行为难以追踪。
普通计算任务使用 asyncio 不会自动提升性能。
代码还没有性能问题时,不要牺牲可读性换取微小性能提升。
正确顺序:
先写正确
再写清晰
进行测试
测量性能
最后优化
第一阶段:
解包
推导式
排序
collections
itertools
第二阶段:
迭代器
生成器
闭包
高阶函数
装饰器
第三阶段:
上下文管理器
特殊方法
数据类
模式匹配
第四阶段:
类型提示
自定义异常
日志
测试
项目结构
第五阶段:
多线程
多进程
asyncio
性能分析
缓存
高级语法不是为了少写几行代码,而是为了更准确地表达意图。
频繁成员判断 → set
先进先出 → deque
计数 → Counter
自动默认值 → defaultdict
大量数据 → generator
固定数据模型 → dataclass
共享状态会增加:
- 并发问题
- 调试难度
- 测试难度
- 维护成本
Python 标准库已经提供了大量成熟工具:
collections
itertools
functools
pathlib
contextlib
concurrent.futures
asyncio
logging
dataclasses
typing
不要猜哪里慢
使用 timeit
使用 cProfile
找到真正瓶颈
再进行优化
真正成熟的 Python 代码通常具有以下特点:
函数短
职责清晰
类型明确
异常明确
数据流清晰
副作用较少
容易测试
容易修改
高级 Python 的最终目标不是写出别人看不懂的代码,而是:
用更少的复杂度,解决更复杂的问题。