20. 函数参数与实参
在第 19 章中,我们学习了如何定义并调用带有基础参数的函数(function)。现在我们将深入探索 Python 灵活的参数(parameter)与实参(argument)系统。理解这些机制能让你编写既强大又易用的函数。
20.1) 位置实参与关键字实参
当你调用一个函数时,你可以用两种基本方式传入实参:按位置或按名称(关键字)。
20.1.1) 位置实参
位置实参会根据它们的顺序与参数进行匹配。第一个实参传给第一个参数,第二个传给第二个,依此类推。
def calculate_discount(price, discount_percent):
"""Calculate the final price after applying a discount."""
discount_amount = price * (discount_percent / 100)
final_price = price - discount_amount
return final_price
# 按位置传入实参
result = calculate_discount(100, 20)
print(result)输出:
80.0在这个例子中,100 会仅仅基于它在函数调用中的位置被赋给 price,而 20 会被赋给 discount_percent。
对于位置实参,顺序至关重要:
# 示例:我们想计算一个 $100 的商品打 20% 折扣
# 正确顺序:先 price,再 discount
print(calculate_discount(100, 20))
# 错误顺序:先 discount,再 price
print(calculate_discount(20, 100))输出:
80.0
-16.0当你交换这些实参时,Python 并不知道你犯了错——它只是按顺序进行赋值。这会产生一个数学上有效但逻辑上错误的结果(负价格!)。
20.1.2) 关键字实参
关键字实参通过使用参数名、等号以及值,明确指定哪个参数接收哪个值。这会让你的代码更易读,并防止因顺序错误导致的问题。
def create_user_profile(username, email, age):
"""Create a user profile with the given information."""
profile = f"User: {username}\nEmail: {email}\nAge: {age}"
return profile
# 使用关键字实参
profile = create_user_profile(username="alice_smith", email="alice@example.com", age=28)
print(profile)输出:
User: alice_smith
Email: alice@example.com
Age: 28使用关键字实参时,顺序不重要:
# 相同结果,不同顺序
profile1 = create_user_profile(username="bob", email="bob@example.com", age=35)
profile2 = create_user_profile(age=35, username="bob", email="bob@example.com")
profile3 = create_user_profile(email="bob@example.com", age=35, username="bob")
# 三者产生完全相同的结果
print(profile1 == profile2 == profile3)输出:
True当一个函数有很多参数时,这种灵活性尤其有价值,因为它能让你很容易看出哪个值对应哪个参数。
20.1.3) 混用位置实参与关键字实参
你可以在一次函数调用中同时结合两种风格,但有一条重要规则:位置实参必须放在关键字实参之前。
def format_address(street, city, state, zip_code):
"""Format a mailing address."""
return f"{street}\n{city}, {state} {zip_code}"
# 合法:先位置实参,再关键字实参
address = format_address("123 Main St", "Springfield", state="IL", zip_code="62701")
print(address)输出:
123 Main St
Springfield, IL 62701这里,"123 Main St" 和 "Springfield" 是位置实参(被赋给 street 和 city),而 state 与 zip_code 则通过名称指定。
如果尝试把位置实参放到关键字实参之后,会导致错误:
# 非法:关键字实参之后出现位置实参
# address = format_address(street="123 Main St", "Springfield", state="IL", zip_code="62701")
# SyntaxError: positional argument follows keyword argumentPython 强制执行这条规则,因为一旦你开始使用关键字实参,后续未命名的实参应该填充哪个位置参数就会变得含糊不清。
20.1.4) 何时使用哪种风格
在以下情况使用位置实参:
- 函数参数很少(通常 1-3 个)
- 参数顺序显而易见且直观
- 函数非常常用,并且顺序广为人知
# 显而易见且简洁
print(len("hello"))
result = max(10, 20, 5)在以下情况使用关键字实参:
- 函数有很多参数
- 参数含义并不是立刻就很清楚
- 你想跳过一些带默认值的参数(下一节会讲)
- 你想让代码“自带说明”(self-documenting)
# 清晰且明确
user = create_user_profile(username="charlie", email="charlie@example.com", age=42)20.2) 默认参数值
函数可以为参数指定默认值。当调用者没有为带默认值的参数提供实参时,Python 会改用默认值。
20.2.1) 定义带默认值的参数
默认值通过赋值运算符在函数定义中指定:
def greet_user(name, greeting="Hello"):
"""Greet a user with a customizable greeting."""
return f"{greeting}, {name}!"
# 使用默认问候语
print(greet_user("Alice"))
# 提供自定义问候语
print(greet_user("Bob", "Good morning"))
print(greet_user("Carol", greeting="Hi"))输出:
Hello, Alice!
Good morning, Bob!
Hi, Carol!参数 greeting 的默认值是 "Hello"。当你调用 greet_user("Alice") 时,Python 会使用这个默认值。当你提供第二个实参时,它会覆盖默认值。
20.2.2) 带默认值的参数必须放在必需参数之后
Python 要求:带默认值的参数必须出现在所有不带默认值的参数之后。这条规则防止出现“哪个实参对应哪个参数”的歧义。
# 正确:先必需参数,再默认值
def create_product(name, price, category="General", in_stock=True):
"""Create a product record."""
return {
"name": name,
"price": price,
"category": category,
"in_stock": in_stock
}
product = create_product("Laptop", 999.99)
print(product)输出:
{'name': 'Laptop', 'price': 999.99, 'category': 'General', 'in_stock': True}如果试图把必需参数放到带默认值的参数之后,会导致语法错误:
# Invalid: required parameter after default parameter
# def invalid_function(name="Unknown", age):
# return f"{name} is {age} years old"
# SyntaxError: non-default argument follows default argument这很合理:如果 name 有默认值但 age 没有,那么 Python 如何判断 invalid_function(25) 是指 name=25 且缺少 age,还是指 age=25 且 name 使用默认值?这条规则消除了这种歧义。
20.2.3) 默认参数的实际用途
默认参数非常适合用于某些实参很少变化的函数:
def calculate_shipping(weight, distance, express=False):
"""Calculate shipping cost based on weight and distance."""
base_rate = 0.50 * weight + 0.10 * distance
if express:
base_rate *= 2 # 加急配送费用加倍
return round(base_rate, 2)
# 大多数运输是标准配送
standard_cost = calculate_shipping(5, 100)
print(f"Standard: ${standard_cost}")
# 偶尔有人需要加急
express_cost = calculate_shipping(5, 100, express=True)
print(f"Express: ${express_cost}")输出:
Standard: $12.5
Express: $25.0这种设计让常见情况(标准配送)的调用很简单,同时在需要时仍能支持不太常见的情况(加急配送)。
20.2.4) 多个默认值与选择性覆盖
当一个函数有多个带默认值的参数时,你可以使用关键字实参覆盖其中任意组合:
def format_currency(amount, currency="USD", show_symbol=True, decimal_places=2):
"""Format a number as currency."""
symbols = {"USD": "$", "EUR": "€", "GBP": "£", "JPY": "¥"}
formatted = f"{amount:.{decimal_places}f}"
if show_symbol and currency in symbols:
formatted = f"{symbols[currency]}{formatted}"
return formatted
# 使用全部默认值
print(format_currency(42.5))
# 只覆盖 currency
print(format_currency(42.5, currency="EUR"))
# 覆盖多个默认值
print(format_currency(42.5, currency="JPY", decimal_places=0))输出:
$42.50
€42.50
¥42这种灵活性允许调用者只定制他们需要的部分,同时保持函数调用简洁。
20.3) 使用 *args 的可变长度实参列表
有时你希望函数能接收任意数量的实参,而不需要事先知道会有多少个。Python 为此提供了 *args。
20.3.1) 理解 *args
在参数列表中使用 *args 语法,会把所有额外的位置实参收集到一个元组中。args 这个名字是一种约定(是 “arguments” 的缩写),但你也可以在星号后使用任何合法的参数名。
def calculate_total(*numbers):
"""Calculate the sum of any number of values."""
total = 0
for num in numbers:
total += num
return total
# 可以传入任意数量的实参
print(calculate_total(10))
print(calculate_total(10, 20))
print(calculate_total(10, 20, 30, 40))
print(calculate_total())输出:
10
30
100
0在函数内部,numbers 是一个元组,包含传给该函数的所有位置实参。当没有提供任何实参时,它是一个空元组。
20.3.2) 将普通参数与 *args 结合使用
你可以在 *args 之前放普通参数。普通参数会先消耗前几个实参,*args 会收集剩下的实参:
def create_team(team_name, *members):
"""Create a team with a name and any number of members."""
member_list = ", ".join(members)
return f"Team {team_name}: {member_list}"
# 第一个实参传给 team_name,其余传给 members
print(create_team("Alpha", "Alice", "Bob"))
print(create_team("Beta", "Carol"))
print(create_team("Gamma", "Dave", "Eve", "Frank", "Grace"))输出:
Team Alpha: Alice, Bob
Team Beta: Carol
Team Gamma: Dave, Eve, Frank, Grace第一个实参("Alpha"、"Beta" 或 "Gamma")会赋给 team_name,其余所有实参会被收集到 members 元组中。
20.4) 仅限关键字参数与 **kwargs 参数
Python 还提供了两种额外机制来处理实参:仅限关键字参数,以及用于收集任意关键字实参的 **kwargs。
20.4.1) 仅限关键字参数
仅限关键字参数必须使用关键字实参来指定——不能用位置方式传递。你可以通过把它们放在参数列表中的 * 之后,或放在 *args 之后来创建它们。
def create_account(username, *, email, age):
"""Create an account. Email and age must be specified by name."""
return {
"username": username,
"email": email,
"age": age
}
# 正确:email 和 age 用关键字指定
account = create_account("alice", email="alice@example.com", age=28)
print(account)
# Invalid: trying to pass email and age positionally
# account = create_account("bob", "bob@example.com", 30)
# TypeError: create_account() takes 1 positional argument but 3 were given输出:
{'username': 'alice', 'email': 'alice@example.com', 'age': 28}参数列表中的 * 充当分隔符。它后面的所有参数都必须以关键字实参形式传递。当你希望强制调用者对某些参数显式指定时,这很有用,可以让代码更可读,也更不容易出错。
你也可以把普通参数、*args 和仅限关键字参数组合在一起:
def log_event(event_type, *details, severity="INFO", timestamp=None):
"""Log an event with optional details and metadata."""
# 我们会在第 39 章详细学习 datetime 模块,
# 但现在你只需要知道这几行会获取当前时间,
# 并把它格式化为一个时间戳字符串
from datetime import datetime
if timestamp is None:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
details_str = " | ".join(details)
return f"[{timestamp}] {severity}: {event_type} - {details_str}"
# event_type 是位置参数,details 由 *details 收集,
# severity 和 timestamp 是仅限关键字参数
print(log_event("Login", "User: alice", "IP: 192.168.1.1"))
print(log_event("Error", "Database connection failed", severity="ERROR"))输出(时间戳会根据你运行代码的时间而变化):
[2025-12-18 19:29:16] INFO: Login - User: alice | IP: 192.168.1.1
[2025-12-18 19:29:16] ERROR: Error - Database connection failed20.4.2) 理解 **kwargs
**kwargs 语法会把所有额外的关键字实参收集到一个字典中。像 args 一样,kwargs 这个名字是惯例(是 “keyword arguments” 的缩写),但你也可以在双星号后使用任何合法名称。
def create_product(**attributes):
"""Create a product with any number of attributes."""
product = {}
for key, value in attributes.items():
product[key] = value
return product
# 你想传哪些关键字实参都可以
laptop = create_product(name="Laptop", price=999.99, brand="TechCorp", in_stock=True)
print(laptop)
phone = create_product(name="Phone", price=699.99, color="Black")
print(phone)输出:
{'name': 'Laptop', 'price': 999.99, 'brand': 'TechCorp', 'in_stock': True}
{'name': 'Phone', 'price': 699.99, 'color': 'Black'}在函数内部,attributes 是一个字典,其中键是参数名,值是传入的实参。
20.4.3) 组合普通参数、*args 与 **kwargs
你可以把这些机制一起使用,但它们必须以特定顺序出现:
- 普通位置参数
*args(如果存在)- 仅限关键字参数(如果存在)
**kwargs(如果存在)
def complex_function(required, *args, keyword_only, **kwargs):
"""Demonstrate all parameter types together."""
print(f"Required: {required}")
print(f"Args: {args}")
print(f"Keyword-only: {keyword_only}")
print(f"Kwargs: {kwargs}")
complex_function(
"value1", # required
"value2", "value3", # args
keyword_only="kw", # keyword_only
extra1="e1", # kwargs
extra2="e2" # kwargs
)输出:
Required: value1
Args: ('value2', 'value3')
Keyword-only: kw
Kwargs: {'extra1': 'e1', 'extra2': 'e2'}这种灵活性非常强大,但应该谨慎使用。大多数函数并不需要所有这些机制。
20.4.4) 实际用例:配置函数
**kwargs 的一个常见用途是创建可接收配置选项的函数:
def connect_to_database(host, port, **options):
"""Connect to a database with flexible configuration options."""
connection_string = f"Connecting to {host}:{port}"
# 处理任何额外选项
if options.get("ssl"):
connection_string += " with SSL"
if options.get("timeout"):
connection_string += f" (timeout: {options['timeout']}s)"
if options.get("pool_size"):
connection_string += f" (pool size: {options['pool_size']})"
return connection_string
# 基础连接
print(connect_to_database("localhost", 5432))
# 使用 SSL
print(connect_to_database("db.example.com", 5432, ssl=True))
# 使用多个选项
print(connect_to_database("db.example.com", 5432, ssl=True, timeout=30, pool_size=10))输出:
Connecting to localhost:5432
Connecting to db.example.com:5432 with SSL
Connecting to db.example.com:5432 with SSL (timeout: 30s) (pool size: 10)这种模式让函数能够接受任意数量的可选配置参数,而无需在参数列表中把它们全部显式定义出来。
20.5) 调用函数时的实参解包
正如 *args 与 **kwargs 会在定义函数时收集实参一样,你也可以在调用函数时用 * 与 ** 来对集合进行解包(unpacking)。
20.5.1) 使用 * 解包序列
* 运算符会把一个序列(列表、元组等)解包成独立的位置实参:
def calculate_rectangle_area(width, height):
"""Calculate the area of a rectangle."""
return width * height
# 不再逐个传入实参
dimensions = [5, 10]
area = calculate_rectangle_area(dimensions[0], dimensions[1])
print(area)
# 直接解包列表
area = calculate_rectangle_area(*dimensions)
print(area)输出:
50
50当你写 *dimensions 时,Python 会把列表 [5, 10] 解包为两个独立的实参,就像你写了 calculate_rectangle_area(5, 10) 一样。
这对任何可迭代对象都适用:
def format_name(first, middle, last):
"""Format a full name."""
return f"{first} {middle} {last}"
# 解包元组
name_tuple = ("John", "Q", "Public")
print(format_name(*name_tuple))
# 解包列表
name_list = ["Jane", "M", "Doe"]
print(format_name(*name_list))
# 甚至可以解包字符串(每个字符变成一个实参)
# 这只有在函数期望正确数量的实参时才可行
def show_first_three(a, b, c):
return f"{a}, {b}, {c}"
print(show_first_three(*"ABC"))输出:
John Q Public
Jane M Doe
A, B, C20.5.2) 使用 ** 解包字典
** 运算符会把字典解包为关键字实参:
def create_user(username, email, age):
"""Create a user profile."""
return f"User: {username}, Email: {email}, Age: {age}"
# 键与参数名匹配的字典
user_data = {
"username": "alice",
"email": "alice@example.com",
"age": 28
}
# 解包字典
profile = create_user(**user_data)
print(profile)输出:
User: alice, Email: alice@example.com, Age: 28当你写 **user_data 时,Python 会把字典解包为关键字实参,等价于:
create_user(username="alice", email="alice@example.com", age=28)字典的键必须与函数的参数名匹配,否则你会得到错误:
# Invalid: dictionary key doesn't match parameter name
invalid_data = {"name": "bob", "email": "bob@example.com", "age": 30}
# profile = create_user(**invalid_data)
# TypeError: create_user() got an unexpected keyword argument 'name'20.5.3) 将解包与普通实参结合使用
你可以把解包的实参与普通实参混合使用:
def calculate_total(base_price, tax_rate, discount):
"""Calculate total price after tax and discount."""
subtotal = base_price * (1 + tax_rate)
total = subtotal * (1 - discount)
return round(total, 2)
# 一部分实参是普通的,一部分是解包的
pricing = [0.08, 0.10] # tax_rate 和 discount
total = calculate_total(100, *pricing)
print(total)输出:
97.2你也可以在一次调用中解包多个集合:
def create_full_address(street, city, state, zip_code, country):
"""Create a complete address."""
return f"{street}, {city}, {state} {zip_code}, {country}"
street_address = ["123 Main St", "Springfield"]
location_details = ["IL", "62701", "USA"]
address = create_full_address(*street_address, *location_details)
print(address)输出:
123 Main St, Springfield, IL 62701, USA20.5.4) 实用示例:灵活的函数调用
当你处理来自外部来源的数据时,解包尤其有用:
def send_email(recipient, subject, body, cc=None, bcc=None):
"""Send an email with optional CC and BCC."""
message = f"To: {recipient}\nSubject: {subject}\n\n{body}"
if cc:
message += f"\nCC: {cc}"
if bcc:
message += f"\nBCC: {bcc}"
return message
# 来自配置文件或数据库的邮件数据
email_config = {
"recipient": "user@example.com",
"subject": "Welcome",
"body": "Thank you for signing up!",
"cc": "manager@example.com"
}
# 直接解包配置
result = send_email(**email_config)
print(result)输出:
To: user@example.com
Subject: Welcome
Thank you for signing up!
CC: manager@example.com这种模式可以让你把函数实参作为数据结构进行传递,这在构建 API 或处理配置文件时很常见。
20.6) 可变默认实参的陷阱(为什么列表默认值会“持久化”)
Python 中最臭名昭著的陷阱之一,是把可变对象(如列表或字典)用作默认参数值。理解这个问题对于编写正确的函数至关重要。
20.6.1) 问题:共享的可变默认值
考虑这个看似无害的函数:
def add_student(name, grades=[]):
"""Add a student with their grades."""
grades.append(name)
return grades
# 第一次调用
students1 = add_student("Alice")
print(students1)
# 第二次调用——期望得到一个全新的列表
students2 = add_student("Bob")
print(students2)
# 第三次调用
students3 = add_student("Carol")
print(students3)输出:
['Alice']
['Alice', 'Bob']
['Alice', 'Bob', 'Carol']这种行为让许多程序员感到惊讶。每次在不提供 grades 实参的情况下调用 add_student(),使用的都是同一个列表对象,而不是一个新的列表。这个列表会在函数调用之间持续存在并累积值。
20.6.2) 为什么会这样:默认值只创建一次
理解这种行为的关键是知道默认值是在什么时候创建的。Python 会在函数定义时对默认参数值求值一次,而不是每次调用函数时都求值。
def demonstrate_default_creation():
"""Show when defaults are created."""
print("Function defined!")
def use_default(value=demonstrate_default_creation()):
"""Use a default that calls a function."""
return value
# 这条消息会在函数被定义时打印,而不是在调用时输出:
Function defined!当 Python 遇到 def use_default 这一行时,它会计算默认参数 value=demonstrate_default_creation()。这会调用 demonstrate_default_creation(),从而立即打印 "Function defined!"。之后再调用 use_default() 时不会再次计算默认值,因此不会再打印任何额外内容。
当 Python 遇到 def add_student(name, grades=[]): 时,它会创建一个空列表对象,并把它存为 grades 的默认值。此后每次调用只要不提供 grades 实参,就会使用同一个列表对象。
下面用对象标识(identity)做一个更清晰的演示:
def show_list_identity(items=[]):
"""Show that the same list object is reused."""
print(f"List ID: {id(items)}")
items.append("item")
return items
# 每次调用都使用同一个列表对象(ID 相同)
show_list_identity()
show_list_identity()
show_list_identity()输出:
List ID: 140234567890123
List ID: 140234567890123
List ID: 140234567890123具体的 ID 数字会因系统而异,但请注意,这三次调用都显示相同的 ID 值,证明它们使用的是同一个列表对象。id() 函数会返回内存中每个对象的唯一标识符——当 ID 相同,就表示是同一个对象。
20.6.3) 正确模式:使用 None 作为默认值
标准解决方案是使用 None 作为默认值,并在函数内部创建一个新的可变对象:
def add_student_correct(name, grades=None):
"""Add a student with their grades (correct version)."""
if grades is None:
grades = [] # 每次都创建一个新的列表
grades.append(name)
return grades
# 现在每次调用都会得到自己的列表
students1 = add_student_correct("Alice")
print(students1)
students2 = add_student_correct("Bob")
print(students2)
students3 = add_student_correct("Carol")
print(students3)输出:
['Alice']
['Bob']
['Carol']这个模式之所以有效,是因为 None 是不可变的,并且当 grades 为 None 时,每次都会在函数体内部创建一个新列表。
20.6.4) 字典也存在同样的问题
这个问题会影响所有可变类型,不仅仅是列表:
# WRONG: Dictionary default
def create_config_wrong(key, value, config={}):
"""Create a configuration (BUGGY VERSION)."""
config[key] = value
return config
config1 = create_config_wrong("theme", "dark")
print(config1)
config2 = create_config_wrong("language", "en")
print(config2)
print("---")
# CORRECT: None as default
def create_config_correct(key, value, config=None):
"""Create a configuration (CORRECT VERSION)."""
if config is None:
config = {}
config[key] = value
return config
config1 = create_config_correct("theme", "dark")
print(config1)
config2 = create_config_correct("language", "en")
print(config2)输出:
{'theme': 'dark'}
{'theme': 'dark', 'language': 'en'}
---
{'theme': 'dark'}
{'language': 'en'}20.6.5) 总结:黄金法则
永远不要把可变对象(列表、字典、集合)作为默认参数值。 始终使用 None,并在函数内部创建可变对象:
# ❌ WRONG
def function(items=[]):
pass
# ✅ CORRECT
def function(items=None):
if items is None:
items = []
# 现在可以安全地使用 items这种模式确保每次函数调用都获得自己独立的可变对象,从而防止出现数据在调用之间泄漏、导致难以解释的 bug。
在本章中,我们深入探索了 Python 灵活的参数(parameter)与实参(argument)系统。你已经学习了如何使用位置实参与关键字实参、提供默认值、用 *args 与 **kwargs 处理可变数量的实参、在调用函数时解包集合,以及避免可变默认实参陷阱。
这些机制为你提供了强大的工具,用于设计既灵活又易用的函数接口。随着你编写更多函数,你会逐渐形成直觉,知道哪种参数模式更适合不同场景。关键在于在灵活性与清晰性之间取得平衡——让你的函数易于被正确调用,并且难以被错误调用。