
先问一个问题你写Python代码的时候是不是经常复制粘贴尤其是同一段逻辑今天处理A数据要写一遍明天处理B数据又写一遍如果你的回答是“是”那你已经站在函数这扇门的门口了。这篇博文面向刚学完循环、列表、条件判断正准备接触Python函数基础的新手也适合那些“看着教程能懂、自己写就卡壳”的同学。我会从最朴素的重复代码开始把函数定义、参数传递、作用域、lambda、递归这些内容挨个讲透最后还会聊几个我见别人踩过无数次的坑——包括那个“无法将××识别为函数”的系统报错。1. 先从一段重复代码说起函数到底解决了什么问题1.1 一个没有函数的脚本长什么样假设你现在要统计班级三次模拟考试的平均分第一次是期中第二次是月考第三次是期末。没有函数的时候大多数人会这么写# 第一次模拟考 scores1 [78, 89, 92, 65, 73, 88] total1 0 for s in scores1: total1 s avg1 total1 / len(scores1) print(第一次平均分:, avg1) # 第二次模拟考 scores2 [85, 91, 76, 82, 69, 95] total2 0 for s in scores2: total2 s avg2 total2 / len(scores2) print(第二次平均分:, avg2)第三次就不用我再贴出来了反正就是再把这段复制一遍改个变量名。这种写法的第一个问题是冗余40个学生的成绩处理逻辑被复制了三遍代码行数直接膨胀。第二个问题更致命如果明天需求改了平均分不算算术平均改成去掉一个最高分再去掉一个最低分后的平均你得回去改三处地方稍不留神漏改一处三次考试数据口径就不一致了。这种“复制-粘贴-改变量名”的操作恰恰是新手最容易上瘾又最容易翻车的习惯。1.2 函数的本质给一段操作起个名字函数的思路很简单把“算一个列表的平均分”这段逻辑抽出来给它起个名字比如叫compute_mean以后不管哪个数据要算平均分直接喊这个名字、把数据传进去就行。定义完的函数不会自动运行只有你“调用”它的时候才执行。用生活里的例子类比菜谱本身不是菜只有你照着菜谱动手做菜才出现在餐桌上。函数定义就是写菜谱函数调用才是真正“做菜”。很多人刚接触函数时总觉得它抽象其实就是没把“定义”和“调用”这两个动作分开理解。定义阶段只检查语法不执行内部代码真正执行是在调用那一行发生的。有了函数之后同样三次考试平均分代码可以写成这样def compute_mean(scores): total 0 for s in scores: total s return total / len(scores) scores1 [78, 89, 92, 65, 73, 88] scores2 [85, 91, 76, 82, 69, 95] scores3 [90, 88, 94, 79, 83, 86] print(第一次平均分:, compute_mean(scores1)) print(第二次平均分:, compute_mean(scores2)) print(第三次平均分:, compute_mean(scores3))以后要改平均规则只动compute_mean里面那几行调用处完全不用改。这就是函数最核心的价值复用逻辑、集中修改、让代码结构清晰。从这一节开始后面所有内容都围绕“把逻辑封装好”这件事展开。2. 定义一个函数从def到return的核心语法2.1 函数声明、调用和缩进Python定义一个函数用def关键字格式是这样的def 函数名(参数1, 参数2, ...): 函数体 return 返回值举一个最简单的例子def greet(name): print(你好 name) greet(小明)这里def greet(name):是函数声明name是参数冒号后面缩进的代码块是函数体调用时用函数名(参数)。关键一点Python靠缩进划分函数体和外部代码缩进不仅是为了好看更是语法结构的一部分。很多人第一次写函数报IndentationError往往就是缩进多了一个空格或者少了一个Tab。我建议新手只用四个空格做缩进不要在同一个工程里混用Tab和空格否则调试时你会被莫名其妙的报错逼疯。函数命名也有规矩只允许字母、数字、下划线不能以数字开头不能和Python关键字撞名比如def、if、for这些都不能当函数名。惯例是使用小写字母加下划线比如compute_mean、get_user_info这种命名方式叫snake_case读起来一目了然。2.2 参数位置参数、关键字参数和默认参数参数是函数和外部数据打交道的通道。最常见的传参方式是位置参数调用时按函数定义里的顺序依次传值def describe_book(title, author, pages): print(f《{title}》作者是{author}共{pages}页) describe_book(Python编程, 张三, 300)你还可以用关键字参数的方式直接指定参数名describe_book(titlePython编程, pages300, author张三)关键字参数的好处是不用记顺序想先传哪个就先传哪个。位置参数和关键字参数可以混用但位置参数必须出现在关键字参数之前否则Python直接报SyntaxError。默认参数是新手最常用的东西比如描述学生信息def introduce(name, age18, city北京): print(f{name}今年{age}岁来自{city}) introduce(小红) # 小红今年18岁来自北京 introduce(小刚, 20) # 小刚今年20岁来自北京 introduce(小丽, city上海) # 小丽今年18岁来自上海定义函数时给参数赋一个值这个参数调用时可不传就用默认值。但这里埋了一个经典大坑默认参数在函数定义时就已经被创建而不是每次调用时重新创建。这个坑我放在第3节专门讲。2.3 return没有return的函数返回什么函数可以没有return比如前面那个greet函数它只负责打印没返回任何结果。Python会隐式返回一个None也就是“空”的意思。你如果写result greet(小明)再打印result看到的就是None。return还有两个常见用法def check_score(score): if score 0 or score 100: return if score 60: return 及格 return 不及格一个是提前结束函数比如分数不合法直接return不带值函数立刻退出后面的代码不执行。另一个是返回多个值用逗号隔开def get_min_max(scores): return min(scores), max(scores) low, high get_min_max([78, 89, 92, 65]) print(low, high) # 65 92这里其实返回的是一个元组(65, 92)Python拿到元组后会自动拆包给low和high。这个概念后面学数据结构时会遇到现在你先记住“多个返回值的本质是一个元组”就行。3. 参数传递不是想当然的“值传递”3.1 不可变对象函数内重新赋值不影响外部很多新手从其他语言转过来本能地认为“函数参数传的是值函数里怎么改都不会影响外面”这个认知在Python里只对了一半。先看不可变对象整数、浮点数、字符串、元组的情况def add_one(n): n n 1 print(函数内部:, n) num 10 add_one(num) print(函数外部:, num) # 10函数内部把n重新赋值为11外部num还是10。原因在于Python的参数传递是“对象引用传递”你可以理解成传给函数的是一个指向“10”这个对象的标签重新赋值n n 1只是把函数内部的标签贴到了“11”这个新对象上外部的num依然指向原来的“10”。这个过程对初学者来说不容易看到但暂时只需要记住结论在函数内部对不可变参数重新赋值不会影响外部变量。3.2 可变对象函数内部修改会“传染”到外部但对象引用传递还有另一面。如果传入的是列表、字典、集合这些可变对象函数内部直接修改对象本身而不是重新赋值外部就能看到变化def add_score(scores, new_score): scores.append(new_score) class_scores [78, 89, 92] add_score(class_scores, 100) print(class_scores) # [78, 89, 92, 100]append是在原列表对象上做修改没有重新创建新列表而外部变量和函数内部参数指向同一个列表对象所以外部数据跟着变了。如果你不想让函数改动外部列表一个办法是开头先复制一份def add_score(scores, new_score): scores list(scores) # 复制一份 scores.append(new_score) return scores这种“函数不修改传入的可变对象而是复制后返回新对象”的写法在数据分析场景很常用。比如你想在原有数据基础上过滤或变换时尽量生成新列表而不是原地改原数据后面排查bug会轻松很多。3.3 默认参数用可变对象经典大坑前面说过默认参数在函数定义时就创建了如果默认参数是列表、字典这类可变对象问题就来了def add_student(name, students[]): students.append(name) return students print(add_student(张三)) # [张三] print(add_student(李四)) # [张三, 李四]第二次调用时students并没有变成空的[]而是沿用上一次调用后的列表。因为你没传studentsPython就拿函数定义时创建的那个列表出来用这个列表被所有的调用共享了。三次调用累积了三个名字完全不是你想要的效果。标准修复方式是用None做默认值在函数体内再创建新列表def add_student(name, studentsNone): if students is None: students [] students.append(name) return students原理很简单None是不可变对象每次调用走if分支都会新建一个空列表后续的append只作用于本次调用。这个坑在真实的项目里几乎人人踩过网上搜“Python 默认参数 可变对象”能看到一堆讨论面试也爱考值得记住。3.4 *args和**kwargs参数个数不确定时的兜底方案有时候你写一个函数事先不知道调用者会传几个参数。比如计算任意多个数的平均值def average(*args): if len(args) 0: return 0 return sum(args) / len(args) print(average(1, 2, 3)) # 2.0 print(average(80, 90, 95, 88)) # 88.25写*args会把所有位置参数打包成一个元组函数内部用args这个元组访问。对应地**kwargs会收集所有关键字参数打包成字典def print_info(**kwargs): for key, value in kwargs.items(): print(f{key}: {value}) print_info(name小红, score92, city上海)很多库的函数都大量使用这种设计比如数据分析时常见的np.sum()函数它的axis参数就是用来控制按行求和还是按列求和这种“同一个功能、不同参数控制行为”的思路贯穿整个Python生态。等你读别人源码时看到*args, **kwargs别慌就是在收集不确定数量的参数。4. 作用域为什么函数里改不了外面的变量4.1 LEGB查找顺序变量从哪来作用域说白了就是“变量在哪里能被看见”。Python在函数内查找变量的顺序是局部Local→ 嵌套函数外层Enclosing→ 全局Global→ 内置Built-in简称LEGB。我给你一个例子你感受一下name 全局名字 def show(): name 局部名字 print(name) show() # 局部名字 print(name) # 全局名字函数内部的name 局部名字创建了一个新的局部变量跟外面的name没有关系函数里访问name时优先找局部作用域所以打印的是“局部名字”。出了函数局部变量就销毁了访问的还是全局的“全局名字”。这个规则理解透彻以后很多“怎么我函数里改了变量外面没变”的困惑都能解开。4.2 global声明真的需要修改全局变量时Python允许函数内部读取全局变量但不允许直接给全局变量重新赋值——除非你显式声明global。看下面的代码count 0 def increase(): count 1 # 运行会报错 increase()报错信息是UnboundLocalError: local variable count referenced before assignment。原因是函数内部有对count的赋值操作Python就把count当作局部变量于是count 1变成了先读后写一个不存在的局部变量。解决办法是加一行global countcount 0 def increase(): global count count 1 increase() print(count) # 1虽然能解决问题但我个人建议尽量少用global。函数的正确用法是“参数传入、返回值传出”全局变量改来改去会让代码变得极难排查。比如你写一个统计用户次数的功能函数内部用global改了三个全局变量后面某一个变量值不对你很难定位是哪里改的。更推荐这样def increase(count): return count 1 count 0 count increase(count) print(count) # 1数据流转清晰改动都发生在返回值里调试时每一步都能看到变化。4.3 嵌套函数与nonlocal闭包的地基如果你在函数内部再定义一个函数内层函数想修改外层函数的局部变量就要用到nonlocaldef outer(): total 0 def inner(x): nonlocal total total x return total return inner add outer() print(add(5)) # 5 print(add(3)) # 8这里inner引用了外层函数outer里面的total并且通过nonlocal声明“这个变量是外层函数作用域里的”才能修改它。如果不写nonlocalPython会像刚才处理global那样把total当作inner内部的局部变量从而报错。这个知识点现在是了解就好等你学到闭包、装饰器时这里的理解会直接决定你能不能看懂高级代码。5. 三个好用的小工具lambda、内置函数与递归5.1 lambda一行写完的匿名函数lambda是Python提供的一种快速定义简单函数的方式适合函数体只有一行表达式、用完就走的场景。语法是lambda 参数: 返回值表达式它和普通函数等价def double(x): return x * 2 double_lambda lambda x: x * 2 print(double(5)) # 10 print(double_lambda(5)) # 10但lambda更常见的用途是作为其他函数的参数。比如一个列表里存了学生字典想按成绩从高到低排序students [ {name: 小白, score: 78}, {name: 小红, score: 92}, {name: 小刚, score: 85}, ] students.sort(keylambda stu: stu[score], reverseTrue) print(students)sort的key参数期望的是一个函数这个函数接收列表里的一个元素返回一个“排序依据”。用lambda写正好一行搞定不用专门为它定义一个def函数。这就是为什么lambda在很多教程里被叫做“匿名函数”——它不需要名字用完即弃非常方便。需要提醒的是lambda只适合写简单的单行表达式。如果逻辑稍微复杂比如需要多个步骤、多条语句老老实实用def定义普通函数代码可读性会更好。为了少写几个字把逻辑塞成一行后面维护的人包括三个月后的你自己真的会谢你。5.2 内置函数Python早就帮你写好了函数很多新手不知道sum、len、max、min、abs、round这些都是函数它们是Python内置函数随解释器一起提供。用函数思维去理解这些内置功能会对“函数”这个概念更亲切。比如刚才的平均分计算其实可以写成def compute_mean(scores): return sum(scores) / len(scores)对比第1节那个手写for循环的版本这个版本简单清晰太多。一个写久了的人会养成“动手写循环之前先想想有没有内置函数”的习惯。字符串处理也一样len(hello)、hello.upper()前者是内置函数后者是字符串对象的方法本质上都是“封装好的代码块”和你自己写的函数没有高下之分只是已经被人写好了直接拿来用。不建议死记硬背全部内置函数常用的sum、max、min、len、abs、round、sorted、enumerate、zip先用顺手其他的需要时再查官方文档。5.3 递归函数自己调用自己以“李白打酒”为例递归是函数基础里最容易让大家挠头的一个点但其实拆开看没什么神秘一个函数在函数体内调用它自己就叫递归。关键是必须有“基线条件”让递归停下来否则会无限循环直到栈溢出。我拿一个经典的“李白打酒”问题来演示。题目是李白街上走提壶去买酒。遇店加一倍见花喝一斗。三遇店和花喝尽壶中酒。问壶中原有多少酒大意是李白带着一壶酒出门遇到酒店就把壶里的酒翻一倍遇到花就喝掉一斗。他一共依次经历了“店、花、店、花、店、花”最后一次遇到花之后酒刚好喝光。问初始壶里有多少斗酒。正向思考很麻烦但倒推非常直观最后一次遇到花喝掉1斗后变成0说明最后一次遇到花之前壶里有1斗最后一次遇到店之前壶里应该是0.5斗因为遇店加一倍0.5翻倍变1继续倒推遇到花之前是1.5斗遇到店之前是0.75斗……这样一路推回初始值答案是0.875斗也就是7/8斗。用函数递归来写这个倒推过程def reverse_wine(n): # n还需要往前回溯的操作步数 if n 0: return 0 if n % 2 1: # 奇数步倒推对应“花”遇花前比遇花后多1斗 return reverse_wine(n - 1) 1 else: # 偶数步倒推对应“店”遇店前是遇店后的一半 return reverse_wine(n - 1) / 2 print(reverse_wine(6)) # 0.875这个递归函数有三要素基线条件n 0时停止问题规模每一步缩小n - 1每一步做什么由当前步是“花”还是“店”决定。运行过程会一路往深处调用自己直到n 0才开始一层层返回结果最终得到0.875。递归并不适合所有场景能写循环的往往用循环性能更好。但它是一种重要的思维方式尤其是处理树形结构、分治问题时递归代码比循环简洁得多。初学者不用急着精通先把“函数可以调用自己”“必须设置终止条件”这两个点记住后面再接触二叉树、快速排序时会用得着。6. 新手最容易踩的坑函数名、变量名与命令行报错6.1 高频报错与定位思维函数相关的报错集中在下面几类我列一个表你可以收藏起来对照报错信息含义常见原因典型修复SyntaxError: invalid syntax语法错误忘记冒号、括号不匹配看报错行前后补冒号、补括号IndentationError: unexpected indent缩进错误函数体内外缩进不一致统一用四个空格检查Tab和空格混用NameError: name xxx is not defined名字未定义函数名拼错、定义前调用、作用域外访问查拼写确认定义顺序TypeError: xxx() missing 1 required positional argument: y参数没传够调用少传了一个位置参数补上参数或补默认值TypeError: xxx() takes 1 positional argument but 2 were given参数传多了调用多传了一个位置参数删掉多余参数或检查函数定义新手调试时最容易犯的毛病是盯着报错第一行看其实程序员应该先看最后一行因为真正的原因往往写在那里。比如NameError: name compute_mean is not defined直接告诉你compute_mean没定义那就先确认这个函数写了吗函数名拼写对了吗是不是在定义之前就调用了“先读最后一行”这个习惯能省下大量瞎猜的时间。6.2 “无法将claude/npm/git识别为函数”到底是怎么回事很多新手在Windows的PowerShell或CMD里敲命令会看到类似这样的报错claude : 无法将“claude”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。npm、git、python、opencode这些命令都可能出现同样的提示。这里最迷惑人的是“函数”这两个字——你刚学Python函数看到“无法将××识别为函数”还以为是自己函数写错了。实际上这个“函数”是PowerShell语境下的“函数”指的是PowerShell脚本里定义的一种命令跟Python里的def函数是两码事。这句报错的真实含义是操作系统在当前环境变量PATH里找不到名为claude、npm、git的可执行程序。处理方式取决于你用的工具如果是Python自带的命令检查安装时有没有勾选“添加到PATH”如果是npm往往需要用官方安装包或npx工具来启动如果是claude这类AI命令行工具需要先完成它的安装步骤并确保可执行文件所在目录加入PATH。装完以后记得重新打开终端因为环境变量只在终端启动时读取一次。还有一类更常见的混淆发生在Python代码里有人想在Python脚本里调用系统命令直接写claude(...)或者npm(...)然后报NameError。在Python中调用外部程序应该用subprocess模块或os.system不能把外部命令名当成Python函数直接用。把这两个场景分清你就能少一大半的“莫名报错”。6.3 写函数时的几个实用建议根据我自己的经验函数基础的练习阶段最容易出效果的是这几件事第一函数名起得“望文生义”。不要用a、b、func1这种名字。compute_mean、get_user_age、filter_students这类名字别人读你代码时不用看函数体就知道它在干什么。Python代码讲究可读性你以后回看自己代码也会庆幸当时没偷懒。第二一个函数只做一件事。如果一个函数既在计算平均分、又在打印报告、还顺带更新了数据库那它职责太多出了问题很难定位。更好的做法是拆分成compute_mean负责算generate_report负责打印各管一段组合使用。第三善用docstring。定义函数后第一行用一个三引号字符串写清楚“这个函数是干什么的、参数是什么、返回什么”不用写多长两三行就好def compute_mean(scores): 计算成绩列表的平均分。 参数: scores: 数字列表。 返回: 平均分列表为空时返回0。 if not scores: return 0 return sum(scores) / len(scores)这会让你在写复杂项目时更容易回忆起“这个函数是干嘛的”。说实话函数这关是Python学习里一个明显的分水岭。很多人前面循环、列表都学会了一看到def和冒号就发懵其实多数是因为练得太少。我的建议很朴素把你这周写过的所有重复代码找出来哪怕只有两处重复都改成函数。改的过程里你会碰到参数传递、返回值、作用域这些今天讲过的问题碰到一个解决一个比再看十遍教程都管用。函数这关过了后面学面向对象和模块化开发会轻松非常多。