第4章 4.4 Python 组合式编程高级用法

🎯 开场 3 分钟:为什么要学这个?

上一章我们学会了如何在插件市场里「淘宝」,找到了好几个省力的轮子。但你有没有这种感觉——用别人的插件虽然爽,可一旦要改点东西,就傻眼了?或者说,写出来的代码和自己写的业务搅在一起,改一行怕影响另一行?

举个真实的坑:你写了一个统计功能,开始还挺好,后来数据多了跑得越来越慢,你想把计算部分单独抽出来优化,结果发现计算逻辑和界面更新代码缠在一起,根本没法单独拎出来。

学完这一章,你能解决这些问题:

  • 把一个复杂功能拆成独立的小块,想改哪块改哪块
  • 代码复用更优雅,不用 Copy-Paste 三遍
  • 写出跑得更快、占内存更少的数据处理代码
  • 用 Python 也能写出「组合式」风格的代码

🧱 基础 25 分钟:核心概念

什么是「组合式」编程?

先说个生活例子。你去奶茶店点单,店员不是一次性做好所有东西,而是分步操作:先加茶底 → 加配料 → 加奶盖 → 封口。每一步都可以单独调整,换个配料不用重新做整杯。

组合式编\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n程就是这个思路——把一个大功能拆成「可独立组合的小零件」。每个零件专注做一件事,用的时候像搭积木一样拼起来。

Python 的函数就是最好的零件。我们之前学的函数只能做固定操作,高级用法能让函数本身也「可定制」。

1. 高阶函数:函数界的「来料加工」

是什么:高阶函数就是「能接收函数作为输入,或者把函数作为输出」的函数。

类比:就像一个「来料加工店」,你把原材料(数据)送进去,店里可以根据你的要求(传入的函数)加工出不同产品。

# 这是一个高阶函数:把处理规则和数据分开
def process_items(items, process_func):
"""对列表中每个元素执行 process_func 处理"""
results = []
for item in items:
    results.append(process_func(item))
return results

# 定义两种不同的「加工规则」
def add_tax(price):
return price * 1.08

def with_discount(price):
return price * 0.9

# 同样的「来料加工店」,不同的加工规则
prices = [100, 200, 300]
print(process_items(prices, add_tax))      # [108.0, 216.0, 324.0]
print(process_items(prices, with_discount)) # [90.0, 180.0, 270.0]

这行 process_items(prices, add_tax) 说白了就是:「把价格列表送进去,按加税规则处理」。

2. 闭包:带「记忆」的函数

是什么:闭包是「记住出生时环境」的函数。就像你小时候养的狗,不管长大后去了哪里,它永远记得小时候你给它起的名字。

解决什么问题:有些参数我们不想每次调用都传,但函数内部需要用到。闭包帮你把这个参数「记住」。

def make_multiplier(factor):
"""创建一个「记住 factor」的乘法器"""
def multiplier(number):
    return number * factor  # factor 是「被记住」的环境变量
return multiplier

# 创建两个不同的乘法器
double = make_multiplier(2)   # 记住 factor=2
triple = make_multiplier(3)   # 记住 factor=3

# 同样调用 multiplier,结果不一样——因为记住的 factor 不同
print(double(5))   # 10  (5 * 2)
print(triple(5))   # 15  (5 * 3)

doubletriple 是两个独立的函数,各自有各自的「记忆」。

3. 装饰器:函数的「包装盒」

是什么:装饰器就像快递包装。你给函数包一层外衣,不改变内容,但能加上新功能(防撞袋、加急标识等)。

类比:你去超市买水果,本来裸着的水果(函数),套上保鲜膜(装饰器),还是水果,但能保鲜、能标价。

import time

def timer(func):
"""装饰器:记录函数运行时间"""
def wrapper(*args, **kwargs):
    start = time.time()
    result = func(*args, **kwargs)
    end = time.time()
    print(f"{func.__name__} 跑了 {end - start:.4f} 秒")
    return result
return wrapper

@timer
def slow_function():
"""模拟一个慢函数"""
time.sleep(1)
return "完成"

@timer
def fast_function():
return "秒完"

# 使用装饰器:给函数包一层「计时」功能
slow_function()   # 输出:slow_function 跑了 1.0000 秒
fast_function()   # 输出:fast_function 跑了 0.0000 秒

注意!@timer 写在哪,Python 就自动把下面的函数包上这层「计时外衣」。

4. 生成器:数据的「自来水厂」

是什么:生成器不是一下子造好所有数据,而是「用多少产多少」。就像自来水厂,你开水龙头才来水,不用的时候不浪费。

解决什么问题:处理海量数据时,一次性加载到内存会爆掉。生成器让你「边用边取」,省内存。

def number_generator(n):
"""生成 0 到 n-1 的数字,但一次只生一个"""
for i in range(n):
    yield i  # yield 就是「产出一个,歇一会儿」

# 使用生成器
gen = number_generator(5)
print(next(gen))  # 0
print(next(gen))  # 1
print(next(gen))  # 2
print(list(gen))  # [3, 4](剩下的全部取出)

# 更常用的方式:直接在循环里用
for num in number_generator(1000000):
print(num)  # 内存里永远只存一个数字,不会爆

5. 上下文管理器:资源的「保管箱」

是什么:上下文管理器帮你管理「打开→使用→关闭」的流程。就像停车场的闸机:入场抬杆(__enter__)、出场抬杆(__exit__),中间不管你停多久、停几辆,流程都是自动化的。

class FileManager:
"""自己写一个上下文管理器"""
def __init__(self, filename, mode):
    self.filename = filename
    self.mode = mode
    self.file = None

def __enter__(self):
    """进入时执行:打开文件"""
    self.file = open(self.filename, self.mode)
    return self.file

def __exit__(self, exc_type, exc_val, exc_tb):
    """退出时执行:关闭文件"""
    if self.file:
        self.file.close()
    return False  # 返回 False 表示不屏蔽异常

# 使用 with 语句,Python 自动帮你处理「打开/关闭」
with FileManager("test.txt", "w") as f:
f.write("Hello, World!")

# 出了 with 块,文件已经自动关闭,不用手动 f.close()

Python 还提供 contextlib 简化写法:

from contextlib import contextmanager

@contextmanager
def timer_context(label):
"""用装饰器语法创建上下文管理器"""
start = time.time()
yield label
end = time.time()
print(f"{label} 用了 {end - start:.4f} 秒")

with timer_context("下载文件"):
time.sleep(1)  # 模拟下载

6. 类型标注:代码的「产品说明书」

是什么:给变量、函数参数加个「类型标签」,让代码自己能「说话」,IDE 也能给你提建议。

from typing import List, Dict, Optional

def process_user_data(
users: List[Dict[str, str]],  # 用户列表,每个用户是「名字→年龄」的字典
filter_age: Optional[int] = None  # 可选的年龄过滤
) -> List[str]:  # 返回名字列表
"""处理用户数据,可按年龄过滤"""
results = []
for user in users:
    if filter_age is None or int(user.get("age", 0)) >= filter_age:
        results.append(user["name"])
return results

# 调用时 IDE 会提示你参数类型
users = [
{"name": "小明", "age": "25"},
{"name": "小红", "age": "17"},
{"name": "老王", "age": "35"}
]

print(process_user_data(users, 18))  # ['小明', '老王']

🔥 实战 35 分钟:3 个递进的小项目

项目 1(5 分钟):待办清单 with 组合式函数

目标:理解高阶函数和闭包的实际用处。

"""待办清单 - 组合式版本"""

# 模拟数据存储
tasks = [
{"id": 1, "title": "写周报", "done": False, "priority": 2},
{"id": 2, "title": "开会", "done": True, "priority": 1},
{"id": 3, "title": "写代码", "done": False, "priority": 3},
]

def filter_tasks(tasks, condition_func):
"""通用的任务过滤函数(高阶函数)"""
return [task for task in tasks if condition_func(task)]

def mark_done(task):
"""标记完成"""
return {**task, "done": True}

# 使用闭包创建不同的「过滤规则」
is_pending = lambda t: not t["done"]
is_high_priority = lambda t: t["priority"] >= 3

# 组合使用
pending_tasks = filter_tasks(tasks, is_pending)
high_priority_pending = filter_tasks(pending_tasks, is_high_priority)

print("待完成的高优先级任务:")
for task in high_priority_pending:
print(f"  [{task['id']}] {task['title']}")

预期输出

待完成的高优先级任务:
[3] 写代码

一句话解释:把「筛选逻辑」抽象成函数,代码更灵活,想换规则直接换函数就行。


项目 2(15 分钟):CSV 数据清洗流水线

目标:从 CSV 读取数据,用生成器处理大数据,用装饰器加功能。

准备一个 sales.csv 文件:

date,product,sales,region
2024-01-01,手机,1200,华北
2024-01-01,电脑,3500,华东
2024-01-02,手机,980,华南
2024-01-02,电脑,4200,华北
2024-01-03,平板,2100,华东

代码:

"""CSV 数据清洗 - 生成器 + 装饰器版本"""

import csv
from typing import Generator, Dict

# 装饰器:给函数加「计时」功能
def log_calls(func):
"""记录函数被调用的次数"""
func.call_count = 0
def wrapper(*args, **kwargs):
    func.call_count += 1
    print(f"[{func.__name__} 调用 #{func.call_count}]")
    return func(*args, **kwargs)
return wrapper

@log_calls
def read_csv_generator(filename: str) -> Generator[Dict, None, None]:
"""生成器方式读取 CSV,一行一行吐出来,不占内存"""
with open(filename, "r", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for row in reader:
        yield row

@log_calls
def clean_sales_data(row: Dict) -> Dict:
"""清洗一条销售记录"""
return {
    "date": row["date"],
    "product": row["product"].strip(),
    "sales": float(row["sales"]),

    "region": row["region"].strip()
}

@log_calls
def filter_by_region(row: Dict, region: str) -> bool:
"""过滤地区"""
return row["region"] == region

@log_calls
def calculate_total_sales(rows: Generator[Dict, None, None]) -> float:
"""计算总销售额"""
total = 0
for row in rows:
    total += row["sales"]
return total

# 主程序
print("=== 读取所有数据(生成器模式)===")
all_rows = read_csv_generator("sales.csv")

print("\n=== 清洗后的数据 ===")
cleaned = (clean_sales_data(row) for row in all_rows)  # 生成器管道

for item in cleaned:
print(f"  {item['date']} | {item['product']:4s} | ¥{item['sales']:>6.0f} | {item['region']}")

# 重新读取计算华东总额
print("\n=== 华东地区销售额 ===")
east_sales = (r for r in read_csv_generator("sales.csv"))
east_clean = (clean_sales_data(r) for r in east_sales)
east_filter = (r for r in east_clean if filter_by_region(r, "华东"))
print(f"华东总销售额:¥{calculate_total_sales(east_filter):.2f}")

预期输出

=== 读取所有数据(生成器模式)===
[read_csv_generator 调用 #1]

=== 清洗后的数据 ===
[clean_sales_data 调用 #1]
2024-01-01 | 手机 |  ¥1200 | 华北
[clean_sales_data 调用 #2]
2024-01-01 | 电脑 |  ¥3500 | 华东
[clean_sales_data 调用 #3]
2024-01-02 | 手机 |   ¥980 | 华南
[clean_sales_data 调用 #4]
2024-01-02 | 电脑 |  ¥4200 | 华北
[clean_sales_data 调用 #5]
2024-01-03 | 平板 |  ¥2100 | 华东

=== 华东地区销售额 ===
[read_csv_generator 调用 #2]
[clean_sales_data 调用 #6]
[filter_by_region 调用 #1]
[clean_sales_data 调用 #7]
[filter_by_region 调用 #2]
[clean_sales_data 调用 #8]
华东总销售额:¥5600.00

一句话解释:用生成器做数据流水线,每个环节独立可替换,用多少取多少,内存不爆炸。


项目 3(15 分钟):个人数据仪表盘

目标:综合使用装饰器、上下文管理器、类型标注,做一个可扩展的数据统计工具。

"""个人数据仪表盘 - 综合实战"""

from typing import List, Dict, Optional
from contextlib import contextmanager
import json

# ==================== 工具函数 ====================

@contextmanager
def timer(label: str = "操作"):
"""计时上下文管理器"""
import time
start = time.time()
yield
print(f"[{label}] 耗时 {time.time() - start:.3f}s")

def log_decorator(func):
"""日志装饰器"""
def wrapper(*args, **kwargs):
    print(f"\n>>> 开始执行:{func.__name__}")
    result = func(*args, **kwargs)
    print(f">>> 完成:{func.__name__}")
    return result
return wrapper

# ==================== 核心功能 ====================

@log_decorator
def load_json_context(filename: str) -> List[Dict]:
"""用上下文管理器安全读取 JSON"""
with open(filename, "r", encoding="utf-8") as f:
    return json.load(f)

@log_decorator
def analyze_expenses(expenses: List[Dict], min_amount: float = 0) -> Dict:
"""分析消费数据,返回统计结果"""
filtered = [e for e in expenses if e.get("amount", 0) >= min_amount]

total = sum(e["amount"] for e in filtered)
categories = {}

for expense in filtered:
    cat = expense.get("category", "其他")
    categories[cat] = categories.get(cat, 0) + expense["amount"]

return {
    "total": total,
    "count": len(filtered),
    "average": total / len(filtered) if filtered else 0,
    "by_category": categories
}

@log_decorator
def generate_report(stats: Dict, period: str = "本月") -> str:
"""生成文字报表"""
lines = [
    f"\n{'='*30}",
    f"📊 {period} 消费报告",
    f"{'='*30}",
    f"总消费:¥{stats['total']:.2f}",
    f"消费笔数:{stats['count']}",
    f"平均每笔:¥{stats['average']:.2f}",
    f"\n分类明细:"
]

for cat, amount in sorted(stats["by_category"].items(), key=lambda x: -x[1]):
    pct = amount / stats['total'] * 100
    lines.append(f"  {cat}: ¥{amount:.2f} ({pct:.1f}%)")

return "\n".join(lines)

# ==================== 主程序 ====================

# 模拟数据(实际使用时从文件读取)
expense_data = [
{"date": "2024-01-15", "category": "餐饮", "amount": 45.5, "note": "午饭"},
{"date": "2024-01-16", "category": "交通", "amount": 8.0, "note": "地铁"},
{"date": "2024-01-17", "category": "餐饮", "amount": 128.0, "note": "请客"},
{"date": "2024-01-18", "category": "购物", "amount": 299.0, "note": "日用品"},
{"date": "2024-01-19", "category": "餐饮", "amount": 35.0, "note": "咖啡"},
]

if __name__ == "__main__":
print("💰 个人消费分析工具 v1.0\n")

with timer("完整分析流程"):
    # 分析所有消费
    stats = analyze_expenses(expense_data)

    # 生成报告
    report = generate_report(stats, "2024年1月中旬")
    print(report)

    # 分析高消费(>= 50元)
    print("\n" + "="*30)
    high_stats = analyze_expenses(expense_data, min_amount=50)
    high_report = generate_report(high_stats, "高消费分析(>=50元)")
    print(high_report)

预期输出

💰 个人消费分析工具 v1.0

>>> 开始执行:analyze_expenses
>>> 完成:analyze_expenses

>>> 开始执行:generate_report
>>> 完成:generate_report

==============================
📊 2024年1月中旬 消费报告
==============================
总消费:¥515.50
消费笔数:5
平均每笔:¥103.10

分类明细:
餐饮: ¥208.50 (40.4%)
购物: ¥299.00 (58.0%)
交通: ¥8.00 (1.6%)

>>> 开始执行:analyze_expenses
>>> 完成:analyze_expenses

>>> 开始执行:generate_report
>>> 完成:generate_report

==============================
📊 高消费分析(>=50元)消费报告
==============================
总消费:¥471.50
消费笔数:3
平均每笔:¥157.17

分类明细:
餐饮: ¥173.00 (36.7%)
购物: ¥299.00 (63.3%)

[完整分析流程] 耗时 0.000s

一句话解释:把「计时」「日志」「数据读取」都做成独立组件,主程序只关心业务逻辑,改起来方便。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:装饰器顺序搞反

# ❌ 错误示例:顺序搞反,效果不对
@timer
@log_decorator
def process():
pass

# ✅ 正确示例:先 log 再 timer
@log_decorator  # 外层:先记录日志
@timer          # 内层:再计时
def process():
pass

解释:装饰器从下往上执行,最近的那个最外层。


坑 2:生成器只能用一次

# ❌ 错误示例:生成器耗尽后再次使用
gen = (x for x in range(3))
print(list(gen))  # [0, 1, 2]
print(list(gen))  # [] —— 已经没了!

# ✅ 正确示例:需要多次使用就转成列表
data = [x for x in range(3)]  # 列表可以反复用
print(data)
print(data)

坑 3:装饰器忘记保留原函数信息

# ❌ 错误示例:wrapper 函数没有属性,原函数信息丢失
def bad_decorator(func):
def wrapper(*args):
    return func(*args)
return wrapper

@bad_decorator
def hello():
"""打招呼"""
pass

print(hello.__name__)  # 'wrapper' —— 不是 'hello' 了!
print(hello.__doc__)   # None —— 文档也没了

# ✅ 正确示例:用 wraps 保留原函数信息
from functools import wraps

def good_decorator(func):
@wraps(func)  # 关键这行!
def wrapper(*args):
    return func(*args)
return wrapper

@good_decorator
def hello():
"""打招呼"""
pass

print(hello.__name__)  # 'hello'
print(hello.__doc__)   # '打招呼'

坑 4:上下文管理器异常处理不当

# ❌ 错误示例:在 __exit__ 中 return True 屏蔽了异常
class BadManager:
def __exit__(self, exc_type, exc_val, exc_tb):
    self.close()
    return True  # 错误!异常被吞掉了

# ✅ 正确示例:只在需要屏蔽异常时返回 True
class GoodManager:
def __exit__(self, exc_type, exc_val, exc_tb):
    self.close()
    return False  # 异常正常传播

坑 5:类型标注用 list 而不是 List

# ❌ 错误示例:运行时类型提示不生效
def process(items: list):  # list 是类型,不是类型标注对象
pass

# ✅ 正确示例:使用 typing 模块
from typing import List, Dict, Tuple
def process(items: List[int], data: Dict[str, str]) -> Tuple[int, str]:
pass

性能小贴士:生成器链式处理大数据

# 场景:处理 1000 万行数据
import time

# ❌ 低效:先全部加载再处理
def bad_way():
data = [i for i in range(1000000)]  # 一次性加载到内存
filtered = [x for x in data if x % 2 == 0]
return sum(filtered)

# ✅ 高效:用生成器链式处理
def good_way():
def generate():
    for i in range(1000000):
        yield i

filtered = (x for x in generate() if x % 2 == 0)  # 永远是生成器
return sum(filtered)  # sum 边读边加,不占额外内存

# 测试
start = time.time()
bad_way()
print(f"低效方式: {time.time() - start:.3f}s")

start = time.time()
good_way()
print(f"高效方式: {time.time() - start:.3f}s")

调试技巧:用 logging 代替 print

import logging

# 设置日志级别和格式
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s"
)

def debug_demo():
data = [1, 2, 3, 4, 5]

logging.debug(f"输入数据: {data}")

filtered = [x for x in data if x > 2]
logging.debug(f"过滤后: {filtered}")

result = sum(filtered)
logging.info(f"计算结果: {result}")

debug_demo()

输出

2024-01-20 10:30:00 [DEBUG] 输入数据: [1, 2, 3, 4, 5]
2024-01-20 10:30:00 [DEBUG] 过滤后: [3, 4, 5]
2024-01-20 10:30:00 [INFO] 计算结果: 12

✏️ 练习题

练习 1(1 分钟):装饰器抄改

# 抄改下面代码,把 timer 装饰器加到 `greet` 函数上

def timer(func):
def wrapper(*args, **kwargs):
    import time
    start = time.time()
    result = func(*args, **kwargs)
    print(f"耗时 {time.time() - start:.2f}s")
    return result
return wrapper

@timer  # 把这行加到下面函数上
def greet(name):
import time
time.sleep(0.5)
return f"Hello, {name}!"

print(greet("小明"))
  • 预期输出Hello, 小明! 加上耗时提示

练习 2(2 分钟):加 if 判断

# 在下面代码的 filter_tasks 中加一个判断:
# 只返回 priority >= 2 的任务

tasks = [
{"title": "写代码", "priority": 3},
{"title": "开会", "priority": 1},
{"title": "写周报", "priority": 2},
]

def filter_tasks(tasks, condition):
return [t for t in tasks if condition(t)]

# 运行这行,打印出 ["写代码", "写周报"]
print(filter_tasks(tasks, lambda t: # 在这里加条件))

练习 3(3 分钟):用生成器处理新数据

# 把下面的列表转成生成器写法,输出前 3 个

numbers = [10, 20, 30, 40, 50]

# 改成生成器
def number_gen():
for n in numbers:
    yield n

gen = number_gen()
print(next(gen))
print(next(gen))
print(next(gen))

练习 4(4 分钟):串起两个项目

# 用项目 2 的装饰器和项目 3 的上下文管理器组合

from contextlib import contextmanager

# 补全下面的代码:
# 1. 写一个 @log_calls 装饰器
# 2. 写一个 @contextmanager 的 timer
# 3. 在 process_data 函数上同时使用两者

def log_calls(func):
# 补全这里
pass

@contextmanager
def timer():
# 补全这里
yield

@log_calls
def process_data(data):
return [x * 2 for x in data]

with timer():
result = process_data([1, 2, 3])
print(result)

练习 5(挑战题,5 分钟):报错分析

以下代码运行时报错,请分析原因并修复:

from functools import wraps

def memoize(func):
cache = {}
@wraps(func)
def wrapper(n):
    if n not in cache:
        cache[n] = func(n)
    return cache[n]
return wrapper

@memoize
def fibonacci(n):
if n < 2:
    return n
return fibonacci(n-1) + fibonacci(n-2)

# 报错信息:
# RecursionError: maximum recursion depth exceeded

print(fibonacci(100))  # 想计算 fibonacci(100)

  • 提示:递归深度限制 + memoize 只对同一对象生效

作业:做一个「数据转换工具箱」

需求描述
做一个命令行工具,可以对 CSV 文件进行「转换 → 过滤 → 统计」操作。

功能点
1. 用生成器读取大文件(防止内存爆炸)
2. 支持按列过滤、按值过滤
3. 支持对数值列做统计(求和、平均、最大、最小)
4. 用装饰器实现「操作日志」和「计时」

加分项
1. 支持导出结果到新 CSV
2. 支持管道式链式调用(transform().filter().aggregate()

验收标准
- 能处理 10000 行 CSV 不卡顿
- 输出符合预期的统计结果
- 代码有注释,关键函数有类型标注


📚 总结

本文学了 3 个核心点:
1. 高阶函数 + 闭包:把「规则」和数据分离,代码更灵活
2. 装饰器 + 上下文管理器:给函数加功能不改变核心逻辑
3. 生成器:处理大数据时不爆内存,边用边取

延伸资源:
- Python 官方文档「functools」模块:https://docs.python.org/zh-cn/3/library/functools.html
- 《流畅的 Python》- 第 7 章:装饰器和上下文管理器
- 视频:B 站「技术人办公桌」的 Python 进阶系列


你在处理数据时遇到过「内存不够」的情况吗?用的什么方法解决的?评论区聊聊,老粉优先回复!

下一章我们要做一个有点意思的综合实战——用组合式编程思想,做一个「数据转换流水线」,把学到的这些「积木」真正拼成一个实用工具。敬请期待!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。