AI 技术博客
返回首页
Python 基础 23 分钟阅读

def 与 return:函数的定义、参数与返回值

Python函数defreturn参数传递

引言

这是课程 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 关键字定义函数,语法如下:

def 函数名(参数列表):
    函数体
    return 返回值

函数名必须遵循标识符命名规则:以字母或下划线开头,只能包含字母、数字和下划线。Python 社区惯例是用小写字母加下划线分隔(snake_case),如 `calculate_tax` 而不是 `calculateTax`。

函数体的缩进级别必须统一,通常是 4 个空格。Python 用缩进来界定代码块,没有大括号或 `end` 关键字——这是 Python 最明显的视觉特征之一。
def greet(name):
    """向用户打招呼。"""
    message = f"Hello, {name}!"
    return message

greet 是一个最简单的函数:接收一个参数 name,返回一个字符串。注意函数体中的冒号 : 不能省略,否则 Python 解释器会报 SyntaxError。

参数的四种传递方式

Python 函数的参数形式比许多语言更丰富。理解参数传递方式,是写出健壮函数的关键。

位置参数(Positional Arguments):按顺序传入,最常见的方式。

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):通过 参数名=值 的形式传参,不依赖顺序。

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):为参数提供默认值,调用时可以不传。

def repeat(text, times=3):
    return text * times

print(repeat("Hi"))       # HiHiHi(使用默认 times=3)
print(repeat("Hi", 5))    # HiHiHiHiHi

默认参数在函数定义时计算一次,这意味着默认值在函数的整个生命周期内是固定的。如果默认值是可变对象(如列表),会出现非常微妙的行为——这个陷阱我们在「易错点」一节详述。

关键字-only 参数(Keyword-Only Arguments):位于 * 之后,必须以关键字形式传入,不能通过位置传入。

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。

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 语句可以出现在函数体任何位置,一旦执行就立即退出函数,后续代码不会被执行:

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 自动读取。

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 中得以实现的根基。

函数可以赋值给变量:

def square(x):
    return x ** 2

f = square  # 不括号,不调用,只是引用
print(f(5))  # 25,效果等价于 square(5)

函数可以作为参数传入:

def apply(func, value):
    return func(value)

print(apply(square, 4))       # 16
print(apply(lambda x: x+1, 10))  # 11

函数可以从另一个函数中返回:

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 关键字来创建匿名函数:

square = lambda x: x ** 2
add = lambda a, b: a + b

print(square(5))   # 25
print(add(3, 4))   # 7

lambda 的本质与 def 定义的函数完全相同——它返回的是一个函数对象。区别在于 lambda 没有名字,不能包含多条语句,只能写一个表达式。正因如此,lambda 通常不作为独立函数长期存在,而是作为参数临时传入高阶函数:

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 的价值就显现出来了。例如按字符串长度排序:

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() 将一个函数依次作用于序列的每个元素,返回映射后的结果:

nums = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x ** 2, nums))
print(squared)  # [1, 4, 9, 16, 25]

filter() 根据条件函数筛选序列中的元素:

evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens)  # [2, 4]

reduce() 将序列中的元素从左到右累积计算为一个值(需从 functools 导入):

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 的惰性求值特性会带来性能优势,后续篇章会深入讨论。

将函数存储在数据结构中

既然函数是一等对象,就可以把它们放进列表、字典等容器,实现灵活的分发逻辑:

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 的组合使用。

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']}")

运行上述代码,输出如下:

subtotal: 92.25
tax: 7.38
tip: 13.8375
discount: 0.0
total: 113.4675
折扣后总计: 106.09

这段代码里有几个值得注意的细节:

1. **默认参数在定义时绑定**:`tax_rate=0.08` 在 `def` 执行时就已经确定,后续改变同名全局变量不会影响函数的默认值。
2. **提前返回**:空账单检查用 `raise ValueError` 提前终止,避免了后续无效的 `sum()` 操作。
3. **返回值是结构化数据**:返回字典而非单个数字,调用方可以按需取用各个字段,灵活度高。
4. **`round()` 精度处理**:货币计算通常保留两位小数,但注意浮点数精度问题在金融计算中需要 Decimal 模块,这里仅为了演示 simplicity 使用 `round()`。

## 易错点与陷阱

### 陷阱一:可变默认参数

这是 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 作为哨兵值:

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。例如:

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:

def info(name, age):
    print(f"{name}, {age}岁")

# info(age=25, "Alice")  # SyntaxError: positional argument follows keyword argument
info("Alice", age=25)   # 正确写法

记住口诀:位置先行,关键字在后。同时注意,同一个参数不能同时以位置形式和关键字形式传入:

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]。

    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 设计的角度分析这种做法的优缺点。