2.1 函数定义、参数与作用域


2.1 函数定义、参数与作用域

本节摘要:函数是代码复用的基本单元,把一段逻辑打包成"输入参数、返回结果"的黑盒。本节讲清楚函数的定义语法、位置参数、关键字参数、默认值、可变参数的取舍,以及局部与全局作用域的边界规则。通过密码强度校验和递归求阶乘两道练习,把"会写函数、会用参数、不踩作用域坑"的能力练出来。

学习目标

阅读完本节,你应当能够:

  1. 定义带参数和返回值的函数,并合理选择参数传递方式
  2. 区分位置参数、关键字参数、默认参数、可变参数的用途
  3. 解释 LEGB 作用域查找规则,避免局部/全局变量的常见陷阱
  4. 理解可变默认参数的坑,并知道如何规避
  5. 独立完成一道涉及函数封装与递归的练习题

问题与直觉

假设你在写一个记账程序,要在好几个地方计算"含税价格"。如果没有函数,你会在每个计算点重复写 price * (1 + tax_rate) 这段逻辑。一旦税率算法变了(比如改成阶梯计税),你得在所有出现这段代码的地方逐个修改——漏改一处就是 bug。

函数解决的就是这个问题:把一段会被重复使用的逻辑打包,取个名字,之后通过名字调用。改逻辑只需要改函数体一处,所有调用点自动生效。这是编程里最基础、也最有价值的"复用"思想。

参数和返回值是函数的两个接口。参数让同一个函数能处理不同的输入,返回值让函数能把处理结果交还给调用者。理解各种参数形式的取舍,本质上是在学"怎么设计一个好用的接口"。作用域则是函数的"隐私边界"——函数内部定义的变量,外面看不见,这个隔离避免了不同函数之间变量名互相污染。

核心原理

2.1 函数定义的基本结构

def add_tax(price, rate=0.1): """给价格加上税率,返回含税价""" return price * (1 + rate) total = add_tax(100) # 110,用默认税率 total = add_tax(100, 0.2) # 120,覆盖默认税率

定义函数用 def,括号里是参数列表,return 把结果交还。紧跟函数体第一行的字符串是文档字符串(docstring),说明这个函数干什么用的——养成写它的习惯,未来你和读你代码的人都会感谢现在的你。

2.2 四种参数形式

Python 函数参数形式丰富,理解它们的分工是设计好函数的关键:

参数形式 写法 用途 例子
位置参数 def f(a, b) 必传,按顺序对应 f(1, 2)
默认参数 def f(a, b=10) 可不传,有默认值 f(1)f(1, 20)
关键字参数 调用时 f(a=1, b=2) 按名字传,顺序无关 提高可读性
可变参数 def f(*args, **kwargs) 接收任意数量参数 灵活的接口

一条重要规则:参数顺序必须是"位置参数 → 默认参数 → 可变位置参数 → 可变关键字参数",否则解释器分不清谁是谁。

2.3 作用域的 LEGB 规则

当你在一个函数里访问一个变量名时,Python 按固定的顺序查找它,这个顺序叫 LEGB

函数内部可以读取外层的全局变量,但如果想在函数内部修改全局变量,必须用 global 关键字声明:

counter = 0 def increment(): global counter # 不写这行,下面的赋值会创建一个局部变量 counter += 1

💡 关键直觉:能用参数传值就别用全局变量。全局变量让函数的依赖隐藏起来——读代码的人看不出这个函数改了哪个外部状态,调试时极难追踪。函数的输入应该全部通过参数进来,输出全部通过返回值出去,这样它才是真正的"黑盒"。

工程实践要点

3.1 可变默认参数的坑

这是 Python 最经典的陷阱之一。看代码:

def add_item(item, lst=[]): # 默认值是空列表——危险! lst.append(item) return lst print(add_item(1)) # [1] print(add_item(2)) # [1, 2] 不是 [2]!默认列表被共享了

原因:默认参数在函数定义时只创建一次,之后所有调用复用同一个对象。可变类型(列表、字典、集合)做默认值时,修改会累积。

正确写法用 None 做哨兵,在函数内部创建:

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

⚠️ 常见坑:凡是默认值是可变对象([]{}set())的,几乎都是 bug。养成用 None 哨兵的习惯。

3.2 返回值的设计

一个函数可以有多个返回值(其实是返回一个元组),用多变量接收很方便:

def min_max(nums): return min(nums), max(nums) lo, hi = min_max([3, 1, 4, 1, 5]) # lo=1, hi=5

但别滥用——如果一个函数动辄返回五六个值,说明它职责太杂,应该拆分。

3.3 lambda:何时用、何时别用

lambda 是匿名小函数,适合"一行就能写完、只用一次"的场景:

# 排序时临时用,写 lambda 比起单独定义一个函数更紧凑 nums.sort(key=lambda x: abs(x))

但一旦逻辑超过一行,或者要复用,就老老实实写 def。把复杂逻辑硬塞进 lambda,可读性会塌方。

3.4 函数文档与类型提示

现代 Python 推崇给函数加类型提示,它不强制类型检查,但能极大提升可读性,也方便编辑器提示:

def greet(name: str, times: int = 1) -> str: """返回重复若干次的问候语""" return (f"Hello, {name}! " * times).strip()

分层练习

基础题:密码强度校验

题面:写一个函数 check_password(pwd),接收一个字符串密码,返回"弱"、"中"、"强"三档之一。规则:长度小于 6 为弱;长度 6 到 9 为中;长度大于等于 10 且同时含字母和数字为强。

思路点拨:典型的"多条件判断 + 返回值"题。先用长度分档,强档再额外检查是否含字母和数字(用字符串方法 isalphaisdigit 配合循环判断)。

参考骨架

def check_password(pwd: str) -> str: # TODO: 实现强度判断 if len(pwd) < 6: return "弱" elif len(pwd) < 10: return "中" else: has_alpha = any(ch.isalpha() for ch in pwd) has_digit = any(ch.isdigit() for ch in pwd) if has_alpha and has_digit: return "强" return "中" print(check_password("123")) # 弱 print(check_password("abc12345")) # 中 print(check_password("abc1234567")) # 强

进阶题:递归求阶乘与斐波那契

题面:用递归实现两个函数:factorial(n) 返回 n 的阶乘,fib(n) 返回第 n 个斐波那契数(fib(0)=0, fib(1)=1, fib(n)=fib(n-1)+fib(n-2))。

思路点拨:递归的核心是"找基准情形 + 找递推关系"。阶乘的基准是 factorial(1)=1,递推是 n * factorial(n-1)。注意递归深度有上限,n 太大会栈溢出。

参考骨架

def factorial(n: int) -> int: if n <= 1: return 1 return n * factorial(n - 1) def fib(n: int) -> int: if n < 2: return n return fib(n - 1) + fib(n - 2)

💡 思考延伸fib 这个递归写法虽然正确,但效率极差——计算 fib(35) 就要等好几秒,因为同一个子问题被反复计算。怎么优化?这就引出"记忆化"和第 4 章的迭代解法。先记下这个痛点。

挑战题:可变参数求和器

题面:写一个函数 sum_all(*args, base=0),能接收任意多个数值参数,加上一个关键字参数 base 作为基数,返回总和。例如 sum_all(1, 2, 3, base=10) 返回 16。

思路点拨*args 收集所有位置参数成元组,base 是带默认值的关键字参数。sum(args) 求和再加 base 即可。

参考骨架

def sum_all(*args, base=0) -> float: return sum(args) + base print(sum_all(1, 2, 3)) # 6 print(sum_all(1, 2, 3, base=10)) # 16

⚠️ 常见坑:可变参数 *args 必须在普通关键字参数之前。写成 def f(base=0, *args) 会让调用时的参数对应混乱。

追问与答疑

参数到底传的是值还是引用? 准确说法是"传对象引用"。对不可变对象(数字、字符串、元组),函数内的重新赋值只影响局部名字,外面纹丝不动;对可变对象(列表、字典),函数内的就地修改外面看得见。看一个最能说明问题的对比:传入列表并 lst.append(x),调用方的列表真的多了元素;传入列表并 lst = [1,2] 重新绑定,调用方毫无感觉。分清"修改对象"和"重新绑定名字"这两种操作,这个问题就再也不会困扰你。

函数应该设计成多少个参数合适? 经验值是一到三个,五个是警戒线。参数多往往说明函数职责太杂,或者该把相关参数收拢成一个字典或类。还有个隐蔽的坏味道:布尔开关参数。create_user(name, is_vip, is_active) 这种设计,调用处的 create_user("张三", True, False) 谁读谁知道——两个布尔根本分不清谁是谁。改成关键字传参能缓解,更彻底的解法是拆成两个函数,各自表达意图。

递归写不出来怎么办? 递归卡壳通常是因为试图在脑子里"展开"整个调用过程,层次一多人脑就栈溢出了。正确姿势是只相信两件事:基准情形是否正确、递推关系是否把问题变小。只要这两条成立,数学归纳法替你保证结果正确,不需要人肉展开。练习时强迫自己用这个"信任跳"的方式写阶乘和斐波那契,写完再挑一个小的 n 手动验证一次即可。

文档字符串要写多长? 一到三行足够:第一句说"做什么",需要时补一句"什么时候用"或"有什么限制"。参数逐一罗列的冗长格式留给公共库,日常函数用一句话讲清职责已经超过八成代码的水准。真正要戒的是空文档——两三个引号夹一句话"见代码"这种,比不写还误导。写文档串的最好时机是写完函数立刻补,隔天再写就要重新读懂一遍自己的代码,成本翻倍。

还有一个初学者常问的问题:函数名到底用中文拼音还是英文?用英文,哪怕词汇简单。原因不只是惯例——编程生态里的关键字、库名、报错全是英文,混入拼音会让代码风格割裂,而且查资料时英文函数名能直接搜到同类讨论。词汇量不够就先用简单词(get、set、calc、check),够用且清晰,没人要求GRE词汇。命名的能力核心是"概括准确",不是"用词高级"。

global 都不能用吗? 能用,但适用面很窄:真正的模块级配置常量不需要 global(那是读不是改);计数器这类会被修改的全局状态,更好的归宿是类的实例属性(第 3 章)或闭包。见到自己想写 global 的时刻,先停一秒问:这个状态能不能作为参数传进来、作为返回值传出去?九成场景答案是能。

补充练习:温度转换函数族

题面:写三个函数:c_to_f(c) 摄氏转华氏(公式为摄氏乘九除五加三十二)、f_to_c(f) 反向转换、convert(temp, to="F") 一个统一入口按目标单位调度前两者。要求带类型提示和文档字符串,并为每个函数写至少两条 assert。

思路点拨:这题练的是"接口分层"——底层两个纯转换函数,上层一个调度入口。调度函数用字符串参数选方向,是"一个函数多种行为"的简单形态。assert 要覆盖正数、负数和零:零度是很好的锚点,摄氏零度对应华氏三十二度。

参考骨架

def c_to_f(c: float) -> float: """摄氏度转华氏度""" return c * 9 / 5 + 32 def f_to_c(f: float) -> float: """华氏度转摄氏度""" return (f - 32) * 5 / 9 def convert(temp: float, to: str = "F") -> float: """统一转换入口,to 为目标单位""" if to == "F": return c_to_f(temp) elif to == "C": return f_to_c(temp) raise ValueError(f"不支持的目标单位:{to}") assert c_to_f(0) == 32 assert round(f_to_c(212), 6) == 100 assert convert(100) == 212

写完留意两处设计细节:浮点断言用 round 容纳精度误差;非法单位抛异常而不是返回原值——把"用错参数"暴露成错误,比静默返回一个语义不明的数字负责得多。

收获清单

  • 函数把重复逻辑打包复用:改一处即处处生效,是代码维护性的基石。
  • 四种参数形式各有用途:位置参数定必传、默认参数给便利、关键字参数提可读性、可变参数增灵活性。
  • 参数顺序固定:位置 → 默认 → *args**kwargs,不能乱。
  • 作用域 LEGB 规则:函数内可读外层,改全局要用 global;但优先用参数传值,别依赖全局。
  • 可变默认参数是经典坑:默认值是 []/{} 的几乎都有 bug,改用 None 哨兵。
  • lambda 只适合单行一次性逻辑:复杂或要复用的就写 def
  • 多返回值用元组:但别返回太多,职责太杂就拆函数。

下一节我们把函数收进模块复用,再加上异常处理,让程序扛得住错误输入。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U