## 引言
这是课程 01《Python 编程基础与进阶》第四章「函数」的第一篇,也是整个 Python 进阶之路的分水岭。前两章你掌握了变量、类型、运算符,第三章又学会了流程控制(if/elif/else、while、for)。但这些零散的控制流拼在一起,代码很快就会变成一锅粥——重复逻辑不断复制粘贴,函数调用的嵌套越来越深,改一处 bug 要牵动十处代码。
本篇要解决的核心问题是:**如何把一段有独立语义的操作封装起来,以后随时调用?** 答案就是函数(function)。函数不只是「减少重复代码」的工具,它是你组织思维的方式——每一个合理的函数都对应一个明确的职责。读完本篇后,你将能够独立编写包含多种参数形式的函数,理解 return 和 None 之间的微妙差别,写出规范的 docstring,并认识到函数在 Python 里是一种「一等公民」,可以被当作数据一样来操作。
阅读前提:熟悉变量赋值、基本数据类型(int、float、str)、条件判断和循环(第二、三章)。本篇不涉及作用域(留到第 2 篇)、lambda(第 4 篇)和装饰器(第 5 篇)。
## 概念与原理
### 为什么需要函数
函数存在的根本原因是**抽象**(abstraction)。人类处理复杂问题的方式是分层抽象:你开车时不需要知道内燃机的燃烧原理,你写爬虫时不需要知道 TCP 握手的细节。每一层向上只提供接口,向下隐藏实现。函数是这一思想在代码层面的体现。
在 Python 中,函数是一段「被命名」的代码块,它接收输入(参数)、执行操作、产出输出(返回值)。同一个函数可以在程序的任何位置被调用无数次,而无需重复书写相同的代码。更重要的是,函数让你的代码变得可读:`calculate_tax(income)` 比 20 行算术运算更清晰地表达了意图。
### def 语句:函数的定义
Python 用 `def` 关键字定义函数,语法如下:
```text
def 函数名(参数列表):
函数体
return 返回值
```
```text
函数名必须遵循标识符命名规则:以字母或下划线开头,只能包含字母、数字和下划线。Python 社区惯例是用小写字母加下划线分隔(snake_case),如 `calculate_tax` 而不是 `calculateTax`。
函数体的缩进级别必须统一,通常是 4 个空格。Python 用缩进来界定代码块,没有大括号或 `end` 关键字——这是 Python 最明显的视觉特征之一。
```
```python
def greet(name):
"""向用户打招呼。"""
message = f"Hello, {name}!"
return message
```
`greet` 是一个最简单的函数:接收一个参数 `name`,返回一个字符串。注意函数体中的冒号 `:` 不能省略,否则 Python 解释器会报 `SyntaxError`。
### 参数的四种传递方式
Python 函数的参数形式比许多语言更丰富。理解参数传递方式,是写出健壮函数的关键。
**位置参数(Positional Arguments)**:按顺序传入,最常见的方式。
```python
def power(base, exponent):
return base ** exponent
print(power(2, 3)) # 8
print(power(3, 2)) # 9
```
位置参数的顺序至关重要:`power(2, 3)` 和 `power(3, 2)` 结果完全不同。
**关键字参数(Keyword Arguments)**:通过 `参数名=值` 的形式传参,不依赖顺序。
```python
def build_address(city, street, number):
return f"{number}号 {street}, {city}"
print(build_address(city="北京", street="长安街", number=1))
print(build_address(number=1, street="长安街", city="北京")) # 顺序不影响结果
```
关键字参数的优点是可读性强,尤其当参数较多时,`build_address(city="北京", ...)` 比 `build_address("北京", ...)` 更容易理解。
**默认参数(Default Arguments)**:为参数提供默认值,调用时可以不传。
```python
def repeat(text, times=3):
return text * times
print(repeat("Hi")) # HiHiHi(使用默认 times=3)
print(repeat("Hi", 5)) # HiHiHiHiHi
```
默认参数在函数定义时计算一次,这意味着默认值在函数的整个生命周期内是固定的。如果默认值是可变对象(如列表),会出现非常微妙的行为——这个陷阱我们在「易错点」一节详述。
**关键字-only 参数(Keyword-Only Arguments)**:位于 `*` 之后,必须以关键字形式传入,不能通过位置传入。
```python
def connect(host, port, *, timeout=30, retries=3):
print(f"连接到 {host}:{port},超时={timeout}s,重试={retries}次")
connect("localhost", 8080) # 正常
connect("localhost", 8080, timeout=60) # 正常
# connect("localhost", 8080, 60) # TypeError!timeout 必须是关键字参数
```
关键字-only 参数在设计 API 时非常有用:它强制调用者明确说出参数名,避免「 positional arguments gone wild 」的混乱调用。
### return 的语义:值返回与 None
Python 的 `return` 语句有两个作用:终止函数执行,并向调用者返回一个值。如果函数中没有 `return` 语句,或者 `return` 后没有值,函数会隐式返回 `None`。
```python
def explicit_return(x):
return x * 2
def implicit_return(x):
result = x * 2
# 没有 return 语句
def return_none(x):
return None
print(explicit_return(5)) # 10
print(implicit_return(5)) # None
print(return_none(5)) # None
print(implicit_return(5) is None) # True
print(return_none(5) is None) # True
```
`implicit_return` 和 `return_none` 虽然写法不同,但效果完全相同——它们都返回 `None`。`None` 是 Python 的单值对象,表示「没有值」。它是 `NoneType` 的唯一实例,用 `is None`(而非 `== None`)来判断是否为空,这是 Python 社区的惯用法。
需要注意:`return` 语句可以出现在函数体任何位置,一旦执行就立即退出函数,后续代码不会被执行:
```python
def check_positive(n):
if n <= 0:
return "非正数" # 提前返回
return f"{n} 是正数"
print(check_positive(-5)) # 非正数
print(check_positive(7)) # 7 是正数
```
提前返回(early return)是良好的编程习惯,它能避免深层嵌套的 if/else 结构,让主路径更清晰。
### docstring:函数的自文档化
`docstring`(文档字符串)是写在函数体第一行的字符串字面量,用于描述函数的功能、参数和返回值。它不是注释,而是函数的正式文档,会被 `help()` 和 IDE 自动读取。
```python
def calculate_discount(price, discount_rate=0.1):
"""
计算折扣后的价格。
参数:
price (float): 原价,必须为正数。
discount_rate (float): 折扣率,范围 0.0~1.0,默认 0.1。
返回:
float: 折扣后的价格。
"""
if price <= 0:
raise ValueError("价格必须为正数")
return price * (1 - discount_rate)
print(calculate_discount.__doc__) # 直接访问 docstring
help(calculate_discount) # help() 读取 docstring
```
Python 官方推荐使用三引号(`"""..."""`)包裹多行 docstring,第一行是简短概述,第二行空行后是详细说明。PEP 257 是 docstring 的官方规范。好的 docstring 让其他开发者(包括未来的你)在不看代码的情况下就能理解函数的用途。
### 函数是一等对象(First-Class Object)
Python 中函数是「一等公民」——它可以像任何其他对象一样被操作:赋值给变量、作为参数传入其他函数、从函数中返回、存储在数据结构中。这一特性是函数式编程在 Python 中得以实现的根基。
**函数可以赋值给变量:**
```python
def square(x):
return x ** 2
f = square # 不括号,不调用,只是引用
print(f(5)) # 25,效果等价于 square(5)
```
**函数可以作为参数传入:**
```python
def apply(func, value):
return func(value)
print(apply(square, 4)) # 16
print(apply(lambda x: x+1, 10)) # 11
```
**函数可以从另一个函数中返回:**
```python
def make_adder(n):
def add(x):
return x + n
return add
add_5 = make_adder(5)
add_10 = make_adder(10)
print(add_5(3)) # 8
print(add_10(3)) # 13
```
函数作为一等对象的特性,为后面的高阶函数、闭包、装饰器等概念奠定了基础——这些内容将在后续篇章中逐一展开。不过在此之前,有必要先认识一下 Python 中与一等对象密切相关的两个工具:**lambda 表达式**和**functools 模块中的 reduce 函数**。它们展示了函数作为数据的威力。
**lambda:匿名函数的速记写法**
当函数逻辑非常简单(通常只有一行表达式)时,用 `def` 写一个命名函数显得过于繁琐。Python 提供了 `lambda` 关键字来创建匿名函数:
```python
square = lambda x: x ** 2
add = lambda a, b: a + b
print(square(5)) # 25
print(add(3, 4)) # 7
```
lambda 的本质与 `def` 定义的函数完全相同——它返回的是一个函数对象。区别在于 lambda 没有名字,不能包含多条语句,只能写一个表达式。正因如此,lambda 通常不作为独立函数长期存在,而是作为参数临时传入高阶函数:
```python
nums = [3, 1, 4, 1, 5, 9, 2, 6]
even_squares = sorted([x**2 for x in nums if x % 2 == 0])
print(even_squares) # [4, 16, 36]
```
上面的列表推导已经足够简洁,但在更复杂的排序场景中 lambda 的价值就显现出来了。例如按字符串长度排序:
```python
words = ["banana", "pie", "Washington", "book"]
words.sort(key=lambda w: len(w))
print(words) # ['pie', 'book', 'banana', 'Washington']
```
这里 `key=lambda w: len(w)` 告诉 `sort()`:排序的依据不是字符串本身,而是每个字符串的长度。lambda 表达式在排序过程中被反复调用,每次接收一个元素作为参数。
**高阶函数:把函数当作数据来操作**
高阶函数(Higher-Order Function)是指「接收函数作为参数」或「返回函数作为结果」的函数。Python 标准库提供了几个经典的高阶函数:`map()`、`filter()` 和 `functools.reduce()`。
`map()` 将一个函数依次作用于序列的每个元素,返回映射后的结果:
```python
nums = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x ** 2, nums))
print(squared) # [1, 4, 9, 16, 25]
```
`filter()` 根据条件函数筛选序列中的元素:
```python
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens) # [2, 4]
```
`reduce()` 将序列中的元素从左到右累积计算为一个值(需从 `functools` 导入):
```python
from functools import reduce
product = reduce(lambda a, b: a * b, [1, 2, 3, 4, 5])
print(product) # 120
```
这三个函数是函数式编程的核心工具。Python 同时支持列表推导式(第 3 章已讲),在大多数简单场景下推导式比 `map/filter` 更易读。但在某些场景下(如流式处理大数据),`map/filter` 的惰性求值特性会带来性能优势,后续篇章会深入讨论。
**将函数存储在数据结构中**
既然函数是一等对象,就可以把它们放进列表、字典等容器,实现灵活的分发逻辑:
```python
operations = {
"add": lambda a, b: a + b,
"sub": lambda a, b: a - b,
"mul": lambda a, b: a * b,
"div": lambda a, b: a / b if b != 0 else float("inf")
}
print(operations["add"](10, 3)) # 13
print(operations["mul"](10, 3)) # 30
```
这种「查表法」在实现状态机、插件系统、命令路由等场景时非常有用——它避免了冗长的 `if/elif` 链,让代码更具扩展性。
## 实现
下面通过一个完整的示例,综合应用本篇所学的所有概念。这个例子模拟一个简单的账单计算器,展示了参数传递、默认值、提前返回和 docstring 的组合使用。
```python
def calculate_bill(items, tax_rate=0.08, tip_rate=0.15, discount=0.0):
"""
计算餐饮账单。
参数:
items (list): 每道菜的价格列表,元素为数字。
tax_rate (float): 税率,默认 8%。
tip_rate (float): 小费率,默认 15%。
discount (float): 折扣金额,默认 0。
返回:
dict: 包含子项(subtotal/tax/tip/discount/total)的字典。
异常:
ValueError: items 为空或含有负数价格时抛出。
"""
if not items:
raise ValueError("账单不能为空")
subtotal = sum(items)
if subtotal < discount:
raise ValueError("折扣金额不能超过消费总额")
tax = subtotal * tax_rate
tip = subtotal * tip_rate
total = subtotal + tax + tip - discount
return {
"subtotal": round(subtotal, 2),
"tax": round(tax, 2),
"tip": round(tip, 2),
"discount": round(discount, 2),
"total": round(total, 2)
}
# ---- 使用示例 ----
order = [28.5, 32.0, 19.75, 12.0]
result = calculate_bill(order)
for key, value in result.items():
print(f"{key}: {value}")
result_with_discount = calculate_bill(order, discount=10.0, tip_rate=0.18)
print(f"折扣后总计: {result_with_discount['total']}")
```
运行上述代码,输出如下:
```text
subtotal: 92.25
tax: 7.38
tip: 13.8375
discount: 0.0
total: 113.4675
折扣后总计: 106.09
```
```text
这段代码里有几个值得注意的细节:
1. **默认参数在定义时绑定**:`tax_rate=0.08` 在 `def` 执行时就已经确定,后续改变同名全局变量不会影响函数的默认值。
2. **提前返回**:空账单检查用 `raise ValueError` 提前终止,避免了后续无效的 `sum()` 操作。
3. **返回值是结构化数据**:返回字典而非单个数字,调用方可以按需取用各个字段,灵活度高。
4. **`round()` 精度处理**:货币计算通常保留两位小数,但注意浮点数精度问题在金融计算中需要 Decimal 模块,这里仅为了演示 simplicity 使用 `round()`。
## 易错点与陷阱
### 陷阱一:可变默认参数
这是 Python 新手最容易踩的坑之一。默认参数在函数**定义时**计算一次,而不是每次调用时重新计算。如果默认值是可变的(如列表、字典),所有调用共享同一个对象。
```
```python
def append_item(item, shopping_list=[]):
shopping_list.append(item)
return shopping_list
print(append_item("苹果")) # ['苹果']
print(append_item("香蕉")) # ['苹果', '香蕉'] ← 不是预期的 ['香蕉']
print(append_item("橙子")) # ['苹果', '香蕉', '橙子']
```
每次调用 `append_item` 都在同一个空列表上追加,因为 `shopping_list=[]` 只在函数定义时被求值一次。修复方法是用 `None` 作为哨兵值:
```python
def append_item_fixed(item, shopping_list=None):
if shopping_list is None:
shopping_list = []
shopping_list.append(item)
return shopping_list
print(append_item_fixed("苹果")) # ['苹果']
print(append_item_fixed("香蕉")) # ['香蕉'] ← 正确
```
### 陷阱二:return 与 None 的混淆
很多初学者写递归或复杂逻辑时,会忘记在某些分支写 `return`,导致函数意外返回 `None`。例如:
```python
def find_max(numbers):
for i in range(1, len(numbers)):
if numbers[i] > numbers[0]:
numbers[0] = numbers[i]
# 忘记 return!
result = find_max([3, 1, 4, 1, 5])
print(result) # None!而不是 5
```
这个 bug 很难发现,因为函数没有报错,只是静默地返回了 `None`。防御性编程的做法是:在函数的每个出口路径上都明确写出 `return`,或者在函数末尾放置一个默认的 `return` 语句。
### 陷阱三:位置参数与关键字参数混用时顺序错误
Python 要求位置参数必须在关键字参数之前,否则报 `SyntaxError`:
```python
def info(name, age):
print(f"{name}, {age}岁")
# info(age=25, "Alice") # SyntaxError: positional argument follows keyword argument
info("Alice", age=25) # 正确写法
```
记住口诀:**位置先行,关键字在后**。同时注意,同一个参数不能同时以位置形式和关键字形式传入:
```python
info("Alice", 25) # 合法
info(name="Alice", age=25) # 合法
info("Alice", age=25) # 合法(混用)
info("Alice", 25, age=25) # TypeError: info() got multiple values for argument 'age'
```
## 小结
本篇系统讲解了 Python 函数的定义与使用。我们从 `def` 的基本语法出发,深入讨论了四种参数传递方式(位置、关键字、默认值、关键字-only),理解了 `return` 的两种形态——显式返回值与隐式返回 `None`,以及如何用 docstring 让函数自文档化。最后通过「函数是一等对象」这一核心概念,揭示了 Python 函数之所以强大的根源:函数可以像数据一样被传递、存储和组合。
函数的本质是**抽象的边界**——它把一段逻辑封装成一个有名字的单元,对外只暴露接口,对内隐藏细节。掌握函数的定义和使用,是写出可维护、可扩展代码的第一步。下一篇我们将深入作用域与命名空间,理解变量在函数内外如何查找,以及闭包是如何形成的。
## 练习与思考题
1. 编写函数 `celsius_to_fahrenheit(celsius)`,将摄氏度转换为华氏度(公式:F = C × 9/5 + 32)。要求:如果输入为负数,打印警告信息但仍完成转换;函数需要包含 docstring。
2. 下面这段代码的输出是什么?为什么?尝试修改它以产生预期输出 `[0, 1, 4, 9]`。
```python
funcs = []
for i in range(4):
funcs.append(lambda x: x * i)
print([f(2) for f in funcs])
```
3. 编写一个函数 `parse_csv_line(line)`,接收一个 CSV 格式的字符串(如 `"Alice,25,Beijing"`),返回一个字典 `{"name": "Alice", "age": 25, "city": "Beijing"}`。使用关键字参数指定列名映射,使函数可以同时处理不同格式的 CSV 行。
4. 思考题:为什么 Python 规定关键字-only 参数要放在 `*` 之后?从函数签名可读性和 API 设计的角度分析这种做法的优缺点。