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

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

## 引言 这是课程 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 设计的角度分析这种做法的优缺点。