Python & AI Tutorials Logo
Python 编程

20. 函数参数与实参

在第 19 章中,我们学习了如何定义并调用带有基础参数的函数(function)。现在我们将深入探索 Python 灵活的参数(parameter)与实参(argument)系统。理解这些机制能让你编写既强大又易用的函数。

20.1) 位置实参与关键字实参

当你调用一个函数时,你可以用两种基本方式传入实参:按位置或按名称(关键字)。

20.1.1) 位置实参

位置实参会根据它们的顺序与参数进行匹配。第一个实参传给第一个参数,第二个传给第二个,依此类推。

python
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

对于位置实参,顺序至关重要:

python
# 示例:我们想计算一个 $100 的商品打 20% 折扣
 
# 正确顺序:先 price,再 discount
print(calculate_discount(100, 20))
 
# 错误顺序:先 discount,再 price
print(calculate_discount(20, 100))

输出:

80.0
-16.0

当你交换这些实参时,Python 并不知道你犯了错——它只是按顺序进行赋值。这会产生一个数学上有效但逻辑上错误的结果(负价格!)。

20.1.2) 关键字实参

关键字实参通过使用参数名、等号以及值,明确指定哪个参数接收哪个值。这会让你的代码更易读,并防止因顺序错误导致的问题。

python
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

使用关键字实参时,顺序不重要:

python
# 相同结果,不同顺序
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) 混用位置实参与关键字实参

你可以在一次函数调用中同时结合两种风格,但有一条重要规则:位置实参必须放在关键字实参之前

python
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" 是位置实参(被赋给 streetcity),而 statezip_code 则通过名称指定。

如果尝试把位置实参放到关键字实参之后,会导致错误:

python
# 非法:关键字实参之后出现位置实参
# address = format_address(street="123 Main St", "Springfield", state="IL", zip_code="62701")
# SyntaxError: positional argument follows keyword argument

Python 强制执行这条规则,因为一旦你开始使用关键字实参,后续未命名的实参应该填充哪个位置参数就会变得含糊不清。

20.1.4) 何时使用哪种风格

在以下情况使用位置实参:

  • 函数参数很少(通常 1-3 个)
  • 参数顺序显而易见且直观
  • 函数非常常用,并且顺序广为人知
python
# 显而易见且简洁
print(len("hello"))
result = max(10, 20, 5)

在以下情况使用关键字实参:

  • 函数有很多参数
  • 参数含义并不是立刻就很清楚
  • 你想跳过一些带默认值的参数(下一节会讲)
  • 你想让代码“自带说明”(self-documenting)
python
# 清晰且明确
user = create_user_profile(username="charlie", email="charlie@example.com", age=42)

20.2) 默认参数值

函数可以为参数指定默认值。当调用者没有为带默认值的参数提供实参时,Python 会改用默认值。

20.2.1) 定义带默认值的参数

默认值通过赋值运算符在函数定义中指定:

python
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 要求:带默认值的参数必须出现在所有不带默认值的参数之后。这条规则防止出现“哪个实参对应哪个参数”的歧义。

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}

如果试图把必需参数放到带默认值的参数之后,会导致语法错误:

python
# 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=25name 使用默认值?这条规则消除了这种歧义。

20.2.3) 默认参数的实际用途

默认参数非常适合用于某些实参很少变化的函数:

python
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) 多个默认值与选择性覆盖

当一个函数有多个带默认值的参数时,你可以使用关键字实参覆盖其中任意组合:

python
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” 的缩写),但你也可以在星号后使用任何合法的参数名。

python
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 会收集剩下的实参:

python
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 之后来创建它们。

python
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 和仅限关键字参数组合在一起:

python
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 failed

20.4.2) 理解 **kwargs

**kwargs 语法会把所有额外的关键字实参收集到一个字典中。像 args 一样,kwargs 这个名字是惯例(是 “keyword arguments” 的缩写),但你也可以在双星号后使用任何合法名称。

python
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

你可以把这些机制一起使用,但它们必须以特定顺序出现:

  1. 普通位置参数
  2. *args(如果存在)
  3. 仅限关键字参数(如果存在)
  4. **kwargs(如果存在)
python
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 的一个常见用途是创建可接收配置选项的函数:

python
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)

这种模式让函数能够接受任意数量的可选配置参数,而无需在参数列表中把它们全部显式定义出来。

位置

额外位置

仅限关键字

额外关键字

函数调用

参数类型?

普通参数

*args 元组

仅限关键字参数

**kwargs 字典

按位置分配

收集到元组

必须使用名称

收集到字典

20.5) 调用函数时的实参解包

正如 *args**kwargs 会在定义函数时收集实参一样,你也可以在调用函数时用 *** 来对集合进行解包(unpacking)

20.5.1) 使用 * 解包序列

* 运算符会把一个序列(列表、元组等)解包成独立的位置实参:

python
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) 一样。

这对任何可迭代对象都适用:

python
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, C

20.5.2) 使用 ** 解包字典

** 运算符会把字典解包为关键字实参:

python
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 会把字典解包为关键字实参,等价于:

python
create_user(username="alice", email="alice@example.com", age=28)

字典的键必须与函数的参数名匹配,否则你会得到错误:

python
# 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) 将解包与普通实参结合使用

你可以把解包的实参与普通实参混合使用:

python
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

你也可以在一次调用中解包多个集合:

python
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, USA

20.5.4) 实用示例:灵活的函数调用

当你处理来自外部来源的数据时,解包尤其有用:

python
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) 问题:共享的可变默认值

考虑这个看似无害的函数:

python
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 会在函数定义时对默认参数值求值一次,而不是每次调用函数时都求值。

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)做一个更清晰的演示:

python
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 作为默认值,并在函数内部创建一个新的可变对象:

python
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 是不可变的,并且当 gradesNone 时,每次都会在函数体内部创建一个新列表。

20.6.4) 字典也存在同样的问题

这个问题会影响所有可变类型,不仅仅是列表:

python
# 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,并在函数内部创建可变对象:

python
# ❌ WRONG
def function(items=[]):
    pass
 
# ✅ CORRECT
def function(items=None):
    if items is None:
        items = []
    # 现在可以安全地使用 items

这种模式确保每次函数调用都获得自己独立的可变对象,从而防止出现数据在调用之间泄漏、导致难以解释的 bug。


在本章中,我们深入探索了 Python 灵活的参数(parameter)与实参(argument)系统。你已经学习了如何使用位置实参与关键字实参、提供默认值、用 *args**kwargs 处理可变数量的实参、在调用函数时解包集合,以及避免可变默认实参陷阱。

这些机制为你提供了强大的工具,用于设计既灵活又易用的函数接口。随着你编写更多函数,你会逐渐形成直觉,知道哪种参数模式更适合不同场景。关键在于在灵活性与清晰性之间取得平衡——让你的函数易于被正确调用,并且难以被错误调用。

© 2025. Primesoft Co., Ltd.
support@primesoft.ai