多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

编程入门:一文讲透函数定义、参数、返回值与常见坑

编程入门:一文讲透函数定义、参数、返回值与常见坑 函数这个话题几乎是每一个学编程的人绕不开的第一道坎。有的人觉得函数难是因为被函数这两个字吓住了总觉得这是某种高深的数学概念。其实在编程世界里函数就是一个把你经常要做的某件事打包好、随取随用的工具。我自己一开始学的时候也被各种术语绕晕过后来带过几个零基础的伙伴发现只要把函数的来龙去脉讲清楚上手真的不难。这篇文章我就用最直白的话把函数的定义、用法和一些容易踩的坑一次讲透。1. 先搞清楚函数到底解决了什么问题1.1 没有函数的时候代码有多痛苦我见过太多新手写代码前几行还好好的越往后越崩溃。比如老师布置一个作业输入三个学生的成绩分别计算他们的平均分。很多人的第一反应是复制粘贴三遍score1 80 score2 90 score3 85 avg1 (score1 score2 score3) / 3 print(第一位学生的平均分, avg1) score4 70 score5 75 score6 92 avg2 (score4 score5 score6) / 3 print(第二位学生的平均分, avg2)这套代码没问题能跑通。但问题紧接着就来了如果老师把要求改成去掉一个最高分再去掉一个最低分再算平均你就得把这段逻辑在每一处都改一遍。改三处还行改三十处、三百处呢改漏一处结果就全错了。重复代码是程序里最隐蔽的定时炸弹。表面上看起来只是麻烦一点实际上每一次复制粘贴都在积累技术债。这个痛点就是函数存在的根本原因。1.2 函数是什么一个装了流程的盒子打个比方函数就像一家街边的早餐店。你不需要知道老板每天几点去进货、怎么和面、用什么火候蒸包子你只需要站在窗口说一句来两个肉包老板就把包子递给你。这个过程里来两个肉包就是一次函数调用老板的后厨操作就是函数体递出来的包子就是返回值。换成专业一点的话函数就是一段事先定义好的、有名字的、可以被反复调用的代码块。它把怎么做的藏在内部把用起来怎么样暴露给外部。定义一次到处调用——这就是函数最核心的价值。1.3 函数带来的三个核心好处第一个好处叫复用。同样一段逻辑不用一遍遍复制写一遍以后每次用一行代码调用就行。第二个好处叫组织。比较长的程序如果全写在主流程里读起来就像一本没有目录的书用函数把每个功能切成独立的模块代码结构会清晰得多。第三个好处叫维护。需求一旦变更只需要改函数内部所有调用这个函数的地方都会自动生效不会出现改了这处忘了那处的情况。2. 函数的定义光会说还不够得动手写2.1 一个最基础的函数长什么样我们拿 Python 举例因为它是目前最友好的一门语言。定义一个函数的语法非常简单def greet(): print(你好欢迎来到这里)def是关键字告诉计算机我要定义一个函数了。greet是这个函数的名字将来调用它就用这个名字。括号()用来放参数这里暂时没有参数所以是空的。冒号:表示后面开始的是函数体。缩进的print(...)就是函数真正要做的事情。有一点特别重要也是新手最容易忽略的定义函数的时候函数体里的代码并不会立刻执行。你写了上面这三行程序跑起来什么都不会发生你屏幕上不会出现你好。因为定义只是告诉计算机有这么个函数存在真正要让函数做事还得单独调用它。2.2 调用让函数动起来的那一步调用函数的格式是函数名 括号greet()这一行代码执行后控制台才会打印出你好欢迎来到这里。定义和调用的区别就好比你查字典字典里写着苹果是一种水果这不算用你把这句话背下来或者引用出来才算真正用了这个词。函数也一样定义是写进字典调用才是翻字典。2.3 函数的组成部分拆解一个完整的函数通常由四个部分组成函数名、参数、函数体、返回值。函数名是门牌号参数是入口函数体是核心加工区返回值是出口。其中参数和返回值不是强制要求一个函数可以一个参数都没有也可以什么都不返回但从设计角度讲一个完全不和外界交互的函数使用价值通常非常有限。2.4 写函数名也是有规矩的函数命名这件事看似小事实际上特别影响代码的可读性。我在实际带项目的时候最怕看到a()、func1()、test()这种名字三天之后连写代码的人自己都看不懂。命名的时候记住三个原则用动词开头calc_avg、get_user_name、send_email一眼就能看出这个函数是干嘛的。用下划线分隔多个单词Python 的惯例是get_total_score而不是getTotalScore。避开关键字比如不能用def、if、for这些 Python 已经占用的词当函数名。3. 参数函数接收数据的入口3.1 为什么要参数函数不能活在真空里如果一个函数只能打印一句固定的话那它和复制粘贴没太大区别。真正让函数变得强大的是它能接受外部传进来的数据然后根据这些数据给出不同的结果。参数就是函数和外界之间的桥梁。继续用早餐店打比方老板蒸包子的流程是固定的但不同的顾客要的包子种类不同、数量不同。这些信息就是参数。你喊来两个肉包两个和肉包就是传给后厨的参数。3.2 形参和实参的区别这是新手最容易混淆的一组概念但其实特别简单def greet(name): # name 是形参 print(你好, name) greet(张三) # 张三 是实参形参name是函数定义时括号里的变量它像一个占位符代表将来会有人从这里传入值。实参张三是调用函数时真正传入的那个数据。函数执行的时候实参的值会被塞进形参变量里然后函数体里就可以使用它了。你可以这样记形参是菜谱上写的取一个鸡蛋实参是你从冰箱里真正拿出来的那个鸡蛋。菜谱本身不变但每次用的鸡蛋都不一定一样。3.3 多个参数和参数顺序函数可以接收多个参数比如def calculate_rectangle_area(length, width): area length * width print(面积为, area)调用的时候calculate_rectangle_area(5, 3)Python 会按顺序把 5 传给length把 3 传给width。参数的顺序是有讲究的你把实参的位置换一下算出来的结果可能就错了。不过后来 Python 提供了关键字参数的写法调用时可以直接指定参数名calculate_rectangle_area(width3, length5)这样写的好处是不用记参数的位置了代码读起来还特别清楚。在参数数量比较多的时候我强烈建议用这种写法能避免很多低级错误。3.4 默认参数值让调用者省点心有些参数大部分时候都有固定取值偶尔才需要变一变这种情况就可以给参数设置默认值def greet(name, prefix你好): print(prefix, name)有了默认值之后调用greet(张三)输出的就是你好 张三调用greet(张三, 欢迎)输出的就是欢迎 张三。默认参数要放在参数列表的最后面否则会报语法错误。这个规则其实很好理解如果前面某个参数没传Python 分不清你传的值到底想给哪个参数。3.5 数量不确定时可用可变参数有时候函数的参数数量是不固定的。比如写一个计算平均分的函数有时候传 3 个分数有时候传 5 个分数。Python 提供了*args这种可变参数语法def calc_avg(*scores): total sum(scores) count len(scores) return total / count print(calc_avg(80, 90, 85)) # 三个参数 print(calc_avg(70, 75, 92, 88)) # 四个参数这里的*scores会把所有传入的参数打包成一个元组函数内部按元组处理就行。这个方法在写工具类函数的时候特别实用。4. 返回值函数向外传递结果的出口4.1 return 语句的用法和作用参数是函数接收数据返回值是函数产出数据。比如计算面积如果你只在函数里print结果那这个函数只能看没法把结果拿去做下一步操作。真正有用的函数应该把结果通过return交出去def calculate_rectangle_area(length, width): area length * width return area result calculate_rectangle_area(5, 3) print(面积是, result)这里result这个变量就拿到了函数计算的结果。你可以把这个结果继续传给其他函数或者参与更多的运算。一个函数从只会打印到能返回结果是从初学者变成合格开发者的一道分水岭。4.2 return 和 print 千万别混为一谈这个坑几乎每个新手都踩过而且踩了还不自知。看看下面这段代码def add(a, b): print(a b) result add(3, 5) print(结果是, result)你猜第三行会打印什么答案是结果是 None。第一行确实打印了8但result拿到的是None因为函数没有用return返回任何东西。print只是把信息显示到屏幕上return才是真正把结果交给外部。我见过太多人在这里纠结半天为什么我在函数里打印明明没问题拿回来用就不行了原因就在这。想把函数的结果存起来或带走就必须用return而不是print。4.3 返回多个值其实很简单Python 有个非常方便的特性一个函数可以同时返回多个值。语法上只需要用逗号分隔def get_min_max(nums): return min(nums), max(nums) low, high get_min_max([3, 1, 4, 1, 5]) print(最小值, low, 最大值, high)看起来像是返回了两个值其实 Python 在底层把它们打包成了一个元组(1, 5)然后用low, high ...这个写法自动拆开赋值。这种写法在写数据处理类函数时经常用到比如一个函数既要返回总数又要返回平均值。4.4 返回值提前结束函数return还有一个隐藏作用函数一旦执行到return函数体的剩余部分就不会再执行了。这个特性可以用来做提前退出让代码逻辑更清楚def check_score(score): if score 0: return 分数不合法 if score 100: return 分数不合法 return 分数合法这个函数执行到第一个return时直接结束不会继续往下判断。这种写法避免了层层嵌套的if-else读起来更舒服。5. 作用域每个函数都有自己的小天地5.1 局部变量和全局变量的区别这也是一个绕不开的概念。函数内部定义的变量默认是局部变量只能在函数内部使用函数外部定义的变量是全局变量理论上在程序的任何地方都能访问。看这段代码total 100 # 全局变量 def change(): total 0 # 这里的 total 是局部变量和外面的 total 没关系 print(函数内部, total) change() print(函数外部, total)输出结果是函数内部0和函数外部100。函数内部的total 0并没有改变全局变量total的值它只是创建了一个新的局部变量。这一点相当反直觉新手经常在这里翻车。5.2 什么时候需要 global 关键字小心使用global。如果你确实想要在函数内部修改全局变量Python 要求你提前声明total 100 def change(): global total total 0 change() print(全局 total, total) # 现在是 0 了但是我在实际开发中会尽量减少用global。全局变量就像公司里的公共仓库每个人都往里放东西、拿东西一旦某个环节出问题你很难查清楚是谁把仓库搞乱的。更好的做法是函数通过参数接收数据通过返回值输出结果不碰任何外部变量。这样每个函数都是独立的个体测试和排查问题都简单得多。5.3 变量的查找顺序Python 在函数内使用一个变量时会按照局部 → 嵌套函数外层 → 全局 → 内置的顺序去查找。这个规则通常被叫做 LEGB 规则。对于初学者来说不需要死记这四个字母只需要理解函数内部能访问全局变量但尽量不要修改全局变量。如果你的代码里出现在函数里改了变量却发现外面没变的情况第一反应就该检查是不是撞上了作用域的墙。6. 实操上手写自己的第一个函数6.1 从需求到函数关键四步很多新手卡在我不知道怎么开始写函数。其实你不需要一开始就设计一个很复杂的函数。按下面四步走基本不会跑偏第一步找出重复的代码。找那种复制粘贴过的段落或者逻辑相似只是数据不同的一整块。第二步确定输入和输出。想一想这段代码需要哪些数据才能运行处理完之后要产生什么结果输入就是参数输出就是返回值。第三步给函数起名字。用动词开头描述它做的事情。第四步把代码搬进函数体。把原来写死的数字和变量替换成参数名最后用return把结果交出去。6.2 一个完整的演练求三个数的平均分假设你现在有这么一个需求老师给了三个学生各三门课的成绩你要分别算平均分。按照上面四步来走。重复的代码是这样的avg1 (80 90 85) / 3 avg2 (70 75 92) / 3 avg3 (88 62 79) / 3输入是三个分数输出是它们的平均值。函数名可以叫calc_avg。于是有了def calc_avg(a, b, c): total a b c result total / 3 return result然后调用print(第一位学生平均分, calc_avg(80, 90, 85)) print(第二位学生平均分, calc_avg(70, 75, 92)) print(第三位学生平均分, calc_avg(88, 62, 79))代码变得更加整齐了。如果老师后来改了规则要求去掉最高分再去掉最低分你只需要改calc_avg函数内部三行调用代码一行都不用动。这就是函数维护价值的直观体现。6.3 试着增加一点复杂度BMI 计算器下面这个例子会用到之前学的多个知识点建议你自己动手敲一遍。需求是写一个函数输入身高和体重输出 BMI 值和对应的体重状态。def bmi_calculator(height_m, weight_kg): bmi weight_kg / (height_m ** 2) if bmi 18.5: category 偏瘦 elif bmi 24: category 正常 elif bmi 28: category 偏胖 else: category 肥胖 return bmi, category bmi, category bmi_calculator(1.75, 65) print(fBMI{bmi:.1f}状态{category})这个函数使用了多个参数、分支判断、多个返回值。写好之后想算谁就算谁几行代码搞定。到这里你会发现函数不是锦上添花而是把逻辑梳理清楚的基本工具。7. 常见错误与排查经验7.1 函数没有调用代码白写最常见的问题是定义了函数但忘了调用它结果屏幕上什么也没输出。很多新手第一反应是我的代码是不是有问题其实只是少了函数名()这一行。排查方法很简单确认定义处缩进正确确认文件末尾有单独的调用语句。7.2 NameError找不到这个名字如果你看到NameError: name xxx is not defined通常是这三种情况之一函数名拼写错了、函数确实没定义过、函数定义在调用语句的后面且没有提前声明。Python 是逐行执行的如果先调用后定义解释器执行到哪一行还不知道后面有什么所以会报错。7.3 参数数量不匹配调用函数时传的参数个数和定义时不一致会报TypeError: xxx() takes 2 positional arguments but 3 were given。这个报错信息其实已经把问题说得很明白了函数定义需要 2 个参数但你给了 3 个。解决方法是数一数定义时括号里的参数个数再数一数调用时括号里的个数。7.4 函数内部改了全局变量却无效这个之前提过根源是作用域的规则。如果你真的在函数内部修改外部变量请明确用global声明但我更建议你把修改的值通过return返回出来然后重新赋值给外部变量。下面这两种方式我更推荐第二种count 0 # 不推荐的写法 def add_one(): global count count 1 # 推荐写法 def add_one_v2(num): return num 1 count add_one_v2(count)7.5 return 位置写错导致函数提前结束有时候函数莫名其妙只走了一半就停了往往是因为return放在了不该放的位置。比如你希望在循环结束后返回统计结果却不小心把return写进了循环体内部导致第一次循环就退出了。排查时先检查return的缩进位置再检查逻辑分支是否有提前 exit 的可能。7.6 常见错误速查表错误类型典型表现排查思路未调用函数定义了但无输出检查是否有调用语句拼写错误NameError核对函数名字母是否一致参数数量不匹配TypeError对照定义和调用的参数个数缺少 return结果为 None检查函数体是否有 returnreturn 提前函数执行不完整检查 return 的缩进和位置全局变量被改外部值不变检查函数内部是否执行了赋值8. 提高函数质量的几个习惯8.1 一个函数只做一件事如果一个函数既读了文件、又做了计算、又打印结果、又发了通知那这个函数一旦出问题你很难快速定位。把大函数拆成小函数并不丢人反而是项目代码质量的重要标志。我习惯的判断标准是如果函数的名字里出现和、并且这种字眼大概率就能拆成两个函数。8.2 命名能读成一句话好的函数名读起来像一句简洁的指令get_user_by_id、send_verification_code、calculate_order_total。函数名不需要写注释本身就应该说明它做什么。如果你发现自己很难给一个函数起名字往往是这个函数做的事情不够纯粹又是一个该拆分的信号。8.3 让函数内部少依赖外部状态一个理想的函数应该是给同样的输入永远得到同样的输出。这样最好测试也最好理解。如果函数内部大量依赖全局变量那它运行的结果就不确定排查起来非常痛苦。写函数的时候把数据通过参数传进去把结果通过 return 传出来这条规则适用于绝大多数场景。8.4 先能跑再重构有些初学者一上来就纠结我要不要用函数导致代码写不下去。我的建议是先把功能写出来哪怕是一团乱麻的代码只要它能跑后面再慢慢把它收拾进函数。第一步是让程序正确第二步才是让程序优雅。等你真正体会过重复代码带来的修改痛苦你自然就会主动拆函数了。8.5 实用的练手题想巩固函数操作的话可以试试下面几个经典练习写一个判断闰年的函数、写一个字符串反转函数、写一个求斐波那契数列第 n 项的函数、写一个判断回文数的函数。这些题目都不复杂但能很好地训练参数设计、返回值设计和边界条件处理。我个人在实际学习过程中的体会是函数的理解分两个阶段第一阶段是会用能照着模板写出函数并调用第二阶段是愿意拆看到重复代码就想把它抽成函数。第二阶段建立起来之后你再看别人写的代码思路会完全不一样——你会发现代码不再是平铺直叙的执行步骤而是一个个可以独立使用的零部件组装起来的成品。这个视角的变化基本就是编程思维开始成形的时候。希望这篇文章能帮你走好这第一步。
返回列表