上一篇把一行表格记录变成了字典,你已经能把一整张工资表装进程序里。但装进去之后会冒出另一个问题:处理每一个人的那几行算式,还是一遍遍抄的。这一篇解决的就是“同一段代码反复出现”——把它打包成一个有名字的东西,也就是函数。
先看看不打包会怎样
假设算加班费的规矩是:加班小时数 × 时薪 × 1.5 倍。公司只有三个人加班:
1时薪 = 50
2
3print(f"张三:{2 * 时薪 * 1.5} 元")
4print(f"李四:{4 * 时薪 * 1.5} 元")
5print(f"王五:{3 * 时薪 * 1.5} 元")三个人还能接受,可它埋了两个麻烦。
第一个麻烦是 改一处要改多处。下个月公司把倍率从 1.5 调成 2,你得把每行的 1.5 都找出来改掉。三个人是三处,表格里三百个人就是三百处;只要漏掉一行,那个人的工资就悄悄算错了,而且不会报错。
第二个麻烦是这段算式 换不了地方用。别的脚本也要算加班费,你只能整段复制过去,从此两份代码各自修改、慢慢分家。
真正要的其实很朴素:给这段算式 起个名字,写一次,以后报名字就行。
def:给一段代码起名字
Python 里起名字的关键字是 def,是 define(定义)的缩写:
1def 算加班费(小时, 时薪):
2 倍数 = 1.5
3 return 小时 * 时薪 * 倍数一行行读:
def开头,告诉 Python“我要定义一个函数”。- 紧跟着是 函数名,你自己起,规则和变量名一样。这里叫
算加班费。 - 名字后面是 圆括号,括号里写这个函数需要外面提供什么数据。这里要两样:加班多少小时、时薪多少。
- 行末的 冒号,和你写
if、for时一样,表示“下面缩进的那几行归属于它”。 - 下面 缩进的那几行 是这个函数的全部内容,叫函数体。前面
if、for学过的缩进规则原样适用。
定义完之后,屏幕上 什么都不会发生——这是零基础最容易愣住的地方。def 做的事只是把这几行代码存起来,并在 算加班费 这个名字下挂一份。它没有执行。
要它真的跑,得再写一行 调用:
1print(算加班费(2, 50)) # 150.0区别只在一对括号:算加班费 是那个名字本身,算加班费(2, 50) 才是“现在把这段代码跑一遍,拿 2 和 50 当输入”。名字后面带括号,才是干活。
参数:定义时留空位,调用时填数据
回头看定义和调用的对应关系:
1def 算加班费(小时, 时薪): # 定义:留两个空位
2 ...
3
4算加班费(2, 50) # 调用:往空位里填 2 和 50定义时括号里写的 小时、时薪,叫 参数(parameter)。它们本身不是数据,而是两个 预留的空位,也可以理解成你在函数体里给“从外面进来的东西”预先起好了名字。调用时括号里填的 2、50,叫 实参(argument),是真正送进去的值。
两边怎么对上?按位置一一对应:第 1 个值给第 1 个参数,第 2 个给第 2 个。所以 2 落到 小时,50 落到 时薪。
这条规矩意味着调用顺序不能随手换。看一个顺序真的会影响的例子:
1def 算差价(原价, 现价):
2 return 原价 - 现价
3
4print(算差价(100, 80)) # 20
5print(算差价(80, 100)) # -20同样两个数,只是换了位置,结果就从正变负。Python 不会替你判断哪个数“应该”是哪一项,它只认位置。更隐蔽的是,如果函数里全是相乘,传反了甚至得到同一个数(2 × 50 和 50 × 2 一样),你根本看不出错。
return:把结果交回去
现在到了函数真正的关键一步。先看一个 看起来也能用 的版本:
1def 算加班费(小时, 时薪):
2 print(小时 * 时薪 * 1.5)
3
4结果 = 算加班费(2, 50) # 屏幕上出现 150.0
5print(结果) # None屏幕上明明打印了 150.0,可 结果 却是 None。原因在于 print 和 return 干的是两件不同的事:
print把值送到屏幕上,只给人看,程序本身拿不到这个值。return把值 交回给那一行调用它的代码,让程序能接着用。
Python 里每个函数都会交出一个值。你没写 return,它就默认交出 None(表示“没有值”)。所以上面那个函数“做了事,但没交出东西”,结果 自然是 None。要是接着写 总计 = 8000 + 结果,就会报 TypeError——等于拿数字去加一个空。
改成 return 就顺了。函数执行到 return 那一行,就把值交出去并立刻结束,return 后面的代码不会再跑:
1def 算加班费(小时, 时薪):
2 return 小时 * 时薪 * 1.5
3
4结果 = 算加班费(2, 50)
5print(f"加班费 {结果} 元,本月总计 {8000 + 结果} 元")这就是“返回值”的价值:它不只是显示出来,而是变成了一个 能继续参与计算、能存进变量、能传给别的函数 的东西。
打包之后,脚本为什么变短
把上一篇的字典列表和这一篇的函数接起来看,才能看清“变短”是什么意思:
1员工们 = [
2 {"姓名": "张三", "加班小时": 2},
3 {"姓名": "李四", "加班小时": 4},
4 {"姓名": "王五", "加班小时": 3},
5]
6
7def 算加班费(小时, 时薪):
8 return 小时 * 时薪 * 1.5
9
10for 员工 in 员工们:
11 金额 = 算加班费(员工["加班小时"], 50)
12 print(f"{员工['姓名']}:{金额} 元")注意几点:
- 循环体里看不到
1.5了。算式被收进函数里,只写了一次。 - 循环每转一圈,就把这一行的加班小时数通过参数送进函数,函数把算好的金额交回来,装进
金额。 - 表格从三个人变成三百个人,代码长度 一点不变;倍率从 1.5 改成 2,也只需要动
return那一行。
这才是函数让脚本变短的机制:不是少打几个字,而是 把“同一件事”的定义只留下一处。一处定义,处处调用,改规则只改那一处,不会漏。
两条边界,先记住
一,函数里的变量外面看不见。 上面 倍数 = 1.5 是在函数体里用 = 造出来的,它在函数内部存在,出了函数就没有了:
1print(倍数)
2# NameError: name '倍数' is not defined这不是缺陷,恰恰是打包的好处:函数内部自己用的临时名字不会和外面的名字打架,所以你可以放心复用。外面要用的东西,得写在参数里送进去;里面算出来的东西要让外面用,得 return 出来。这条边界也顺便解释了“打包”这个词——括号里进,return 出,中间的细节外面不必关心。
二,def 要写在调用之前。 脚本是从上往下执行的,def 是“把名字挂上去”的那一步;名字还没挂上就调用,会报 NameError。所以习惯上把函数定义写在文件靠上的位置,主流程放下面。
等以后参数多到三四个,Python 还有一种按名字传的写法(算加班费(小时=2, 时薪=50)),能避开位置顺序的麻烦,那是下一篇要讲的内容。