☰
Python函数从语法到设计思维:参数、作用域与递归实战练习
2026/10/9 3:58:12 网站建设 项目流程

1. 函数练习到底在练什么:先想清楚再动手

我刚带完一批从零开始学 Python 的初学者,发现大家卡得最多的知识点,不是循环也不是列表,而是函数。说实在的,函数本身语法并不复杂,真正难的是"脑子里有没有函数思维"。标题是"python函数练习",那就得先搞清楚:我们练的到底是什么?是记住def怎么写吗?不是。是背熟各种参数类型吗?也不全是。

函数练习的核心,是帮你在脑袋里建立一种"拆分任务、封装逻辑、复用代码"的思维方式。用生活里的话说,函数就是一台"加工机器":你从左边丢进去原料,它从右边吐出成品。至于机器内部怎么运作、要不要换零件、能不能换成更快的版本,只要外部接口不变,调用的人根本不用关心。

我把函数练习拆成三个层次,这是我自己教学和写代码时反复验证过的一条路径。

第一层:语法层。能写出合法的函数定义,知道参数怎么传、返回值怎么拿。这是最基础的,也是大部分人以为的"会函数"。

第二层:机制层。理解参数传递本质上是"对象引用传递",理解作用域、命名空间,知道为什么在函数内部修改列表会影响外部,为什么整数却不会。这一层决定了你调试 bug 的速度。

第三层:设计层。拿到一个需求,能敏锐地识别出哪些代码应该抽成函数,函数边界画在哪里,参数和返回值怎么设计才合理,函数怎么复用、怎么测试。这一层才是工程师和脚本小子的分水岭。

练习时要刻意往第二层和第三层靠。很多同学刷了几十道题,把def写到条件反射,但一遇到稍微复杂的项目依然无从下手,就是因为只练了第一层。我后面给出的练习序列,也是按这三个层次设计的,大家跟着走就行。

2. 语法细节抠死:定义、调用、参数与返回值

2.1 函数的基本定义结构

先看最简单的一段代码:

def greet(name): """向指定的人问好""" return f"你好,{name}!"

这里有几个容易忽略的点。def关键字后面是函数名,命名要能表达用途,建议用动词或动词加名词的组合,比如calc_average、is_valid_user,别用func1、test2这种名字。冒号不能丢,函数体要有缩进,通常 4 个空格。

函数体里第一个字符串如果是文档字符串,会被 Python 自动识别为__doc__属性,很多工具和 IDE 都能读取它来做提示。别嫌麻烦,哪怕练手的函数也写出那句注释,养成的习惯会在你写正式项目时省下大量沟通成本。

这里有个细节值得多说一句:函数体内的代码在定义阶段并不会执行,只有调用时才会真正运行。很多人第一次写函数时发现没报错、也没输出,以为写错了,其实这是正常现象。你可以理解成"定义阶段只是把配方登记在册,真正炒菜是调用的时候"。

2.2 五种参数类型与调用方式

Python 的参数体系比很多语言都灵活,初学者容易混。我建议直接记一张表:

参数类型定义位置特性示例
位置参数无默认值的参数按顺序传入,必填def f(a, b)
默认参数有默认值的参数可省略,省略时用默认值def f(a, b=10)
关键字参数调用时指定不受顺序限制f(b=2, a=1)
可变位置参数*args收集多余的位置参数为元组def f(*args)
可变关键字参数**kwargs收集多余的关键字参数为字典def f(**kwargs)

这里最容易踩的坑是"默认参数和普通参数的顺序"。定义时,默认参数必须放在非默认参数后面,否则直接语法报错。

# 正确 def build_user(name, age=18): pass # 错误:SyntaxError def build_user(name=18, age): pass

调用时也有规矩:位置参数必须在关键字参数之前。

# 正确 print(calc(2, 3, mode="sum")) # 错误:SyntaxError print(calc(2, mode="sum", 3))

*args和**kwargs是很多进阶代码里的常客,它们的价值在于提高函数的扩展性。比如你要写一个日志函数,未来可能加各种额外字段,用**kwargs兜底就不会因为参数变化而破坏已有调用。

2.3 返回值与 None 的隐藏问题

很多初学者不知道,Python 里每个函数都有返回值。如果你没有写return,函数会隐式返回None。这个特性带来一个经典问题:函数到底是有返回值还是只是执行副作用?

def add(a, b): result = a + b # 忘记 return total = add(1, 2) print(total) # 输出 None

我在教学里反复强调:如果一个函数名为add、calc、build,它应该是纯计算函数,必须有return;如果一个函数名为print_xxx、send_xxx、save_xxx,它主要负责副作用,可以不返回值。这种命名约定能帮你和读代码的人快速建立预期。

另一个容易踩的坑是return会立即结束函数,return后面的代码不会执行。这在写条件分支时特别容易出错。我见过一个同学在if条件里先返回了一个值,然后在函数末尾又写了一个return,第二段代码永远跑不到,他还硬说是"保险起见"。如果你也有这个习惯,建议尽早戒掉,因为这种写法往往掩盖了逻辑错误。

2.4 作用域:局部变量与全局变量

作用域是函数练习里最容易被忽略、却最影响排查 bug 的部分。简单来说:函数内部定义的变量是局部变量,函数外部定义的是全局变量。局部变量只在函数内部可见。

count = 10 def change(): count = 20 change() print(count) # 输出 10

这段代码里,函数内的count = 20并不会修改外面的全局变量,而是创建了一个新的局部变量。如果想在函数里修改全局变量,必须用global声明:

count = 10 def change(): global count count = 20 change() print(count) # 输出 20

我的建议是:练习阶段可以试试global,但实际项目里要极力避免。无节制地用全局变量会让函数失去独立性,你不再能"把函数搬走单独使用",所有状态都藏在全局环境里,排错成本直线上升。正确的思路是:把外部数据通过参数传入,把计算结果通过return交出去。函数越"封闭",越可靠。

3. 实操:从计算器到递归再到高阶函数

3.1 基础练习:用计算器案例开胃

建议第一组练习写一个带菜单的四则运算计算器,用一个主函数把逻辑串起来。这个练习覆盖了函数定义、参数、返回值、循环和条件判断,是很好的综合热身。

def add(a, b): return a + b def subtract(a, b): return a - b def multiply(a, b): return a * b def divide(a, b): if b == 0: return "除数不能为 0" return a / b def run(): while True: print("1:加 2:减 3:乘 4:除 0:退出") choice = input("请选择:") if choice == "0": break a = float(input("输入第一个数:")) b = float(input("输入第二个数:")) if choice == "1": print(add(a, b)) elif choice == "2": print(subtract(a, b)) elif choice == "3": print(multiply(a, b)) elif choice == "4": print(divide(a, b)) else: print("无效选项") if __name__ == "__main__": run()

这段代码里有个值得借鉴的设计:把每种运算都做成独立的小函数,主函数只负责交互流程。这就是"单一职责"的雏形,后面会详细讲。

练习时可以尝试给这个程序加点功能,比如连续计算、历史记录。每加一个功能,你都会更强烈地体会到:如果把所有逻辑堆在run()里,函数会越来越臃肿;只有一开始就拆好函数,扩展才不痛苦。

3.2 参数练习:默认值、可变参数与关键字参数

第二组练习专门打磨参数理解。我出过一个经典题:写一个函数,接收任意多个数字,返回它们的平均值;如果调用时传入一个weighted参数,则按权重计算加权平均。

def average(*nums, weighted=False, **kwargs): if not nums: return 0 if weighted: weights = kwargs.get("weights", []) if len(weights) != len(nums): raise ValueError("权重数量与数据数量不一致") total = sum(n * w for n, w in zip(nums, weights)) weight_sum = sum(weights) return total / weight_sum if weight_sum else 0 return sum(nums) / len(nums) print(average(1, 2, 3, 4)) # 2.5 print(average(1, 2, 3, 4, weighted=True, weights=[1, 1, 2, 2]))

这个函数是参数类型的综合练兵场。*nums收集任意多个位置参数,weighted是带默认值的关键字参数,**kwargs接收了额外的weights。练习时你还可以试试把weights改成普通关键字参数,比较一下两者的调用体验差异。

我建议每天都做一组"手写参数签名"的小练习:给定一个业务需求,你自己设计函数签名,再写测试用例去验证。这个过程训练的是你对数据入口和出口的判断力,比机械刷题有用得多。

3.3 递归练习:阶乘、斐波那契与汉诺塔

递归是函数练习里绕不开的难点。它的本质是"函数调用自己",但很多人只记住了这个语感,没有真正理解递归需要两个要素:基线条件(终止条件)和递推关系(向基线逼近)。

先看阶乘:

def factorial(n): if n <= 1: return 1 return n * factorial(n - 1)

n <= 1是基线,n * factorial(n - 1)是递推。每调用一次,问题规模就缩小一点,直到触底。

再看斐波那契数列:

def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2)

注意:这个写法虽然简洁,但效率极差。fib(40)会重复计算海量子问题,运行时间会明显卡顿。练习时可以用fib(35)感受一下,然后试试用字典做记忆化缓存:

cache = {} def fib_memo(n): if n in cache: return cache[n] if n <= 1: return n result = fib_memo(n - 1) + fib_memo(n - 2) cache[n] = result return result

从"能写递归"到"能写出高效递归",中间隔着一个重要认知:递归不是炫技,它是在问题天然具有自相似性时才该用的工具。像遍历树形结构、计算目录大小、解析嵌套括号这类场景,递归非常顺手;而像求和、遍历列表这种线性问题,用循环更直观。练习时最好把同一个问题用循环和递归各写一遍,对比两种思路的差异。汉诺塔则是检验递归理解的经典案例,代码只有几行,但完整推演一遍移动顺序,你会对"函数调用栈"有更直观的感受。

3.4 高阶函数练习:map、filter、sorted 与 lambda

当函数能作为参数传给另一个函数时,它就变成了"高阶函数"。Python 里最常见的几个高阶函数是map、filter、sorted,搭配lambda使用非常简洁。但这里我要提醒一句:初学者喜欢追逐这种"一行代码"的炫技写法,可以先练习,但不能沉迷。比如:

nums = [1, 2, 3, 4, 5, 6] # map:对每个元素应用函数,得到一个迭代器 squared = list(map(lambda x: x ** 2, nums)) # filter:按条件筛选 evens = list(filter(lambda x: x % 2 == 0, nums)) # sorted:按自定义规则排序 words = ["banana", "apple", "cherry"] sorted_by_len = sorted(words, key=lambda w: len(w))

我自己实战中的习惯是:如果逻辑比较直白,用map/filter没问题;但一旦逻辑复杂起来,我宁愿写成列表推导式或普通循环。因为列表推导式[x ** 2 for x in nums]的阅读门槛更低,大多数人扫一眼就能看懂。可读性永远是第一位的。

lambda的局限性也值得注意:它只能写单行表达式,不能包含赋值和语句。练习时见过不少同学硬把复杂逻辑塞进lambda,写出来又长又难读,那还不如定义一个普通函数。高阶函数练习的真正目标是让你理解"函数是一等公民"——函数可以赋值给变量、放进列表、作为参数传递、作为返回值。这种思维方式是通往闭包和装饰器的必经之路。

4. 常见错误与排查技巧实录

4.1 NameError:函数未定义或命名冲突

NameError: name 'xxx' is not defined是初学函数阶段出现频率最高的报错。原因通常有两种:

  1. 函数名拼写错误或漏写定义。
  2. 函数定义在调用语句之后,但调用语句在模块顶层先执行了。

比如下面这种:

print(double(10)) def double(x): return x * 2

模块加载时,Python 是先从上到下执行的。执行到print(double(10))这一行时,double还没有被定义,所以立刻报NameError。解决办法是:把函数定义放在调用之前,或者把调用代码挪进if __name__ == "__main__":块里。这也是我一直建议养成的好习惯,它能让模块在被 import 时不触发意外执行。

4.2 UnboundLocalError:局部变量陷阱

UnboundLocalError: local variable 'x' referenced before assignment这个报错很能迷惑人。看这段代码:

total = 100 def add_to_total(n): total = total + n # 这里报错 return total print(add_to_total(10))

你以为total用的是全局变量,但 Python 在编译函数时发现函数体内有total = ...的赋值语句,于是把total标记为局部变量。既然它是局部变量,那么在赋值之前被引用就是错误的。

这种报错让人抓狂的地方在于:代码看起来"挺对的",报错信息却是"未定义"。如果你真需要在函数里修改全局变量,用global;但更推荐的做法是把它改成纯函数:

def add_to_total(total, n): return total + n print(add_to_total(100, 10))

用参数传入旧值,返回新值,函数就不依赖外部状态了。

4.3 可变默认参数:同一个列表的坑

这是 Python 函数面试题里最经典的坑,练习阶段很值得亲手踩一次。

def add_item(item, items=[]): items.append(item) return items print(add_item(1)) # [1] print(add_item(2)) # [1, 2] ?!

第二次调用时,items默认值居然是上一次调用后留下的[1]。原因是:默认参数在函数定义时只会被创建一次,之后每次调用如果没有传入新值,用的都是同一个列表对象。这个现象对不可变对象没影响,比如默认值是0、"hello"、None就没事,但默认值是列表、字典、集合时风险很大。

正确写法是用None占位,在函数体内新建可变对象:

def add_item(item, items=None): if items is None: items = [] items.append(item) return items

这一条我强烈建议你亲手跑一遍,记住报错时的表情,以后写代码就会条件反射地避免可变默认参数了。

4.4 RecursionError:递归过深的处理

递归写起来简洁,但 Python 默认的递归深度限制只有约 1000 层。一旦你的递归没有正确触底,或者问题规模太大,就会报RecursionError: maximum recursion depth exceeded。

遇到这个报错,先别急着调大递归限制。我的检查顺序是:

  1. 确认基线条件是否写对,是否会无限向下递归。
  2. 确认每次递归是否确实向基线逼近。
  3. 如果递归深度确实无法降低,考虑改用循环或迭代算法。

可以使用以下方式查看和调整限制,但这只是临时方案:

import sys print(sys.getrecursionlimit()) # 默认 1000 sys.setrecursionlimit(2000) # 谨慎调整

实际上,真正生产环境里大部分递归场景都可以改成循环或使用栈模拟。练习阶段,我更建议把每个递归函数都追问一遍:这个递归的深度会随输入增长到什么级别?如果随输入线性增长到百万级,那无论如何也不能用递归。

4.5 调试小技巧:打印和不打印

初学者调试函数最常用的手段是print,但很多人不知道打印要讲究策略。我的习惯是:在函数的入口打印进入函数 + 参数值,在出口打印返回值。像这样:

def calc_discount(price, rate=0.8): print(f"[DEBUG] 进入 calc_discount, price={price}, rate={rate}") result = price * rate print(f"[DEBUG] 返回 {result}") return result

这段代码跑完,你立刻能看出参数有没有传错、逻辑有没有走到意外分支。排查完之后再把这些调试打印删掉,或者用一个统一的调试开关控制。在函数练习阶段,这种"入口出口打印法"比单步调试的效率高得多,也更容易帮你理解调用的嵌套顺序。

5. 从练习走向实战:函数的设计思维

5.1 单一职责原则:一个函数只做一件事

函数练习练到一定程度,重点就不再是语法了,而是设计质量。我见过很多同学能把每个知识点写得很溜,但写出来的函数动辄一百多行,参数七八个,逻辑剪不断理还乱。

一个合格函数的验证标准很简单:你用一句话能不能说清它做了什么?如果这句话里出现"并且""同时"这种词,说明函数职责太多,应该拆分。比如"读取文件并统计词频并返回结果"应该拆成read_file、count_words、format_result三个函数。每个函数只做一件事,测试、排错、复用都容易得多。

练习时可以拿一个旧函数做"重构练习":把一个大函数拆成多个小函数,确保调用结果完全不变。这个过程会让你逐渐建立"函数边界"的直觉。

5.2 类型提示与文档字符串

从 Python 3.5 开始,类型提示逐渐成为工程标配。写函数时给参数和返回值标注类型,对阅读者和编辑器都是一种保障。练习时可以顺手加上:

def calculate_bmi(weight: float, height: float) -> float: """计算 BMI 指数。 Args: weight: 体重,单位千克 height: 身高,单位米 Returns: BMI 值,保留两位小数 """ if height <= 0: raise ValueError("身高必须大于 0") return round(weight / (height ** 2), 2)

类型提示并不是强制约束,Python 也不会因为标注类型而拦截错误参数。它的真正价值是让函数签名变成一种"自动文档",配合 IDE 的提示,写代码时不容易犯低级错误。

5.3 闭包与装饰器:进阶的必经之路

函数返回函数、函数嵌套函数,这是函数练习里最让新手觉得"烧脑"的部分。先看闭包:

def make_multiplier(n): def multiplier(x): return x * n return multiplier double = make_multiplier(2) triple = make_multiplier(3) print(double(5)) # 10 print(triple(5)) # 15

multiplier能访问外层函数make_multiplier的局部变量n,即使外层函数已经执行完毕,这个绑定关系依旧存在。这个特性就叫闭包。理解闭包后,装饰器就水到渠成了。

装饰器本质上就是一个接收函数、返回新函数的函数:

def log_decorator(func): def wrapper(*args, **kwargs): print(f"调用 {func.__name__}, 参数: {args} {kwargs}") result = func(*args, **kwargs) print(f"返回结果: {result}") return result return wrapper @log_decorator def add(a, b): return a + b print(add(2, 3))

这段代码里的*args, **kwargs是装饰器能够"适配任意函数"的关键。练习时可以自己写一个计时装饰器、一个重试装饰器、一个权限检查装饰器。每一个都能加深你对"函数也是一等公民"的理解。

5.4 练习如何沉淀成工程能力

函数练习的终极目标不是做题,而是把它变成日常写代码的肌肉记忆。我个人的体会是,当你写任何一段代码前,先花十秒钟想三个问题:

  1. 这段逻辑以后会不会重复出现?
  2. 它需要的输入是什么?应该输出什么?
  3. 能不能把它写成不依赖全局状态的纯函数?

如果答案是"会重复""输入输出明确""可以做成纯函数",那就别犹豫,拆函数。哪怕是十行的小逻辑,拆出来也值得。坚持一段时间,你会发现自己的代码结构越来越清晰,调试时间越来越短。

最后分享一个我常用的练习扩展:把每天写的脚本,第二天回来再重构一次。很多当时觉得"只能这么写"的函数,过了一天再看,往往能找出更精简、更清晰的写法。这种往复迭代,才是函数能力真正提升的过程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询