Appearance
章节3 函数与模块化编程
授课时长:约4-5学时
适用对象:已掌握Python基础语法和数据类型的学员
教学方式:讲授 + 代码推导 + 模式分析
一、学习目标
| 目标分类 | 具体目标 |
|---|---|
| 知识目标 | 理解函数定义、参数传递机制、作用域规则、闭包与装饰器原理 |
| 技能目标 | 熟练编写装饰器、生成器、高阶函数,掌握模块导入与包管理 |
| 素养目标 | 建立模块化编程思维,写出可复用、可维护的代码 |
二、核心知识点
1️⃣ 函数定义参数传递返回值
1.1 函数定义基础
python
# 基本语法
def 函数名(参数列表):
"""文档字符串"""
函数体
return 返回值
# 示例
def greet(name):
"""向指定的人打招呼"""
return f"你好,{name}!"
print(greet("小明")) # 你好,小明!1.2 参数传递机制
Python的参数传递方式称为 "传对象引用"(call by object reference)。
python
"""
核心规则:
- 可变对象(list/dict/set)在函数内修改会影响原对象
- 不可变对象(int/str/tuple)在函数内修改不会影响原对象
- 但若将可变对象重新赋值,则不会影响原对象
"""
def modify_list(lst):
lst.append(4) # ✅ 修改原列表
print(f"函数内: {lst}")
def reassign_list(lst):
lst = [5, 6, 7] # ❌ 重新赋值,原列表不受影响
print(f"函数内: {lst}")
my_list = [1, 2, 3]
modify_list(my_list)
print(f"函数外: {my_list}") # [1, 2, 3, 4]
reassign_list(my_list)
print(f"函数外: {my_list}") # [1, 2, 3, 4](不变)1.3 参数类型详解
python
# ──────────────────────────────────────────
# 1. 位置参数
def power(base, exp):
return base ** exp
print(power(2, 3)) # 8 位置对应
# ──────────────────────────────────────────
# 2. 默认参数
def register(name, age, city="北京"):
return f"{name}({age}岁) - {city}"
print(register("Alice", 20)) # 使用默认city
print(register("Bob", 22, "上海")) # 覆盖默认值
# ⚠️ 默认参数的陷阱(重要!)
def add_student(name, courses=[]): # ❌ 可变对象做默认参数
courses.append(name)
return courses
print(add_student("Alice")) # ['Alice']
print(add_student("Bob")) # ['Alice', 'Bob'] ❌ 共享了同一个列表
# ✅ 正确做法
def add_student_correct(name, courses=None):
if courses is None:
courses = []
courses.append(name)
return courses
# ──────────────────────────────────────────
# 3. 关键字参数
def introduce(name, age, city):
print(f"{name}, {age}岁, 来自{city}")
introduce(age=25, city="深圳", name="Charlie") # 顺序可乱
# ──────────────────────────────────────────
# 4. 可变位置参数 *args
def sum_all(*args):
"""接收任意数量的位置参数"""
print(f"参数元组: {args}")
return sum(args)
print(sum_all(1, 2, 3, 4, 5)) # 15
print(sum_all(10, 20)) # 30
# ──────────────────────────────────────────
# 5. 可变关键字参数 **kwargs
def build_profile(**kwargs):
"""接收任意数量的关键字参数"""
print(f"参数字典: {kwargs}")
profile = []
for key, value in kwargs.items():
profile.append(f"{key}={value}")
return ", ".join(profile)
print(build_profile(name="Alice", age=25, city="北京"))
# ──────────────────────────────────────────
# 6. 参数组合(顺序固定)
def func(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
"""
参数顺序:
1. 仅位置参数 (/) 前的参数只能按位置传递
2. 普通参数
3. 仅关键字参数 (*) 后的参数只能按关键字传递
"""
print(f"仅位置: {pos1}, {pos2}")
print(f"位置或关键字: {pos_or_kwd}")
print(f"仅关键字: {kwd1}, {kwd2}")
func(1, 2, 3, kwd1="a", kwd2="b") # ✅
# func(pos1=1, pos2=2, 3, kwd1="a", kwd2="b") # ❌ pos1不能传关键字1.4 返回值
python
# 单个返回值
def square(x):
return x ** 2
# 多个返回值(本质是元组)
def stats(numbers):
return min(numbers), max(numbers), sum(numbers) / len(numbers)
result = stats([1, 2, 3, 4, 5])
print(result) # (1, 5, 3.0)
mini, maxi, avg = stats([1, 2, 3, 4, 5])
# 提前返回
def divide(a, b):
if b == 0:
return None # 提前返回
return a / b
# 无返回值的函数返回 None
def say_hello(name):
print(f"Hello, {name}")
result = say_hello("Alice")
print(result) # None2️⃣ 作用域命名空间与函数嵌套
2.1 LEGB 规则
Python 查找变量遵循 LEGB 规则(由内到外):
python
"""
L — Local(局部作用域)—— 当前函数内部
E — Enclosing(嵌套作用域)—— 外层函数
G — Global(全局作用域)—— 模块级别
B — Built-in(内置作用域)—— Python内置命名空间
"""
x = "global x" # 全局作用域
def outer():
x = "outer x" # 嵌套作用域
def inner():
x = "inner x" # 局部作用域
print(f"inner: {x}")
inner()
print(f"outer: {x}")
outer()
print(f"global: {x}")
# 输出:
# inner: inner x
# outer: outer x
# global: global x2.2 global 与 nonlocal
python
count = 0
def increment():
global count # 声明使用全局变量
count += 1
increment()
print(count) # 1
# nonlocal — 修改嵌套作用域变量
def outer():
x = 10
def inner():
nonlocal x # 声明使用外层函数的x
x += 5
print(f"inner: {x}")
inner()
print(f"outer: {x}")
outer() # inner: 15, outer: 152.3 函数嵌套
python
def make_math_operation(operation):
"""根据参数返回不同的内部函数"""
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
if operation == "+":
return add
elif operation == "-":
return subtract
elif operation == "*":
return multiply
else:
raise ValueError("不支持的运算")
# 使用
op = make_math_operation("+")
print(op(10, 5)) # 15
op = make_math_operation("*")
print(op(10, 5)) # 503️⃣ 闭包与装饰器原理及应用
3.1 闭包(Closure)
定义:内层函数引用了外层函数的变量(非全局),即使外层函数已经返回,内层函数仍然能记住这些变量。
python
def make_counter():
"""创建一个计数器闭包"""
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
# 创建两个独立的计数器
c1 = make_counter()
c2 = make_counter()
print(c1()) # 1
print(c1()) # 2
print(c1()) # 3
print(c2()) # 1 (c2独立,从1开始)
print(c2()) # 2
# 检查闭包
print(c1.__closure__) # (<cell at ...>,)
print(c1.__code__.co_freevars) # ('count',)闭包的应用场景:
- 数据隐藏(封装私有状态)
- 延迟计算
- 函数工厂
- 装饰器的底层实现
3.2 装饰器(Decorator)
定义:装饰器是一个接受函数作为参数并返回新函数的可调用对象,用于在不修改原函数代码的情况下扩展其功能。
python
# ──────────────────────────────────────────
# 基础装饰器
def my_timer(func):
"""计算函数执行时间的装饰器"""
import time
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} 执行耗时: {elapsed:.4f}秒")
return result
return wrapper
# 使用装饰器
@my_timer
def slow_function():
import time
time.sleep(1)
return "完成"
slow_function()
# ──────────────────────────────────────────
# 带参数的装饰器
def repeat(n=2):
"""让函数重复执行 n 次"""
def decorator(func):
def wrapper(*args, **kwargs):
results = []
for _ in range(n):
result = func(*args, **kwargs)
results.append(result)
return results
return wrapper
return decorator
@repeat(n=3)
def say_hello(name):
return f"Hello, {name}!"
print(say_hello("Alice"))
# ['Hello, Alice!', 'Hello, Alice!', 'Hello, Alice!']
# ──────────────────────────────────────────
# 保持元数据(重要!)
import functools
def log_call(func):
@functools.wraps(func) # ✅ 保持原函数的 __name__ 和 __doc__
def wrapper(*args, **kwargs):
print(f"调用: {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_call
def add(a, b):
"""两数相加"""
return a + b
print(add.__name__) # 'add'(没有@wraps会变成'wrapper')
print(add.__doc__) # '两数相加'3.3 装饰器实际应用
python
import functools
import time
from typing import Callable, Any
# ──────────────────────────────────────────
# 应用1:权限检查
def require_admin(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not getattr(user, "is_admin", False):
raise PermissionError("需要管理员权限")
return func(user, *args, **kwargs)
return wrapper
# ──────────────────────────────────────────
# 应用2:缓存(记忆化)
def memoize(func: Callable) -> Callable:
"""缓存函数计算结果"""
cache = {}
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 用参数作为键(简单版本)
key = str(args) + str(sorted(kwargs.items()))
if key not in cache:
cache[key] = func(*args, **kwargs)
return cache[key]
return wrapper
@memoize
def fibonacci(n: int) -> int:
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# 第一次计算缓慢,之后极快
print(fibonacci(35))
# ──────────────────────────────────────────
# 应用3:重试机制
def retry(max_attempts: int = 3, delay: float = 1.0):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
print(f"第{attempt}次尝试失败: {e}")
if attempt < max_attempts:
time.sleep(delay)
raise last_exception
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def unstable_network_request(url: str) -> str:
"""模拟不稳定的网络请求"""
import random
if random.random() < 0.7: # 70%概率失败
raise ConnectionError("网络连接失败")
return f"成功获取: {url}"4️⃣ 迭代器生成器与高阶函数
4.1 迭代器(Iterator)
python
"""
迭代器协议:
- __iter__() 返回迭代器对象自身
- __next__() 返回下一个元素,无元素时抛出 StopIteration
"""
# 手动使用迭代器
my_list = [1, 2, 3]
iterator = iter(my_list) # 获取迭代器
print(next(iterator)) # 1
print(next(iterator)) # 2
print(next(iterator)) # 3
# print(next(iterator)) # StopIteration
# 自定义迭代器类
class CountDown:
"""倒计时迭代器"""
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for num in CountDown(5):
print(num, end=" ") # 5 4 3 2 14.2 生成器(Generator)
定义:使用 yield 关键字的函数,返回一个生成器对象,支持惰性求值(按需生成值,节省内存)。
python
# ──────────────────────────────────────────
# 基本生成器
def count_up_to(n):
"""从1数到n(惰性生成)"""
count = 1
while count <= n:
yield count
count += 1
# 使用
counter = count_up_to(5)
print(next(counter)) # 1
print(next(counter)) # 2
for num in count_up_to(5):
print(num, end=" ") # 1 2 3 4 5
# ──────────────────────────────────────────
# 生成器表达式(生成器推导式)
gen = (x ** 2 for x in range(1000000))
print(next(gen)) # 0 (不占用大内存)
# 对比列表推导式(内存差异巨大)
# list_comp = [x ** 2 for x in range(1000000)] # 占用大量内存
# gen_expr = (x ** 2 for x in range(1000000)) # 几乎不占内存
# ──────────────────────────────────────────
# 生成器的实际应用:大文件逐行读取
def read_large_file(file_path):
"""逐行读取大文件,不一次性加载到内存"""
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
yield line.strip()
# 使用
# for line in read_large_file("huge_file.txt"):
# process(line)
# ──────────────────────────────────────────
# yield from 语法(委托给子生成器)
def chain(*iterables):
"""串联多个可迭代对象"""
for it in iterables:
yield from it # 委托给子生成器
combined = chain("ABC", [1, 2, 3])
print(list(combined)) # ['A', 'B', 'C', 1, 2, 3]
# ──────────────────────────────────────────
# 生成器的 send() 方法(双向通信)
def echo():
while True:
received = yield
print(f"收到: {received}")
gen = echo()
next(gen) # 启动生成器(执行到yield)
gen.send("Hello") # 收到: Hello
gen.send(42) # 收到: 424.3 高阶函数
python
# ──────────────────────────────────────────
# map() — 映射
nums = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x ** 2, nums))
print(squared) # [1, 4, 9, 16, 25]
# 多个可迭代对象
a = [1, 2, 3]
b = [10, 20, 30]
result = list(map(lambda x, y: x + y, a, b))
print(result) # [11, 22, 33]
# ──────────────────────────────────────────
# filter() — 过滤
nums = [1, 2, 3, 4, 5, 6, 7, 8]
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens) # [2, 4, 6, 8]
# ──────────────────────────────────────────
# reduce() — 归约
from functools import reduce
nums = [1, 2, 3, 4, 5]
total = reduce(lambda x, y: x + y, nums) # ((((1+2)+3)+4)+5)
print(total) # 15
# 计算阶乘
factorial = reduce(lambda x, y: x * y, range(1, 6))
print(factorial) # 120
# ──────────────────────────────────────────
# sorted() — 排序(key参数是高阶用法)
students = [
{"name": "Alice", "score": 92},
{"name": "Bob", "score": 85},
{"name": "Charlie", "score": 95},
]
# 按分数排序
sorted_students = sorted(students, key=lambda s: s["score"], reverse=True)
print(sorted_students)
# ──────────────────────────────────────────
# 综合案例:数据管道
def process_data(numbers):
"""函数式数据处理管道"""
return (
numbers
|> map(lambda x: x * 2) # 但Python没有管道操作符
)
# Python 方式(链式调用)
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = list(
map(lambda x: x ** 2,
filter(lambda x: x % 2 == 0,
map(lambda x: x + 1, data)))
)
print(result) # 先+1,再过滤偶数,再平方4.4 lambda 表达式
python
# 语法:lambda 参数: 表达式
# 基础
square = lambda x: x ** 2
print(square(5)) # 25
# 多参数
add = lambda a, b: a + b
print(add(3, 5)) # 8
# 排序时指定key(最常见的用途)
pairs = [(1, 'one'), (3, 'three'), (2, 'two')]
pairs.sort(key=lambda x: x[0]) # 按第一个元素排序
print(pairs)
# ⚠️ 不要滥用lambda —— 复杂逻辑用 def
# ✅ lambda 适用于简单表达式
# ❌ lambda x: (lambda y: x + y)(x) # 过度使用,难以阅读5️⃣ 模块导入机制与包管理
5.1 模块导入
python
# ──────────────────────────────────────────
# 五种导入方式
import math # 导入整个模块
from math import sqrt # 导入特定函数
from math import sin, cos, tan # 导入多个
from math import * # ⚠️ 不推荐(命名空间污染)
import math as m # 别名
print(math.pi) # 3.141592653589793
print(sqrt(16)) # 4.0
print(m.log(100, 10)) # 2.0
# ──────────────────────────────────────────
# __name__ 变量的使用
# my_module.py
def main():
print("这是模块的主函数")
if __name__ == "__main__":
# 只有直接运行此文件时才会执行
main()5.2 包结构
python
"""
包是一个包含 __init__.py 文件的目录
项目结构示例:
my_project/
├── main.py
├── requirements.txt
└── mypackage/
├── __init__.py # 包的初始化文件
├── module1.py # 模块1
├── module2.py # 模块2
└── subpackage/
├── __init__.py
└── module3.py
# __init__.py 中可以定义:
"""
# mypackage/__init__.py
__version__ = "1.0.0"
__all__ = ["module1", "module2"] # 控制 from package import * 的行为
# 使用
# from mypackage import module1
# from mypackage.subpackage import module35.3 模块搜索路径
python
import sys
# 查看模块搜索路径
for path in sys.path:
print(path)
# 动态添加搜索路径
sys.path.append("/path/to/my/modules")
# 更好的方式:使用 PYTHONPATH 环境变量
# export PYTHONPATH=/path/to/my/modules:$PYTHONPATH5.4 常用标准库模块
python
# ──────────────────────────────────────────
# os — 操作系统接口
import os
print(os.name) # 'nt' (Windows) / 'posix' (Linux/macOS)
print(os.getcwd()) # 当前工作目录
# os.mkdir("new_folder") # 创建目录
# os.listdir(".") # 列出目录内容
# os.path.join("a", "b") # 路径拼接
# ──────────────────────────────────────────
# sys — Python运行时
import sys
print(sys.version) # Python版本
print(sys.argv) # 命令行参数
print(sys.platform) # 操作系统平台
# ──────────────────────────────────────────
# datetime — 日期时间
from datetime import datetime, timedelta
now = datetime.now()
print(now.strftime("%Y-%m-%d %H:%M:%S"))
tomorrow = now + timedelta(days=1)
# ──────────────────────────────────────────
# json — JSON处理
import json
data = {"name": "Alice", "scores": [95, 88, 92]}
json_str = json.dumps(data, ensure_ascii=False, indent=2)
print(json_str)
parsed = json.loads(json_str)
print(parsed["name"])
# ──────────────────────────────────────────
# random — 随机数
import random
print(random.random()) # 0.0 ~ 1.0
print(random.randint(1, 100)) # 1 ~ 100 随机整数
print(random.choice(["a", "b", "c"])) # 随机选择
random.shuffle(["a", "b", "c"]) # 打乱顺序5.5 第三方包管理
bash
# requirements.txt 格式
requests==2.28.1
numpy>=1.21.0
pandas
flask~=2.3.0
# 安装
pip install -r requirements.txt
# 生成
pip freeze > requirements.txt三、代码实操演示
演示1:装饰器实现日志系统
python
import functools
import logging
from datetime import datetime
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("App")
def log_function_call(level=logging.INFO):
"""记录函数调用的装饰器"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
logger.log(level, f"调用 {func.__name__}({signature})")
try:
result = func(*args, **kwargs)
logger.log(level, f"{func.__name__} 返回: {result!r}")
return result
except Exception as e:
logger.error(f"{func.__name__} 抛出异常: {e}")
raise
return wrapper
return decorator
@log_function_call()
def calculate_discount(price: float, rate: float = 0.8) -> float:
"""计算折扣价"""
return price * rate
# 测试
print(calculate_discount(100, 0.75))演示2:生成器实现无限斐波那契数列
python
def fibonacci_generator():
"""无限生成斐波那契数列"""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 使用
fib = fibonacci_generator()
# 取前10个
first_10 = [next(fib) for _ in range(10)]
print("前10个:", first_10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# 跳过10个,再取5个
for _ in range(10):
next(fib)
next_5 = [next(fib) for _ in range(5)]
print("再5个:", next_5)
# 使用 itertools.islice 截取
import itertools
fib2 = fibonacci_generator()
selected = list(itertools.islice(fib2, 10, 20))
print("第11-20项:", selected)四、常见错误与排错
| 错误类型 | 示例 | 原因与解决 |
|---|---|---|
| UnboundLocalError | 函数内赋值变量后被当作局部变量 | 需要在函数内声明 global 或 nonlocal |
| TypeError: missing argument | 调用函数时参数个数不匹配 | 检查函数定义与调用时的参数数量 |
| TypeError: 'list' object is not callable | list = [1,2,3] 后 list() | 变量名覆盖了内置函数名 |
| RecursionError | 递归调用超过最大深度 | 检查递归终止条件,或增加 sys.setrecursionlimit |
| NameError: name 'xxx' is not defined | 模块未导入 | import xxx 后再使用 |
| ModuleNotFoundError | 导入不存在的模块 | 检查是否已安装 (pip install) |
五、小结
知识图谱
函数与模块化编程
├── 函数定义
│ ├── 参数:位置/默认/关键字/*args/**kwargs
│ ├── 返回值:单值/多值(元组)/None
│ └── 传递机制:传对象引用
├── 作用域
│ ├── LEGB规则:Local → Enclosing → Global → Built-in
│ ├── global:修改全局变量
│ └── nonlocal:修改外层变量
├── 高阶特性
│ ├── 闭包:函数+环境变量
│ ├── 装饰器:@语法糖,AOP编程
│ ├── 生成器:yield惰性求值
│ └── 高阶函数:map/filter/reduce
└── 模块化
├── import机制
├── 包结构(__init__.py)
└── pip包管理重点回顾
- 装饰器是Python最强大的特性之一,理解闭包是理解装饰器的前提
- 生成器使用惰性求值,处理大数据集时比列表内存效率高成千上万倍
- 参数传递的核心是"传对象引用"——可变对象的内容可能被函数修改
- 避免使用可变对象作为默认参数,这是新手最常踩的坑之一
- 模块化编程让代码可复用、可维护、可测试
六、课后练习
基础题
- 函数练习:编写函数
is_prime(n)判断素数,并利用它找出100以内的所有素数。 - 默认参数陷阱:解释为什么下面的代码输出不符合预期,并修复:
def add_item(item, lst=[]): lst.append(item); return lst - 模块导入:创建一个工具模块
string_utils.py,包含reverse_str和count_vowels两个函数,然后在另一个文件中导入使用。
进阶题
- 装饰器实现:编写一个
@timeout(max_seconds)装饰器,当函数执行超过指定时间时抛出异常。 - 生成器应用:实现一个
read_chunks(file_path, chunk_size=1024)生成器函数,每次返回指定大小的数据块,用于处理大文件的逐块读取。 - 高阶函数:使用
map、filter和reduce(或结合lambda)处理一个学生成绩列表,完成:过滤掉不及格(<60)的、将分数加5分、计算总分和平均分。
挑战题
- 缓存装饰器:实现一个带过期时间的缓存装饰器
@cache(ttl=60),缓存在ttl秒后失效。支持函数参数为任意可哈希类型。 - 管道模式:实现一个简单的函数管道,让多个函数可以串联执行。例如:
pipe(数据, 函数1, 函数2, 函数3)等价于函数3(函数2(函数1(数据)))。提示:可以使用reduce。
参考资料
- Python 官方文档-函数定义:https://docs.python.org/zh-cn/3/reference/compound_stmts.html#function-definitions
- PEP 318 — Decorators:https://peps.python.org/pep-0318/
- itertools 文档:https://docs.python.org/zh-cn/3/library/itertools.html
- functools 文档:https://docs.python.org/zh-cn/3/library/functools.html