第3章 3.1 def 定义函数

🎯 开场:为什么你需要一个"函数"?

上一章我们做了两个小项目:猜数字游戏九九乘法表。不知道你有没有这种感觉——写到最后,代码越来越长,有些地方反复出现(比如猜数字里的"猜错了提示",九九乘法表里的"换行逻辑")。

举个例子你就懂了。

你写了一个计算工资的程序,里面有5个地方都要"判断是否超过个税起征点"。结果产品经理说"起征点改成6000了",你得翻遍整个文件,一个一个改。

痛点来了:

  1. 重复代码太多:同样的逻辑复制粘贴了 N 遍,改一处要改 N 处
  2. 代码读不懂:一个函数写 200 行,里面绕来绕去,自己都看不懂
  3. 没法复用:写过的代码换个项目想用,还得复制粘贴一堆

学完这一章,上面的问题全部消失。你会掌握一个编程界的"神器"——函数


🧱 基础:什么是函数?

3.1.1 生活类比:函数就像"外卖小哥"

想象你点外卖。你不需要知道外卖小哥怎么骑车、怎么规划路线、怎么爬楼梯——你只做一件事:下单,然后等外卖送到手里

函数就是这样一段"可以被反复调用"的代码。你定义一次,可以调用无数次。

定义函数 = 制定一个标准操作流程(比如"做一杯奶茶")
调用函数 = 下单,让系统执行这个流程

3.1.2 第一个函数:say_hello

废话少说,先看代码:

def say_hello():
"""打招呼函数"""
print("你好呀,我是小张!")

# 调用函数
say_hello()

输出:

你好呀,我是小张!

解释:
- def 是 Python 里定义函数的关键字(英文"define"的缩写)
- say_hello 是函数名,你爱叫什么叫,但最好见名知意
- () 里是参数(后面讲),这里先空着
- """...""" 是函数的说明文档,叫 docstring,写清楚这个函数干嘛的

3.1.3 带参数的函数:让函数更灵活

刚才的 say_hello 太死板了,每次都只能说"小张"。加上参数就不一样了:

def say_hello(name):
"""向指定的人打招呼"""
print(f"你好呀,我是{name}!")

say_hello("小明")
say_hello("小红")
say_hello("老王")

输出:

你好呀,我是小明!
你好呀,我是小红!
你好呀,我是老王!

解释:
- name 是个参数,像是一个"占位符"
- 调用时传进去的值叫实参(实际参数)
- f"..." 是格式化字符串,{name} 会显示变量的值

💡 类比:参数就像点外卖时的"备注"——你备注"少糖",外卖小哥就知道做少糖的。

3.1.4 有返回值的函数:函数不只打印,还能"交货"

刚才的函数都是"打印出来",但有时候我们需要函数"返回一个结果",而不是直接打印。

def add(a, b):
"""计算两个数的和"""
result = a + b
return result

# 把返回值存到变量里
total = add(3, 5)
print(f"3 + 5 = {total}")

输出:

3 + 5 = 8

解释:
- return result 就是函数"交货"的方式
- 调用 add(3, 5) 会返回一个值 8,存到 total
- 没有 return 的函数,返回 None

配图1 - 配图1

3.1.5 默认参数:让参数有"默认值"

有时候我们希望某些参数是可选的,不传就使用默认值:

def greet(name, greeting="你好"):
"""带默认问候语的打招呼"""
print(f"{greeting},{name}!")

greet("小明")              # 使用默认问候语
greet("小红", "早上好")    # 自定义问候语

输出:

你好,小明!
早上好,小红!

解释:
- greeting="你好" 这个就是默认参数
- 调用时不传 greeting,就用默认值
- 传了,就用你传的值

⚠️ 注意:默认参数必须放在普通参数后面!def greet(name, greeting="你好") 可以,但 def greet(greeting="你好", name) 不行。

3.1.6 多返回值:一次返回多个值

函数能不能一次返回多个值?可以!Python 会把它打包成一个元组

def get_stats(numbers):
"""计算一组数的和与平均值"""
total = sum(numbers)
average = total / len(numbers)
return total, average

result = get_stats([10, 20, 30, 40, 50])
print(f"总和:{result[0]},平均值:{result[1]}")

# 也可以用解包,更优雅
total, average = get_stats([10, 20, 30, 40, 50])
print(f"总和:{total},平均值:{average}")

输出:

总和:150,平均值:30.0
总和:150,平均值:30.0

3.1.7 参数传递:值传递 vs 引用传递(生活类比)

这是新手最容易懵的地方。记住两个原则:

类型 能不能改原值 例子
不可变类型(int, str, tuple) 不能改 数字、字符串
可变类型(list, dict) 可能改 列表、字典
def try_modify(x, nums):
"""尝试修改参数"""
x = 100          # 这改不了外面的 x
nums.append(99)  # 这会改外面的 nums

a = 5
b = [1, 2, 3]
try_modify(a, b)

print(f"a = {a}")   # a 还是 5,没变!
print(f"b = {b}")   # b 变成了 [1, 2, 3, 99],被改了!

输出:

a = 5
b = [1, 2, 3, 99]

💡 类比:想象你把一本纸质书复印了一份给别人。他可以在复印件上涂涂画画,但你的原书不受影响(值传递)。但如果你把书的借书证借给他,他用借书证去图书馆改了你的借阅记录,你的书就真的被改了(引用传递)。

3.1.8 函数的文档:docstring

写清楚你的函数是干嘛的,方便自己和别人以后阅读:

def calculate_area(width, height):
"""
计算矩形面积

参数:
    width (float): 宽度
    height (float): 高度

返回:
    float: 矩形面积
"""
return width * height

# 查看文档
print(calculate_area.__doc__)
help(calculate_area)

配图2 - 配图2


🔥 实战:三个小项目

📦 项目 1:温度转换器(5分钟)

场景:你有个外国朋友,他习惯用华氏度,你习惯用摄氏度,需要互相转换。

def celsius_to_fahrenheit(c):
"""摄氏转华氏"""
return c * 9/5 + 32

def fahrenheit_to_celsius(f):
"""华氏转摄氏"""
return (f - 32) * 5/9

# 演示
print("=== 温度转换器 ===")
print(f"0°C = {celsius_to_fahrenheit(0):.1f}°F")
print(f"100°C = {celsius_to_fahrenheit(100):.1f}°F")
print(f"32°F = {fahrenheit_to_celsius(32):.1f}°C")
print(f"212°F = {fahrenheit_to_celsius(212):.1f}°C")

预期输出:

=== 温度转换器 ===
0°C = 32.0°F
100°C = 212.0°F
32°F = 0.0°C
212°F = 212.0°C

一句话解释:定义了2个函数,分别处理两个方向的转换,调用时直接传值就行。


📦 项目 2:个人账单统计工具(15分钟)

场景:你记了一周的消费记录,想知道总花费、平均日花费、最高的单笔支出。

def calculate_total(amounts):
"""计算总花费"""
return sum(amounts)

def calculate_average(amounts):
"""计算平均日花费"""
return sum(amounts) / len(amounts)

def find_max_expense(amounts, descriptions):
"""找出最高的一笔消费"""
max_idx = amounts.index(max(amounts))
return descriptions[max_idx], amounts[max_idx]

def find_min_expense(amounts, descriptions):
"""找出最低的一笔消费"""

min_idx = amounts.index(min(amounts))
return descriptions[min_idx], amounts[min_idx]

# 模拟一周的消费记录
descriptions = ["早餐", "午餐", "地铁", "咖啡", "晚餐", "网购"]
amounts = [15, 25, 5, 30, 80, 200]

# 统计
print("=== 本周消费账单 ===")
print(f"消费笔数:{len(amounts)} 笔")
print(f"总花费:{calculate_total(amounts)} 元")
print(f"平均日花费:{calculate_average(amounts):.1f} 元")

max_desc, max_amount = find_max_expense(amounts, descriptions)
print(f"最高消费:{max_desc},{max_amount} 元")

min_desc, min_amount = find_min_expense(amounts, descriptions)
print(f"最低消费:{min_desc},{min_amount} 元")

预期输出:

=== 本周消费账单 ===
消费笔数:6 笔
总花费:355 元
平均日花费:59.2 元
最高消费:网购,200 元
最低消费:地铁,5 元

一句话解释:把统计逻辑拆成4个独立的函数,每个函数只做一件事,清晰又好维护。


📦 项目 3:待办事项管理器(15分钟)

场景:你需要一个命令行版的待办清单,能添加、查看、标记完成、删除事项。

def show_menu():
"""显示菜单"""
print("\n=== 待办事项管理器 ===")
print("1. 查看所有事项")
print("2. 添加新事项")
print("3. 标记事项完成")
print("4. 删除事项")
print("5. 退出")

def show_todos(todos):
"""显示所有待办事项"""
if not todos:
    print("📝 暂无待办事项")
    return
print("\n📋 你的待办清单:")
for i, todo in enumerate(todos, 1):
    status = "✅" if todo["done"] else "⬜"
    print(f"  {i}. {status} {todo['content']}")

def add_todo(todos, content):
"""添加新事项"""
todos.append({"content": content, "done": False})
print(f"✅ 已添加:{content}")

def complete_todo(todos, index):
"""标记事项完成"""
if 0 < index <= len(todos):
    todos[index-1]["done"] = True
    print(f"✅ 已完成:{todos[index-1]['content']}")
else:
    print("❌ 无效的序号")

def delete_todo(todos, index):
"""删除事项"""
if 0 < index <= len(todos):
    removed = todos.pop(index-1)
    print(f"🗑️ 已删除:{removed['content']}")
else:
    print("❌ 无效的序号")

# 主程序
todos = [
{"content": "完成 Python 函数章节学习", "done": False},
{"content": "练习本章节的5道题", "done": False},
]

while True:
show_menu()
choice = input("\n请选择操作(1-5):")

if choice == "1":
    show_todos(todos)
elif choice == "2":
    content = input("请输入待办内容:")
    add_todo(todos, content)
elif choice == "3":
    show_todos(todos)
    index = int(input("请输入要完成的事项序号:"))
    complete_todo(todos, index)
elif choice == "4":
    show_todos(todos)
    index = int(input("请输入要删除的事项序号:"))
    delete_todo(todos, index)
elif choice == "5":
    print("👋 再见!")
    break
else:
    print("❌ 无效选择,请重新输入")

预期输出(交互示例):

=== 待办事项管理器 ===
1. 查看所有事项
2. 添加新事项
3. 标记事项完成
4. 删除事项
5. 退出

请选择操作(1-5):1

📋 你的待办清单:
1. ⬜ 完成 Python 函数章节学习
2. ⬜ 练习本章节的5道题

一句话解释:把每个操作都写成独立函数,主循环只负责调用,代码清晰得像写中文一样。


💪 进阶:常见坑 + 技巧

❌ 坑 1:忘记加冒号

# ❌ 错误
def say_hello(name)
print(f"你好,{name}")

# ✅ 正确
def say_hello(name):
print(f"你好,{name}")

解释def 那一行末尾必须加 :,这是 Python 的语法规则。


❌ 坑 2:默认参数写成可变对象

# ❌ 错误 - 踩坑预警
def add_item(item, items=[]):
items.append(item)
return items

print(add_item("苹果"))  # ['苹果']
print(add_item("香蕉"))  # ['苹果', '香蕉'] ← 预期是只有'香蕉'!

# ✅ 正确
def add_item(item, items=None):
if items is None:
    items = []
items.append(item)
return items

print(add_item("苹果"))  # ['苹果']
print(add_item("香蕉"))  # ['香蕉']

解释:默认参数 [] 在函数定义时创建一次,可变类型会被记住。用 None 代替,每次调用都会创建新列表。


❌ 坑 3:变量名和函数名冲突

# ❌ 容易混淆
def sum(a, b):
return a + b

sum = 10  # 这行让 sum 不再是函数了!
print(sum(1, 2))  # TypeError: 'int' object is not callable

# ✅ 正确:换个变量名
def sum_numbers(a, b):
return a + b

total = 10  # 用 total 存结果
print(sum_numbers(1, 2))  # 3

❌ 坑 4:函数调用顺序错误

# ❌ 错误 - 调用在定义之前
greet("小明")

def greet(name):
print(f"你好,{name}")

# ✅ 正确 - 先定义后调用
def greet(name):
print(f"你好,{name}")

greet("小明")

❌ 坑 5:return 和 print 混淆

# ❌ 错误 - 想返回结果却打印了
def multiply(a, b):
print(a * b)  # 这是打印,不是返回

result = multiply(3, 4)
print(result)  # None,因为没有 return

# ✅ 正确
def multiply(a, b):
return a * b  # 返回结果

result = multiply(3, 4)
print(result)  # 12

🔧 调试技巧:用 print 大法

def buggy_function(x):
"""一个可能有 bug 的函数"""
print(f"🔍 DEBUG: x = {x}")  # 打印中间值
result = x * 2 + 1
print(f"🔍 DEBUG: result = {result}")
return result

buggy_function(5)

输出:

🔍 DEBUG: x = 5
🔍 DEBUG: result = 11

⚡ 性能小贴士:避免重复计算

# ❌ 低效:每次调用都重新计算
def calculate_circle_area(r):
return 3.14159 * r ** 2

# ✅ 高效:用局部变量缓存常量
def calculate_circle_area(r):
PI = 3.14159  # 常量放函数内,避免全局查找
return PI * r ** 2

# ✅ 更高效:用 math 模块的 PI
import math

def calculate_circle_area(r):
return math.pi * r ** 2

✏️ 练习题

练习 1(2 分钟):基础题 - 改个问候语
- 输入:调用 greet("老张")
- 预期输出:晚上好,老张!
- 提示:看看项目 1 的 greet 函数,默认参数是什么?

练习 2(2 分钟):基础题 - 加个判断
- 输入:给定数字 87,判断是否及格(60分)
- 预期输出:87 分,及格!45 分,不及格!
- 提示:用一个 check_pass(score) 函数,里面加 if 判断

练习 3(3 分钟):进阶题 - 算平均分
- 输入:一组成绩 [85, 90, 78, 92, 88]
- 预期输出:平均分:86.6
- 提示:参考项目 2 的 calculate_average 函数

练习 4(5 分钟):进阶题 - 串起两个项目
- 输入:用项目 2 的账单统计 + 项目 3 的待办管理,实现一个"记账待办"功能
- 预期输出:能添加消费记录,并标记某笔账已付款
- 提示:两个函数可以独立工作,组合在一起需要共享数据

练习 5(5 分钟):挑战题 - 找 bug
- 输入:以下代码运行后输出什么?

def process(items=[]):
items.append("x")
return items

print(process())
print(process())
  • 预期输出:['x']['x', 'x']
  • 提示:为什么会这样?怎么修?

📝 作业:做一个「个人小工具箱」

需求描述:做一个命令行小工具箱,提供 3-5 个常用函数工具供用户选择使用。

功能点
1. 计算器:加减乘除,支持小数
2. 单位转换:比如公里↔英里、公斤↔磅
3. 字符串工具:统计字数、反转字符串、判断回文
4. 至少再实现 1 个你自己想加的功能

加分项
1. 用 while True 循环让工具箱一直可用
2. 给每个函数加上 docstring

验收标准
- 能跑起来
- 输入有互动提示
- 每个工具函数都有 return 返回值(不是直接 print)

提交方式:评论区贴代码或 GitHub 链接


📚 总结

本文学了 3 个核心点:

  1. 函数是代码的"封装":把重复逻辑写成函数,一次定义,多次调用
  2. 参数让函数更灵活:默认参数、多返回值、值传递 vs 引用传递
  3. return 交货,print 展示:需要返回结果用 return,只需要看用 print

延伸学习资源:

  1. Python 官方文档 - 定义函数:权威、详细
  2. 《Python 编程:从入门到实践》第 4 章:实战导向,适合巩固
  3. 视频:B 站小甲鱼《零基础入门学习 Python》第 38-40 集

互动钩子

🎯 你在工作中有没有遇到过"同样的代码复制了 N 遍"的场景?后来是怎么解决的?评论区聊聊,老粉优先回复!


下章剧透

📖 学会了 def 定义函数,下一章我们要学习函数的"灵魂"——参数。默认参数、位置参数、关键字参数、不定参数……掌握了这些,你写的函数就能像瑞士军刀一样灵活多变!

(全文约 5200 字)

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