第16章-函数定义与参数


先说说你多半已经遇到的麻烦:程序里有一段代码要重复用好几遍,于是复制粘贴,粘贴完想改个地方,得挨个改好几处,漏改一处就是 bug。函数就是专门治这个的——把一段代码打包、起个名字,以后想用就叫它的名字,Python 会把那段代码从头跑一遍。就像微波炉,你不需要每次重新造一个加热的炉子,按一下"加热"按钮就行。

这一周后面要做文件读写、异常处理、学生信息管理系统,全靠函数把代码组织起来,所以今天的内容是后面所有章节的地基。我们从最基础的问题开始:怎么定义一个函数,怎么叫它干活。

定义函数:把代码打包起名字

定义函数用 def 开头,后面跟函数名和括号,再跟一个冒号;函数体要缩进 4 个空格,和 iffor 里的代码块一个规矩。调用函数就是写函数名加括号:函数名()

先看这段,跑一下试试——

# 定义函数:def + 函数名 + 括号 + 冒号,函数体缩进
def greet():
    print("你好,Python!")

# 调用函数:函数名 + 括号
greet()
greet()
greet()

输出长这样:

你好,Python!
你好,Python!
你好,Python!

函数体缩进不缩进,结果完全不同:不缩进会报 IndentationError(缩进错误),这是今天最常踩的坑,报错了就检查函数体是不是比 def 行多缩进了一截。

一个完整的小例子,把"欢迎语 + 分隔线"打包起来重复用:

def show_banner():
    print("=" * 20)
    print("欢迎来到学习系统")
    print("=" * 20)

show_banner()   # 调用一次
show_banner()   # 想用几次调用几次

输出:

====================
欢迎来到学习系统
====================
====================
欢迎来到学习系统
====================

两个容易犯的错先说在前面,报错了别慌,对号入座:

  • 调用时忘了写括号,写成 greet——不报错,但也不会执行,那只是"指向函数本身"。这个坑很隐蔽,因为程序安静地跑完了,什么都没发生。
  • 调用写在定义前面——Python 从上往下读代码,还没见过 greet 就去调它,报 NameError。所以有条铁规矩:先定义,再调用,定义永远放在调用之前。

有人会问:这函数只会喊同一句口号,也太死板了吧。别急,参数就是让它活起来的办法。

参数:给函数"递东西"

def greet(name) 里的 name形参(形式上的参数,占个位置);调用时写的 greet("小明") 里的 "小明"实参(实际的参数,真实的数据)。函数拿到实参后,函数体里就能用 name 这个名字了。

可以这么记:形参是插座上的孔,实参是插进去的插头。孔告诉别人"这里能插什么形状",插头提供真实的电。函数可以有多个参数,用逗号隔开,调用时按顺序把值传进去——这叫位置参数(按位置一一对应)。

# 形参:name 是一个"占位"的名字
def greet(name):
    print(f"你好,{name}!")

# 实参:调用时把真实的值传进去
greet("小明")
greet("小红")
greet("小刚")

输出:

你好,小明!
你好,小红!
你好,小刚!

多个参数时按顺序对应:第一个实参给第一个形参,第二个给第二个,以此类推。

def introduce(name, city, age):
    print(f"我叫{name},来自{city},今年{age}岁。")

introduce("小明", "北京", 18)
introduce("小红", "上海", 19)

输出:

我叫小明,来自北京,今年18岁。
我叫小红,来自上海,今年19岁。

这里有两个坑:

  • 参数个数对不上:定义了 3 个形参只传 2 个实参,报 TypeError: introduce() missing 1 required positional argument(缺少参数)。报错了就数一数括号里的逗号。
  • 顺序搞反introduce("北京", "小明", 18) 会把城市和姓名弄混。程序不会报错,你自己读一遍 f-string 输出的内容就能发现。

还有个小提醒:参数名和函数外面的变量重名没关系,参数是函数内部的局部名字,不用故意改名避开。

return:把结果"还"给你

有的函数只管做事(比如打印),有的函数要算出一个结果给你用。算结果靠 return:一碰到 return,函数立刻结束,把它后面的值送回调用处,你可以用变量接住。

def add(a, b):
    result = a + b
    return result        # 把结果"还"给调用处

total = add(3, 5)        # 用变量接住返回值
print(total)             # 8
print(total * 2)         # 返回值还能继续参与计算

输出:

8
16

判断什么时候用 return,有个简单的标准:调用处需不需要这个值继续算?需要就用 return,只是打印就用 print。下面这个对比能帮你彻底分清两件事:

def with_return(a, b):
    return a + b

def no_return(a, b):
    print(a + b)

x = with_return(3, 5)
y = no_return(3, 5)
print("x 的值:", x)    # 8
print("y 的值:", y)    # None!因为函数没有 return

输出:

8
x 的值 8
y 的值 None

注意 yNone——没写 return 的函数,调用完拿到的是 None(什么都没有)。这是新手最容易懵的地方:明明函数里打印了 8,怎么变量是 None?因为打印是打印,返回是返回,两回事。return 结果 后面的代码不会执行,函数已经"下班"了。

和 return 有关的三个细节:

  • 忘写 return:函数里只 printreturn,调用处拿到 None,后面拿它一运算就报 TypeError
  • return 后面还有代码:那些代码永远不执行,Python 不会提醒你,写的时候自己留意。
  • return 只执行一次:函数里第一个被碰到的 return 生效;后面的 return 只有在前面的条件没触发时才有机会走到。

默认参数:不传就用默认值

有些参数大多数时候是同一个值,只有偶尔要换。这时候给它设个默认参数:定义时在形参后面写 参数名=默认值,调用时不传就用默认值,传了就按你传的来。好处是调用更省事,而且以后加新功能不破坏老代码。

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

greet("小明")              # 没传 word,用默认值"你好"
greet("小红", "早上好")      # 传了 word,用"早上好"
greet("小刚", "晚上好")

输出:

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

再看一个贴近生活的例子——计算打折价:

def discount_price(price, off=0.8):
    return price * off

print(discount_price(100))       # 默认打 8 折:80.0
print(discount_price(100, 0.5))  # 传 0.5,打 5 折:50.0

输出:

80.0
50.0

默认参数有三个讲究:

  • 位置不能放错:默认参数必须写在所有普通参数的后面。def f(a, b=1) 没问题,def f(a=1, b) 直接报语法错误 SyntaxError: non-default argument follows default argument
  • 默认值别用列表/字典def f(items=[]) 这种写法有坑——列表会被所有调用共享,新手阶段先记住别这么写;需要可变默认值就写 None,函数里再初始化。
  • "没传"和"传了 None"是两回事greet("小明")greet("小明", None) 不一样,后者会把 word 真的变成 None

文档字符串:给函数写说明书

代码是给人看的。函数多了以后,连自己都容易忘这个函数是干嘛的。def 下面第一行用三引号 """...""" 写的说明叫文档字符串(docstring),相当于挂在函数上的说明书:说明这个函数干什么、参数是什么、返回什么。三引号内容不执行,只是说明。

def add(a, b):
    """计算两个数的和。

    参数:
        a: 第一个数
        b: 第二个数
    返回:
        两数之和
    """
    return a + b

print(add(3, 5))

# 用 help() 查看函数的"说明书"
help(add)

输出:

8
Help on function add in module __main__:

add(a, b)
    计算两个数的和

    参数
        a: 第一个数
        b: 第二个数
    返回
        两数之和

加文档字符串不是必须的,但养成习惯有三个好处:自己回头看得懂;让 AI 帮你改代码时它读得懂;后面学到 help() 可以直接查看。

写的时候注意三点:一两句话讲清楚"干嘛的"就行,别写成小作文;# 注释是给读代码的人看的解释,"""...""" 文档字符串是挂在函数上、能被 help() 查到的说明书,两者不是一回事;改完代码记得同步改说明,注释和代码对不上,比没有注释更坑。

随堂练习

  1. 练习一(比大小):写一个函数 get_max(a, b),返回两个数中较大的那个,调用 3 次验证结果。 提示:先用 if 判断谁大,再用 return 返回;调用时用变量接住再打印。

  2. 练习二(判断奇偶):写一个函数 is_even(n),判断一个数是不是偶数,是偶数返回 True,否则返回 False,调用验证。 提示:偶数的标志是 n % 2 == 0;打印返回值看看是 True 还是 False。

  3. 练习三(改造计算器,必做):把第 2 周写过的"计算器"(加减乘除四则运算)改造成函数版:写 add(a, b)sub(a, b)mul(a, b)div(a, b) 四个函数,主程序只负责输入数字和调用函数打印结果。 提示:改造前先把旧代码找出来;主程序用 if 判断运算符,然后调用对应函数。

  4. 进阶题(乘法表函数):写一个函数 multi_table(n),打印到 n 的九九乘法表(n=9 就是完整的九九乘法表)。 提示:要用 for 嵌套循环;格式用 f"{i}x{j}={i*j}";先写 9 乘 9,再把 9 换成参数 n

写不出来?让 AI 给思路

这两段提示词今天就能用上,卡住的时候直接复制过去——

场景:写函数卡壳、不知道返回值怎么设计

提示词:
我是 Python 初学者,今天学了函数。请帮我完成下面这个练习:
写一个函数 get_max(a, b),返回两个数中较大的那个。
请先给我思路(不要直接给完整代码),用大白话解释形参和实参
的区别,然后我再自己写,写不出来你再给代码。代码要简单、加注释。
场景:改造旧代码为函数版本

提示词:
我有一段计算器代码(加、减、乘、除),现在想改造成用函数
封装:add(a, b)、sub(a, b)、mul(a, b)、div(a, b),主程序只负责
输入和调用。请帮我看这个改造思路对不对,并告诉我哪些地方
容易写错。我是初学者,请用大白话解释。

常见报错速查

今天遇到的报错基本就这几类,对号入座:

报错信息 什么意思 怎么办
IndentationError: expected an indented block 函数体没有缩进 函数体比 def 行多缩进 4 个空格
NameError: name 'greet' is not defined 函数名拼错,或调用写在定义前面 检查拼写;把调用移到定义后面
TypeError: greet() missing 1 required positional argument: 'name' 调用时少传了参数 数一数定义了几个参数,调用时传齐
TypeError: unsupported operand type(s) for +: 'int' and 'str' 返回值类型不对(比如把字符串当数字加) 检查传入的参数类型,用 int() / float() 转换
调用后结果一直是 None 函数里没写 return 需要结果就给函数加 return 值
SyntaxError: non-default argument follows default argument 默认参数写在了普通参数前面 默认参数统一放函数参数列表最后

收个尾

今天的东西说穿了就一句话:函数 = 把重复代码打包起名字,参数让它灵活,return 让它能交结果。你的程序从"一条一条往下念的长卷纸"变成"一抽屉一个的收纳盒",每个功能一格,想用哪格拿哪格。

写代码前先问自己一句:这段逻辑会不会用到第二次?会,就写成函数——第 6 周的项目全靠这个习惯撑起来。明天学 Python 自带的"工具箱"——标准库,你会见识到怎么让程序"随机掷骰子、随机点名"(random)、算数学(math)、看时间(datetime)。有了函数的基础,标准库的用法基本是一看就懂。

临走前想一想:def greet(name, word="你好") 这种写法,为什么默认参数必须放在所有普通参数的后面?