前言
刚开始学 Python,最容易被两个问题卡住:一是「为什么要把代码写成函数」,二是「函数里的变量到底归谁管」。前者关系到代码能不能复用和维护,后者关系到你会不会在某个深夜突然收到NameError或UnboundLocalError。这篇入门篇不追求面面俱到,只把函数最基本的三件事讲透:怎么定义、怎么传参、怎么拿返回值。
常见的误解有三类。第一类是把函数当成「压缩代码行数的工具」,觉得几行代码直接写就行,何必包成函数——其实函数的价值在复用和命名,不在省行数。第二类是以为「函数里能随便改外面的变量」,结果一赋值就报错。第三类是带着 Python 2 的习惯写print x,在 Python 3 里直接语法报错。
顺便把版本讲清楚:Python 2.7 已于 2020 年 1 月 1 日停止维护。Python 2 里print是语句,可以写print "hello";Python 3 里print是函数,必须写print("hello")。同理 Python 2 的raw_input在 3 里改叫input,dict.iteritems()在 3 里已移除,要用dict.items()。本文一律用 Python 3 的写法。
一、函数是什么:给一段代码起名字
把完成某件事的代码写成一个函数,就是给这段逻辑起了个名字,以后用名字调用它即可:
# 适用于 Python 3.8+
def say_hello():
print("你好,欢迎学习 Python 函数")
say_hello()
say_hello()这里的def是 define 的缩写。say_hello是函数名,()是参数表(这里为空),冒号后面缩进的部分是函数体。调用时写函数名加圆括号。
函数带来的直接好处是:逻辑只写一次,想用几次就调用几次;以后要改行为,只改函数体一处。这比到处复制粘贴可靠得多。
二、让函数接收数据:参数
没有参数的函数只能做固定的事。要让函数处理不同的数据,就在参数表里声明参数(parameter),调用时把数据(实参,argument)传进去:
# 适用于 Python 3.8+
def greet(name):
print(f"你好,{name}")
greet("小明") # 你好,小明
greet("小红") # 你好,小红函数可以接收多个参数,按位置一一对应:
# 适用于 Python 3.8+
def introduce(name, age):
print(f"{name} 今年 {age} 岁")
introduce("小明", 18) # 小明 今年 18 岁如果参数有合理的缺省情况,可以给默认值。默认值让调用方可以少传:
# 适用于 Python 3.8+
def greet(name, greeting="你好"):
print(f"{greeting},{name}")
greet("小明") # 你好,小明
greet("小明", "早上好") # 早上好,小明
greet("小明", greeting="嗨") # 嗨,小明调用时既可以按位置传,也可以写成名字=值的形式(关键字参数)。但关键字参数必须写在位置参数后面。
一个必须记住的点:默认值只在函数定义时求值一次。如果默认值是可变的,比如列表或字典,就会出现「上一次调用的残留」。正确做法是用None当哨兵:
# 适用于 Python 3.8+
def add_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
print(add_item("a")) # ['a']
print(add_item("b")) # ['b'],而不是 ['a', 'b']三、拿回结果:return 与 None
函数做完事,可以用return把结果交回给调用方:
# 适用于 Python 3.8+
def add(a, b):
return a + b
total = add(3, 5)
print(total) # 8return会立即结束函数,后面的代码不再执行:
# 适用于 Python 3.8+
def check(n):
if n < 0:
return "负数"
return "非负数"
print(check(-1)) # 负数
print(check(5)) # 非负数如果函数没有return,或者写了光秃秃的return,它返回的是None。这是新手最容易忽略的地方:
# 适用于 Python 3.8+
def show(msg):
print(msg)
value = show("一条消息") # 打印:一条消息
print(value) # None所以像lst.sort()这种原地排序的方法返回的是None,千万不要写result = lst.sort()。想让「排序后得到新列表」,用内置函数sorted():
# 适用于 Python 3.8+
data = [3, 1, 2]
new = sorted(data)
print(new) # [1, 2, 3]
print(data) # [3, 1, 2],原列表没变return想一次给出多个结果时,把它们用逗号隔开,实际上会打包成元组,接收方可以用多个变量接:
# 适用于 Python 3.8+
def divmod_like(a, b):
return a // b, a % b
q, r = divmod_like(7, 3)
print(q, r) # 2 1(顺便说,标准库的内置函数divmod(a, b)做的就是这件事,返回(a // b, a % b)。)
四、作用域:函数里的名字归谁管
函数体是一个新的作用域。函数内部可以读外层的变量,但一旦在里面给某个名字赋值,这个同名变量就会被当成函数的局部变量:
# 适用于 Python 3.8+
name = "全局"
def show():
print(name) # 读取外层变量,没问题
show() # 全局# 适用于 Python 3.8+
count = 0
def bump():
count = count + 1 # 这里 count 被当作局部变量,自引用时报错
# bump() # UnboundLocalError: local variable 'count' referenced before assignment要在函数里修改全局变量,必须用global声明;要修改外层函数的变量,用nonlocal:
# 适用于 Python 3.8+
count = 0
def bump():
global count
count += 1
bump()
bump()
print(count) # 2新手记住一句话就够了:函数里看到赋值,默认就当局部变量。想动外面的,先声明。
实战:一个完整可运行的小程序
下面把定义、参数、默认值、返回值、作用域揉进一个小例子,可以直接复制进hello_functions.py运行。
# 适用于 Python 3.8+
def bmi(weight, height, /):
"""根据体重(千克)和身高(米)计算 BMI 指数。"""
if height <= 0:
return None
return weight / (height ** 2)
def grade(value, *, thresholds=(18.5, 24.0, 28.0)):
"""把 BMI 数值映射为中文档位,只接受关键字参数。"""
if value is None:
return "输入无效"
low, mid, high = thresholds
if value < low:
return "偏瘦"
if value < mid:
return "正常"
if value < high:
return "偏胖"
return "肥胖"
def report(weight, height):
value = bmi(weight, height)
if value is None:
print("身高必须大于 0")
return
print(f"BMI = {value:.1f},档位:{grade(value)}")
if __name__ == "__main__":
report(70, 1.75)
report(50, 1.60)
report(70, 0)逐段说明:
def bmi(weight, height, /)里的/表示这两个参数是仅位置参数(Python 3.8 起支持),只能按位置传,不能写bmi(weight=70, height=1.75)。def grade(value, *, thresholds=...)里的*表示后面的thresholds是仅关键字参数,调用时只能写thresholds=...。- 两个函数都有明确的返回;
bmi在非法输入时返回None,grade对None单独处理。 report用f"..."格式化,{value:.1f}表示保留一位小数。
常见坑点
- 忘了调用,只写了函数名
❌def work(): ...之后写work—— 只是引用了函数对象,什么都不会执行。 ✅ 写work(),带上圆括号才是调用。
- 可变默认参数
❌def add(item, bucket=[]): bucket.append(item); return bucket—— 默认列表被所有调用共享。 ✅def add(item, bucket=None): bucket = [] if bucket is None else bucket; ...
- 把
None当有效返回值用
❌if find_user(uid) > 0:—— 找不到时函数返回None,和数字比较会TypeError。 ✅ 先判断result = find_user(uid),再写if result is None: ...。
- 误以为
sort()返回新列表
❌result = data.sort()——result是None,data已被原地排序。 ✅ 要新列表用result = sorted(data);要原地改就用data.sort()且不接收返回值。
- 函数内赋值忘了
global/nonlocal
❌ 直接count += 1想改全局count,结果UnboundLocalError。 ✅ 先global count(外层函数则用nonlocal count),再count += 1。
- 关键字参数写在位置参数前面
❌greet(name="小明", "早上好")—— 位置参数跟在关键字参数后,SyntaxError。 ✅greet("小明", "早上好")或greet("小明", greeting="早上好")。
- 拿 Python 2 的语法写 Python 3
❌print "你好"、raw_input()、d.iteritems()—— 前者是 Python 2 语句,后两个在 Python 3 里已改名或移除。 ✅print("你好")、input()、d.items()。
总结
| 概念 | 一句话要点 |
|---|
| 定义 | def 名字(参数):加缩进函数体 |
| 调用 | 函数名加圆括号,别漏括号 |
| 参数 | 位置参数、关键字参数、默认值;默认值定义时求值一次 |
| 可变默认值 | 换成None哨兵,调用内新建 |
| 返回值 | 无return返回None;多值返回是元组 |
| 作用域 | 函数内赋值默认为局部;改外层要global/nonlocal |
入门阶段把这几条理顺,写函数基本不会出大问题:先把「参数怎么进、返回值怎么出」想清楚,再记住「默认值只算一次」和「内部赋值即局部」这两条作用域规律。剩下的,等对函数更熟了,再去接触*args、**kwargs、闭包和装饰器这些进阶玩法。