函数
这一课你将能做到
- 用 def 定义带参数、带返回值、带默认值的函数,并区分位置参数与关键字参数
- 读懂并会写 *args / **kwargs,理解「可变参数」在什么场景用
- 说清局部变量与全局变量的规则,避免「函数里改了变量外面没生效」的困惑
为什么要学
写到第三课你一定发现了很多重复代码:算两次折扣要抄两遍、打印不同用户信息要复制粘贴。函数就是「给一段代码起名字、留几个口子」——写一次,到处调。而且 Python 标准库、第三方包、面试题里全是函数思维:不理解函数,后面类与对象、异常、模块全都悬空。
人话:函数像自动售货机——投币(传参数)、按键(选函数)、出货(拿返回值)。机器内部怎么转的你不用管,接口清楚就能用。自己写函数,就是造一台自己的售货机。
先建立图像
def greet(name): ← 定义:造机器(此时不执行内部代码)
return f"你好, {name}" ← return:出货口
greet("小明") ← 调用:投币启动
│
▼
┌──────────────────┐
│ name = "小明" │ ← 参数在函数内部就是普通变量
│ return "你好, 小明" │
└──────────────────┘
│
▼
'你好, 小明' ← 返回值交还给调用处
与 Java 方法对照:
// Java:类型写死在签名里
static int add(int a, int b) { return a + b; }
# Python:不声明类型,参数即变量
def add(a, b):
return a + b
跟着做
步骤 1:定义与调用最简单的函数
新建 func_demo.py:
def say_hi(): # 无参数
print("hi")
def add(a, b): # 两个参数
return a + b
say_hi()
result = add(3, 5) # 返回值接住
print(result) # 8
# 没写 return 的函数,返回值是 None
def no_return(x):
x * 2 # 忘了 return,计算结果被丢弃
print(no_return(21)) # None ← 常见 bug 来源
输出:
hi
8
None
步骤 2:默认参数与关键字参数
def make_coffee(size="中杯", sugar=0, milk=False):
parts = f"{size}杯"
if sugar:
parts += f",{sugar} 份糖"
if milk:
parts += ",加奶"
return parts
# 位置参数:按顺序对应
print(make_coffee("大杯", 2))
# 关键字参数:指名道姓,顺序随便
print(make_coffee(milk=True, size="小杯"))
# 混用:位置在前,关键字在后
print(make_coffee("大杯", milk=True))
输出:
大杯杯,2 份糖
小杯杯,加奶
大杯杯,加奶
默认参数让调用方「只传必要的」;关键字参数让调用处自文档化——make_coffee(milk=True) 一眼知道在干嘛。
步骤 3:可变默认参数的世纪大坑
def add_item(item, items=[]): # ← 错误示范:默认值是可变对象
items.append(item)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] ← 惊!默认列表被跨调用共享了
默认值在函数定义时只创建一次,不是每次调用都新建。正确写法:
def add_item_ok(item, items=None): # 标准解法:用 None 哨兵
if items is None:
items = []
items.append(item)
return items
print(add_item_ok(1)) # [1]
print(add_item_ok(2)) # [2] 每次都是新列表
步骤 4:*args 与 **kwargs
def demo(*args, **kwargs):
print("args:", args) # 元组,收住所有多余位置参数
print("kwargs:", kwargs) # 字典,收住所有多余关键字参数
demo(1, 2, 3, name="小明", age=18)
args: (1, 2, 3)
kwargs: {'name': '小明', 'age': 18}
def total(*nums):
result = 0
for n in nums: # nums 在函数内是元组
result += n
return result
print(total(1, 2)) # 3
print(total(1, 2, 3, 4, 5)) # 15
# 反过来:* 还能把序列「拆开」传给函数
def power(base, exp):
return base ** exp
pair = (2, 10)
print(power(*pair)) # 1024,等价于 power(2, 10)
步骤 5:返回多个值与文档字符串
def min_max(nums):
"""返回列表的最小值和最大值。
参数 nums: 数字列表
"""
return min(nums), max(nums) # 实际返回一个元组
low, high = min_max([3, 1, 4, 1, 5]) # 解包接收
print(low, high) # 1 5
print(min_max.__doc__) # 文档字符串会被存下来
1 5
返回列表的最小值和最大值。
参数 nums: 数字列表
「返回多个值」是假象——Python 只是把元组打包返回、调用处解包。这是 Python 函数特别顺手的一点。
步骤 6:作用域——函数内外是两个世界
count = 0 # 全局变量
def increase():
n = 100 # 局部变量:只在函数内存在
print("函数内 n =", n)
def try_read():
print("函数内读全局 count =", count) # 读全局:可以
increase()
try_read()
# print(n) # NameError: name 'n' is not defined
counter = 0
def bump():
global counter # 想在函数内修改全局变量,必须声明 global
counter += 1
bump()
print(counter) # 1
函数内 n = 100
函数内读全局 count = 0
1
规则:函数内可以读全局变量,不能直接改(改会创建同名局部变量);要改就 global 声明——但更好的做法是把值当参数传进去、用 return 传出来,global 尽量少用。
讲清楚要点
- def 是「定义」不是「执行」:函数体在调用时才跑。定义时参数名拼错了都不报错,调用那一刻才炸——所以函数写完必须实际调用测一次。
- 参数四种形态,顺序固定:位置参数 → 默认参数 →
*args → **kwargs。写成 def f(a, b=1, *args, **kwargs) 是标准结构。
- 默认参数只求值一次:默认值在 def 时创建。可变默认值(list/dict/set)会跨调用共享,一律用
None 哨兵代替。
- return 立刻结束函数:return 后面的代码不执行;没有 return 或光写
return,返回 None。函数要么「返回值」要么「产生副作用」,两者混着写是可读性大坑。
- 传参传的是「对象引用」:把列表传进函数并在函数内 append,外面的列表也会变(可变对象共享);把 int 传进去做加法,外面不变(不可变对象)。这不是「值传递 vs 引用传递」二选一,而是「引用的值传递」。
- 变量查找顺序 LEGB:Local(函数内)→ Enclosing(外层函数)→ Global(模块级)→ Built-in(内置)。局部遮蔽全局,全局遮蔽内置。
- lambda 是一行式小函数:
sorted(pairs, key=lambda p: p[1]) 里的 key 回调是它的主战场。复杂逻辑别硬塞 lambda,老实用 def 起名。
你可能会懵的地方
-
参数传「值」还是传「引用」?
都不是,是「传对象引用」。行为上:不可变对象(int/str/tuple)在函数内「改不动」外面的;可变对象(list/dict)在函数内原地修改会影响外面的。根源是第 3 课说的「变量是名字标签」。
-
什么时候用 global?
几乎可以永远不用。脚本级小程序里改一个配置标志可以接受;正式代码用参数传入、返回值带出,或者干脆封装成类。global 让函数行为依赖外部状态,测试和排错都变难。
-
* 到底几种意思?
三种:定义时 *args 收集多余位置参数;调用时 f(*mylist) 拆开序列逐个传;单独出现 def f(a, *, b) 表示 b 必须用关键字传(强制关键字参数)。本课掌握前两种即可。
-
函数名能当变量用吗?
能。函数是对象,f = add 之后 f(1, 2) 照样跑;函数还能当参数传给别的函数(如 sorted 的 key)、当返回值返回。这是第 10 课之前你能接触到的「一切皆对象」最直观的例子。
-
为什么我的函数改了列表,外面真的变了?这和 global 冲突吗?
不冲突。global 管的是「名字绑定」(counter += 1 是重新绑定名字);而 lst.append(x) 是「修改对象内容」,名字没变、对象变了,所以不需要 global。记住:重新绑定才要 global,原地修改不要。
错法对照
| 错法 | 问题 | 正解 |
|---|
def f(items=[]) | 可变默认值跨调用共享 | def f(items=None) + 函数内判 None |
| 函数里算了结果忘了 return | 调用方拿到 None | 明确 return 结果 |
def f(a=1, b) | 默认参数后不能跟必选参数,SyntaxError | 必选在前:def f(b, a=1) |
| 函数内直接给全局变量赋值 | 生成同名局部变量,外面不变 | 参数传入 + return 带出;确需修改用 global |
| 函数太长干五件事 | 不可测不可复用 | 一个函数一件事,超过 30 行考虑拆分 |
| 用 print 代替 return | 结果没法参与后续运算 | 函数返回值,print 留给调用方 |
卡点急救
- 调用报 TypeError: missing 1 required positional argument:必选参数没传。对照 def 签名逐个检查,默认参数可以不传,必选不行。
- 函数返回 None:忘了 return,或只写了
return;也可能是调用了「原地修改型」方法(如 .sort())当作返回值。检查函数体最后一行。
- 改了全局变量外面不生效:函数内赋值只动局部。要么 return 出去,要么
global 声明(慎用)。
- 默认列表/字典里「攒」了上次的数据:可变默认参数坑,改成 None 哨兵写法。
*args 打印出来是元组不是我要的数:*args 把多个参数收成元组是正常行为;遍历它或用 sum(args) 处理,别直接当单个值用。
面试常问
-
***args 和 kwargs 的区别与用途?
*args 把多余位置参数收成元组,**kwargs 把多余关键字参数收成字典;常用于写包装函数、透传参数(def wrapper(*args, **kwargs): return real(*args, **kwargs))。调用侧的 * / ** 则是反向解包。
-
Python 函数传参机制?
传对象引用(call by object reference)。函数拿到的是实参对象的引用;对可变对象原地修改会影响外部,对参数重新赋值(绑定新对象)不影响外部。可结合 list.append 与 int 赋值举例。
-
可变默认参数为什么危险?
默认值在函数定义时求值一次,存进 __defaults__,所有调用共享同一个对象;对它的原地修改会跨调用累积。解法是 None 哨兵。能用 f.__defaults__ 亲眼验证是加分项。
-
lambda 与 def 的区别?
lambda 是匿名单表达式函数,语法上只能放一个表达式(不能有语句、多行逻辑),返回表达式的值;def 可以有完整函数体和文档字符串。lambda 典型用途是 sorted/max/min 的 key、map/filter 回调。
-
什么是 LEGB 作用域规则?
变量查找顺序 Local → Enclosing → Global → Built-in。函数内赋值默认创建局部变量;读写全局需注意遮蔽;修改全局要 global、修改外层函数变量要 nonlocal。
自测三问
def f(a, b=2, *args, **kwargs),调用 f(1, 3, 4, x=9) 时 a、b、args、kwargs 分别是什么?
- 为什么默认参数永远不该写成
=[]?标准替代写法是什么?
- 函数内
lst.append(1) 能影响外部列表,count += 1 却影响不了外部整数,原因是什么?
小结一句话
函数 = 起名字的代码块:def 定义、参数开洞、return 出货;记住可变默认参数用 None、*args/**kwargs 收杂货、改全局要么 return 要么 global,就能放心造积木了。
延伸