第3章 3.4 mixin 与组合式函数对比

📌 上一章我们学会了自定义指令与插件,把 Vue 的能力往外「加装」了一层。但加装多了会发现一个问题:这些工具函数散落在各处,改一处要改四五 个文件,烦死了。

这一章我们来解决这个问题——如何优雅地复用代码。不管是 Vue 的 mixin 还是 React 的 hooks,说白了都在回答同一个问题:「我不想写重复代码,但也不想把不相关的代码揉成一团,怎么办?」

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

想象你开了家奶茶店,一开始只卖珍珠奶茶:

# 奶茶店 v1.0
def make_boba_tea():
print("煮珍珠")
print("加茶水")
print("加奶精")
print("封口")
print("完成!")

生意好了,你又加了椰果奶茶、芋泥奶茶……每种都要写一遍「封口」「打包」?

后来你聪明了,把「通用流程」抽出来:

# 奶茶店 v2.0:通用流程
def pack_and_serve(dr\n\n![Simple tech illustration expla](https://blog.xxyye.com/wp-content/uploads/2026/06/dded0b7101eede0.png)\n\n![AI comic creation scene, creat](https://blog.xxyye.com/wp-content/uploads/2026/06/a122bf05ccca988.png)\n\nink_type):
print(f"制作{drink_type}")
print("封口")
print("打包")
print("递给顾客")

这就是 mixin 思维——把公共部分抽出来,混进不同的饮料里。

但有一天你发现,珍珠奶茶要「先加冰」,椰果奶茶要「最后加椰果」,流程不一样了……

这时 mixin 就不够用了,你需要更灵活的方式——组合式函数(Composables)。

简单说:
- Mixin:把代码「混」进去,适合简单复用
- Composables:把代码「组合」起来,适合复杂逻辑

学完这章,你能在 5 秒内判断「该用 mixin 还是 composable」,写出干净不打架的代码。

🧱 基础 25 分钟:核心概念

3.4.1 什么是 mixin?—— 多人合作的「便签贴」

生活类比:办公室里,你和同事共用一块白板。每个人在自己便签上写任务,贴上去,大家都能看到。

技术定义:Mixin 是一种代码复用模式,通过合并(mix)多个对象的属性/方法来扩展功能。

Python 实现——用类继承的 mixin 模式:

# 1. 定义一个 Mixin 类(名字带 Mixin 是约定俗成)
class LogMixin:
def log(self, message):
    print(f"[LOG] {message}")

def save(self):
    print(f"[SAVE] {self.__class__.__name__} 已保存")

# 2. 定义一个普通类
class User:
def __init__(self, name):
    self.name = name

def say_hello(self):
    print(f"你好,我是{self.name}")

# 3. 用 mixin 扩展它!
class UserWithLog(LogMixin, User):
pass  # 空的,因为功能都从 mixin 来了

# 用一下试试
user = UserWithLog("小明")
user.say_hello()   # 继承自 User
user.log("用户登录")  # 来自 LogMixin
user.save()        # 来自 LogMixin

输出:

你好,我是小明
[LOG] 用户登录
[SAVE] UserWithLog 已保存

这行在干嘛:

  • class UserWithLog(LogMixin, User) —— 把 LogMixin 和 User 的功能「混」在一起

3.4.2 什么是组合式函数?—— 乐高积木拼装

生活类比:乐高积木,每块积木独立、功能单一。你想拼什么就拼什么,需要什么就拿什么。

技术定义:组合式函数是「接受参数、返回数据/方法」的普通函数,调用者按需组合使用。

Python 实现——最直接的 composable:

# 1. 一个计算器组合式函数
def use_counter(start=0):
"""每次调用创建一个独立的计数器"""
count = [start]  # 用列表包一下,躲避 Python 的闭包陷阱

def increment():
    count[0] += 1
    return count[0]

def get():
    return count[0]

def reset():
    count[0] = start

return {"increment": increment, "get": get, "reset": reset}

# 2. 组合式地使用
counter_a = use_counter(0)
counter_b = use_counter(100)

print(counter_a["increment"]())  # 1
print(counter_a["increment"]())  # 2
print(counter_b["get"]())        # 100(独立的,不受 counter_a 影响)
print(counter_a["reset"]())      # 重置为 0
print(counter_a["get"]())        # 0

输出:

1
2
100
None
0

这行在干嘛:
- use_counter(0) —— 每次调用创建独立的状态,不和别的计数器共享

3.4.3 两种方式的核心区别

维度 Mixin 组合式函数
数据来源 继承来的,隐式 函数返回的,显式
冲突处理 容易属性名冲突 不会冲突(变量名自己定)
调试难度 高(不知道哪个 mixin 的) 低(函数返回什么一目了然)
适用场景 简单、低风险 复杂、需要灵活组合

看个冲突例子:

# Mixin 的属性冲突问题
class MixinA:
name = "来自 MixinA"

class MixinB:
name = "来自 MixinB"

class MyClass(MixinA, MixinB):
pass

print(MyClass.name)  # 打印什么?—— "来自 MixinA"(排前面的优先)
# 组合式函数不会有这个问题
def use_mixin_a():
return {"name": "来自 MixinA"}

def use_mixin_b():
return {"name": "来自 MixinB"}

result_a = use_mixin_a()
result_b = use_mixin_b()
print(result_a["name"])  # "来自 MixinA"
print(result_b["name"])  # "来自 MixinB"

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

项目 1(5 分钟):计数器全家桶

目标:用组合式函数创建一个「带日志的计数器」,理解核心 API。

def use_counter_with_log(initial=0):
"""带日志的计数器"""
count = [initial]
history = []

def increment():
    count[0] += 1
    history.append(f"+1 → {count[0]}")
    return count[0]

def decrement():
    count[0] -= 1
    history.append(f"-1 → {count[0]}")
    return count[0]

def get():
    return count[0]

def show_history():
    for h in history:
        print(h)

return {
    "increment": increment,
    "decrement": decrement,
    "get": get,
    "history": show_history
}

# 使用
counter = use_counter_with_log(10)
counter["increment"]()
counter["increment"]()
counter["decrement"]()
print(f"当前值: {counter['get']()}")
print("操作历史:")
counter["history"]()

预期输出:

当前值: 11
操作历史:
+1 → 11
+1 → 12
-1 → 11

一句话解释:每个 use_counter_with_log() 调用都返回一个独立的「计数器+日志」工具包。


项目 2(15 分钟):读取 CSV 数据并统计

目标:用组合式函数封装「读取文件」和「统计」逻辑,演示 composable 的组合能力。

准备一个测试文件 students.csv

name,score,grade
小明,85,A
小红,72,B
小刚,93,A
小美,68,C
小丽,88,A

完整代码:

import csv
from typing import List, Dict

# 组合式函数 1:读取 CSV
def use_csv_reader(filename: str) -> Dict:
"""读取 CSV 文件并返回数据"""
data = []
with open(filename, "r", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for row in reader:
        data.append(row)
return {"data": data, "count": len(data)}

# 组合式函数 2:统计分析
def use_statistics(data: List[Dict]) -> Dict:
"""对数据进行统计分析"""
scores = [int(row["score"]) for row in data]
return {
    "average": sum(scores) / len(scores),
    "max": max(scores),
    "min": min(scores),
    "pass_count": len([s for s in scores if s >= 60])
}

# 组合式函数 3:按等级分组
def use_grouper(data: List[Dict]) -> Dict:
"""按等级分组"""
groups = {}
for row in data:
    grade = row["grade"]
    if grade not in groups:
        groups[grade] = []
    groups[grade].append(row["name"])
return groups

# 组合使用
reader = use_csv_reader("students.csv")
stats = use_statistics(reader["data"])
groups = use_grouper(reader["data"])

print(f"共 {reader['count']} 名学生")
print(f"平均分: {stats['average']:.1f}")
print(f"最高分: {stats['max']},最低分: {stats['min']}")
print(f"及格人数: {stats['pass_count']}")
print("\n按等级分组:")
for grade, names in groups.items():
print(f"  {grade}级: {', '.join(names)}")

预期输出:

共 5 名学生
平均分: 81.2
最高分: 93,最低分: 68
及格人数: 5

按等级分组:
A: 小明, 小刚, 小丽
B: 小红
C: 小美

一句话解释:把「读文件」「统计」「分组」拆成三个独立的组合式函数,想用哪个就用哪个。


项目 3(15 分钟):一个简易的「待办事项 + 持久化」工具

目标:组合前两个项目的能力,做一个带文件保存功能的待办清单。

import json
import os

# 组合式函数 1:待办事项管理
def use_todo_list() -> Dict:
"""创建一个待办事项管理器"""
todos = []

def add(task: str):
    todos.append({"task": task, "done": False})
    return len(todos)

def done(index: int) -> bool:
    if 0 <= index < len(todos):
        todos[index]["done"] = True
        return True
    return False

def undone(index: int) -> bool:
    if 0 <= index < len(todos):
        todos[index]["done"] = False
        return True
    return False

def list_all():
    return todos.copy()

def list_pending():
    return [t for t in todos if not t["done"]]

def list_done():
    return [t for t in todos if t["done"]]

def load_from(data: List):
    todos.clear()
    todos.extend(data)

return {
    "add": add,
    "done": done,
    "undone": undone,
    "list_all": list_all,
    "list_pending": list_pending,
    "list_done": list_done,
    "load_from": load_from
}

# 组合式函数 2:文件持久化
def use_file_storage(filename: str):
"""文件读写组合式函数"""

def save(data: List):
    with open(filename, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    return True

def load() -> List:
    if not os.path.exists(filename):
        return []
    with open(filename, "r", encoding="utf-8") as f:
        return json.load(f)

return {"save": save, "load": load}

# === 组合使用 ===
TODO_FILE = "my_todos.json"

# 初始化
todo = use_todo_list()
storage = use_file_storage(TODO_FILE)

# 尝试从文件加载
todo["load_from"](storage["load"]())

# 演示添加和标记完成
todo["add"]("学习 Python mixin")
todo["add"]("学习组合式函数")
todo["add"]("写一个实战项目")

print("当前待办:")
for i, t in enumerate(todo["list_all"]()):
status = "✅" if t["done"] else "⬜"
print(f"  {i+1}. [{status}] {t['task']}")

# 标记第一项完成
todo["done"](0)

# 保存到文件
storage["save"](todo["list_all"]())

print("\n已保存到文件,重新运行程序会恢复数据")

预期输出:

当前待办:
1. [⬜] 学习 Python mixin
2. [⬜] 学习组合式函数
3. [⬜] 写一个实战项目

已保存到文件,重新运行程序会恢复数据

一句话解释:把「业务逻辑」和「存储逻辑」拆成两个独立的组合式函数,职责清晰,易测试。

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

坑 1:闭包里的可变对象陷阱

# ❌ 错误写法:计数器不独立
def use_bad_counter():
count = 0  # 普通变量
def increment():
    nonlocal count
    count += 1
    return count
return increment

# 测试
c1 = use_bad_counter()
c2 = use_bad_counter()
print(c1())  # 1
print(c1())  # 2
print(c2())  # 3 (错了!应该还是从 1 开始)
# ✅ 正确写法:用列表包起来
def use_good_counter():
count = [0]  # 用列表包住
def increment():
    count[0] += 1
    return count[0]
return increment

c1 = use_good_counter()
c2 = use_good_counter()
print(c1())  # 1
print(c1())  # 2
print(c2())  # 1 (正确!独立计数)

坑 2:Mixin 的方法解析顺序(MRO)让人迷糊

# ❌ 容易混淆的 MRO 问题
class Base:
def greet(self): print("Base 说你好")

class MixinA:
def greet(self): print("MixinA 说你好")

class MyClass(Base, MixinA):
pass

MyClass().greet()  # 打印什么?—— Base 说你好(不是 MixinA!)
# ✅ 正确做法:Mixin 约定俗成只加方法,不覆盖同名方法
# 或者,明确知道 MRO 规则:从左到右,先继承的优先
class MyClass2(MixinA, Base):
pass

MyClass2().greet()  # 这才打印 "MixinA 说你好"


坑 3:在循环里创建组合式函数,忘了参数传递

# ❌ 错误:所有函数共享同一个计数器
def create_incrementers():
counters = []
for i in range(3):
    count = [0]  # 这里创建了,但闭包问题...
    counters.append(lambda: count[0] + 1)
return counters

# 测试
funcs = create_incrementers()
print(funcs[0]())  # 1
print(funcs[1]())  # 1 (错了,都一样)
# ✅ 正确:用默认参数捕获当前值
def create_incrementers_fixed():
counters = []
for i in range(3):
    count = [0]
    counters.append(lambda c=count: c[0] + 1)  # 默认参数捕获
return counters

funcs = create_incrementers_fixed()
print(funcs[0]())  # 1
print(funcs[1]())  # 1

坑 4:Mixin 里用 self 拿不到正确的调用者

class LoggerMixin:
def log(self, msg):
    # 这里的 self 指向的是子类实例
    # 但如果子类没定义 name,就会报错
    print(f"{self.__class__.__name__}: {msg}")

class Good(LoggerMixin):
name = "Good类"

class Bad(LoggerMixin):
pass  # 没定义 name

Good().log("测试")  # ✅ 正常
Bad().log("测试")  # ❌ 可能出问题(如果 mixin 依赖子类的属性)

性能小贴士:按需计算,不要提前做

# ❌ 低效:每次调用都重新计算
def use_bad_stats(data):
def get_average():
    return sum(data) / len(data)  # 每次都算一遍

def get_max():
    return max(data)  # 又算一遍

return {"get_average": get_average, "get_max": get_max}

# ✅ 高效:缓存计算结果
def use_cached_stats(data):
_average = sum(data) / len(data)  # 只算一次
_max = max(data)


def get_average():
    return _average

def get_max():
    return _max

return {"get_average": get_average, "get_max": get_max}

调试技巧:用 inspect 查看组合式函数返回什么

import inspect

def use_counter(start=0):
"""带完整调试信息的计数器"""
count = [start]

def increment():
    count[0] += 1
    return count[0]

return {"increment": increment}

# 调试:看看返回了什么
counter = use_counter(5)
print("返回的键:", list(counter.keys()))
print("返回值类型:", {k: type(v).__name__ for k, v in counter.items()})

输出:

返回的键: ['increment']
返回值类型: {'increment': 'function'}

✏️ 练习题 + 作业题

练习题(10 分钟)

练习 1(2 分钟):改改初始值
- 输入:use_counter(100) 创建计数器
- 预期输出:第一次 increment() 返回 101
- 提示:传不同的初始值给函数

练习 2(2 分钟):加一个判断
- 输入:在 use_counter 里,如果值大于 10,不让继续加
- 预期输出:初始 10,再加一次返回 "已达上限"
- 提示:在 increment 函数开头加个 if 判断

练习 3(3 分钟):统计新数据
- 输入:替换 students.csv 为以下内容:

name,score
张三,95
李四,55
王五,78
  • 预期输出:平均分 76.0,及格人数 2
  • 提示:直接替换文件,运行项目 2 的代码

练习 4(3 分钟):串起两个项目
- 输入:用项目 2 的 use_csv_reader 读取 students.csv,用项目 1 的方式给每个等级创建一个计数器
- 预期输出:打印 A 级人数、B 级人数……
- 提示:遍历分组结果,对每个组调用 use_counter(0)

练习 5(挑战题,0 分钟):分析报错
- 输入:下面代码运行时报错 TypeError: unsupported operand type(s) for +: 'int' and 'str'

def use_stats(data):
return {"avg": sum(data) / len(data)}

print(use_stats(["1", "2", "3"])["avg"])
  • 预期输出:修复后输出 2.0
  • 提示:读取数据时要转换类型

作业题(30 分钟-2 小时)

作业:做一个「支出追踪器」

  • 需求描述:记录每天花了多少钱,按周统计,自动生成报表保存到文件
  • 功能点
    1. 添加支出(金额 + 描述 + 日期)
    2. 按周统计总支出、平均日支出
    3. 找出本周花费最高的一天
    4. 数据持久化到 JSON 文件
  • 加分项
    1. 支持按月份筛选
    2. 生成简单的文字报表
  • 验收标准
  • 能运行不报错
  • 添加 3 笔支出后统计正确
  • 关闭程序再打开,数据还在
  • 提交方式:评论区贴代码或 GitHub 链接

📚 总结 + 资源

一句话总结本文学到的 3 个核心点:

Mixin 适合简单场景(继承就完事),组合式函数适合复杂场景(要什么组合什么)。

延伸学习资源:

  1. Python 官方文档:类与类继承 —— 搞清楚 MRO 看这里
  2. Real Python: Python Mixins —— mixin 的正确打开方式
  3. 《Python 设计模式》—— 组合优于继承的更多案例

互动钩子:你在项目中用过 mixin 还是组合式函数?有没有踩过什么奇怪的坑?评论区聊聊,老粉优先回复!


📌 下章剧透:学会了代码复用,下一章我们要用它做一个「可复用的 Modal 弹窗组件」——学了 mixin 和 composable,这个组件简直小菜一碟!

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