【Python全栈开发】第3讲 | 函数式编程、装饰器精要与模块化设计架构
1. 为什么我们需要函数?因为我“懒”
兄弟们,如果你发现自己一直在复制、粘贴同一段代码,只是改了改里面的几个数字,那你就得小心了。这在编程里叫 “Code Smell”(代码坏味道)。
函数(Function)的本质就是:把一段逻辑打包,给它起个好听的名字,以后想用的时候直接喊名字就行了。
一个优秀的架构师,不是看他写了多少行代码,而是看他把复杂的逻辑拆分成了多少个清爽、可复用的函数。今天咱们就来拆解 Python 函数的那些硬核玩法。
2. 基础:写一个合格的函数
2.1 别再写“三无”函数了
一个好的函数应该有:清晰的名字、明确的参数、确定的返回值。
def calculate_tax(salary: float) -> float:
"""
计算个人所得税(这是一个简单的模拟逻辑)
"""
if salary <= 5000:
return 0.0
return (salary - 5000) * 0.2
# 调用
my_tax = calculate_tax(8000)
print(f"我要交的税:¥{my_tax}")
老司机建议:看到上面的 : float 和 -> float 没?这叫 Type Hints(类型提示)。虽然 Python 不强制要求,但在团队开发里,这能让你的同事少掉一半头发,因为他们一眼就能看出该传什么、会返回什么。
2.2 函数作为一等公民的实现原理
在 Python 中,函数是一等公民(First-Class Citizen)。这意味着函数可以像普通变量一样被传递、赋值、存储在数据结构中,甚至作为其他函数的返回值。
实现原理:Python 中的一切都是对象,函数也不例外。当你用 def 定义一个函数时,Python 实际上创建了一个函数对象(function object),并将其绑定到函数名上。
def greet(name):
return f"Hello, {name}!"
# 函数可以赋值给变量
say_hello = greet
print(say_hello("张三")) # Hello, 张三!
# 函数可以存储在数据结构中
func_list = [greet, str.upper, len]
print(func_list[0]("李四")) # Hello, 李四!
# 查看函数对象的类型
print(type(greet)) # <class 'function'>
print(greet.__name__) # greet
这种设计让 Python 具备了强大的函数式编程能力。你可以把函数当作"数据"来操作,实现高度抽象的代码逻辑。
3. 进阶:灵活的参数传递
有时候你不知道用户会传多少个参数进来,这时候 Python 的“黑魔法”就派上用场了。
3.1 *args:打包成元组
如果你要写一个求和函数,用户可能传 2 个数,也可能传 100 个数:
def super_sum(*args):
# args 此时是一个元组 (1, 2, 3, ...)
return sum(args)
print(super_sum(1, 2, 3, 4, 5))
3.2 **kwargs:打包成字典
这在配置系统里超级好用,它能接收任何“键值对”:
def save_user_profile(**kwargs):
# kwargs 此时是一个字典 {"name": "张三", "age": 18}
for key, value in kwargs.items():
print(f"正在保存 - {key}: {value}")
save_user_profile(name="张三", age=18, city="北京")
4. 装饰器 (Decorator):给代码穿上“外挂”
这是 Python 最优雅、也是新手最头疼的特性。
直白解释:你想给几个函数都加个功能(比如记录运行时间),但你又不想改动这些函数的源码。装饰器就是那个“外挂”。
4.1 实战:写个性能监控外挂
import time
def timer_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time() # 记录开始时间
result = func(*args, **kwargs) # 执行原函数
end_time = time.time() # 记录结束时间
print(f"--- 函数【{func.__name__}】运行耗时: {end_time - start_time:.4f}s ---")
return result
return wrapper
@timer_decorator
def complex_logic():
print("正在执行复杂的逻辑...")
time.sleep(1.5)
complex_logic()
看到那个 @timer_decorator 没?这就叫**“无侵入式编程”**。你不需要动 complex_logic 里的任何一行代码,就让它拥有了计时功能。
4.2 装饰器的执行时机详解
很多人误以为装饰器是在被装饰函数运行时才执行的。实际上,装饰器在**模块导入时(定义时)**就已经执行了!
def decorator(func):
print(f"装饰器执行了!被装饰的函数是: {func.__name__}")
return func
@decorator
def my_func():
print("函数运行了")
# 输出:装饰器执行了!被装饰的函数是: my_func
# 调用函数时,装饰器不会再执行
my_func() # 只输出:函数运行了
执行流程解析:
- Python 解释器读到
@decorator时,会立即执行decorator(my_func) - 装饰器函数接收被装饰函数作为参数,并返回一个(通常是包装后的)函数
- 被装饰的函数名
my_func被重新绑定到装饰器返回的函数上
4.3 装饰器的闭包机制
装饰器的核心在于闭包(Closure)。闭包是指一个函数记住了它被定义时的环境(即外部函数的局部变量),即使外部函数已经执行完毕。
def outer_decorator(msg):
# 这是外部函数的局部变量
prefix = f"[{msg}]"
def decorator(func):
# wrapper 函数形成了闭包,记住了 prefix
def wrapper(*args, **kwargs):
print(f"{prefix} 开始执行 {func.__name__}")
result = func(*args, **kwargs)
print(f"{prefix} 结束执行 {func.__name__}")
return result
return wrapper
return decorator
@outer_decorator("INFO")
def say_hi():
print("Hi!")
say_hi()
# 输出:
# [INFO] 开始执行 say_hi
# Hi!
# [INFO] 结束执行 say_hi
闭包的关键要素:
- 必须有一个嵌套函数(内部函数引用外部函数的变量)
- 内部函数必须被返回(或赋值给外部变量)
- 外部函数的局部变量被内部函数"记住",形成了闭包
4.4 使用 functools.wraps 保留元数据
使用装饰器后,原函数的元数据(如函数名、文档字符串)会丢失:
def my_decorator(func):
def wrapper(*args, **kwargs):
"""这是 wrapper 的文档"""
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet():
"""这是 greet 的文档"""
print("Hello")
print(greet.__name__) # wrapper(原函数名丢失了!)
print(greet.__doc__) # 这是 wrapper 的文档(原文档丢失了!)
解决方案:使用 functools.wraps:
from functools import wraps
def my_decorator(func):
@wraps(func) # 关键:保留原函数的元数据
def wrapper(*args, **kwargs):
"""这是 wrapper 的文档"""
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet():
"""这是 greet 的文档"""
print("Hello")
print(greet.__name__) # greet(正确!)
print(greet.__doc__) # 这是 greet 的文档(正确!)
5. 作用域与闭包:谁才是"话事人"?
Python 遵循 LEGB 原则(Local -> Enclosing -> Global -> Built-in)。理解这个规则,你就能明白为什么有时候变量"找不到",或者为什么修改全局变量会报错。
5.0 LEGB规则详解
LEGB 是 Python 查找变量名的搜索顺序:
| 层级 | 英文 | 中文 | 说明 |
|---|---|---|---|
| L | Local | 局部作用域 | 当前函数内部定义的变量 |
| E | Enclosing | 嵌套作用域 | 外层嵌套函数的局部变量 |
| G | Global | 全局作用域 | 模块级别定义的变量 |
| B | Built-in | 内置作用域 | Python 内置的变量(如 len, print) |
搜索流程:当 Python 遇到一个变量名时,会按照 L -> E -> G -> B 的顺序查找,找到第一个匹配的即停止。
# 演示 LEGB 规则
x = "global" # G: 全局作用域
def outer():
x = "enclosing" # E: 嵌套作用域
def inner():
x = "local" # L: 局部作用域
print(f"inner 中的 x: {x}") # 找到 L 层的 "local"
inner()
print(f"outer 中的 x: {x}") # 找到 E 层的 "enclosing"
outer()
print(f"全局中的 x: {x}") # 找到 G 层的 "global"
# 输出:
# inner 中的 x: local
# outer 中的 x: enclosing
# 全局中的 x: global
重要规则:
- 读取变量:Python 会自动按 LEGB 顺序查找
- 修改变量:默认只能修改 L(局部)层的变量
- 要修改 E 层用
nonlocal,修改 G 层用global
5.1 别乱碰全局变量!
count = 10
def my_func():
global count # 必须声明,否则你改不了全局的 count
count += 1
print(f"函数内:{count}")
my_func()
老司机的忠告:在函数里用 global 是非常危险的行为。它会让你的程序变得难以预测。尽量通过参数传进去、通过返回值传出来,这才是“函数式编程”的精髓。
5.2 闭包 (Closure):让变量“长生不老”
有时候你希望一个函数运行完后,它里面的变量不要被销毁。
def make_counter():
count = 0
def counter():
nonlocal count # 修改外部函数的局部变量
count += 1
return count
return counter
c1 = make_counter()
print(c1()) # 1
print(c1()) # 2
这就是闭包。它让函数拥有了"记忆",这在写一些状态保持的工具时非常有用。
5.3 偏函数(Partial Function)
偏函数是指固定一个函数的部分参数,生成一个参数更少的新函数。这在需要反复调用某个函数、且部分参数固定不变时非常有用。
from functools import partial
# 原始函数:计算幂次
def power(base, exponent):
return base ** exponent
# 创建偏函数:固定 exponent=2,专门用于平方
square = partial(power, exponent=2)
print(square(5)) # 25
print(square(10)) # 100
# 创建偏函数:固定 base=2,专门用于计算2的幂
# 注意:固定位置参数后,调用时需要使用关键字参数
power_of_2 = partial(power, 2) # 固定第一个参数 base=2
print(power_of_2(3)) # 8 (2的3次方)
print(power_of_2(10)) # 1024 (2的10次方)
实际应用场景:
from functools import partial
# 场景:创建专用的比较函数
# 创建偏函数:固定 min 的默认值
at_least_10 = partial(max, 10) # 确保结果至少为10
# 批量处理数据,确保所有值不小于10
values = [5, 15, 8, 20, 3]
adjusted = [at_least_10(v) for v in values]
print(adjusted) # [10, 15, 10, 20, 10]
5.4 柯里化(Currying)
柯里化是把一个接受多个参数的函数,转换成一系列只接受单个参数的函数。每个函数返回下一个接收单个参数的函数,直到所有参数都被收集完毕。
# 普通函数
def add(a, b, c):
return a + b + c
# 柯里化版本
def curried_add(a):
def inner_b(b):
def inner_c(c):
return a + b + c
return inner_c
return inner_b
# 使用柯里化函数
result = curried_add(1)(2)(3)
print(result) # 6
# 也可以分步调用
add_1 = curried_add(1) # 固定第一个参数
add_1_2 = add_1(2) # 固定第二个参数
result = add_1_2(3) # 传入第三个参数
print(result) # 6
柯里化的优势:
- 延迟计算:可以分步传递参数,在最后一步才执行计算
- 参数复用:可以预先固定某些参数,生成专用函数
- 函数组合:便于构建复杂的函数管道
使用装饰器实现自动柯里化:
from functools import partial
def curry(func):
"""简单的柯里化装饰器"""
def curried(*args):
if len(args) >= func.__code__.co_argcount:
return func(*args)
return partial(curried, *args)
return curried
@curry
def multiply(a, b, c):
return a * b * c
# 现在可以像使用柯里化函数一样调用
print(multiply(2, 3, 4)) # 24
print(multiply(2)(3)(4)) # 24
# 或者分步调用
double = multiply(2)
triple = double(3)
print(triple(4)) # 24
6. 递归:自己调用自己,是艺术也是坑
递归就像是《盗梦空间》,你进入一层梦境,处理完再出来。
def factorial(n):
""" 计算阶乘:n! """
if n == 1: # 递归出口,没这个就死循环了
return 1
return n * factorial(n - 1)
print(factorial(5)) # 120
避坑指南:Python 的递归深度是有限制的(默认 1000 层)。如果你递归太深,会报 RecursionError。所以,能用循环解决的,尽量别用递归。
6.5 函数式编程范式详解
**函数式编程(Functional Programming)**是一种编程范式,它将计算视为数学函数的求值,避免使用可变状态和循环。Python 虽然不是纯函数式语言,但提供了丰富的函数式编程工具。
6.5.1 纯函数(Pure Function)
纯函数是指相同的输入永远产生相同的输出,且没有副作用(不修改外部状态)的函数。
# 纯函数:没有副作用,输出只依赖输入
def pure_add(a, b):
return a + b
# 非纯函数:有副作用(修改了外部变量)
total = 0
def impure_add(a):
global total
total += a # 副作用:修改了外部状态
return total
# 非纯函数:输出依赖外部状态
import random
def impure_random():
return random.random() # 每次调用结果不同
纯函数的优势:
- 可测试性:给定输入,输出确定,易于单元测试
- 可缓存性:结果可以被缓存(Memoization),提高性能
- 并行安全:没有共享状态,可以安全地在多线程/多进程中运行
6.5.2 不可变性(Immutability)
函数式编程鼓励使用不可变数据,避免修改原有数据,而是创建新的数据。
# 命令式风格(修改原数据)
def add_item_imperative(items, item):
items.append(item) # 修改了传入的列表
return items
# 函数式风格(创建新数据)
def add_item_functional(items, item):
return items + [item] # 创建新列表,原列表不变
# 测试
original = [1, 2, 3]
# 命令式:原数据被修改
result1 = add_item_imperative(original.copy(), 4)
print(result1) # [1, 2, 3, 4]
# 函数式:原数据保持不变
original = [1, 2, 3]
result2 = add_item_functional(original, 4)
print(original) # [1, 2, 3] - 原数据未被修改
print(result2) # [1, 2, 3, 4]
6.5.3 高阶函数(Higher-Order Function)
高阶函数是指接收函数作为参数或返回函数作为结果的函数。
# 接收函数作为参数
def apply_operation(numbers, operation):
"""对列表中的每个元素应用指定的操作"""
return [operation(n) for n in numbers]
# 定义一些操作函数
def square(x):
return x ** 2
def double(x):
return x * 2
numbers = [1, 2, 3, 4, 5]
# 传递不同的函数,实现不同的行为
print(apply_operation(numbers, square)) # [1, 4, 9, 16, 25]
print(apply_operation(numbers, double)) # [2, 4, 6, 8, 10]
Python 内置的高阶函数:map(), filter(), reduce(), sorted() 等。
6.5.4 函数组合(Function Composition)
函数组合是将多个简单的函数组合成一个复杂的函数,一个函数的输出作为下一个函数的输入。
from functools import reduce
def compose(*functions):
"""组合多个函数,从右向左执行"""
def composed(arg):
# reduce 从右向左应用函数
return reduce(lambda v, f: f(v), reversed(functions), arg)
return composed
# 定义简单的处理函数
def add_one(x):
return x + 1
def double(x):
return x * 2
def to_string(x):
return f"结果是: {x}"
# 组合函数:先加1,再翻倍,最后转字符串
process = compose(to_string, double, add_one)
print(process(5)) # 结果是: 12 ((5+1)*2 = 12)
6.5.5 惰性求值与生成器
惰性求值是指只在需要时才计算值,这在处理大数据集时非常有用。
# 惰性求值:生成器函数
def fibonacci():
"""生成无限斐波那契数列"""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 只取前10个,不会无限计算
fib = fibonacci()
first_10 = [next(fib) for _ in range(10)]
print(first_10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# 惰性管道处理大数据
def read_large_file(file_path):
"""惰性读取大文件,一次只读一行"""
with open(file_path, 'r') as f:
for line in f:
yield line.strip()
def filter_lines(lines, keyword):
"""惰性过滤包含关键字的行"""
for line in lines:
if keyword in line:
yield line
def count_words(lines):
"""惰性计算每行的单词数"""
for line in lines:
yield len(line.split())
# 构建惰性处理管道(不会立即执行)
# lines = read_large_file('huge_file.txt')
# filtered = filter_lines(lines, 'Python')
# word_counts = count_words(filtered)
#
# 只在遍历时才真正处理数据
# for count in word_counts:
# print(count)
6.5.6 记忆化(Memoization)
记忆化是一种优化技术,缓存函数的返回结果,避免重复计算。
from functools import lru_cache
# 使用内置装饰器实现记忆化
@lru_cache(maxsize=128)
def fibonacci(n):
"""计算斐波那契数列,带缓存"""
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# 第一次计算较慢,后续从缓存读取
print(fibonacci(100)) # 瞬间完成,因为缓存了中间结果
# 查看缓存信息
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
6.5.7 Python 函数式编程工具总结
| 工具 | 作用 | 示例 |
|---|---|---|
map() |
对每个元素应用函数 | map(func, iterable) |
filter() |
按条件过滤元素 | filter(pred, iterable) |
reduce() |
累积计算 | reduce(func, iterable) |
lambda |
匿名函数 | lambda x: x**2 |
sorted() |
排序 | sorted(iterable, key=func) |
zip() |
合并多个序列 | zip(list1, list2) |
enumerate() |
带索引遍历 | enumerate(iterable) |
any() / all() |
逻辑判断 | any(pred(x) for x in iterable) |
functools.partial |
偏函数 | partial(func, arg=val) |
functools.lru_cache |
缓存 | @lru_cache() |
7. 模块化:别把所有鸡蛋放在一个篮子里
当你的项目超过 200 行时,就该考虑分文件了。
6.1 if __name__ == "__main__": 是干啥的?
你肯定在很多代码底部见过这行。它的意思是:“只有当你直接运行这个文件时,才执行下面的代码;如果你是作为模块被别人 import 的,那就别乱动。”
6.2 目录结构建议
my_project/
├── main.py # 程序入口
├── utils/ # 工具函数包
│ ├── __init__.py # 声明这是一个包
│ ├── logger.py # 日志工具
│ └── calc.py # 计算工具
└── .venv/ # 虚拟环境
7. 避坑小贴士(老司机的叮嘱)
- 函数别写太长:一个函数如果超过 50 行,大概率说明你写得太复杂了,该拆分了。“一个函数只干一件事”,这是金科玉律。
- Lambda 表达式别滥用:一行小逻辑可以用 Lambda,逻辑稍微复杂点,求你正经写个
def。代码是写给人看的,不是写给机器看的。 - 闭包的坑:闭包虽然高级,但容易造成内存泄漏或者逻辑混乱。新手建议先玩明白装饰器。
8. 实战演练:巩固你的内功
纸上得来终觉浅,绝知此事要躬行。这里有几个实战小练习,看看你能搞定几个?
题目 1:编写一个“带重试机制”的装饰器
需求:
写一个装饰器 retry,它可以接收一个参数 max_retries(最大重试次数)。
如果被装饰的函数抛出异常,就自动重试,直到超过最大次数再抛出异常。
import time
import random
def retry(max_retries=3):
def decorator(func):
def wrapper(*args, **kwargs):
for i in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"第 {i+1} 次尝试失败:{e},正在重试...")
time.sleep(1) # 稍微歇会儿
# 最后一次还没成功,那就真报错吧
print("重试次数耗尽,任务失败。")
raise Exception("Function failed after max retries")
return wrapper
return decorator
@retry(max_retries=3)
def unstable_network_request():
# 模拟 70% 概率失败
if random.random() < 0.7:
raise ValueError("网络连接超时")
return "数据下载成功!"
# 测试
try:
print(unstable_network_request())
except Exception as e:
print(f"最终结果:{e}")
题目 2:闭包实战——简易存钱罐
需求:
编写一个函数 piggy_bank,它返回一个内部函数。
每次调用这个内部函数存钱,它都会累加金额,并打印当前余额。
如果不传金额,默认返回当前余额。
def piggy_bank():
balance = 0 # 自由变量,藏在闭包里
def operation(amount=0):
nonlocal balance
if amount > 0:
balance += amount
print(f"存入 {amount} 元,当前余额:{balance} 元")
elif amount < 0:
print("只准存不准取!")
else:
print(f"当前余额:{balance} 元")
return balance
return operation
# 测试
my_bank = piggy_bank()
my_bank(100) # 存入 100
my_bank(50) # 存入 50
my_bank() # 查询余额
题目 3:函数式编程——筛选质数
需求:
使用 filter 函数和 lambda 表达式(或者自定义函数),从一个列表中筛选出所有的质数。
给定列表:numbers = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
numbers = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
# 使用 filter
# filter(函数, 可迭代对象):保留函数返回 True 的元素
primes = list(filter(is_prime, numbers))
print(f"质数列表:{primes}")
# 或者用列表推导式(更 Pythonic)
primes_v2 = [x for x in numbers if is_prime(x)]
print(f"推导式结果:{primes_v2}")
系列索引
写在最后:
函数是 Python 进阶的分水岭。如果你能玩转装饰器和模块化,那你已经具备了开发中型项目的能力。
别光看不练,试着把上一讲的学生管理系统,重构成“函数版本”:增加、删除、查询都写成独立的函数。
觉得有收获的话,点赞、收藏 走一波!咱们下一讲聊聊 Python 里的“降龙十八掌”——面向对象。
更多推荐


所有评论(0)