Python函数入门:从def到装饰器,搞懂核心逻辑与常见坑

发布时间:2026/9/20 3:11:24
Python函数入门:从def到装饰器,搞懂核心逻辑与常见坑 函数这玩意儿学Python的朋友早晚都得正面碰它。不管你是刚装好Python准备跑第一行print(hello)还是已经被各种教程里的def、return绕晕了这期内容都值得你花十分钟慢慢看。我当年带新手踩坑的时候发现大部分人卡在函数上不是因为语法复杂而是没弄明白函数到底在解决什么问题。所以这篇文章我不打算只给你抄语法而是把函数拆开讲清楚它背后的逻辑、常见的易错点再用实际例子一步步带你把代码跑通。1. 为什么写代码一定要学会拆函数1.1 没有函数的世界是什么样的先想象一下不写函数代码会变成什么样。比如你想计算一个三角形的面积公式是底乘高除以2一次两次直接写公式没问题# 计算一个三角形的面积 base 10 height 4 area base * height / 2 print(area) # 输出 20.0但问题是你的程序里如果到处都需要算三角形面积每次就得复制粘贴这行公式。今天还好明天你要把面积结果保留成整数就得满项目搜索这行公式一处一处改。要是漏了一处程序就浮现一个隐藏bug等用户用到时候才崩溃那时候排查起来才叫酸爽。这还只是一个小公式真实项目里一段逻辑可能有几十行复制粘贴之后代码会迅速膨胀、臃肿改一处动全身。很多初学者学了一两个月感觉代码还能跑但项目一大就完全驾驭不住根本原因就是没有学会用函数去组织代码。1.2 函数就是给代码块贴了一张“使用说明书”函数的核心价值是把一段实现具体功能的代码封装成一个可以反复调用的模块。你只需要给它输入——也就是参数它就返回结果给你。至于它内部怎么实现调用方可以完全不关心。上面三角形的例子用函数来写就是这样的def triangle_area(base, height): 计算三角形面积 return base * height / 2 print(triangle_area(10, 4)) # 输出 20.0 print(triangle_area(5, 8)) # 输出 20.0这样就实现了三件事复用同样的逻辑不用复制粘贴需要就调用一次。隔离每个函数只管自己那一摊事出了问题去对应函数里排查而不是在几百行代码里大海捞针。抽象看代码的人不需要先理解三角形面积公式看一眼函数名就知道它干什么。这个思路放大了看就是编程领域的核心思想之一分解问题。把大问题拆成小问题小问题再拆成更小的函数每个函数都清晰明确最后组合起来整个项目就变得可控了。2. Python函数的定义、调用与基础参数详解2.1 def关键字与函数定义结构Python里定义一个函数用def关键字后面跟函数名、括号、冒号缩进的代码块就是函数体。def 函数名(参数1, 参数2, ...): 文档字符串 函数体 return 返回值有几个新手特别容易忽略的点我一个个说第一函数体必须缩进。这跟if、for一样Python是靠缩进区分代码层级的。缩进不一致或者干脆忘了缩进程序直接报错IndentationError。我见过很多新手卡在这一步其实养成习惯就好——统一用四个空格别混用Tab。第二没有return的函数会返回None。这是Python语言的特性。很多人在函数里print了一下以为这个函数“返回”了结果其实它只是输出了内容返回值是空值None。比如def say_hello(name): print(f你好{name}) result say_hello(张三) print(result) # 输出 None这里result拿到的是None因为函数里压根没有return语句。你想用返回值去参与计算就会踩坑。所以如果函数需要出结果一定要显式return。第三函数名要能说明用途。不要用func1、func2这种。我见过有些人写代码函数名全都叫handle_data到了调试的时候根本不知道是哪个函数出的问题。名字起得清楚代码就等于有了一半的注释。2.2 位置参数与关键字参数调用函数传参有两大类方式这是新手经常搞混的地方。先看位置参数。调用时按定义顺序传入第一参数对应第一个形参def introduce(name, age, city): print(f我叫{name}今年{age}岁来自{city}) introduce(李四, 25, 上海)这样调用位置固定如果顺序写错语义就全错了。比如introduce(李四, 上海, 25)年龄和城市就反了。再看关键字参数。调用时通过形参名值的方式传入这样就不用关心顺序introduce(city广州, age30, name王五)关键字参数的一个重要优势是提高代码可读性。你看到调用处写age30不用去翻函数定义就知道这个30是年龄。两种方式还可以混着用但有个硬性规则位置参数要在关键字参数之前。这么写是合法的introduce(赵六, city北京, age28)但这么写是错的Python会直接报SyntaxErrorintroduce(city北京, 赵六, 28) # 错误这个规则其实就是防止语义矛盾如果位置参数跟在关键字参数后面解释器不知道该把“赵六”塞给哪个形参。2.3 默认参数值的使用与一个大坑定义函数时可以给参数一个默认值调用时这个参数可以不传def greet(name, greeting你好): print(f{greeting}{name}) greet(小明) # 输出 你好小明 greet(小红, 早上好) # 输出 早上好小红默认参数的价值在于给常用的调用方式提供便利。比如日志系统里的levelINFO、分页接口里的page1都是默认参数的典型应用。但默认参数有一个绝大多数人都踩过的坑默认参数不要用可变对象比如列表、字典。什么意思看这个例子def add_item(item, items[]): items.append(item) return items print(add_item(苹果)) # [苹果] print(add_item(香蕉)) # [苹果, 香蕉]而不是 [香蕉]第二次调用返回了[苹果, 香蕉]items列表里残留了第一次调用的结果。这是因为默认参数[]在函数定义的时候就被创建了每次调用这个函数用的都是同一个列表对象。正确做法是使用不可变默认值比如None然后在函数内部自己创建新列表def add_item(item, itemsNone): if items is None: items [] items.append(item) return items这个陷阱在面试里出现频率很高但更重要的是在工作中真的会绕到。我当年写一个批量处理脚本就是个漏了恢复默认列表的bug排查到怀疑人生最后发现是默认参数在作祟。所以这一点一定要刻在脑子里。3. 参数进阶可变参数与解包操作3.1 *args任意个数的位置参数实际问题里你常常不确定一个函数到底会接收几个参数。比如写一个求和函数用户可能传入两个数也可能传入十个。用*args可以接收任意数量的位置参数它在函数内部是一个元组tupledef my_sum(*args): total 0 for num in args: total num return total print(my_sum(1, 2, 3)) # 6 print(my_sum(1, 2, 3, 4, 5)) # 15我拆开讲一下*args这个写法星号*是语法关键args只是约定俗成的名字你换成*numbers也能用。它的原理是在函数调用时Python把所有多余的位置参数打包成一个元组丢给args这个变量。有些同学可能好奇为什么要有*args直接传一个列表不就行了当然可以传列表但*args让调用方更自然my_sum(1, 2, 3)直接传多个数就行不用先拼好[1, 2, 3]再传。这在设计灵活接口的时候非常有用。3.2 **kwargs任意个数的关键字参数跟*args对应的是**kwargs它负责接收任意数量的关键字参数在函数内部是一个字典dictdef print_info(**kwargs): for key, value in kwargs.items(): print(f{key}: {value}) print_info(name小明, age18, city上海)这段代码输出name: 小明 age: 18 city: 上海**kwargs常用于需要接收大量配置项的场景。比如一个绘图函数用户可能想自定义颜色、线宽、标记样式你不可能把所有参数都写死在函数定义里用**kwargs接收然后在内部统一处理既灵活又优雅。3.3 参数解包调用时反过来用星号前面说的*args和**kwargs是在函数定义时的打包操作。反过来在函数调用时也可以给一个列表、元组或字典前面加上星号让它们“解包”成多个参数。def introduce(name, age, city): print(f{name}, {age}, {city}) info_list [张三, 25, 北京] introduce(*info_list) # 相当于 introduce(张三, 25, 北京) info_dict {name: 李四, age: 30, city: 广州} introduce(**info_dict) # 相当于 introduce(name李四, age30, city广州)这里*info_list把列表中的三个元素依次传给name、age、city三个形参**info_dict把字典键值对转换成关键字参数传给函数。这是一个常用且容易看的技巧尤其是从配置文件里读出一堆参数后直接解包传给函数代码会非常简洁。3.4 几种参数混用时的定义顺序如果函数定义里既有普通参数又有默认参数还有*args和**kwargs顺序是有规定的普通参数 → 默认参数 → *args → **kwargs一个标准且清晰的例子def func(a, b, c10, *args, **kwargs): print(a, b, c, args, kwargs) func(1, 2, 3, 4, 5, x6, y7) # 输出 1 2 3 (4, 5) {x: 6, y: 7}解析一下1给a2给b3给c4和5被*args打包成元组x6和y7被**kwargs收进字典。这个顺序不遵守Python就会报语法错误。所以记不住的时候可以把它想成先解决“必须给”的参数再解决“可省略”的参数然后解决“有多少收多少”的两个星号参数。4. 变量作用域函数内外变量的关系4.1 局部变量与全局变量函数内部定义的变量叫局部变量只在函数内部生效。函数外部定义的变量叫全局变量整个程序都能访问。看这个例子total 100 # 全局变量 def add_to_total(x): total 50 # 这是一个新的局部变量和全局 total 无关 return total x print(add_to_total(10)) # 输出 60 print(total) # 输出 100全局变量没变这里函数内部的total 50创建了一个新的局部变量并不会修改外面的全局total。新手最常见的疑惑是我在函数里不是给total赋值了吗为什么外面还是100原因在于赋值操作默认是在当前作用域内创建一个新变量。想让函数内部修改全局变量得用global关键字声明total 100 def change_total(x): global total total total x change_total(50) print(total) # 150但说实话实际工作中我极其不建议频繁使用global。因为全局变量意味着这个变量可以被任何函数修改代码一多你根本不知道是谁在什么时候改动了它。出bug时排查难度成倍上升。最好的实践是一个函数尽量只通过参数接收数据、通过返回值输出结果不依赖外部变量。4.2 LEGB规则Python查找变量的顺序Python在函数内部查找一个变量时遵循一个固定的顺序缩写为LEGBLLocal当前函数内部EEnclosing外层嵌套函数后面讲闭包时你会遇到GGlobal全局作用域BBuilt-inPython内置作用域比如print、len这些内置函数所以如果你在函数内部写print(len(hello))Python先看函数内有没有len变量没有就往外层找再没有就去全局找最后在内置作用域找到len函数。这个规则解释了为什么你在函数里给一个变量赋值后它会“屏蔽”外层的同名变量。比如x 10 def func(): x 20 print(x) # 输出 20用的是局部变量 func() print(x) # 输出 10全局变量不受影响理解LEGB规则对后期读注释不清的代码、排查奇怪bug非常有帮助。5. 匿名函数lambda与常用场景5.1 lambda的语法和基本用法Python里有一种更简单的函数定义方式lambda表达式也叫匿名函数。它适合那些逻辑很简单、只在一两处使用的函数不需要专门用def定义add lambda a, b: a b print(add(3, 5)) # 8这个lambda的含义是接收a和b两个参数返回a b。冒号前面是参数冒号后面是表达式表达式的结果就是返回值。不过lambda并非为了“定义一个变量然后调用”而生。如果给lambda起名字赋给变量那还不如直接def add(a, b): return a b可读性更好。lambda的真正用武之地是配合其他函数使用比如sorted、map、filter。5.2 sorted搭配lambda做复杂排序sorted函数接收一个容器返回排序后的新列表。它可以接收key参数指定排序的规则。lambda在这里就非常顺手students [ {name: 小明, score: 88}, {name: 小红, score: 95}, {name: 小刚, score: 76}, ] # 按分数从低到高排序 sorted_students sorted(students, keylambda stu: stu[score]) print(sorted_students) # 按分数从高到低排序 sorted_students_desc sorted(students, keylambda stu: stu[score], reverseTrue) print(sorted_students_desc)我拆解一下key参数接收一个“对每个元素进行操作并返回比较依据的函数”lambda表达式lambda stu: stu[score]就是取出这个学生的分数然后sorted按分数排序整个列表。如果你的排序逻辑相当复杂比如按姓名长度排序、按字典的第二个键排序lambda都能写得很简洁。但如果逻辑超过一行或者需要复用那就果断用def定义普通函数。5.3 map和filter配合lambdamap的作用是对可迭代对象里的每个元素应用一个函数返回一个新的迭代器。典型例子是把列表中所有数字平方nums [1, 2, 3, 4] squared list(map(lambda x: x * x, nums)) print(squared) # [1, 4, 9, 16]filter的作用是根据条件过滤可迭代对象保留函数返回为真的元素。典型例子是筛选出偶数nums [1, 2, 3, 4, 5, 6] evens list(filter(lambda x: x % 2 0, nums)) print(evens) # [2, 4, 6]这里要注意的是Python 3中map和filter返回的是迭代器不是列表所以要用list()转换或者用for循环逐一取出来。我习惯在调试的时候直接包一层list()这样能看到结果。顺带一提现在Python圈子里也流行“列表推导式”来做同样的事比如[x * x for x in nums]。相比之下列表推导式在很多场景下可读性更高所以我个人更推荐能用推导式就用推导式lambda用在sorted的key参数、事件回调这种场景会更合适。6. 函数式编程利器闭包与装饰器6.1 闭包函数里面造函数Python允许在函数内部再定义函数并且内部函数可以访问外部函数里的变量。当内部函数被返回出去使用就形成了闭包。看个例子def make_multiplier(factor): def multiplier(x): return x * factor return multiplier double make_multiplier(2) print(double(10)) # 20 triple make_multiplier(3) print(triple(10)) # 30这里make_multiplier接收一个factor然后定义了一个内部函数multiplier这个内部函数可以使用外层factor。当我们调用make_multiplier(2)时返回的不是一个数字而是一个“会记住factor2”的函数赋给double以后调用double(10)就相当于计算10 * 2。闭包的核心在“记住”两个字。它把外部变量和内部函数绑定成了一个整体相当于把一个小的“状态”封装了起来。这个特性在需要生成多个不同配置函数的场景里非常实用。闭包有一个常见陷阱内部函数引用外部循环变量时拿到的是循环结束后的最终值。比如functions [] for i in range(3): functions.append(lambda: i) for f in functions: print(f()) # 输出 3 3 3而不是 0 1 2很多人想当然以为会输出0 1 2实际是3 3 3。原因是lambda里的i是一个自由变量运行时才去外部查找这时i已经在循环结束后变成了2。解决办法是把当前循环值作为默认参数传入functions [] for i in range(3): functions.append(lambda ii: i) for f in functions: print(f()) # 输出 0 1 2这个坑面试题经常考实际项目中也有不少同事踩过。记不住原理也没关系遇到“循环里造函数”的代码留个心眼。6.2 装饰器不修改原函数给函数加功能装饰器是Python一个非常经典的高级特性。它的本质是接收一个函数返回一个新函数在不改动原函数代码的前提下给原函数附加额外的功能。最常见的应用是日志打印、运行时间统计、权限校验。举个例子我写一个统计函数运行时间的装饰器import time def timer(func): def wrapper(*args, **kwargs): start time.time() result func(*args, **kwargs) end time.time() print(f{func.__name__} 运行耗时 {end - start:.4f} 秒) return result return wrapper timer def slow_function(): time.sleep(1) return 完成 print(slow_function())输出类似slow_function 运行耗时 1.0003 秒 完成我来拆解一下timer接收一个函数func。它定义了一个wrapper函数这个函数接收任意参数*args, **kwargs。在wrapper里先记录开始时间再调用原函数func(*args, **kwargs)把返回值存起来。然后打印耗时最后返回原函数的结果。timer是语法糖相当于执行了slow_function timer(slow_function)。这样原函数slow_function的代码完全没变但功能上多了一个计时功能。以后想给别的函数也能计时直接在上面加一行timer就行。装饰器是函数进阶的一个分水岭。刚开始看不懂没关系可以把装饰器想成是一层“包装纸”原函数是礼物装饰器是包装包装并不会改变礼物本身但让礼物看起来更体面、还能附带一张纸条。理解了这个思想就掌握了一半。7. 内置函数与实操案例把函数用在真实场景里7.1 高频内置函数盘点Python有大量内置函数不用导入任何模块就能直接用。这里我挑几个高频的、配合我们前面讲的函数知识一起看# len取长度 len([1, 2, 3]) # 3 # type查看类型 type(42) # class int # range生成整数序列 list(range(5)) # [0, 1, 2, 3, 4] # enumerate同时拿到索引和值 for idx, value in enumerate([a, b, c]): print(idx, value) # 输出 0 a / 1 b / 2 c # zip把多个可迭代对象按位置打包成元组 list(zip([1, 2, 3], [a, b, c])) # [(1, a), (2, b), (3, c)] # sum / max / min求和、求最大、求最小 sum([1, 2, 3]) # 6 max([1, 5, 3]) # 5 min([1, 5, 3]) # 1这里面enumerate和zip新手经常会想手动用range(len(list))去模拟但其实直接用内置函数更简洁、更不容易出错。7.2 案例写一个学生成绩管理小工具为了把函数知识串起来用我现场写一个学生的成绩管理工具。需求是录入学生姓名和三科成绩计算总分和平均分并按总分从高到低排序。def create_student(name, chinese, math, english): 创建学生信息字典 return { name: name, chinese: chinese, math: math, english: english, } def calc_total(student): 计算总分 return student[chinese] student[math] student[english] def calc_average(student): 计算平均分 return calc_total(student) / 3 def build_report(student): 生成学生成绩报告 return { name: student[name], total: calc_total(student), average: calc_average(student), } # 录入三个学生 students [ create_student(小明, 88, 92, 85), create_student(小红, 95, 89, 96), create_student(小刚, 76, 82, 78), ] # 生成报告并排序 reports [build_report(s) for s in students] reports.sort(keylambda item: item[total], reverseTrue) for r in reports: print(f{r[name]} 总分 {r[total]} 平均分 {r[average]:.1f})运行结果小红 总分 280 平均分 93.3 小明 总分 265 平均分 88.3 小刚 总分 236 平均分 78.7这个案例虽然小但把前面讲的几个知识点全用上了create_student、calc_total、calc_average、build_report每个函数只做一件事职责清晰。build_report内部调用了calc_total和calc_average体现了“函数可以调用其他函数”的组合思想。reports.sort(keylambda item: item[total], reverseTrue)用lambda指定排序依据。如果不用函数把这些逻辑全堆在一起也能跑通但你想加一个“输出不及格名单”的功能就得重新梳理整个流程。而现在只需再写一个新函数然后在主流程里调用清爽很多。7.3 递归函数函数自己调用自己递归是函数里另一个绕不开的概念。它的定义很简单函数内部调用自身。一个经典例子是计算阶乘def factorial(n): if n 0: return 1 return n * factorial(n - 1) print(factorial(5)) # 120递归的思路是“把大问题拆成规模更小的同类问题”。算factorial(5)就是计算5 * factorial(4)而factorial(4)又等于4 * factorial(3)一直拆到factorial(0)它有一个明确的返回条件return 1然后逐层返回最终得到120。这里的“返回条件”叫基线条件是递归必须有的东西。没有基线条件递归就会无限调用下去直到Python报RecursionError。递归的优点是代码简洁、贴近数学定义适合处理树形结构、目录遍历等问题。但缺点是如果层数太深会占用大量调用栈性能不如循环。所以我一般建议能用循环解决的简单问题优先用循环递归用在那些“用循环写起来非常绕、用递归秒解”的场景比如遍历JSON树、计算斐波那契数列之类的题目。8. 常见问题与排查技巧实录8.1 新手最容易撞上的函数报错这部分我整理一个速查表全是新手期最容易出的问题附排查思路报错信息原因解决办法SyntaxError: invalid syntax少写了冒号、括号不匹配、函数名拼错检查def那一行结尾有没有冒号检查括号是否配对IndentationError: unexpected indent缩进错误函数体没有统一缩进统一用4个空格不要混用Tab和空格NameError: name xxx is not defined变量或函数名拼错或者调用时还没定义检查拼写检查函数是否在调用之前定义TypeError: missing 1 required positional argument调用函数时少传了参数对比函数定义补上对应参数TypeError: got an unexpected keyword argument传了一个函数定义里不存在的关键字参数检查参数名是否拼错或者函数是否需要改成**kwargsRecursionError: maximum recursion depth exceeded递归没有基线条件或递归太深检查基线条件或改用循环ValueError: not enough values to unpack解包时数量不匹配比如2个变量去接3个值检查返回值的数量和接收变量的个数关于报错我给一个非常实用的建议报错信息一定要逐字逐句读。Python的报错已经非常友好了它通常会告诉你是在哪一行、什么类型的错误。新手最容易犯的毛病是看到红色报错就慌了其实静下心读一遍大部分问题都能自己解决。8.2 为什么函数里修改列表外面也跟着变了有个问题经常让人困惑我在函数内部给列表添加元素为什么外面的列表也变了def add_item(lst, item): lst.append(item) my_list [1, 2, 3] add_item(my_list, 4) print(my_list) # [1, 2, 3, 4]这是不是违反了“函数内部不影响外部”的规则并不是。关键要理解Python传参传的是引用。列表是可变对象当你把列表变量传给函数时lst和my_list指向的是同一个列表对象。append是在这个列表对象上原地操作所以外部当然能看到变化。那为什么前面total 50不影响外部变量因为赋值操作是“把变量指向一个新对象”并没有改变原来整数对象本身。整数是不可变对象本来也没有“原地修改”的说法。区别可以简单记忆可变对象列表、字典、集合函数内部原地修改外部会感知到。不可变对象整数、字符串、元组函数内部重新赋值不影响外部变量。如果你希望函数不要改动传入的列表可以在函数内部先复制一份比如lst lst.copy()或list(lst)然后再操作。8.3 环境相关的报错如何理解热搜词里出现了很多类似“无法将‘pip’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”“无法将‘git’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”的问题。这其实是命令行环境变量配置的问题和Python函数本身没关系但很多新手会在这个阶段被卡住。这类报错的本质是系统在当前的PATH环境变量里找不到你输入的命令对应的可执行文件。解决办法一般是两条安装时勾选“Add Python to PATH”选项或者找到对应的可执行文件路径手动添加到系统环境变量具体操作步骤网上一搜一大堆但关键是理清原理命令行的命令解析是靠环境变量去定位可执行文件的。还有“claude : 无法将‘claude’项识别为 cmdlet”这类问题也是同一个道理。它说的是你安装的某个工具没有把它的可执行文件加入PATH所以你在终端里敲它是找不到的。解决办法和上面一样去找这个工具的实际安装路径把它配到环境变量里。8.4 排查函数bug的实操心得最后分享一点我自己排查函数相关bug的经验可能比语法知识更值钱第一用print大法缩小范围。函数出问题时最快的方法是在关键节点加print打印参数值、中间变量、返回值。比如def func(x): print(f传入的x{x}) result x * 2 print(f中间结果是{result}) return result跑一次你就能看到数据在哪一步变得不对然后针对性修改。很多新手debug无从下手其实就是因为没有在关键节点留下观测点。第二写一个小测试调用。不管函数多简单写完之后至少跑一个用例验证一下。我习惯随手在一些临时文件里测试函数行为输入输出确认无误再整合进正式代码。这样能避免把问题带到项目深处。第三函数越小越好。如果你发现一个函数超过几十行那它多半承担了太多职责可以拆分了。拆出来的小函数每个都能单独测试、单独复用出问题也好定位。第四善用help和dir。在交互式环境里用help(函数名)查看函数的帮助文档用dir(内置函数)查看模块或对象的属性。特别是写代码忘了某个内置函数的用法时这就是最快的手册。这还不是函数的尽头写到这里我把函数从基础语法一直讲到了装饰器和递归但坦白说函数能聊的东西远不止这些。函数式编程里的柯里化、生成器函数、functools模块里的partial、lru_cache这些都是函数这个主题下的进阶玩法。不过对一个正在学习Python的新手来说先把这篇文章里的内容吃透能熟练拆分任务、写出清晰的小函数就已经赢过了很多人。我个人的体会是函数的学习不在于一次记住多少语法而在于有没有养成“先拆解任务再设计函数”的思维习惯。平时写代码时多问自己一句这段逻辑会不会被复用这个函数的职责是否单一带着这样的意识去练习用不了多久你写出来的代码就会从“能跑”进化到“清晰”。最后再分享一个小技巧你可以试着在不运行代码的情况下手写一个小程序然后对照结果检查自己有没有理解错函数的作用域和返回逻辑。这种练习我当年做过很多次效果非常好。函数的坑亲手踩过一遍比看十遍教程都管用。