Python函数详解:参数传递、返回值与作用域避坑指南
发布时间:2026/10/9 10:53:03 锦皓数字建站

Python基础知识点函数先说结论函数是Python里最值得你花时间吃透的基础概念之一没有它你写的东西顶多叫“脚本”有了它你写的东西才配叫“程序”。哪怕你只是刚学Python三个月函数也是你从“跟着教程敲代码”跨到“自己解决问题”的那道门槛。这篇文章不打算讲那些教科书上已经写烂的车轱辘话我尽量用做了多年开发的实际视角把函数这玩意儿掰开揉碎聊清楚它到底是什么、参数怎么传才不踩坑、返回值有哪些隐藏细节、作用域怎么理解最后再用几个真实场景的代码示例告诉你函数设计得好后面能帮你省多少事。如果你是刚接触编程不久的新手这篇文章能帮你把函数的基础打得比一般教程更扎实如果你已经写了一段时间代码但总觉得自己对参数、返回值的理解模模糊糊那这篇也能帮你把那些模糊地带扫一遍。Python里函数这东西真的是“会用的人觉得简单不会用的人觉得玄乎”但本质上它就是一句话的事把一段逻辑打包起来取个名字随时调用。1. 函数到底是什么——先有这个思维框架再谈语法很多人学函数一上来就背def、背return、背参数列表背完就忘。我建议你先别急着写代码先把这个思维框架搭起来函数就是给一段代码起个名字然后你能通过这个名字反复执行它还能在每次执行时给它喂不同的数据它处理完以后把结果还给你。生活中的例子到处都是。你去食堂打饭窗口师傅就是一个函数。你的输入是“我要一份红烧肉盖饭”师傅的肚子里那套“接单、盛饭、加菜、递出来”的流程就是函数体最后给你的那盘饭就是返回值。你不需要知道师傅后厨怎么切菜、怎么炒肉你只需要知道“报菜名拿饭”这个接口就够了。这其实就是计算机里最核心的思想之一抽象与封装。函数帮你把复杂的内部实现藏起来对外只暴露一个简单直接的名字和参数入口。1.1 为什么需要用函数——不只是为了少写几行代码新手最容易陷入的一个误区就是把函数当成“把重复代码换个地方放”。少写重复代码只是最表面的收益真正用函数是为了三件事第一控制复杂度。人的脑子同时能记的事情有限一段200行的线性代码你看到后面就忘了前面。但如果你把它拆成5个函数每个函数20行每个函数只干一件事你就能像看目录一样理解整个程序而不是像看长篇小说一样从头啃到尾。第二隔离错误。代码放一起的时候一个变量能在你毫无察觉的情况下被改得面目全非。但如果你把逻辑关进函数里每个函数就是一个隔间变量在隔间之间不乱跑出问题了也好定位——先判断是哪个函数出的错再进去排查那几十行效率高一个量级。第三复用与组合。今天写的排序逻辑明天另一个项目可能还要用这个爬虫里面解析网页的函数换个目标网站可能只需要改两个参数。函数就像乐高积木你手里攒的“积木块”种类越多将来搭新东西就越快。提示如果你发现自己写了三遍几乎一样的代码那就应该停下来把这段逻辑抽成函数了。这被称为“三次法则”是很多开发团队推动重构的一条朴素经验。1.2 定义和调用的基础语法——第一次把函数跑起来在Python里定义一个函数语法比大多数语言都简洁。直接看代码def greet(name): 输出一句问候语 print(f你好{name}欢迎来到Python的世界。)这里def是定义函数的关键字greet是函数名name是参数冒号后面缩进的代码块就是这个函数的函数体。当你写下这段定义时Python只是“记下”了这个函数的存在并不会真的执行里面的print。真正让代码跑起来的是调用也就是写下面这行greet(小明)运行结果就是你好小明欢迎来到Python的世界。这里有一个特别重要的点也是很多新手第一次崩溃的地方函数定义和函数调用的顺序不能反。Python是逐行解释执行的如果你先调用一个还没定义的函数解释器会直接抛出NameError: name greet is not defined。所以一般情况下要么把所有函数定义放在文件前面要么保证调用发生在定义之后。关于缩进因为我对Python太熟了反而容易忘记强调函数体的每一行都必须缩进同一个函数体内的缩进必须一致。之前见过某同学函数第一行用了4个空格第二行用了Tab结果Tab和空格混用直接报错他盯着屏幕看了十分钟没看出问题。Python对缩进的敏感程度接近苛刻这是它的设计哲学——用缩进强制你写出结构清晰的代码。2. 参数传递——这里面的门道最值得细品参数是函数最核心的交互界面。你把数据从外面传给函数函数才能针对你的具体需求干活。但Python的参数传递机制和很多语言不一样这里面的水远比你想象的深。2.1 位置参数与关键字参数——两种传递姿势最朴素的传参方式就是按位置来。调用函数时第一个值给第一个参数第二个值给第二个参数顺序错了结果就全错了。来看这个例子def introduce(name, age, city): print(f我叫{name}今年{age}岁来自{city}。) introduce(小明, 18, 杭州)输出我叫小明今年18岁来自杭州。如果你写introduce(18, 小明, 杭州)那程序不会报错但输出会变成“我叫18今年小明岁”——这比报错更坑因为程序看起来在跑逻辑却已经错了。所以当你觉得某个函数的参数顺序容易记混的时候一定要用关键字参数来调用introduce(city杭州, name小明, age18)关键字参数的本质就是在调用的时候直接指定“这个值给哪个参数”这样顺序随便打乱都不影响结果。我写代码的习惯是只有一两个参数的时候用位置参数自解释性很强参数一旦超过三个尤其是那种name、age、city很容易混的就一律用关键字参数调用可读性立刻上一个台阶。2.2 默认参数——给函数装上“自动挡”有些参数在大多数情况下都是一个固定的值那就没必要让调用者每次都输入一遍。默认参数就是为此设计的def connect(host, port8080): print(f正在连接 {host}:{port} ...) connect(127.0.0.1) # 输出正在连接 127.0.0.1:8080 ... connect(127.0.0.1, 3306) # 输出正在连接 127.0.0.1:3306 ...把port设置成默认值8080之后调用时只要传host就行只有需要连别的端口时才手动指定。这相当于给函数安了一个“自动挡”大多数使用场景下挂D挡就走特殊需求再手动操作。但默认参数这里有一个全Python社区都公认的大坑默认参数值在函数定义时就已确定只计算一次而且这个值在多次调用之间会被保留。最经典的翻车现场是def add_item(item, items[]): items.append(item) return items print(add_item(苹果)) # 输出[苹果] print(add_item(香蕉)) # 输出[苹果, 香蕉]而不是预期的[香蕉]第一次调用时items默认是空列表加入“苹果”后变成了[苹果]第二次调用时items仍然是那个列表——就是第一次调用时已经变成[苹果]的那个——所以又往里加了“香蕉”。这就是为什么Python官方文档原话劝你默认值不要用可变对象。要写就写成下面这样def add_item(item, itemsNone): if items is None: items [] items.append(item) return items这个“None判断法”是Python写默认参数的标准姿势我至今写每段代码都用这个模式因为它的逻辑清晰能彻底避开那个隐蔽的坑。2.3 可变参数——*args和**kwargs到底在干嘛你的函数需要处理不确定数量的参数时Python有两个专门的语法糖。*args会把调用者传来的多余位置参数打包成一个元组**kwargs则会把多余的关键字参数打包成一个字典def make_smoothie(base, *fruits, **options): print(f基底{base}) print(f水果{fruits}) print(f选项{options}) make_smoothie(牛奶, 草莓, 香蕉, 芒果, 冰块多, 糖少)输出结果基底牛奶 水果(草莓, 香蕉, 芒果) 选项{冰块: 多, 糖: 少}从这个输出就能看出来fruits是一个元组装着所有没指名道姓的额外参数options是一个字典装着所有以keyvalue形式传入的参数。但我想提醒你一句args和kwargs这两个名字本身没有魔法真正有魔法的是前面的*和**。你完全可以写*numbers、**config名字只是个约定俗成的惯例。这个惯例被用得如此普遍以至于你读别人的代码时看到*args就知道“哦这儿收了不定数量的位置参数”看到**kwargs就知道“这儿收了额外配置项”。我实际写项目的时候**kwargs最常用的场景是往函数里传配置项。比如写一个发通知的函数调用者的需求五花八门有的要加签名有的要延时发送你不可能把这些全部写成固定参数那就统一收进**kwargs在函数体里按需取用。这样接口保持稳定新增需求时又不用改函数签名非常灵活。2.4 参数传递机制的真相——Python是“传对象引用”有一道面试题特别经典Python函数的参数到底是值传递还是引用传递我自己的理解是这两个说法都不完全准确更精确的说法是传对象引用。当你向函数传递一个变量时传的不是这个变量本身而是变量所指向的那个对象的引用。这就引出一个结果如果传入的是不可变对象如整数、字符串、元组函数内部无论怎么改外面的变量不受影响但如果传入的是可变对象如列表、字典函数内部修改这个对象外面的变量也会跟着变因为内外操作的是同一个对象。def change_list(lst): lst.append(新增元素) my_list [原有元素] change_list(my_list) print(my_list) # 输出[原有元素, 新增元素]外面的列表也被改了很多人第一次遇到这种情况会觉得很懵“我明明已经把列表传进函数了为什么外面也变了”其实原因就一句话传进去的是引用不是副本。如果你希望函数内部的操作不影响外面就需要自己手动复制一份——用my_list.copy()或者干脆切片my_list[:]。搞清楚这一点后面处理数据时才能做到心里有数。3. 返回值与作用域——函数内外的边界在哪里3.1 return的细节——没写就是None多值返回其实是元组函数干完活总得把结果交出来。return就是往外递结果的那只手。但有几个细节我几乎每次教人都会强调一下。第一个细节没有return的函数调用完后会隐式返回None。None是Python里一个特殊的对象表示“什么都没有”。这本身没什么问题但如果你写了这样的代码result print(你好) print(result) # 输出Noneprint函数的功能是“往屏幕输出”它不返回任何值所以你拿到的是None。理解了这一点你以后再也不会写出“为什么我把print赋值给变量后变量是空的”这种疑惑帖子。第二个细节return语句一旦执行函数立刻结束后面所有代码都不会再跑了。所以return不能放在循环里随便用如果你希望循环把所有结果都收集起来那应该在循环内部把值累加到一个列表循环结束后再返回整个列表。第三个细节也是最多人喜欢的Python支持用逗号分隔返回多个值。比如def get_min_max(numbers): return min(numbers), max(numbers) low, high get_min_max([3, 1, 4, 1, 5, 9, 2, 6]) print(low, high) # 输出1 9这里其实不是真的“返回多个值”而是返回了一个元组(min, max)然后Python自动帮你拆包赋给了两个变量。这个语法用起来很爽但切忌过度——一次返回超过三四个值的函数通常说明它该拆成多个函数了因为调用者已经记不清顺序了。3.2 全局变量与局部变量——函数内部的“小世界”每个函数都像一个独立的小房间里面定义的变量叫局部变量只有房间里的人能看见。外面定义的变量叫全局变量理论上所有人都能看见但“看见”和“修改”是两码事。counter 0 # 全局变量 def increment(): counter 1 # 这里会报错 increment()这段代码会直接抛出UnboundLocalError: local variable counter referenced before assignment。原因比较微妙因为函数内部对counter进行了赋值操作Python编译器就把counter判定为局部变量既然它是局部变量你就不能用它之前的值那会儿它还没被定义呢。想在函数里面修改外面的全局变量唯一的办法是加global声明counter 0 def increment(): global counter counter 1 increment() print(counter) # 输出1但我想真心劝你一句尽量别用global。全局变量是代码里最难追踪的隐形依赖你根本不知道是哪个函数在哪个时间点把这个变量改掉了。工程上更好的方案是把状态封装到类里或者干脆用参数传进传出counter 0 def increment(value): return value 1 counter increment(counter) # 显式地把新值重新赋给外部变量这种写法逻辑一目了然函数不偷摸改外部的东西一切改变都在调用处显式发生。别人读你代码的时候一眼就能看穿数据流向不用费劲去翻函数体。3.3 函数内调用函数——嵌套与链式思维函数内部不仅可以调用其他函数还可以调用自己定义在内部的函数这就是嵌套函数。嵌套函数最常见的应用场景有三个一是内部只需要用一次的辅助逻辑不想污染外层命名空间二是需要捕获外层函数的临时变量形成闭包三是写装饰器的时候。先看一个简单的嵌套例子def outer(): message 外层消息 def inner(): print(message) # 内部函数可以访问外层函数里的变量 inner() outer() # 输出外层消息这种“内部函数能记住外层变量”的能力叫闭包。闭包是Python进阶路上一道大坎但你现在只需要知道函数和变量一样也能被当作对象传来传去、也能嵌套定义。Python里那句名言“函数是一等公民”说的就是这回事。链式思维就是一个函数的输出直接作为另一个函数的输入。比如print(len(Python))实际是先计算len(Python)得到6再把6交给print输出。写复杂逻辑的时候试着把一个大任务拆成几个小函数再像流水线一样把它们串起来这种思维模式不管以后是做数据分析还是写后端接口都用得上。4. 实战演练——从简单到复杂把函数用起来说了这么多理论还是要落到代码上。这个部分我给三个从易到难的实战例子每一步都用函数实现你看完可以自己敲一遍。4.1 定义一个带参数和返回值的函数——数字处理Demo先从最简单的来。写一个判断数字是否是偶数的函数def is_even(number): 判断一个整数是否为偶数是则返回True否则返回False return number % 2 0 print(is_even(4)) # True print(is_even(7)) # False这就是一个最标准的函数形态接收参数处理逻辑返回结果。别看它短它已经把三种要素凑齐了。有了这个函数你可以方便地复用它——比如筛出一组数里的偶数numbers [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] even_numbers [x for x in numbers if is_even(x)] print(even_numbers) # [2, 4, 6, 8, 10]写函数的好处就在于这里is_even这个名字本身就是注释读代码的人不用去猜x % 2 0是什么意思直接看函数名就明白了。4.2 递归函数——函数调用自己的奇妙用法递归指的是函数在函数体里调用自己。这是个让很多初学者头疼的概念但你别怕它背后的思想其实很朴素把一个大问题拆成一个更小的同类问题直到小到可以直接解决。经典例子是斐波那契数列第1项和第2项是1从第3项开始每一项等于前两项之和def fibonacci(n): if n 1 or n 2: return 1 return fibonacci(n - 1) fibonacci(n - 2) print(fibonacci(6)) # 输出8这个过程你可以这样理解要求第6项就必须先求第5项和第4项要求第5项就得求第4项和第3项……一层层拆下去直到拆到第1项和第2项——它们是已知的基准答案递归就到头了然后一层层往回带答案。递归写起来很优雅但有两个致命代价一是每层调用都占用栈空间递归层数太深会直接抛出RecursionError二是重复计算太多比如上面求fibonacci(6)时fibonacci(3)被重复算了好几次效率很低。所以实际生产环境里能用循环解决的问题我一般不写递归只有那些天然具有递归结构的问题——比如遍历文件夹、解析嵌套的JSON——才用递归。4.3 综合小项目——把函数组合起来做一个迷你计算器现在我们把这些知识点拼装起来写一个带菜单的迷你计算器系统。它同时用到定函数、循环、条件判断、返回值、默认参数和可变参数算是基础阶段的综合练习def add(*numbers): return sum(numbers) def subtract(a, b): return a - b def multiply(*numbers): result 1 for n in numbers: result * n return result def divide(a, b): if b 0: return 错误除数不能为0 return a / b def calculator(): print(欢迎使用迷你计算器) print(可选操作1 加法 2 减法 3 乘法 4 除法 0 退出) while True: choice input(请输入操作编号) if choice 0: print(再见) break if choice not in (1, 2, 3, 4): print(无效选项请重新输入) continue if choice 2 or choice 4: a float(input(请输入第一个数)) b float(input(请输入第二个数)) if choice 2: print(结果, subtract(a, b)) else: print(结果, divide(a, b)) else: nums [] while True: value input(请输入数字输入q结束) if value.lower() q: break nums.append(float(value)) if choice 1: print(结果, add(*nums)) else: print(结果, multiply(*nums)) calculator()这个程序用到的知识点都是前面讲过的加法乘法用*nums这种可变参数接任意多个数减法和除法只要两个参数除法里还做了除以0的保护。所有运算逻辑都在独立函数里主函数calculator只负责交互流程。如果将来要加一个“求平方根”的功能你只需要新写一个函数然后在菜单和判断逻辑里各加两行——这就是函数化设计的扩展优势。5. 常见问题排查与避坑速查——我替你先踩过的那些坑函数这块儿的坑说多不多但每一个都够新手折腾一阵子。我把这些年见过的、自己踩过的高频问题整理成一张速查表你遇到类似情况直接对着查。报错信息根本原因解决办法NameError: name xxx is not defined函数还没定义就被调用了或函数名拼写错了把调用挪到定义之后检查函数名拼写TypeError: greet() missing 1 required positional argument: name调用时参数个数不够补上缺失的参数或给这个参数设置默认值UnboundLocalError: local variable x referenced before assignment函数内部对全局变量赋值后被当成局部变量在函数内加global x声明但建议少用TypeError: xxx() takes 2 positional arguments but 3 were given参数个数超了比如忘了某个参数其实用*args接收检查函数定义或用*args吸收多余参数RecursionError: maximum recursion depth exceeded递归没有合适的终止条件或者递归层级过深检查递归基准条件或改成循环除了这些报错之外我再分享几个我实际写代码时才总结出来的经验教科书一般不会专门提醒第一写函数时尽量不要超过10行。如果一个函数体超过10行你就要认真考虑是不是该拆了。拆分的标准是一个函数只做一件事比如“读取文件”和“解析内容”就应该是两个函数而不是混在一起。控制函数体长度不是刻意追求什么风格纯粹是为了让代码好读、好查错。第二适当用docstring记录函数功能。Python有专门的文档字符串语法在函数体的第一行写一个三引号包裹的字符串解释这个函数干什么、参数是什么、返回什么。这不是为了给自己看而是给三个月后的自己看——到那时候你的记忆早就模糊了读docstring比读代码快得多。第三注意函数命名。名字要能表达行为比如get_user_name、calculate_total_price、validate_email。动词开头是惯例因为函数本质上都是在做某个动作。我曾经见过有人把变量命名成l、x1、data2那种代码维护半个月后自己都不想打开看。第四Python内置函数能用的就用。比如len()、sum()、max()、min()、sorted()这些都是经过千锤百炼的底层实现性能比你手写循环要好得多。复制粘贴手写代码前先想一想Python标准库是不是已经给了现成的工具。6. 函数该怎么继续深入——下一步进阶的方向如果你把前面的内容都吃透了下一步就可以往三个方向继续深入。这三个方向都是从“会用函数”走向“用好函数”的必经之路。第一个方向是匿名函数lambda与高阶函数。lambda允许你用一行写出极简的函数体square lambda x: x * x print(square(5)) # 25配合map、filter、sorted这些高阶函数时特别有用。比如按字典里的某个字段排序用lambda写key参数非常清爽students [{name: 小明, score: 90}, {name: 小红, score: 85}] sorted_students sorted(students, keylambda s: s[score], reverseTrue)这个不说你可能不知道但它会在你写数据处理相关代码时频繁出现。第二个方向是装饰器。装饰器本质上是一个接收函数、返回新函数的函数它能在不改动原函数代码的前提下给函数“加功能”。比如你想给某个函数加日志不用去改函数体写个装饰器一套就完事。装饰器是Python里“优雅”二字的代表理解了装饰器你才算真正摸到了Python函数进阶的门。第三个方向是函数式编程思维。用不可变数据和纯函数组合来构建程序少用全局状态多用函数组合。这种思维尤其适合写数据分析管线、后端服务里的核心逻辑处理测试起来极其舒服——输入确定输出就确定不用管上下文。这些进阶内容我就不在这篇基础文里详细展开了但你只要记住一点函数不是语法游戏它是一种组织代码、控制复杂度的思维方式。你越往后写越会发现同样一段逻辑函数拆得好与拆得烂后续的维护成本差距是数量级的。我在带过几个刚入行的开发之后最大的体会就是函数基础打得牢不牢基本决定了一个人三个月后写代码的流畅度。很多人一开始贪快跳过细节直接写项目结果碰到参数传递的坑、作用域的坑往往要卡很久。不如从一开始就把这块吃透——你省下的其实是后面大把的debug时间。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。