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 是局部变量
 

函数调用过程

  1. 调用 swap(x, y) → 创建局部变量 a = 10, b = 20

  2. 函数内部交换 a 和 b → a = 20, b = 10

  3. 函数结束 → a 和 b 被销毁

  4. 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 改自身,深浅拷贝看嵌套。

相关链接


技术学习路线图 > 函数与 OOP