ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python函数入门:从def到return、嵌套与拆包,一文拆解核心概念

Python函数入门:从def到return、嵌套与拆包,一文拆解核心概念 这个系列写到第三篇前两篇我们把环境折腾明白也把变量、数据类型、流程控制这些地基打了一遍。到了函数这一篇很多人的学习节奏会第一次慢下来——不是它有多难而是它太不像前面那些看见就能懂的语法了def、参数、return返回值、函数嵌套、拆包每一个词拉出来都能讲半天串在一起更是让新手原地转圈。更折磨的是好不容易敲完一段函数代码运行时终端突然冒出一行无法将pip识别为cmdlet、函数、脚本文件或可运行程序的名称这时候很多人会下意识怀疑是不是函数这个写法有问题其实这两者八竿子打不着。这篇文章就专门把函数这块拆开揉碎。我会从最基础的定义语法讲起接着把return返回值讲透然后涉及函数嵌套和Python拆包最后用一个把四个知识点串起来的综合小项目收尾。每段代码都直接给你每句解释都尽量说人话。适合刚学完循环和条件判断的零基础读者也适合被各种函数概念绕晕的初学者照着敲一遍。1. 函数还没开始学先把三座概念大山挪开1.1 Python函数和数学函数差着一个宇宙你中学学过的数学函数比如f(x) x²本质是输入一个数经过某种规则输出一个数强调的是映射关系。但Python函数不是这个意思至少不完全是。Python函数是一段有名字、可以反复调用的代码块它内部可以干任何事算一个结果、打印一句话、读写文件、发网络请求甚至什么都不干就占个位置。初学者最容易踩的第一个思维误区就是拿数学函数去硬套Python函数然后产生一连串疑问函数是不是必须有参数是不是必须有return答案都是否定的。你完全可以直接定义一个函数def hello(): print(hello world)这个函数没有参数也没有return但它合法。它做的事情就是打印一句话。这种过程式函数在数学里不存在在编程里却非常常见。对比项数学函数Python函数本质输入到输出的映射关系一段命名的可执行代码块是否有参数通常必须有可以有0个或多个是否必须有返回值必须有结果可以没有默认返回None内部能做什么只能计算一个结果可以做打印、文件操作、循环、条件等所有事我见过不少教程喜欢说函数就是数学函数在编程里的体现这话说对了一半但给零基础读者造成的困惑比帮助更大。你只要记住一句话Python函数就是一个工具把一段逻辑打包起来下次用名字就能调用它。1.2 终端里那条cmdlet、函数、脚本文件报错和Python函数毫无关系这里必须单独拎出来说因为它坑了太多人。Windows的PowerShell终端里当你输入pip、python、npm这类命令但系统找不到时会报错无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。注意这里的函数指的是PowerShell里的自定义函数是PowerShell命令行环境的概念和Python里写的def函数没有半点关系。这句话的完整意思是PowerShell在系统的命令搜索路径PATH环境变量里找遍了所有目录也没找到一个叫pip.exe或pip.bat的可执行文件所以它只能把这几个候选身份挨个列出来提醒你。也就是说遇到这个报错你去检查Python代码里的def语法纯属南辕北辙。问题出在环境变量配置上后面第7章我会专门给一套排查步骤。现在你只需要建立正确的认知命令行里的函数这个词只是PowerShell对命令的一种分类跟你写的Python函数是两套完全不同的体系。1.3 函数的价值给重复代码一个可复用的名字有人可能会问既然函数可以没有参数、没有返回值那它到底解决了什么问题一句话解决重复代码的问题。比如你现在要统计五个学生的成绩等级如果不用函数你得把同一段if/elif/else复制五遍每份只是分数不同。万一判断标准变了——比如及格线从60改成65——你就得去改五处漏改一处就是bug。这就是典型的复制粘贴式编程的代价。函数化之后判断成绩的逻辑只写一次def score_to_grade(score): if score 60: return 及格 return 不及格以后不管多少个学生调用score_to_grade(分数)就行。改判断标准只需要改这一个函数。这种把逻辑集中起来、通过名字反复调用的能力是函数最核心的价值也是后面封装、模块化、面向对象的起点。2. 基本语法def定义、参数匹配、变量作用域一次讲透2.1 定义一个函数的最小规则一个函数由几部分组成def关键字、函数名、圆括号、参数列表、冒号、缩进的函数体。函数名的命名有讲究只能由字母、数字、下划线组成不能以数字开头不能跟Python内置关键字冲突比如不能叫if、for、class习惯上用全小写字母加下划线也就是所谓的snake_case风格。def calculate_average(scores): # 函数体从这里开始必须缩进 total sum(scores) avg total / len(scores) return avg这里有一个很多人会犯的低级错误def这一行结尾的冒号忘了写或者函数体忘了缩进。Python用缩进表示代码块的归属关系函数体内部的所有代码必须比def所在行多一层缩进而且整个函数内缩进数量要一致。函数体里的第一行如果是一个字符串它会被当成函数的文档字符串docstring用来描述这个函数是干什么的。虽然不是强制要求但我建议你养成习惯写上def calculate_average(scores): 计算列表的平均值返回浮点数结果。 total sum(scores) return total / len(scores)这个字符串在IDE里悬停函数名时能看到也是自动生成文档的基础后面写代码查文档都会用上。2.2 参数匹配的三个规则位置、关键字、默认值定义函数时圆括号里的参数叫形式参数简称形参。调用函数时真正传进去的值叫实际参数简称实参。形参和实参的匹配有三种方式新手最容易踩的是第一种。位置参数是最直观的调用时按顺序传def introduce(name, age, city): print(f我叫{name}, 今年{age}岁, 来自{city}) introduce(张三, 25, 北京)如果顺序写错比如introduce(张三, 北京, 25)输出的内容就完全不对了而且Python不会报错——因为它不知道你的语义。这是位置参数最大的风险代码能跑结果离谱。关键字参数可以解决这个问题调用时写上参数名introduce(name张三, city北京, age25)用关键字参数传参时顺序已经无所谓了因为每个值都明确绑定到了对应的形参名。可读性也更强。默认参数是在定义函数时给某个参数直接赋值def introduce(name, age18, city北京): print(f我叫{name}, 今年{age}岁, 来自{city})调用introduce(李四)时age和city会自动用默认值。但这里有一个硬性语法规则默认参数必须放在非默认参数的后面。你写def introduce(name张三, age)会直接报SyntaxError因为Python不知道该按什么顺序匹配参数。我个人的建议如果一个函数有超过三个参数调用时一律用关键字参数能省掉后面排错的大量时间。2.3 函数内部的变量与外部变量默认是两套系统很多初学者学到这里开始懵为什么我在函数里给变量赋值外面还是原值total 100 def add_ten(x): total x 10 # 这里的total是函数内部的局部变量 return total print(add_ten(20)) # 30 print(total) # 100全局变量没变原因很简单函数内部赋值的变量默认都是局部变量它只在函数执行期间存在函数结束后就没了。函数外部的total是全局变量两者只是碰巧名字一样实际上是两个不同的变量。如果确实想在函数里修改全局变量需要用global关键字声明但我不建议零基础阶段这么干。用global去改外部状态会让函数的调用结果变得不可预测它依赖函数外面的环境这在工程上是个坏味道。更干净的做法是函数里算好结果用return把结果交出来在外面重新赋值。2.4 动手练把一段成绩判断逻辑函数化学语法最快的方式是写一个具体的东西。假设你要判断一个分数属于哪个等级不写函数是这样的score 85 if score 90: grade 优 elif score 80: grade 良 elif score 60: grade 及格 else: grade 不及格 print(f{score}分 - {grade})这段代码逻辑没问题但每次要判断一个分数都得复制一遍。改成函数def score_to_grade(score): 根据分数返回成绩等级。 if score 90: return 优 elif score 80: return 良 elif score 60: return 及格 return 不及格 print(score_to_grade(85)) print(score_to_grade(42))注意我最后一个分支没有写else直接return因为前面的if和elif把所有情况都覆盖了代码会更简洁。函数化之后成绩判断逻辑只存在一份。将来要调整分数线只需要改这一个函数体不用满项目去找if score 。3. return返回值交付一个结果和打印一个结果完全是两码事3.1 return到底在返回什么对象引用不是复制品先看一个很简单的代码def make_list(): data [1, 2, 3] return data result make_list() result.append(99) print(result) # [1, 2, 3, 99]result拿到的是make_list内部那个data列表的引用本质上是同一个对象只是换了名字。你用result去修改列表改的也是data指向的那同一个列表对象。这里可以把它类比成给同一个人贴了两个不同标签data和result是两张标签贴的都是同一个列表对象。搞清这一点后面学可变对象、浅拷贝、深拷贝时才不会发懵。函数返回一个对象交出去的是这个对象的引用地址不是把里面的数据重新复制一份。3.2 没写return的函数统统返回None这是一个会被无数教程带过的重点但我要强调它因为很多写代码的人工作了几年还会在这里翻车。Python有一个隐式规定函数如果没有return语句或者执行到return但没有给值那么这个函数调用后会返回None。def say_hi(): print(hi) result say_hi() print(result) # None print(result is None) # True你可能会想这个函数明明打印了hi怎么赋值给result之后变成None了原因就是print只是把内容输出到屏幕上它不是函数的结果。函数真正的产出物必须通过return明确交出去。没有returnPython就默认交给你一个None——表示这个函数什么都不返回。3.3 return和print的分工结果交付和屏幕展示是两码事我见过不少初学者把print当return用写出这样的代码def add(a, b): print(a b) # 只是打印没有return result add(3, 5) print(result) # None想拿到加法结果去做后续计算结果拿到的是None。正确写法是def add(a, b): return a b result add(3, 5) print(result) # 8这里的分工原则值得现在就说清楚计算和展示要分离。函数内部负责算出结果用return交付至于要不要打印、在哪里打印是调用方的事情。这样函数才具备可复用性——同一个函数算出来的结果既能在控制台打印也能存入文件还能参与别的运算。3.4 return多个值与提前退出两个很有用的行为Python允许在return后面跟多个值用逗号分隔def get_min_max(numbers): return min(numbers), max(numbers) low, high get_min_max([3, 1, 4, 1, 5]) print(low, high) # 1 5这里return min(numbers), max(numbers)实际上返回的是一个元组(min值, max值)然后在接收时用low, high ...这样一行代码把元组里的两个元素分别赋给了两个变量。这种写法已经踩到了拆包的门槛第5章我会专门展开讲。return还有一个容易被忽略的作用提前终止函数。函数执行到return那一行无论后面还有多少代码都会立即结束并返回。利用这个特性可以做早退式的判断def check_number(n): if n 0: return 负数 if n 0: return 零 return 正数这种写法比层层嵌套的if/else清晰得多。还有更实用的场景在循环里找到第一个符合条件的元素立即return不用把整个列表跑完def find_first_even(numbers): for n in numbers: if n % 2 0: return n return None print(find_first_even([1, 3, 5, 8, 9])) # 84. 函数嵌套作用域链与闭包的起点4.1 函数里面再写函数合法且常见函数嵌套指的是在一个函数内部再定义另一个函数def outer(a, b): def inner(x): return x * 2 return inner(a) inner(b) print(outer(3, 4)) # 14看着好像只是把代码换个位置但这里出现了一个新东西内层函数inner是在outer的函数体里定义的它对外部来说是不可见的。你没有直接调用inner的途径只有outer内部能使用它。为什么要这么做核心理由就是封装内部细节。如果某一段辅助逻辑只在当前函数里使用没必要暴露给外界把它定义为内层函数可以避免污染外部命名空间也让外层函数的调用者只看一个入口就行。4.2 内层函数找变量的顺序局部找完往外层找嵌套函数引入了作用域的概念。Python约定了一套查找变量的顺序简单记就是局部优先逐层向外当前函数内部的局部变量找不到就去外层函数里找还找不到就去模块全局找最后去找Python的内置名字。可以用一个经典例子感受x 全局 def outer(): x 外层 def inner(): x 内层 print(x) inner() outer() # 内层把inner函数体里的x 内层这一行注释掉再运行输出会变成外层再把outer里的x 外层也注释掉输出会变成全局。这个递进关系就是作用域链——内层函数在寻找一个变量时是从内往外一层层问的谁先给出答案就用谁。顺带说一句如果在inner里给x赋值了那这个x就是inner的局部变量和外层outer里的x没有关系——还是前面讲过的变量隔离原则嵌套函数并没有打破它。4.3 嵌套函数干粗活把辅助逻辑藏在内层实战里嵌套函数最常见的场景就是辅助处理。比如我要统计一组数据的方差计算方差时需要先算每个数与平均数的偏差这个偏差列表只在方差计算过程中用到没必要暴露在外面def variance(numbers): def deviations(): avg sum(numbers) / len(numbers) return [n - avg for n in numbers] avg sum(numbers) / len(numbers) return sum(d ** 2 for d in deviations()) / len(numbers) print(variance([85, 92, 78])) # 约32.67deviations这个函数被藏在variance内部外部永远看不到这个名字。好处是如果以后方差算法调整只需要改variance函数内部不会误伤别的地方。4.4 从嵌套到闭包内层函数记住外层变量的秘密嵌套函数如果再往前走一步就遇到了一个概念叫闭包它长得像这样def make_multiplier(factor): def multiply(x): return x * factor return multiply double make_multiplier(2) print(double(5)) # 10 print(double(7)) # 14这里的double是一个函数它的函数体来自内层multiply但它又神奇地记住了外层factor2这个值。外层make_multiplier早就执行完了按道理它内部的变量应该被销毁了可multiply仍然可以使用factor。这个现象之所以成立就是因为multiply这个内层函数在定义时把外层作用域里的变量绑定在了自己身上一起带走了。这种函数带上外层数据一起走的结构就是闭包。零基础阶段你只需要知道这个现象存在就行不必急着深究。等后面学到装饰器、回调函数时你会发现很多魔法都建立在闭包之上。5. 拆包Python里让人上瘾的赋值技巧专治多个返回值5.1 元组拆包等号左边写多个变量就行拆包unpacking是Python里一个非常顺手的小技巧意思是把一个容器里的元素一次性取出来分别赋给多个变量。最简单的形式a, b 10, 20 print(a, b) # 10 20这行代码里右边(10, 20)其实是一个元组等号左边的a, b负责把元组里的两个元素按顺序取出来。一个经常被拿出来当炫技的用法是交换两个变量a, b b, a不用临时变量也不用三条赋值语句一行搞定。它的原理就是先把右边的(b, a)构造成一个临时元组然后拆包赋值给左边的a和b。这个技巧和函数的多个返回值是天作之合。第3章那个get_min_max返回的元组接收时直接拆包low, high get_min_max([3, 1, 4, 1, 5])除了元组函数返回列表、字符串甚至任何可迭代对象都可以用相同方式拆包。5.2 列表、字符串也都能拆但要注意长度匹配拆包不局限于元组任何可迭代对象都可以colors [red, green, blue] r, g, b colors print(r, g, b) # red green blue s1, s2, s3 abc print(s1, s2, s3) # a b c这种写法在处理固定长度的数据时很清爽比如坐标点(x, y)、RGB颜色(255, 128, 0)、日期(2024, 01, 15)。但有一个硬性要求变量数量必须和元素数量完全一致。a, b [1, 2, 3] # ValueError: too many values to unpack报错信息是too many values to unpack或者not enough values to unpack。这是拆包最常见的报错新手遇到先检查两边的数量是否对上了。5.3 调用函数时用*和**把容器展开成参数拆包还有一个方向很多人容易和上面的赋值拆包搞混但逻辑是相通的。当你调用一个函数时可以用星号*把一个列表或元组展开成位置参数def introduce(name, age, city): print(f{name}, {age}岁, 来自{city}) info [张三, 25, 北京] introduce(*info)这里*info相当于把列表里的三个元素按顺序展开成了introduce(张三, 25, 北京)。同样字典可以用双星号**展开成关键字参数info_dict {name: 李四, age: 30, city: 上海} introduce(**info_dict)**info_dict会把字典里的键值对转成关键字参数传进去等价于introduce(name李四, age30, city上海)。注意字典的键必须和函数形参名一致否则会报位置参数冲突。这个用法在实际工程里很常见比如把配置dict直接传给函数。5.4 星号表达式多余的元素交给一个变量兜底Python 3引入的扩展拆包让多余元素的处理优雅了很多first, *middle, last [1, 2, 3, 4, 5] print(first) # 1 print(middle) # [2, 3, 4] print(last) # 5带星号的变量会把剩下的所有元素收集成一个列表。如果你只关心一头一尾中间是什么样无所谓这个写法的可读性极好。还有一个配套习惯刻意用一个下划线忽略不需要的值。username, _, status [admin, 临时占位, online]这里的_只是个普通变量名按照惯例表示我不关心这个位置的元素算是一种代码自注释手法看到的人会自然忽略它。6. 综合小项目成绩统计工具把四个知识点拧成一股绳6.1 需求场景与目标现在把本章学的四个知识点——基本函数、return返回值、函数嵌套、拆包——全部放进一个真实场景里串一遍。假设你手头有一份学生成绩列表需要做三件事计算平均分、找出最高分和最低分、给每个分数打等级。6.2 第一版不写函数全部线性堆出来scores [85, 92, 67, 58, 74] avg sum(scores) / len(scores) high max(scores) low min(scores) grades [] for s in scores: if s 90: grades.append(优) elif s 80: grades.append(良) elif s 60: grades.append(及格) else: grades.append(不及格) print(avg, high, low, grades)这段代码能跑但问题一眼就能看出来如果再来一份班级成绩整块代码要全部复制一遍。而且平均分的计算逻辑、成绩等级的判断逻辑全混在一起别人读的时候得一行行看完才知道在干什么。6.3 第二版函数化让逻辑可以复用把重复可能最高的等级判断先抽成独立函数再把整体统计逻辑也包进一个函数def score_to_grade(score): if score 90: return 优 elif score 80: return 良 elif score 60: return 及格 return 不及格 def summarize(scores): avg sum(scores) / len(scores) high max(scores) low min(scores) grades [score_to_grade(s) for s in scores] return avg, high, low, grades avg, high, low, grades summarize([85, 92, 67, 58, 74]) print(f平均分: {avg}, 最高: {high}, 最低: {low}) print(f等级列表: {grades})对比一下就看出区别了summarize函数只接收一个成绩列表返回四个结果。调用处用拆包一次性接收。以后还想统计其他班级一行代码复用现有的逻辑。score_to_grade是独立的也可以单独测试、单独修改。6.4 第三版嵌套拆包把封装再往前推一步如果score_to_grade只在summarize内部用到暂时不用暴露出去就可以把它定义成内层函数完成函数嵌套的封装def summarize(scores): 统计平均分、最高分、最低分以及每个分数的等级。 def score_to_grade(score): if score 90: return 优 elif score 80: return 良 elif score 60: return 及格 return 不及格 avg sum(scores) / len(scores) high max(scores) low min(scores) grades [score_to_grade(s) for s in scores] return avg, high, low, grades avg, high, low, grades summarize([85, 92, 67, 58, 74]) print(f平均分: {avg:.1f}, 最高: {high}, 最低: {low}) print(等级:, grades)现在外部调用者只面对summarize一个入口拿到的结果通过拆包变得一目了然。如果你后续希望在别处单独使用score_to_grade那就把它移出来放到顶层作为公开函数如果它只是内部辅助放在内层就是最合适的位置。这个哪里用就放哪里、不随便暴露的取舍能力其实已经是一种设计思维了。6.5 再扩展一点字典拆包在函数调用中的应用综合项目里的拆包主要用在了接收返回值这一侧。反过来在传参这一侧也有呼应。比如现在要打印某班平均分def show_avg(name, avg): print(f{name}的平均分是{avg}) class_info {name: 一班, avg: 78.5} show_avg(**class_info) # 一班平均分78.5 的格式化输出这只是演示。真正项目里的常见场景是你从一个配置文件或接口拿到一个字典字段名和函数形参正好对应那**class_info一行就能把参数全部展开传进去省去手动写name...、avg...的重复劳动。拆包的两种方向配合起来整个数据传递过程会非常顺滑。7. 学习过程中跑不掉的报错以及我的排查套路7.1 无法将xxx识别为cmdlet、函数、脚本文件或可运行程序的名称到底怎么解决回到开头那个把无数人卡住的报错。它在Windows PowerShell里出现一般有几种情形你可以按顺序排查Python安装时没勾选Add Python to PATH。安装包默认有这个选项很多人图快直接点了Install Now没注意底部那一行小字。解决方案是重新运行安装包选Modify把Add Python to PATH勾上再继续。安装后没有重新打开终端。PATH环境变量是在打开终端时加载的如果你在安装Python之前就开着那个终端窗口它会一直使用旧的环境变量。关掉终端重新开一个新的很多时候问题就解决了。安装位置比较特殊。如果你手动指定过安装目录可能需要手动把Python安装目录和它的Scripts子目录加入系统环境变量Path。路径一般长这样C:\Users\用户名\AppData\Local\Programs\Python\Python311\Scripts目录在它的下面。操作路径是Windows设置 → 系统 → 关于 → 高级系统设置 → 环境变量 → 在用户变量的Path中新增这两条路径然后一路确定。改完记得重启终端。这个报错跟def函数的语法没有关系别再怀疑自己代码写错了。7.2 缩进错误函数体写飞了IndentationError: expected an indented block是另一个高频报错。典型原因是def那一行写完后函数体第一行没有缩进或者某一行不自觉地回退到了顶格。Python用缩进判断代码块归属一旦缩进乱了整个函数结构就散了。我的建议是把编辑器配置成用空格代替Tab全项目统一用4个空格缩进。如果你手头代码已经缩进混乱可以在IDE里全选代码用Tab或ShiftTab批量调整缩进层级。7.3 TypeError: 参数数量对不上调用函数时参数个数和定义时不匹配Python会直接抛TypeError。常见的两种报错文案missing 2 required positional arguments调用时少传了两个参数。unexpected keyword argument xxx传了一个函数定义里不存在的关键字参数。排查思路很简单先看报错信息的最后一行它通常会给出函数名和实参列表然后回过头对照def那一行数一数形参数量检查有没有拼错参数名。IDE的自动补全这时就很有用——它会在你敲括号时提示函数签名照着填就不会少。7.4 忘了return拿到一堆None写函数时忘记写return或者把return写成了print调用后的结果就是None。排查这类问题的通用套路是在调用处打印结果并用type()确认类型。result get_something() print(type(result))如果输出是class NoneType说明函数没有把结果交出来。这时回到函数定义处检查所有分支是否都覆盖了return。尤其是函数里有if/else结构时容易漏掉某个分支的返回语句让函数走到最后隐式返回了None。一个我个人复现问题的经验是遇到诡异结果先别猜直接在交互式环境里拆出来一步步验证——先测纯函数再测嵌套函数再测拆包接收。因为我踩过太多次问题出在数据没传对而不是逻辑算错的坑了。把怀疑对象逐层缩小是解决一切报错最朴素也最有效的办法。
返回列表