19. 定义与调用函数
19.1) 什么是函数以及为什么它们很重要
函数(function) 是一段有名字的代码块,用于执行特定任务。你在本书中已经一直在使用函数——print()、input()、len()、type() 等等。这些是 Python 提供的 内置函数(built-in functions)。现在你将学习创建自己的 自定义函数(custom functions),用来组织代码并让它可复用。
为什么函数很重要
函数是编写清晰、可维护程序的基础。它们提供了几个关键优势:
1. 代码可复用性
如果没有函数,每当你想执行某个任务时,都需要复制并粘贴相同的代码。考虑在多个地方计算矩形面积:
# 没有函数——重复代码
length1 = 5
width1 = 3
area1 = length1 * width1
print(f"Area 1: {area1}")
length2 = 8
width2 = 4
area2 = length2 * width2
print(f"Area 2: {area2}")
length3 = 10
width3 = 6
area3 = length3 * width3
print(f"Area 3: {area3}")Output:
Area 1: 15
Area 2: 32
Area 3: 60这种重复既繁琐又容易出错。如果你需要更改面积的计算方式(也许要包含单位或进行舍入),你就必须更新每一个位置。函数通过让你把代码写一次并多次使用来解决这个问题。
2. 代码组织
函数把大型程序拆分为更小、更易管理的部分。每个函数处理一个具体任务,使你的代码更容易理解和维护。与其写一个包含数百行的长脚本,不如把相关操作组织到一些有名字的函数中,用清晰的名称表达它们的用途。
3. 抽象(abstraction)
函数把实现细节隐藏在一个简单接口后面。当你调用 len(my_list) 时,你不需要知道 Python 如何统计元素个数——你只需要得到结果。类似地,你的函数也可以为复杂操作提供简单接口,让代码更易用、更易理解。
4. 测试与调试
函数让测试程序的各个部分更容易。你可以先在隔离环境中验证每个函数都能正确工作,然后再把它们组合成更大的程序。当出现问题时,函数帮助你缩小问题发生的位置。
在本章接下来的内容中,你将学习如何定义自己的函数,向它们传递信息,获取返回结果,并清晰地为它们编写文档。这些技能对于编写专业的 Python 代码至关重要。
19.2) 使用 def 定义函数
要在 Python 中创建函数,你需要使用 def 关键字(“define”的缩写)。函数定义的基本结构如下所示:
def function_name():
# 调用函数时运行的代码块
statement1
statement2
# ... 更多语句让我们分解每一部分:
def:告诉 Python 你正在定义函数的关键字function_name:你为函数选择的名称(遵循与变量名相同的规则)():括号,最终会在里面放参数(我们将在下一节讲解)::冒号,标记函数头的结束- 缩进的代码块:构成函数体的语句(必须缩进)
你的第一个函数
下面是一个打印问候语的简单函数:
def greet():
print("Hello!")
print("Welcome to Python functions.")
# Call the function
greet()Output:
Hello!
Welcome to Python functions.当你定义一个函数时,Python 会记住它,但不会立即执行其中的代码。只有当你通过写下函数名并加上括号来 调用(call) 该函数时,代码才会运行:greet()。
函数命名约定
函数名遵循与变量名相同的规则(正如我们在第 3 章学到的):
- 使用小写字母
- 用下划线分隔单词(snake_case)
- 以字母或下划线开头,而不是数字
- 使用能表明函数作用的描述性名称
# 好的函数名
def calculate_total():
pass
def get_user_age():
pass
def display_menu():
pass
# 差的函数名(但语法上有效)
def x(): # 不具描述性
pass
def CalculateTotal(): # 应该使用小写
pass
def calc(): # 过于缩写
pass注意:这里我们使用 pass 作为占位符(正如我们在第 8 章学到的)。它什么也不做,但允许函数定义在语法上完整。
函数可以包含任何代码
函数体可以包含你目前学过的任何 Python 语句:变量赋值、条件判断、循环(loop),甚至调用其他函数。
def check_temperature():
temperature = 72
if temperature > 75:
print("It's warm.")
elif temperature > 60:
print("It's comfortable.")
else:
print("It's cool.")
check_temperature()Output:
It's comfortable.多个函数定义
你可以在程序中按需定义任意数量的函数。每个函数都是独立的,并且可以分别调用:
def morning_greeting():
print("Good morning!")
def evening_greeting():
print("Good evening!")
# Call each function
morning_greeting()
evening_greeting()Output:
Good morning!
Good evening!函数定义顺序
在 Python 中,你必须先定义函数,再调用它。Python 解释器从上到下读取代码,所以如果你尝试在定义之前调用函数,就会得到错误:
# WARNING: This will cause a NameError - for demonstration only
# PROBLEM: Function called before it's defined
say_hello() # NameError: name 'say_hello' is not defined
def say_hello():
print("Hello!")正确顺序是先定义,再调用:
# 正确:先定义
def say_hello():
print("Hello!")
# 然后调用
say_hello()Output:
Hello!不过,只要这些调用发生在所有定义之后,函数也可以调用文件中稍后才定义的其他函数:
def first_function():
print("First function")
second_function() # 这样没问题——在运行时调用
def second_function():
print("Second function")
# 在我们调用第一个函数之前,这两个函数都已定义
first_function()Output:
First function
Second function函数会创建局部作用域
在函数内部创建的变量只在该函数内部存在。这称为 局部作用域(local scope)(我们会在第 21 章详细探讨)。现在先理解:函数内部发生的事情只会留在函数内部:
def create_message():
message = "This is local"
print(message)
create_message()
# This would cause an error:
# print(message) # NameError: name 'message' is not definedOutput:
This is local变量 message 只在函数运行期间存在。一旦函数结束,变量就会消失。
使用 pass 的空函数
有时你想先定义函数结构,之后再实现它。可以使用 pass 作为占位符:
def future_feature():
pass # TODO: 稍后实现这里
# 该函数存在且可以被调用,但什么也不做
future_feature() # Runs without error, does nothing在你先勾勒程序结构、再补充细节时,这很有用。
19.3) 调用函数并传递实参
定义函数会创建一段可复用的代码,但要让函数真正强大,你需要向它们传递信息。这些信息通过 实参(arguments) 传入。
参数 vs 实参
在继续之前,我们先澄清两个经常被混淆的术语:
- 参数(parameter):函数定义中的变量名,用来接收一个值
- 实参(argument):调用函数时传入的实际值
def greet(name): # 'name' is a parameter
print(f"Hello, {name}!")
greet("Alice") # "Alice" is an argumentOutput:
Hello, Alice!可以把参数看作占位符,把实参看作填入这些占位符的实际数据。
定义带参数的函数
要定义一个接收输入的函数,在括号内添加参数名即可:
def greet_person(name):
print(f"Hello, {name}!")
print("Nice to meet you.")
# 使用不同的实参调用
greet_person("Alice")
print() # 为了可读性的空行
greet_person("Bob")Output:
Hello, Alice!
Nice to meet you.
Hello, Bob!
Nice to meet you.参数 name 在函数内部充当变量。每次调用函数时,name 都会取得你提供的实参值。
多个参数
函数可以接收多个参数,用逗号分隔:
def calculate_rectangle_area(length, width):
area = length * width
print(f"A rectangle with length {length} and width {width}")
print(f"has an area of {area} square units.")
calculate_rectangle_area(5, 3)
print()
calculate_rectangle_area(10, 7)Output:
A rectangle with length 5 and width 3
has an area of 15 square units.
A rectangle with length 10 and width 7
has an area of 70 square units.调用带多个参数的函数时,顺序很重要。第一个实参会传给第一个参数,第二个实参传给第二个参数,以此类推。这些称为 位置实参(positional arguments)。
位置实参
使用位置实参时,Python 会根据位置把实参与参数匹配起来:
def describe_pet(animal_type, pet_name):
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name}.")
describe_pet("dog", "Buddy")
print()
describe_pet("cat", "Whiskers")Output:
I have a dog.
My dog's name is Buddy.
I have a cat.
My cat's name is Whiskers.如果你把顺序弄混,就会得到意外结果:
def describe_pet(animal_type, pet_name):
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name}.")
# Arguments in wrong order
describe_pet("Buddy", "dog")Output:
I have a Buddy.
My Buddy's name is dog.这在技术上是有效的 Python,但会产生毫无意义的输出,因为实参位于错误的位置。
关键字实参
为了避免与位置相关的错误,你可以在调用函数时通过显式指定参数名来使用 关键字实参(keyword arguments):
def describe_pet(animal_type, pet_name):
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name}.")
# Using keyword arguments - order doesn't matter
describe_pet(animal_type="dog", pet_name="Buddy")
print()
describe_pet(pet_name="Whiskers", animal_type="cat")Output:
I have a dog.
My dog's name is Buddy.
I have a cat.
My cat's name is Whiskers.使用关键字实参时,顺序并不重要,因为 Python 会按名称而不是位置将实参与参数匹配。
混用位置实参与关键字实参
你可以在一次函数调用中混用位置实参与关键字实参,但位置实参必须在前:
def create_profile(username, email, age):
print(f"Username: {username}")
print(f"Email: {email}")
print(f"Age: {age}")
# Mixing positional and keyword arguments
create_profile("alice123", email="alice@example.com", age=25)Output:
Username: alice123
Email: alice@example.com
Age: 25不过,你不能把位置实参放在关键字实参之后:
# WARNING: This will cause a SyntaxError - for demonstration only
# PROBLEM: Positional argument after keyword argument
# create_profile(username="alice123", "alice@example.com", 25)
# SyntaxError: positional argument follows keyword argument实参数量必须匹配
调用函数时,你必须提供正确数量的实参(除非函数有默认值,我们会在第 20 章讲解):
def add_numbers(a, b):
result = a + b
print(f"{a} + {b} = {result}")
add_numbers(5, 3) # 正确:2 个参数对应 2 个参数形参Output:
5 + 3 = 8提供太少或太多实参会导致错误:
# WARNING: These will cause TypeErrors - for demonstration only
# PROBLEM: Too few arguments
# add_numbers(5)
# TypeError: add_numbers() missing 1 required positional argument: 'b'
# PROBLEM: Too many arguments
# add_numbers(5, 3, 2)
# TypeError: add_numbers() takes 2 positional arguments but 3 were given使用表达式作为实参
实参不一定是简单的值——你可以使用任何表达式:
def display_total(price, quantity):
total = price * quantity
print(f"Total cost: ${total:.2f}")
# 使用表达式作为实参
base_price = 10
display_total(base_price * 1.1, 5) # 加价 10% 后的价格
display_total(15 + 5, 3 * 2) # 两个实参都是表达式Output:
Total cost: $55.00
Total cost: $120.00Python 会先计算每个表达式,然后再把得到的值传给函数。
在函数内部调用函数
函数可以调用其他函数,从而形成一个操作层级结构。这是一种强大的技术,能让你把复杂任务拆分为更小、更易管理的部分。
下面是一个计算房间面积的示例:
def calculate_area(length, width):
return length * width
def display_room_info(room_name, length, width):
area = calculate_area(length, width)
print(f"Room: {room_name}")
print(f"Dimensions: {length} x {width}")
print(f"Area: {area} square feet")
display_room_info("Living Room", 15, 12)Output:
Room: Living Room
Dimensions: 15 x 12
Area: 180 square feet注意:这里我们使用了 return,我们将在下一节详细探讨。现在只要理解:calculate_area() 会把结果发送回调用它的函数即可。
下面是另一个示例,展示函数如何在彼此基础上构建——一个温度转换系统:
def celsius_to_fahrenheit(celsius):
return (celsius * 9/5) + 32
def format_temperature(fahrenheit):
return f"{fahrenheit:.1f}°F"
def display_temperature_conversion(celsius):
fahrenheit = celsius_to_fahrenheit(celsius)
formatted = format_temperature(fahrenheit)
print(f"{celsius}°C equals {formatted}")
# Use the complete conversion system
display_temperature_conversion(25)
display_temperature_conversion(0)
display_temperature_conversion(100)Output:
25°C equals 77.0°F
0°C equals 32.0°F
100°C equals 212.0°F在这个例子中,display_temperature_conversion() 调用 celsius_to_fahrenheit() 来完成转换,然后调用 format_temperature() 来格式化结果。每个函数都有一个单一、清晰的职责,让代码易于理解和维护。
19.4) 使用 return 返回结果
到目前为止,我们的函数执行的是动作(例如打印),但它们没有把值返回给调用它们的代码。return 语句让函数计算一个结果,并把它发送回调用者。
基本的 return 语句
下面是一个计算并返回值的简单函数:
def add_numbers(a, b):
result = a + b
return result
# 捕获返回值
sum_value = add_numbers(5, 3)
print(f"The sum is: {sum_value}")Output:
The sum is: 8当 Python 遇到 return 语句时,会发生两件事:
- 函数立即停止执行(
return之后的任何代码都会被忽略) - 指定的值被发送回调用者
直接返回值
你不需要在返回之前把结果存到变量里。你可以直接返回一个表达式:
def multiply(a, b):
return a * b
result = multiply(4, 7)
print(f"4 × 7 = {result}")Output:
4 × 7 = 28这更简洁,也是简单计算时的首选风格。
使用返回值
一旦函数返回一个值,你就可以在任何需要使用值的地方使用它:
def calculate_discount(price, discount_percent):
discount_amount = price * (discount_percent / 100)
return discount_amount
original_price = 100
discount = calculate_discount(original_price, 20)
# 在计算中使用返回值
final_price = original_price - discount
print(f"Original price: ${original_price:.2f}")
print(f"Discount: ${discount:.2f}")
print(f"Final price: ${final_price:.2f}")Output:
Original price: $100.00
Discount: $20.00
Final price: $80.00return 会立即退出函数
当 Python 执行 return 语句时,函数会立刻停止。任何在 return 之后的代码都不会被执行:
def check_age(age):
if age < 18:
return "Minor"
# 这一行只会在 age >= 18 时运行
return "Adult"
print(check_age(15))
print(check_age(25))Output:
Minor
Adult这种行为对于处理函数中的不同情况很有用。一旦你确定了结果,就可以立即返回,而不需要再检查其他条件。
下面是一个示例,演示 return 如何停止执行:
def process_number(n):
if n < 0:
return "Negative"
print("This line runs for non-negative numbers")
if n == 0:
return "Zero"
print("This line runs for positive numbers")
return "Positive"
print(process_number(-5))
print()
print(process_number(0))
print()
print(process_number(10))Output:
Negative
This line runs for non-negative numbers
Zero
This line runs for non-negative numbers
This line runs for positive numbers
Positive没有 return 的函数
如果一个函数没有 return 语句,或者它只有一个不带值的 return,那么函数会返回 None:
def greet(name):
print(f"Hello, {name}!")
# 没有 return 语句
result = greet("Alice")
print(f"The function returned: {result}")Output:
Hello, Alice!
The function returned: None类似地,一个单独的 return(不带值)也会返回 None:
def process_data(data):
if not data:
return # 提前退出,返回 None
print(f"Processing: {data}")
return "Success"
result1 = process_data("")
result2 = process_data("some data")
print(f"Result 1: {result1}")
print(f"Result 2: {result2}")Output:
Processing: some data
Result 1: None
Result 2: Success返回多个值
Python 函数可以通过用逗号分隔来返回多个值。Python 会自动将它们打包成一个元组(tuple)(正如我们在第 15 章学到的):
def calculate_rectangle(length, width):
area = length * width
perimeter = 2 * (length + width)
return area, perimeter
# 解包返回的元组
rect_area, rect_perimeter = calculate_rectangle(5, 3)
print(f"Area: {rect_area}")
print(f"Perimeter: {rect_perimeter}")Output:
Area: 15
Perimeter: 16你也可以将该元组作为单个值来接收:
def get_student_info():
name = "Alice"
age = 20
grade = "A"
return name, age, grade
# 作为元组捕获
student = get_student_info()
print(f"Student info: {student}")
print(f"Name: {student[0]}")Output:
Student info: ('Alice', 20, 'A')
Name: Alice返回不同类型
函数可以根据情况返回不同类型的值:
def divide(a, b):
if b == 0:
return "Error: Division by zero"
return a / b
result1 = divide(10, 2)
result2 = divide(10, 0)
print(f"10 / 2 = {result1}")
print(f"10 / 0 = {result2}")Output:
10 / 2 = 5.0
10 / 0 = Error: Division by zero虽然这样可行,但通常更好的做法是用不同方式处理错误(我们会在第 VII 部分学习异常)。现在只要理解:函数可以返回不同类型,不过保持一致通常更清晰。
19.5) 使用文档字符串为函数编写文档
随着程序规模增长、你创建更多函数,为每个函数记录它做什么就变得至关重要。Python 提供了使用 文档字符串(docstrings)(documentation strings)来记录函数的内置方式。
什么是文档字符串?
文档字符串(docstring) 是一个字符串字面量,它作为函数(或模块、类、方法)中的第一条语句出现。它描述函数做什么、接受哪些参数、以及返回什么。文档字符串用三引号括起来(""" 或 '''),因此可以跨多行。
def calculate_area(length, width):
"""Calculate the area of a rectangle.
Takes the length and width of a rectangle and returns the area.
"""
return length * width为什么文档字符串很重要
文档字符串有几个重要用途:
- 自文档化(Self-Documentation):它们解释函数做什么,而不需要读者去分析代码
- IDE 支持:许多开发工具会在你使用函数时以工具提示显示文档字符串
- help() 函数:Python 内置的
help()函数会显示文档字符串 - 专业实践:有良好文档的代码更容易维护,也更容易与他人分享
基本文档字符串格式
对于简单函数,一行文档字符串就足够了:
def greet(name):
"""Print a personalized greeting."""
print(f"Hello, {name}!")
# Access the docstring
print(greet.__doc__)Output:
Print a personalized greeting.文档字符串应该是对函数作用的简洁描述,用命令式写法(“Calculate...”“Return...”“Print...”),而不是描述式写法(“This function calculates...”)。
多行文档字符串
对于更复杂的函数,使用包含以下内容的多行文档字符串:
- 第一行的简要摘要
- 一个空行
- 更详细的描述
- 关于参数的信息
- 关于返回值的信息
def calculate_discount(price, discount_percent):
"""Calculate the discounted price.
Takes an original price and a discount percentage, then returns
the amount of discount that should be applied.
Parameters:
price (float): The original price before discount
discount_percent (float): The discount percentage (0-100)
Returns:
float: The discount amount in the same currency as the price
"""
return price * (discount_percent / 100)
# Use help() to see the full docstring
help(calculate_discount)Output:
Help on function calculate_discount in module __main__:
calculate_discount(price, discount_percent)
Calculate the discounted price.
Takes an original price and a discount percentage, then returns
the amount of discount that should be applied.
Parameters:
price (float): The original price before discount
discount_percent (float): The discount percentage (0-100)
Returns:
float: The discount amount in the same currency as the price文档字符串约定
Python 针对如何编写文档字符串有既定约定(记录在 PEP 257 中)。以下是关键指南:
1. 使用三重双引号:"""docstring"""
def good_example():
"""This follows the convention."""
pass
def also_valid():
'''This works but is less common.'''
pass2. 一行文档字符串应该放在一行内:
def add(a, b):
"""Return the sum of a and b."""
return a + b3. 多行文档字符串应先有摘要行,然后是一个空行:
def process_order(order_id, items):
"""Process a customer order and update inventory.
This function validates the order, checks inventory availability,
calculates the total cost, and updates the inventory database.
Parameters:
order_id (str): Unique identifier for the order
items (list): List of item dictionaries with 'product' and 'quantity'
Returns:
dict: Order summary with 'total', 'status', and 'confirmation_number'
"""
# 函数实现写在这里
pass描述参数与返回值
在记录参数与返回值时,要具体说明:
- 参数名:与函数中的实际参数名一致
- 类型:期望的数据类型是什么(我们会在第 43 章学习类型提示)
- 用途:该参数用于做什么
- 返回值:函数返回什么,以及在什么条件下返回
def find_student(student_id, students):
"""Find a student by ID in a list of student records.
Parameters:
student_id (int): The unique ID number of the student to find
students (list): List of student dictionaries, each containing 'id' and 'name'
Returns:
dict: The student dictionary if found, None if not found
"""
for student in students:
if student['id'] == student_id:
return student
return None对具有多种返回类型的函数编写文档字符串
当一个函数根据情况可能返回不同类型时,要记录所有可能性:
def safe_divide(a, b):
"""Divide two numbers with error handling.
Parameters:
a (float): The dividend
b (float): The divisor
Returns:
float: The quotient if division is successful
str: An error message if b is zero
"""
if b == 0:
return "Error: Cannot divide by zero"
return a / b访问文档字符串
你可以通过三种方式访问函数的文档字符串:
1. 使用 __doc__ 属性:
def example():
"""This is an example function."""
pass
print(example.__doc__)Output:
This is an example function.2. 使用 help() 函数:
def calculate_bmi(weight, height):
"""Calculate Body Mass Index.
Parameters:
weight (float): Weight in kilograms
height (float): Height in meters
Returns:
float: BMI value
"""
return weight / (height ** 2)
help(calculate_bmi)Output:
Help on function calculate_bmi in module __main__:
calculate_bmi(weight, height)
Calculate Body Mass Index.
Parameters:
weight (float): Weight in kilograms
height (float): Height in meters
Returns:
float: BMI value3. 在交互式开发环境中:大多数 IDE 和代码编辑器会在你将鼠标悬停在函数名上,或输入函数名时,以工具提示显示文档字符串。
什么时候写文档字符串
你应该为以下情况编写文档字符串:
- 所有公共函数:旨在被程序的其他部分或其他程序员使用的函数
- 复杂函数:任何仅凭名称与参数无法立刻看出用途或行为的函数
- 参数不直观的函数:当仅靠参数名不足以完全说明期望值时
你可能会跳过以下情况的文档字符串:
- 非常简单、显而易见的函数:例如
def add(a, b): return a + b,其名称与参数已经把目的说得非常清楚 - 私有的辅助函数:仅在某个更大函数内部使用的小型内部函数(尽管这些也能从简短文档字符串中受益)
文档字符串不是注释
请记住,文档字符串和注释的用途不同:
- 文档字符串:描述函数做什么以及如何使用它(接口)
- 注释(comments):解释代码内部如何工作(实现)
def calculate_grade(score, total):
"""Calculate the percentage grade from a score.
Parameters:
score (int): Points earned
total (int): Total points possible
Returns:
float: The percentage grade (0-100)
"""
# 避免除以零
if total == 0:
return 0.0
# 计算百分比并四舍五入到小数点后 2 位
percentage = (score / total) * 100
return round(percentage, 2)文档字符串告诉使用者函数做什么以及如何使用。注释则向阅读代码的人解释特定的实现细节。
养成良好的文档习惯
编写清晰的文档字符串是一种能带来回报的习惯:
- 写函数的同时写文档字符串:不要等到以后——当函数目的在你脑中最清晰时就记录下来
- 保持文档字符串更新:当你改变函数行为时,更新它的文档字符串
- 简洁但完整:包含所有必要信息,但避免不必要的冗长
- 在有帮助时使用示例:对于复杂函数,在文档字符串中提供一个用法示例可能非常有价值
良好的文档会让你的代码更专业、更易维护,并且对他人(包括未来的你自己)更有价值。