Day 06:函数基础
🎯 学习目标:理解函数的作用与原理,掌握参数传递机制,学会深浅拷贝
1. 函数定义与抽取
1.1 为什么需要函数
两个场景对比——没有函数 vs 有函数:
# ❌ 没有函数——代码重复
a1, b1 = 3, 5
result1 = a1 + b1
print(f"{a1}+{b1}={result1}")
a2, b2 = 10, 20
result2 = a2 + b2
print(f"{a2}+{b2}={result2}")
a3, b3 = 7, 8
result3 = a3 + b3
print(f"{a3}+{b3}={result3}")
# ✅ 使用函数——代码复用
def add_and_print(a, b):
"""计算并打印两个数的和"""
result = a + b
print(f"{a}+{b}={result}")
add_and_print(3, 5)
add_and_print(10, 20)
add_and_print(7, 8)
# 优势:改一处就全部生效,代码行数更少
1.2 函数的四大好处
| 好处 | 说明 |
|---|---|
| 代码复用 | 同样的逻辑只写一次,到处调用 |
| 模块化 | 大问题拆成小函数,每个函数只做一件事 |
| 可测试 | 函数可以独立测试,更容易发现 Bug |
| 可读性 | 好的函数名本身就是注释 |
1.3 函数文档字符串
def calculate_bmi(weight, height):
"""
计算 BMI 指数
参数:
weight: 体重(千克)
height: 身高(米)
返回:
BMI 值(浮点数)
示例:
>>> calculate_bmi(70, 1.75)
22.86
"""
return weight / (height ** 2)
# 查看文档
print(calculate_bmi.__doc__)
help(calculate_bmi)
2. 形参和实参
def greet(name, greeting="Hello"):
# name 和 greeting 是形参(形式参数)——函数定义时的占位符
return f"{greeting}, {name}!"
# "World" 和 "Hi" 是实参(实际参数)——函数调用时传入的实际值
print(greet("World", "Hi")) # "Hi, World!"
关键理解:
-
形参 = 定义时的占位变量
-
实参 = 调用时传入的真实数据
-
调用时实参的值被赋给形参
3. 函数执行内存分析
def swap(a, b):
print(f"swap 内部(交换前):a={a}, b={b}")
temp = a
a = b
b = temp
print(f"swap 内部(交换后):a={a}, b={b}")
x, y = 10, 20
print(f"调用前:x={x}, y={y}")
swap(x, y)
print(f"调用后:x={x}, y={y}")
# 输出:x 和 y 没有变!因为函数内的 a、b 是局部变量
函数调用过程:
-
调用
swap(x, y)→ 创建局部变量a = 10,b = 20 -
函数内部交换 a 和 b →
a = 20,b = 10 -
函数结束 → a 和 b 被销毁
-
x 和 y 从未被修改 → 仍然是 10 和 20
4. 传递不可变对象与可变对象 ⭐⭐
这是 Python 函数参数传递中最核心的概念:
4.1 不可变对象(int, str, tuple, float, bool)
def modify(n, s):
n = 100 # n 指向新对象 100,原参数不受影响
s = "changed" # s 指向新对象,原参数不受影响
num = 10
text = "original"
modify(num, text)
print(num) # 10(不变!)
print(text) # "original"(不变!)
4.2 可变对象(list, dict, set)
def append_item(lst):
lst.append(4) # ⚠️ 修改了 lst 指向的对象本身!
nums = [1, 2, 3]
append_item(nums)
print(nums) # [1, 2, 3, 4](被修改了!)
def reassign(lst):
lst = [4, 5, 6] # 重新赋值:让局部变量 lst 指向新列表
nums2 = [1, 2, 3]
reassign(nums2)
print(nums2) # [1, 2, 3](不变!因为只是改了局部变量的指向)
核心区别
obj.method()修改对象本身 → 影响外部
param = new_value改变变量指向 → 不影响外部
5. 参数传递形式
5.1 位置参数
def register(name, age, city):
print(f"{name}, {age}岁, 来自{city}")
register("张三", 20, "北京") # 按位置一一对应
# register(20, "张三", "北京") # ❌ 顺序错了,语义全乱
5.2 关键字参数
register(age=20, city="北京", name="张三") # 指定参数名,顺序无所谓
register("张三", city="北京", age=20) # 混合使用(位置在前)
5.3 默认参数
def register(name, age, city="北京"):
print(f"{name}, {age}岁, 来自{city}")
register("张三", 20) # 使用默认值:city="北京"
register("李四", 25, "上海") # 覆盖默认值
# ⚠️ 默认参数只初始化一次!可变对象是陷阱
def add_item(item, lst=[]): # ❌ 危险!
lst.append(item)
return lst
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2](使用了同一个列表!)
print(add_item(3)) # [1, 2, 3](共享同一个默认列表)
# 正确做法
def add_item(item, lst=None):
if lst is None:
lst = [] # 每次调用时创建新列表
lst.append(item)
return lst
5.4 可变参数 *args
def sum_all(*args):
"""求和任意数量的参数"""
print(type(args)) # <class 'tuple'>
print(args) # (1, 2, 3, 4, 5)
return sum(args)
print(sum_all(1, 2)) # 3
print(sum_all(1, 2, 3, 4, 5)) # 15
# *args 把多余的**位置参数**打包为元组
5.5 关键字参数 **kwargs
def print_info(**kwargs):
"""打印任意数量的关键字参数"""
print(type(kwargs)) # <class 'dict'>
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="张三", age=20, city="北京")
# **kwargs 把多余的**关键字参数**打包为字典
5.6 组合使用
# 参数顺序:位置 → *args → 关键字 → **kwargs
def func(a, b, *args, c=10, **kwargs):
print(f"a={a}, b={b}")
print(f"*args={args}")
print(f"c={c}")
print(f"**kwargs={kwargs}")
func(1, 2, 3, 4, 5, c=20, x=100, y=200)
# **kwargs={'x': 100, 'y': 200}
5.7 解包传参
# * 解包列表/元组为位置参数
nums = [1, 2, 3]
print(*nums) # 等价于 print(1, 2, 3)
def add(a, b, c):
return a + b + c
print(add(*[1, 2, 3])) # 6
# ** 解包字典为关键字参数
info = {"name": "张三", "age": 20, "city": "北京"}
register(**info) # 等价于 register(name="张三", age=20, city="北京")
6. 浅拷贝与深拷贝 ⭐⭐
import copy
# 原始数据
original = [1, 2, [10, 20]]
print(f"原始:{original}")
# === 赋值(不是拷贝!)===
ref = original # 只是两个变量指向同一个对象
ref[0] = 999
print(f"赋值后原始:{original}") # [999, 2, [10, 20]] ← 变了!
# === 浅拷贝 ===
original = [1, 2, [10, 20]]
shallow = copy.copy(original)
shallow[0] = 100 # 修改第一层 → 不影响原始
shallow[2][0] = 999 # 修改嵌套层 → 影响原始!
print(f"浅拷贝后原始:{original}") # [1, 2, [999, 20]]
# === 深拷贝 ===
original = [1, 2, [10, 20]]
deep = copy.deepcopy(original)
deep[0] = 100 # 不影响原始
deep[2][0] = 999 # 也不影响原始!
print(f"深拷贝后原始:{original}") # [1, 2, [10, 20]] ← 完全不变
graph TB subgraph 赋值 A1[original] --> OBJ1[[1,2,内层列表]] A2[ref=original] --> OBJ1 end subgraph 浅拷贝 B1[original] --> OBJ2a[[1,2,→]] B2[shallow] --> OBJ2b[[1,2,→]] OBJ2a --> INNER[[10,20]] OBJ2b --> INNER end subgraph 深拷贝 C1[original] --> OBJ3a[[1,2,→]] C2[deep] --> OBJ3b[[1,2,→]] OBJ3a --> INNER1[[10,20]] OBJ3b --> INNER2[[10,20新]] end
选择指南
- 只有不可变数据(int, str 等) → 赋值就够了
- 只有一层嵌套的 list/dict → 浅拷贝
- 有多层嵌套的 list/dict → 深拷贝(或用 copy.deepcopy)
7. 方法的返回值
# 单个返回值
def square(x):
return x ** 2
# 多个返回值(实际返回一个元组)
def statistics(nums):
return min(nums), max(nums), sum(nums)/len(nums)
lo, hi, avg = statistics([1, 2, 3, 4, 5])
print(f"最小:{lo}, 最大:{hi}, 平均:{avg}")
# 没有 return → 返回 None
def no_return():
print("我什么都不返回")
# 隐式 return None
result = no_return()
print(result) # None
速记卡(面试闪卡)
Q1:一句话讲清「Day 06:函数基础」到底是什么?
A:函数是为“复用和拆解”而生的代码块。面试最爱考两件事:一是参数传递机制(Python 是“传对象引用”,不是传值也不是传引用那么简单),二是可变/不可变对象在传递时的行为差异,外加浅拷贝与深拷贝。
Q2:1–2. 函数定义与形参实参 —— 怎么理解?
A:函数的四大好处是代码复用、模块化、可测试、可读(好函数名即注释)。形参(parameter)是定义时的占位变量,实参(argument)是调用时传入的真实值,调用时实参被“赋值”给形参。生活比喻:形参像快递柜的空格子(占个位置),实参像你真正塞进去的包裹,格子本身不等于包裹。
Q3:4. 传递不可变对象与可变对象 —— 怎么理解?
A:这是 Python 参数传递最常被问的点。准确说 Python 是“传对象引用”:函数拿到的是外部那个对象的引用。modify(n) 里 n=100 只是让局部变量 n 指向新对象,外部的 num 不受影响(不可变 int/str/tuple 同理);但 append_item(lst) 里 lst.append(4) 改的是 lst 指向的那个列表对象本身,外部 nums 跟着变。一句话铁律:obj.method() 改对象本身→影响外部;param = new_value 改指向→不影响外部。
Q4:5. 参数传递形式 —— 怎么理解?
A:五种形式——位置参数(按顺序一一对应)、关键字参数(指名,顺序无所谓)、默认参数(注意只初始化一次,可变默认值是陷阱,要用 None)、*args(把多余位置参数打包成元组)、**kwargs(把多余关键字参数打包成字典)。组合顺序固定为:位置 → *args → 关键字 → **kwargs。还有解包:* 解列表、** 解字典。
Q5:6. 浅拷贝与深拷贝 —— 怎么理解?
A:普通赋值 ref = original 根本不是拷贝,俩变量指向同一个对象,改一个另一个也变。copy.copy 浅拷贝:第一层独立,但嵌套的列表/字典还是共享同一份。copy.deepcopy 深拷贝:连嵌套层也clone一份,完全独立。选择指南:纯不可变数据→赋值;只有一层嵌套→浅拷贝;多层嵌套→深拷贝。
Q6:核心速记主线有哪些?
A:抓住这几根:1. 函数定义与抽取(四大好处)、2. 形参实参(占位 vs 真实值)、3. 函数执行内存分析(局部变量销毁)、4. 传递不可变/可变对象(改自身 vs 改指向)、5. 参数传递形式(位置/关键字/默认/*args/**kwargs)、6. 浅拷贝与深拷贝。
口诀
A:参数传递传引用,不可变改不动外;
可变 method 改自身,深浅拷贝看嵌套。
相关链接
-
上一篇:05_容器类型
-
下一篇:07_函数进阶与文件