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="小明", age=18)关键字参数的本质就是在调用的时候直接指定“这个值给哪个参数”,这样顺序随便打乱都不影响结果。我写代码的习惯是:只有一两个参数的时候用位置参数,自解释性很强;参数一旦超过三个,尤其是那种name、age、city很容易混的,就一律用关键字参数调用,可读性立刻上一个台阶。
2.2 默认参数——给函数装上“自动挡”
有些参数在大多数情况下都是一个固定的值,那就没必要让调用者每次都输入一遍。默认参数就是为此设计的:
def connect(host, port=8080): 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, items=None): 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是一个字典,装着所有以key=value形式传入的参数。
但我想提醒你一句: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, key=lambda s: s["score"], reverse=True)这个不说你可能不知道,但它会在你写数据处理相关代码时频繁出现。
第二个方向是装饰器。装饰器本质上是一个接收函数、返回新函数的函数,它能在不改动原函数代码的前提下给函数“加功能”。比如你想给某个函数加日志,不用去改函数体,写个装饰器一套就完事。装饰器是Python里“优雅”二字的代表,理解了装饰器,你才算真正摸到了Python函数进阶的门。
第三个方向是函数式编程思维。用不可变数据和纯函数组合来构建程序,少用全局状态,多用函数组合。这种思维尤其适合写数据分析管线、后端服务里的核心逻辑处理,测试起来极其舒服——输入确定,输出就确定,不用管上下文。
这些进阶内容我就不在这篇基础文里详细展开了,但你只要记住一点:函数不是语法游戏,它是一种组织代码、控制复杂度的思维方式。你越往后写,越会发现同样一段逻辑,函数拆得好与拆得烂,后续的维护成本差距是数量级的。
我在带过几个刚入行的开发之后,最大的体会就是:函数基础打得牢不牢,基本决定了一个人三个月后写代码的流畅度。很多人一开始贪快,跳过细节直接写项目,结果碰到参数传递的坑、作用域的坑,往往要卡很久。不如从一开始就把这块吃透——你省下的,其实是后面大把的debug时间。