1 函数是什么 —— 把「复制粘贴」升级成「命名复用」
是什么?为什么学?
函数(function)是给一段代码起个名字,以后通过名字反复调用它的语法结构。你有没有发现,前面写的代码里有些功能反复出现:求和、判断奇偶、算平均……每次都要重新写一遍。函数就是「给一段代码起个名字,以后想用就叫名字」——一次写好,到处使用。它是编程里最重要的「复用」工具,也是拆解复杂任务的唯一方式。
底层原理
def 定义函数时,Python 并不会执行函数体,而是把「函数体代码」和「参数信息」打包成一个函数对象,绑定到函数名上;只有调用(写 函数名())时,Python 才会新建一个独立的「执行空间」(栈帧),把函数体从头执行一遍,执行完销毁这个空间。这就是「定义不执行、调用才执行」的底层原因,也是函数内变量与函数外变量互不干扰的根源。
术语小词典:栈帧(stack frame)就是「函数运行时临时搭起的工作台」,函数跑完工作台就拆掉,上面的局部数据也随之消失。
生活类比
函数像菜谱:把菜谱写进笔记本(def 定义)不等于菜做好了;照着菜谱做一遍(调用)才出菜。同一本菜谱可以被不同的人、不同的时间反复使用——这就是「一次编写、到处调用」。
注意事项总结
1 手写三遍 vs 函数写一遍调三遍:
def say_hello():
print("你好!")
say_hello()
say_hello()
say_hello()
运行结果:
你好!
你好!
你好!
函数体只写一份,想调用多少次都行——这就是复用的意义。
2 函数体可以有多行:
def print_plan():
print("今天的学习计划:")
print("1. 复习列表")
print("2. 学函数")
print_plan()
3 为什么用函数:一是复用(同一段逻辑只写一次);二是分工(一个大问题拆成几个小函数,每个只管一件事);三是好改(要改逻辑只改一个地方)。
易错点
- 错误写法:
def say_hello()行尾忘了冒号 → 问题:报SyntaxError: invalid syntax→ 正确写法:def 行尾必须有冒号:def say_hello():。 - 错误写法:调用写在定义之前(
say_hello()放第一行) → 问题:报NameError,因为 def 还没执行、名字还不存在 → 正确写法:先定义、后调用。 - 错误写法:
f = say_hello误写成f = say_hello()→ 问题:前者拿到函数本身;后者执行函数并把返回值(None)赋给 f → 正确写法:想传函数本身不写括号;想拿结果才写括号。
记忆口诀:def 登记不动手,括号调用才执行;名字绑定函数体,一次编写处处用。
2 参数 —— 实参按位置「对号入座」
是什么?为什么学?
参数(parameter)是函数的「入口」:定义时括号里写的名字是形参(占位符),调用时传进去的值是实参(真数据)。没有参数的函数只能做固定的事,有了参数,同一个函数才能处理不同的数据——这是函数通用的关键。
底层原理
调用 greet("小明") 时,Python 按位置一一对应:第 1 个实参给第 1 个形参,第 2 个实参给第 2 个形参。形参在函数执行时成为「局部名字」,指向实参传入的对象。注意 Python 传的是「对象的引用」——形参和实参指向同一个对象(好比把同一张快递单号抄了一份递给函数,单号指向同一个包裹)。
生活类比
参数像点餐单:菜单上写「汉堡(夹什么、加不加辣)」是形参;你下单写「汉堡夹牛肉、加辣」是实参。后厨按你填的单子做——每一项都要对上,写错位置、漏写一项,菜就做不对。
注意事项总结
1 多个参数按顺序一一对应:
def describe(name, age):
print(name, "今年", age, "岁")
describe("小明", 18)
describe("小红", 17)
运行结果:
小明 今年 18 岁
小红 今年 17 岁
调用时实参按位置配对传给形参:"小明" 给 name、18 给 age。
2 形参和全局变量同名也互不干扰:
def show(x):
print("函数内 x =", x)
x = 100 # 全局的 x
show(7) # 传入的 7 只给函数里的 x
print("函数外 x =", x)
运行结果:
函数内 x = 7
函数外 x = 100
函数内的 x 是形参(局部名字),和函数外的全局 x 只是同名不同物。
3 实参个数和形参个数必须一致(错误示范):
def add(a, b):
return a + b
# add(3) # 报错!TypeError: add() missing 1 required positional argument: 'b'
# add(3, 5, 7) # 报错!TypeError: add() takes 2 positional arguments but 3 were given
报错信息里会明确告诉你「缺了哪个参数」或「多了几个参数」——学会读报错信息,就能自己定位问题。
易错点
- 错误写法:
def greet(name, age)调用写成greet(18, "小明")→ 问题:参数顺序反了,name 拿到 18,逻辑错乱且不报错 → 正确写法:实参顺序必须与形参定义顺序一致。 - 错误写法:调用时少传一个参数 → 问题:报
TypeError: missing 1 required positional argument→ 正确写法:检查调用处参数个数是否与定义一致。 - 错误写法:形参名写错(
def greet(nmae),函数体内用name) → 问题:报NameError,函数体内找不到这个名字 → 正确写法:形参名定义和使用保持一致。
记忆口诀:形参占位实参值,调用按位对号坐;个数顺序都对齐,函数才能跑得对。
3 return —— 函数与调用者的「交接棒」
是什么?为什么学?
return 是函数把结果交还给调用者的语句。print 只是把内容显示在屏幕上,看完就没了;return 把结果真正「递」出来,调用处能把它存进变量、继续参与计算。想让函数「有用」,十有八九要靠 return。
底层原理
函数调用会创建独立的执行空间,函数里的计算结果默认带不出来。return 的作用就是把一个值从函数内部「递交」回调用者的执行空间,然后立即结束函数。如果函数没有 return,Python 会自动补一个「返回 None」——None 是表示「什么都没有」的特殊值。另外 return a, b 实际上返回的是一个元组,可以一次性交回多个结果。
生活类比
食堂打饭窗口:print 是窗口上方的显示屏——大家都看得见,但你拿不到;return 是打饭阿姨从窗口递出来的餐盘——只有递到你手里(存进变量),你才能吃(继续使用)。没有 return 的函数就像只给你看了一眼菜,什么也没递出来。
注意事项总结
1 return 的结果可以存进变量继续用:
def add(a, b):
return a + b
result = add(3, 5) # 把返回的 8 接住
print(result) # 8
print(result * 10) # 还能继续参与运算:80
运行结果:
8
80
对比:如果 add 里用的是 print 而不是 return,result 会拿到 None,第 6 行就会报错。
2 没有 return 的函数,调用结果是 None:
def show():
print("我在打印")
r = show()
print(r) # None
print(r is None) # True
运行结果:
我在打印
None
True
is None 是判断「是不是 None」的标准写法。调试时发现「函数结果用不了」,先检查函数里有没有写 return。
3 return 返回多个值(自动打包成元组):
def get_min_max(nums):
return min(nums), max(nums)
a, b = get_min_max([3, 8, 1, 9]) # 一次接住两个结果
print("最小值:", a)
print("最大值:", b)
运行结果:
最小值: 1
最大值: 9
return min(nums), max(nums) 看起来返回两个值,实际返回一个元组 (1, 9);a, b = ... 是元组解包。
4 遇到 return 函数立即结束:
def check(score):
if score < 0:
return "分数不合法" # 提前返回,后面的代码不执行
return "分数: " + str(score)
print(check(-5))
print(check(85))
运行结果:
分数不合法
分数: 85
易错点
- 错误写法:用 print 代替 return(
def add(a, b): print(a + b)) → 问题:调用处拿到 None,结果无法参与计算 → 正确写法:需要结果就return a + b。 - 错误写法:
return a, b但只用x = get_min_max(...)接收 → 问题:x 拿到的是元组 (a, b),不是两个独立变量 → 正确写法:用a, b = get_min_max(...)解包。 - 错误写法:忘了写 return → 问题:函数「算完就丢」,调用处拿到 None → 正确写法:检查函数是否有返回值需求,需要就写 return。
记忆口诀:print 只展示,return 真递出;return 一到就收工,没写默认返 None。
4 默认参数与变量作用域 —— 两个经典坑提前避开
是什么?为什么学?
默认参数(default parameter)让某些参数「不传也有值」;作用域(scope)决定「变量在哪些地方可见」。这两个概念是新手写函数时踩坑最多的两个地方,今天一次讲透,后面能少掉很多头发。
底层原理
默认参数的值在 def 定义的那一刻只计算一次,之后每次调用共用同一个对象——如果默认值是列表、字典这类「可变对象」,多次调用会互相污染。作用域方面,Python 查找变量按 LEGB 顺序:局部(Local)→ 嵌套函数(Enclosing,Day 9 才接触)→ 全局(Global)→ 内置(Built-in)。函数里读取全局变量没问题;但函数里赋值(count = count + 1)时,Python 会认为 count 是新的局部变量——想改全局必须显式声明 global。
生活类比
默认参数像酒店房间的默认配置:你不特别要求,前台就按「大床房、无烟」安排;你提了要求,就按你的来。作用域像公司的储物柜:大厅的公共柜(全局)谁都能看;工位的私人抽屉(局部)只有你自己能用——想动公共柜里的东西(改全局变量),得先和管理员打招呼(写 global)。
注意事项总结
1 默认参数——不传用默认,传了用你的:
def greet(name, greeting="你好"):
print(greeting, name)
greet("小明") # 不传 greeting,用默认「你好」
greet("小红", "早上好") # 传了就用传入的
运行结果:
你好 小明
早上好 小红
有默认值的参数必须写在没有默认值的参数后面(def greet(name, greeting="你好") 对;def greet(name="小明", greeting) 会直接报语法错误)。
2 可变默认值陷阱(错误示范):
def add_item(item, bag=[]):
bag.append(item)
return bag
print(add_item("苹果")) # 第一次:['苹果']
print(add_item("香蕉")) # 期望 ['香蕉'],实际 ['苹果', '香蕉']!
运行结果:
['苹果']
['苹果', '香蕉']
bag=[] 这个列表在 def 定义时只创建一次,所有没传 bag 的调用共用它。正确做法是默认值用 None,函数里再创建新列表:
def add_item(item, bag=None):
if bag is None:
bag = []
bag.append(item)
return bag
print(add_item("苹果")) # ['苹果']
print(add_item("香蕉")) # ['香蕉']
规则:默认值只用不可变类型(数字、字符串、None),绝不用列表/字典。
3 作用域——函数里读全局、写局部(同名遮蔽):
x = 10 # 全局 x
def show():
x = 20 # 这是函数里的局部 x,和全局 x 没关系
print("函数内 x =", x)
show() # 函数内 x = 20
print("函数外 x =", x) # 全局 x 没被改动,还是 10
运行结果:
函数内 x = 20
函数外 x = 10
函数里 x = 20 创建的是局部 x,只是「遮蔽」了全局 x 的名字,并不会修改全局变量。想修改全局变量必须加 global 声明,但最佳实践是尽量别用 global——用参数传进去、用 return 传出来,函数才独立、好维护。
易错点
- 错误写法:
def f(a=1, b):默认参数写在普通参数前面 → 问题:直接报SyntaxError: non-default argument follows default argument→ 正确写法:默认参数一律写在普通参数后面。 - 错误写法:
def f(lst=[])且函数里修改 lst → 问题:多次调用共用同一个列表,数据互相污染 → 正确写法:默认值写 None,函数里if lst is None: lst = []。 - 错误写法:函数里写
count = count + 1想改全局 count → 问题:报UnboundLocalError(或行为异常) → 正确写法:函数内声明global count,或改用参数 + return。 - 错误写法:以为函数里的局部变量在函数外还能用 → 问题:报
NameError,局部变量随函数结束而销毁 → 正确写法:需要结果就用 return 传出来。
记忆口诀:默认参数放后面,可变默认用 None;全局修改加 global,能不用就尽量不。
5 动手实践:数学工具函数库
把常用的数学运算封装成一个个小函数,放进一个文件,这就是「函数库」的雏形。
# 数学工具函数库
import math
def add(a, b):
"""两数相加"""
return a + b
def multiply(a, b):
"""两数相乘"""
return a * b
def average(nums):
"""求列表的平均值(空列表返回 0)"""
if len(nums) == 0:
return 0
return sum(nums) / len(nums)
def factorial(n):
"""计算 n 的阶乘(1*2*3*...*n)"""
result = 1
for i in range(2, n + 1):
result = result * i
return result
def circle_area(radius):
"""计算圆的面积(πr²)"""
return math.pi * radius * radius
# 调用测试
print(add(3, 4)) # 7
print(multiply(3, 4)) # 12
print(average([85, 92, 78])) # 85.0
print(average([])) # 0
print(factorial(5)) # 120
print(round(circle_area(2), 2)) # 12.57(保留两位小数)
完成标准:每个函数都能独立调用、输出符合预期;再自己给函数库加一个「求列表最大值」的函数(提示:函数内可以用循环,也可以直接用 max)。
小知识:
calc(price, rate=0.1)里用round(price * (1 + rate), 2)保留两位小数——计算机用二进制存小数,0.1这种小数无法精确表示,所以100 * 1.1的真实结果是110.00000000000001,用round(..., 2)结果才和日常计算一致,写金额计算都建议这么做。
今日总结
函数是什么:def 定义、函数名() 调用,定义不执行、调用才执行;函数本身也是「值」,可以赋值给变量
函数的底层:def 把函数体打包成对象,调用时才创建独立执行空间(栈帧)
参数:形参是占位符、实参是真实数据,按位置一一对应,个数必须一致
返回值:print 只显示、return 真递出;return 一执行函数立即结束;没写 return 返回 None;return a, b 返回元组可解包
默认参数:写在普通参数后面,不传有默认值;可变默认值(列表/字典)必须用 None 代替
作用域:局部变量函数外访问不到;函数内读取全局没问题,修改全局要 global,最好用参数 + return 代替
完成数学工具函数库,至少含 5 个函数并通过自测
| 报错 | 原因 | 修复 |
|---|---|---|
SyntaxError: invalid syntax | def 行尾忘了冒号,或函数名/参数名有非法字符 | 检查 def 函数名(参数): 的冒号和命名 |
IndentationError: expected an indented block | 函数体忘了缩进 | def 下一行统一缩进 4 个空格 |
NameError: name 'x' is not defined | 在函数外访问函数内的局部变量 | 需要结果就用 return 传出来,或把变量定义在函数外 |
TypeError: add() missing 1 required positional argument: 'b' | 调用时少传了一个参数 | 检查调用是否与定义参数个数一致 |
UnboundLocalError: local variable 'count' referenced before assignment | 函数里给全局变量赋值但没声明 global | 在函数内先写 global 变量名 |