导航菜单

  • 1.vector
  • 2.milvus
  • 3.pymilvus
  • 4.rag
  • 5.rag_measure
  • ragflow
  • heapq
  • HNSW
  • cosine_similarity
  • math
  • typing
  • etcd
  • minio
  • collections
  • jieba
  • random
  • beautifulsoup4
  • chromadb
  • sentence_transformers
  • numpy
  • lxml
  • openpyxl
  • PyMuPDF
  • python-docx
  • requests
  • python-pptx
  • text_splitter
  • all-MiniLM-L6-v2
  • openai
  • llm
  • BPETokenizer
  • Flask
  • RAGAS
  • BagofWords
  • langchain
  • Pydantic
  • abc
  • faiss
  • MMR
  • scikit-learn
  • Runnable
  • PromptEngineering
  • dataclasses
  • LaTeX
  • rank_bm25
  • TF-IDF
  • asyncio
  • sqlalchemy
  • fastapi
  • Starlette
  • uvicorn
  • argparse
  • Generic
  • ssl
  • urllib
  • python-dotenv
  • RRF
  • CrossEncoder
  • Lost-in-the-middle
  • Jinja2
  • logger
  • io
  • venv
  • concurrent
  • parameter
  • SSE
  • 1. 前置知识:理解对象和引用
    • 1.1 什么是对象
    • 1.2 变量是什么
    • 1.3 多个变量可以指向同一个对象
  • 2. Python 参数传递的本质
    • 2.1 示例:理解引用传递
  • 3. 可变对象 vs 不可变对象
    • 3.1 不可变对象(Immutable)
    • 3.2 示例:不可变对象的行为
    • 3.3 可变对象(Mutable)
    • 3.4 示例:可变对象的行为
  • 4. 参数传递的四种形式
    • 4.1 位置参数
    • 4.2 关键字参数
    • 4.3 混合使用位置参数和关键字参数
    • 4.4 默认参数的陷阱
  • 5. 可变长度参数
    • 5.1 *args - 接收任意数量的位置参数
    • 5.2 **kwargs - 接收任意数量的关键字参数
    • 5.3 混合使用所有参数类型
  • 6. 参数解包
    • 6.1 列表/元组解包(*)
    • 6.2 字典解包(**)
    • 6.3 混合解包
  • 7. 实际应用示例
    • 7.1 示例:灵活的数据处理函数
  • 8. 常见陷阱和注意事项
    • 8.1 陷阱 1:意外修改可变对象
    • 8.2 陷阱 2:默认参数使用可变对象
  • 9. Python 函数参数传递规则
    • 9.1. 参数定义顺序规则(函数定义时)
    • 9.2. 参数传递顺序规则(函数调用时)
    • 9.3. 必须参数 vs 可选参数
    • 9.4. 关键字参数传递规则
    • 9.5. 重复传递参数规则
    • 9.6. 可变参数规则
    • 9.7. 解包参数规则
    • 9.8. 仅位置参数和仅关键字参数(Python 3.8+)
    • 9.9. 默认参数陷阱
    • 9.10. 参数传递优先级
    • 9.11 快速记忆口诀
  • 10. 最佳实践总结
  • 11. 总结

1. 前置知识:理解对象和引用 #

在深入学习参数传递之前,我们需要先理解几个基础概念。

1.1 什么是对象 #

在 Python 中,一切都是对象。数字、字符串、列表、字典,甚至函数本身都是对象。每个对象都有:

  • 类型(type):决定了对象能做什么
  • 值(value):对象存储的数据
  • 身份标识(id):对象在内存中的唯一地址

1.2 变量是什么 #

变量实际上是对对象的引用(可以理解为"标签"或"指针")。当你写 a = 5 时,Python 创建了一个整数对象 5,然后让变量 a 指向这个对象。

# 创建一个整数对象,让变量 a 指向它
a = 5

# 查看对象的内存地址(身份标识)
print(f"变量 a 指向的对象地址: {id(a)}")

# 查看对象的类型
print(f"对象类型: {type(a)}")

# 查看对象的值
print(f"对象的值: {a}")

# 输出示例:
# 变量 a 指向的对象地址: 140712345678912
# 对象类型: <class 'int'>
# 对象的值: 5

1.3 多个变量可以指向同一个对象 #

# 创建两个变量,都指向同一个对象
a = [1, 2, 3]
b = a  # b 和 a 指向同一个列表对象

# 查看它们是否指向同一个对象
print(f"a 的地址: {id(a)}")
print(f"b 的地址: {id(b)}")
print(f"a 和 b 指向同一个对象: {a is b}")

# 通过 b 修改列表
b.append(4)

# a 也会看到变化,因为它们指向同一个对象
print(f"a 的值: {a}")
print(f"b 的值: {b}")

# 输出:
# a 的地址: 140712345678912
# b 的地址: 140712345678912
# a 和 b 指向同一个对象: True
# a 的值: [1, 2, 3, 4]
# b 的值: [1, 2, 3, 4]

2. Python 参数传递的本质 #

Python 的参数传递方式是 "按对象引用传递" 或 "按共享传递"。这意味着:

  1. 当你调用函数时,传递的是对象的引用,而不是对象本身
  2. 函数内的参数变量和函数外的变量可能指向同一个对象
  3. 对可变对象的修改会影响原始对象,对不可变对象的"修改"实际上是创建了新对象

2.1 示例:理解引用传递 #

# 定义一个函数,尝试修改参数
def modify(x):
    """
    尝试修改传入的参数
    """
    # 打印函数内参数 x 的初始地址
    print(f"函数内初始 x 的地址: {id(x)}")

    # 尝试修改 x(实际上是创建新对象)
    x = 10

    # 打印修改后 x 的地址
    print(f"函数内修改后 x 的地址: {id(x)}")

# 创建一个变量
a = 5

# 打印函数调用前 a 的地址
print(f"函数调用前 a 的地址: {id(a)}")

# 调用函数
modify(a)

# 打印函数调用后 a 的值和地址
print(f"函数调用后 a 的值: {a}")  # a 的值没有改变
print(f"函数调用后 a 的地址: {id(a)}")  # a 的地址没有改变

# 输出示例:
# 函数调用前 a 的地址: 140712345678912
# 函数内初始 x 的地址: 140712345678912  (x 和 a 指向同一个对象)
# 函数内修改后 x 的地址: 140712345678944  (x 现在指向新对象 10)
# 函数调用后 a 的值: 5  (a 仍然指向原来的对象)
# 函数调用后 a 的地址: 140712345678912  (地址没有改变)

关键理解:函数内的 x = 10 并没有修改原来的对象,而是让 x 指向了一个新的对象 10。原来的变量 a 仍然指向对象 5。

3. 可变对象 vs 不可变对象 #

这是理解参数传递的关键区别。Python 中的对象分为两类:

3.1 不可变对象(Immutable) #

不可变对象一旦创建,就不能被修改。如果要对它进行"修改"操作,Python 会创建一个新对象。

常见的不可变对象类型:

  • 数字(int, float, complex)
  • 字符串(str)
  • 元组(tuple)
  • 布尔值(bool)
  • frozenset

3.2 示例:不可变对象的行为 #

# 定义一个函数,尝试修改不可变对象
def modify_immutable(x):
    """
    尝试修改不可变对象(字符串)
    """
    # 打印函数内初始 x 的地址
    print(f"函数内初始 x 的地址: {id(x)}")

    # 尝试"修改"字符串(实际上是创建新对象)
    x += " World"

    # 打印修改后 x 的地址(地址会改变)
    print(f"函数内修改后 x 的地址: {id(x)}")
    print(f"函数内 x 的值: {x}")

# 创建一个字符串变量
s = "Hello"

# 打印函数调用前 s 的地址
print(f"函数调用前 s 的地址: {id(s)}")

# 调用函数
modify_immutable(s)

# 打印函数调用后 s 的值和地址
print(f"函数调用后 s 的值: {s}")  # 原始字符串没有改变
print(f"函数调用后 s 的地址: {id(s)}")  # 地址没有改变

# 输出示例:
# 函数调用前 s 的地址: 140712345678912
# 函数内初始 x 的地址: 140712345678912  (x 和 s 指向同一个对象)
# 函数内修改后 x 的地址: 140712345678944  (x 现在指向新对象)
# 函数内 x 的值: Hello World
# 函数调用后 s 的值: Hello  (原始字符串没有改变)
# 函数调用后 s 的地址: 140712345678912  (地址没有改变)

3.3 可变对象(Mutable) #

可变对象可以在原地修改,修改后仍然是同一个对象。

常见的可变对象类型:

  • 列表(list)
  • 字典(dict)
  • 集合(set)
  • 自定义对象

3.4 示例:可变对象的行为 #

# 定义一个函数,修改可变对象
def modify_mutable(lst):
    """
    修改可变对象(列表)
    """
    # 打印函数内初始 lst 的地址
    print(f"函数内初始 lst 的地址: {id(lst)}")

    # 修改列表(在同一个对象上操作)
    lst.append(4)

    # 打印修改后 lst 的地址(地址不会改变)
    print(f"函数内修改后 lst 的地址: {id(lst)}")
    print(f"函数内 lst 的值: {lst}")

# 创建一个列表变量
my_list = [1, 2, 3]

# 打印函数调用前 my_list 的地址
print(f"函数调用前 my_list 的地址: {id(my_list)}")

# 调用函数
modify_mutable(my_list)

# 打印函数调用后 my_list 的值和地址
print(f"函数调用后 my_list 的值: {my_list}")  # 原始列表被修改了!
print(f"函数调用后 my_list 的地址: {id(my_list)}")  # 地址没有改变

# 输出示例:
# 函数调用前 my_list 的地址: 140712345678912
# 函数内初始 lst 的地址: 140712345678912  (lst 和 my_list 指向同一个对象)
# 函数内修改后 lst 的地址: 140712345678912  (仍然是同一个对象)
# 函数内 lst 的值: [1, 2, 3, 4]
# 函数调用后 my_list 的值: [1, 2, 3, 4]  (原始列表被修改了!)
# 函数调用后 my_list 的地址: 140712345678912  (地址没有改变)

关键理解:对可变对象的修改是在原对象上进行的,所以函数外的变量也会看到变化。

4. 参数传递的四种形式 #

Python 提供了多种参数传递方式,让我们可以灵活地调用函数。

4.1 位置参数 #

位置参数是最基本的参数传递方式,按照函数定义时的顺序传递参数。

# 定义一个计算幂的函数
def power(base, exponent):
    """
    计算 base 的 exponent 次方
    base: 底数
    exponent: 指数
    """
    return base ** exponent

# 使用位置参数调用函数
# 第一个参数 2 传给 base,第二个参数 3 传给 exponent
result = power(2, 3)

# 打印结果
print(f"2 的 3 次方 = {result}")

# 输出:
# 2 的 3 次方 = 8

4.2 关键字参数 #

关键字参数通过参数名传递,顺序可以任意。

# 定义一个描述人物的函数
def describe_person(name, age, city):
    """
    描述一个人的信息
    name: 姓名
    age: 年龄
    city: 城市
    """
    return f"{name} is {age} years old, living in {city}"

# 使用关键字参数调用函数
# 顺序可以任意,只要参数名正确
info = describe_person(age=25, city="Beijing", name="Alice")

# 打印结果
print(info)

# 输出:
# Alice is 25 years old, living in Beijing

4.3 混合使用位置参数和关键字参数 #

# 定义一个函数,既有位置参数也有关键字参数
def greet(name, greeting="Hello", punctuation="!"):
    """
    生成问候语
    name: 姓名(位置参数)
    greeting: 问候语(关键字参数,有默认值)
    punctuation: 标点符号(关键字参数,有默认值)
    """
    return f"{greeting}, {name}{punctuation}"

# 只传递位置参数,使用默认值
result1 = greet("Alice")
print(result1)

# 覆盖默认值
result2 = greet("Bob", "Hi", "!!!")
print(result2)

# 混合使用:位置参数 + 关键字参数
result3 = greet("Charlie", punctuation="?")
print(result3)

# 输出:
# Hello, Alice!
# Hi, Bob!!!
# Hello, Charlie?

4.4 默认参数的陷阱 #

默认参数有一个重要的特性:默认值只计算一次。这可能导致意外的行为。

# 危险示例:使用可变对象作为默认参数
def append_to_list(item, lst=[]):
    """
    将 item 添加到列表 lst 中
    注意:这是一个有问题的实现!
    """
    # 将 item 添加到列表
    lst.append(item)
    # 返回列表
    return lst

# 第一次调用
result1 = append_to_list(1)
print(f"第一次调用结果: {result1}")

# 第二次调用(期望得到 [2],但实际得到 [1, 2])
result2 = append_to_list(2)
print(f"第二次调用结果: {result2}")

# 第三次调用(期望得到 [3],但实际得到 [1, 2, 3])
result3 = append_to_list(3)
print(f"第三次调用结果: {result3}")

# 输出:
# 第一次调用结果: [1]
# 第二次调用结果: [1, 2]  (不是期望的 [2]!)
# 第三次调用结果: [1, 2, 3]  (不是期望的 [3]!)

问题原因:默认参数 lst=[] 在函数定义时只创建一次,之后所有调用都共享同一个列表对象。

正确做法:使用 None 作为默认值,在函数内部创建新对象。

# 正确做法:使用 None 作为默认值
def append_to_list_fixed(item, lst=None):
    """
    将 item 添加到列表 lst 中
    这是正确的实现方式
    """
    # 如果 lst 是 None,创建一个新列表
    if lst is None:
        lst = []

    # 将 item 添加到列表
    lst.append(item)

    # 返回列表
    return lst

# 第一次调用
result1 = append_to_list_fixed(1)
print(f"第一次调用结果: {result1}")

# 第二次调用(现在得到期望的 [2])
result2 = append_to_list_fixed(2)
print(f"第二次调用结果: {result2}")

# 第三次调用(现在得到期望的 [3])
result3 = append_to_list_fixed(3)
print(f"第三次调用结果: {result3}")

# 输出:
# 第一次调用结果: [1]
# 第二次调用结果: [2]  (正确!)
# 第三次调用结果: [3]  (正确!)

5. 可变长度参数 #

有时候,我们不知道函数会接收多少个参数。Python 提供了 *args 和 **kwargs 来处理这种情况。

5.1 *args - 接收任意数量的位置参数 #

*args 可以接收任意数量的位置参数,它们会被收集到一个元组中。

# 定义一个可以接收任意数量参数的求和函数
def sum_all(*args):
    """
    计算所有参数的和
    *args: 接收任意数量的位置参数,收集到元组中
    """
    # 打印 args 的类型(应该是元组)
    print(f"args 的类型: {type(args)}")

    # 打印 args 的内容
    print(f"args 的内容: {args}")

    # 使用内置的 sum 函数求和
    return sum(args)

# 调用函数,传递 3 个参数
result1 = sum_all(1, 2, 3)
print(f"sum_all(1, 2, 3) = {result1}")
print()

# 调用函数,传递 5 个参数
result2 = sum_all(1, 2, 3, 4, 5)
print(f"sum_all(1, 2, 3, 4, 5) = {result2}")

# 输出:
# args 的类型: <class 'tuple'>
# args 的内容: (1, 2, 3)
# sum_all(1, 2, 3) = 6
# 
# args 的类型: <class 'tuple'>
# args 的内容: (1, 2, 3, 4, 5)
# sum_all(1, 2, 3, 4, 5) = 15

5.2 **kwargs - 接收任意数量的关键字参数 #

**kwargs 可以接收任意数量的关键字参数,它们会被收集到一个字典中。

# 定义一个可以接收任意数量关键字参数的函数
def print_info(**kwargs):
    """
    打印所有关键字参数
    **kwargs: 接收任意数量的关键字参数,收集到字典中
    """
    # 打印 kwargs 的类型(应该是字典)
    print(f"kwargs 的类型: {type(kwargs)}")

    # 遍历字典,打印所有键值对
    for key, value in kwargs.items():
        print(f"{key}: {value}")

# 调用函数,传递关键字参数
print_info(name="Alice", age=25, city="Beijing")

# 输出:
# kwargs 的类型: <class 'dict'>
# name: Alice
# age: 25
# city: Beijing

5.3 混合使用所有参数类型 #

在实际应用中,我们经常需要混合使用各种参数类型。参数顺序很重要:

标准顺序:位置参数 → *args → 关键字参数 → **kwargs

# 定义一个包含所有参数类型的函数
def func(a, b, *args, option=True, **kwargs):
    """
    演示所有参数类型的混合使用
    a, b: 位置参数
    *args: 可变位置参数
    option: 关键字参数(有默认值)
    **kwargs: 可变关键字参数
    """
    # 打印位置参数
    print(f"位置参数 a: {a}, b: {b}")

    # 打印可变位置参数
    print(f"可变位置参数 args: {args}")

    # 打印关键字参数
    print(f"关键字参数 option: {option}")

    # 打印可变关键字参数
    print(f"可变关键字参数 kwargs: {kwargs}")

# 调用函数
func(1, 2, 3, 4, 5, option=False, x=10, y=20)

# 输出:
# 位置参数 a: 1, b: 2
# 可变位置参数 args: (3, 4, 5)
# 关键字参数 option: False
# 可变关键字参数 kwargs: {'x': 10, 'y': 20}

6. 参数解包 #

参数解包允许我们将列表、元组或字典"展开"后传递给函数。

6.1 列表/元组解包(*) #

使用 * 可以将列表或元组解包为位置参数。

# 定义一个需要三个参数的函数
def func(a, b, c):
    """
    计算三个数的和
    """
    return a + b + c

# 创建一个列表
numbers = [1, 2, 3]

# 使用 * 解包列表,相当于 func(1, 2, 3)
result = func(*numbers)

# 打印结果
print(f"func(*{numbers}) = {result}")

# 输出:
# func(*[1, 2, 3]) = 6

6.2 字典解包(**) #

使用 ** 可以将字典解包为关键字参数。

# 定义一个需要关键字参数的函数
def func(name, age, city):
    """
    格式化个人信息
    """
    return f"{name}, {age}, {city}"

# 创建一个字典
person = {"name": "Alice", "age": 25, "city": "Beijing"}

# 使用 ** 解包字典,相当于 func(name="Alice", age=25, city="Beijing")
result = func(**person)

# 打印结果
print(result)

# 输出:
# Alice, 25, Beijing

6.3 混合解包 #

可以同时使用 * 和 ** 解包。

# 定义一个函数
def func(a, b, c, name, age):
    """
    演示混合解包
    """
    return f"数字: {a}, {b}, {c} | 人物: {name}, {age}"

# 创建列表和字典
numbers = [1, 2, 3]
person = {"name": "Alice", "age": 25}

# 同时使用 * 和 ** 解包
result = func(*numbers, **person)

# 打印结果
print(result)

# 输出:
# 数字: 1, 2, 3 | 人物: Alice, 25

7. 实际应用示例 #

下面是一个完整的实际应用示例,展示如何在实际项目中使用这些参数传递技巧。

7.1 示例:灵活的数据处理函数 #

# 定义一个灵活的数据处理函数
def process_data(data, *, validate=True, log_level="INFO", **options):
    """
    处理数据,支持多种选项
    data: 要处理的数据(位置参数)
    validate: 是否验证数据(关键字参数,只能通过关键字传递)
    log_level: 日志级别(关键字参数)
    **options: 其他选项(可变关键字参数)
    """
    # 如果启用验证,打印验证信息
    if validate:
        print(f"[{log_level}] 正在验证数据...")

    # 打印处理信息
    print(f"正在处理 {len(data)} 个数据项")

    # 处理额外选项
    if "timeout" in options:
        print(f"超时时间设置为 {options['timeout']} 秒")

    if "max_retries" in options:
        print(f"最大重试次数: {options['max_retries']}")

    # 实际的数据处理逻辑(这里简单地将每个元素乘以 2)
    result = [item * 2 for item in data]

    # 返回处理结果
    return result

# 准备数据
data = [1, 2, 3, 4, 5]

# 调用函数,使用各种参数
result = process_data(
    data,                    # 位置参数
    validate=True,           # 关键字参数
    log_level="DEBUG",       # 关键字参数
    timeout=30,              # 可变关键字参数
    max_retries=3            # 可变关键字参数
)

# 打印结果
print(f"处理结果: {result}")

# 输出:
# [DEBUG] 正在验证数据...
# 正在处理 5 个数据项
# 超时时间设置为 30 秒
# 最大重试次数: 3
# 处理结果: [2, 4, 6, 8, 10]

8. 常见陷阱和注意事项 #

8.1 陷阱 1:意外修改可变对象 #

# 定义一个函数,意外修改了传入的列表
def add_item(item, lst):
    """
    将 item 添加到列表
    注意:这会修改原始列表!
    """
    lst.append(item)
    return lst

# 创建一个列表
my_list = [1, 2, 3]

# 调用函数
result = add_item(4, my_list)

# 打印结果
print(f"函数返回值: {result}")

# 原始列表也被修改了(这可能不是我们想要的)
print(f"原始列表: {my_list}")

# 输出:
# 函数返回值: [1, 2, 3, 4]
# 原始列表: [1, 2, 3, 4]  (原始列表被修改了!)

解决方案:如果不想修改原始对象,先创建副本。

# 改进版本:不修改原始列表
def add_item_safe(item, lst):
    """
    将 item 添加到列表(不修改原始列表)
    """
    # 创建列表的副本
    new_lst = lst.copy()
    # 或者使用 new_lst = lst[:]

    # 在副本上操作
    new_lst.append(item)

    # 返回新列表
    return new_lst

# 创建一个列表
my_list = [1, 2, 3]

# 调用函数
result = add_item_safe(4, my_list)

# 打印结果
print(f"函数返回值: {result}")

# 原始列表没有被修改
print(f"原始列表: {my_list}")

# 输出:
# 函数返回值: [1, 2, 3, 4]
# 原始列表: [1, 2, 3]  (原始列表没有被修改)

8.2 陷阱 2:默认参数使用可变对象 #

这个陷阱在前面已经详细讲解过,这里再强调一次。

# 错误示例
def bad_function(item, lst=[]):  # 危险!
    lst.append(item)
    return lst

# 正确示例
def good_function(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

9. Python 函数参数传递规则 #

9.1. 参数定义顺序规则(函数定义时) #

Python 定义函数时,参数的书写顺序必须遵循一定规范。通常应先书写普通位置参数(无默认值),之后是 *args(可变数量的位置参数),再是有默认值的关键字参数(即命名参数),最后是 **kwargs(可变数量的关键字参数)。如果顺序写错,会导致语法错误。比如,有默认值的参数不能出现在没有默认值的位置参数之前,否则 Python 无法解析参数传递的意图。

def func(位置参数, *args, 关键字参数=默认值, **kwargs):
    pass

必须顺序:

  1. 普通位置参数(无默认值)
  2. *args(可变位置参数)
  3. 关键字参数(有默认值)
  4. **kwargs(可变关键字参数)

示例:

# 正确
def func(a, b, *args, c=10, **kwargs):
    pass

# 错误:有默认值的参数不能在无默认值参数之前
def func(a=10, b):  # SyntaxError
    pass

9.2. 参数传递顺序规则(函数调用时) #

在调用函数时,实参的传递也有顺序规定。所有的位置参数必须在关键字参数前面传递,否则会导致语法错误。即先按顺序给出所有需要位置传递的参数,后面才可以通过参数名“key=value”的方式传递关键字参数。这样做是为了让 Python 明确知道每个值对应哪个参数名。

def greet(name, greeting, punctuation):
    return f"{greeting}, {name}{punctuation}"

# 正确:全部位置参数
greet("Charlie", "Hello", "!")

# 正确:全部关键字参数
greet(name="Charlie", greeting="Hello", punctuation="!")

# 正确:位置参数在前,关键字参数在后
greet("Charlie", "Hello", punctuation="!")
greet("Charlie", greeting="Hello", punctuation="!")

# 错误:位置参数在关键字参数之后
greet(name="Charlie", "Hello", "!")  # SyntaxError
greet("Charlie", greeting="Hello", "!")  # SyntaxError(如果还有位置参数)

9.3. 必须参数 vs 可选参数 #

在函数定义中,没有默认值的参数称为“必须参数”,调用时必须赋值;带有默认值的参数称为“可选参数”,在调用时可以选择性地赋值或省略,如果省略会自动填充默认值。注意如果缺少了必须参数,Python 会抛出 TypeError 错误。

def func(required1, required2, optional1=10, optional2=20):
    pass

# 必须参数必须传递
func(1, 2)  # 正确
func(1)     # 错误:缺少 required2

# 可选参数可以不传
func(1, 2)           # 使用默认值
func(1, 2, 30)       # optional1=30, optional2=20
func(1, 2, 30, 40)   # optional1=30, optional2=40

9.4. 关键字参数传递规则 #

可选参数和带默认值的参数可以通过“关键字参数”的方式指定传递,且可以跳过某些可选参数,给出你想设置的参数。而且多个关键字参数的顺序可以随意交换,不影响结果。这极大地增强了函数调用的灵活性和可读性。

def func(a, b, c=10, d=20):
    pass

# 可以跳过中间的可选参数
func(1, 2, d=40)  # a=1, b=2, c=10(默认), d=40

# 可以任意顺序传递关键字参数
func(1, 2, d=40, c=30)  # 正确
func(a=1, b=2, d=40, c=30)  # 正确

9.5. 重复传递参数规则 #

同一个参数只能被赋值一次,否则会报错。这包括既用位置参数赋值又用关键字参数赋值(重复)或者一个关键字参数名写两次(重复)。Python 禁止这种情况,以免导致参数值不明确。

def func(a, b, c):
    pass

# 错误:不能重复传递同一个参数
func(1, 2, a=3)  # TypeError: got multiple values for argument 'a'
func(1, b=2, b=3)  # SyntaxError: keyword argument repeated

9.6. 可变参数规则 #

可变参数允许函数接受任意数量的位置参数(*args)或关键字参数(**kwargs)。*args 会把多余的位置实参收集成元组,**kwargs 会把多余的关键字实参收集成字典。如果在 *args 之后出现新的参数(如 b),必须用关键字方式来传递,否则会报错。

def func(a, *args, b=10, **kwargs):
    pass

# 正确使用
func(1, 2, 3, 4, b=20, x=100, y=200)
# a=1, args=(2,3,4), b=20, kwargs={'x':100, 'y':200}

# 错误:*args 之后的位置参数必须用关键字传递
def func(a, *args, b):  # b 必须用关键字传递
    pass

func(1, 2, 3, 4)  # 错误:缺少 b
func(1, 2, 3, 4, b=5)  # 正确

9.7. 解包参数规则 #

函数调用时,可以利用 * 或 ** 操作符解包序列(如列表/元组)或字典,将其自动“分解”为单独的参数传递给函数。这对于参数数量不固定或参数来源灵活的场景非常实用。

def func(a, b, c):
    pass

# 使用 * 解包位置参数
args = (1, 2, 3)
func(*args)  # 等同于 func(1, 2, 3)

# 使用 ** 解包关键字参数
kwargs = {'a': 1, 'b': 2, 'c': 3}
func(**kwargs)  # 等同于 func(a=1, b=2, c=3)

# 混合使用
func(1, *[2, 3])  # 正确
func(1, **{'b': 2, 'c': 3})  # 正确
func(*[1, 2], c=3)  # 正确

9.8. 仅位置参数和仅关键字参数(Python 3.8+) #

从 Python 3.8 开始,可以用 / 指定“仅位置参数”,用 * 指定“仅关键字参数”。/ 之前的参数只能用位置方式传递,* 之后的参数只能用关键字方式传递。这种写法更细致地控制参数的传递方式,提高了接口的可读性和安全性。

# 使用 / 和 * 分隔参数
def func(pos_only, /, normal, *, kwd_only):
    pass

# 正确
func(1, 2, kwd_only=3)      # pos_only=1, normal=2, kwd_only=3
func(1, normal=2, kwd_only=3)  # 正确

# 错误
func(pos_only=1, 2, kwd_only=3)  # pos_only 只能用位置传递
func(1, 2, 3)  # kwd_only 必须用关键字传递

9.9. 默认参数陷阱 #

Python 的默认参数只在函数定义时计算一次,如果默认参数是可变对象(比如列表),每次调用实际上用的是同一个对象。这会导致意想不到的行为。一般推荐将默认参数写为 None,并在函数体内重新创建对象,避免副作用。

# ⚠️ 注意:默认参数只计算一次
def func(items=[]):  # 危险!
    items.append(1)
    return items

func()  # [1]
func()  # [1, 1]  # 不是 [1]!

# 正确做法
def func(items=None):
    if items is None:
        items = []
    items.append(1)
    return items

9.10. 参数传递优先级 #

当你既给参数提供了默认值,又在调用时显示传递了这个参数,显式传递的参数值会覆盖默认值,也就是“谁最后赋值谁生效”。这也是多数编程语言的一致做法,可帮助你灵活配置参数行为。

def func(a, b=10, c=20):
    pass

# 优先级:显式传递 > 默认值
func(1, 2, 3)        # a=1, b=2, c=3
func(1, 2)           # a=1, b=2, c=20
func(1, c=30)        # a=1, b=10, c=30

9.11 快速记忆口诀 #

  1. 定义顺序:位置 → *args → 关键字 → **kwargs
  2. 调用顺序:位置参数必须在关键字参数之前
  3. 必须参数:无默认值的参数必须传递
  4. 可选参数:有默认值的参数可以不传
  5. 不能重复:同一参数不能传递两次
  6. 关键字灵活:关键字参数可以任意顺序传递

10. 最佳实践总结 #

  1. 理解对象引用:记住 Python 传递的是对象引用,不是对象本身
  2. 区分可变和不可变对象:对可变对象的修改会影响原始对象
  3. 避免修改传入的可变对象:除非这是函数的目的,否则应该创建副本
  4. 默认参数使用不可变对象:使用 None 作为默认值,在函数内部创建新对象
  5. 合理使用 *args 和 kwargs**:提高函数的灵活性
  6. 参数顺序要正确:位置参数 → *args → 关键字参数 → **kwargs
  7. 函数设计原则:函数应该只做一件事,并做好它

11. 总结 #

理解 Python 的参数传递机制对于编写正确、高效的代码至关重要。关键要点:

  1. 传递的是对象引用:函数调用时传递的是对象的引用,不是对象本身
  2. 不可变对象:对不可变对象的"修改"实际上是创建了新对象,原始对象不变
  3. 可变对象:对可变对象的修改是在原对象上进行的,会影响所有指向该对象的变量
  4. 默认参数陷阱:不要使用可变对象作为默认参数,使用 None 代替
  5. 灵活的参数形式:合理使用位置参数、关键字参数、*args 和 **kwargs
← 上一节 openpyxl 下一节 PromptEngineering →

访问验证

请输入访问令牌

Token不正确,请重新输入