别再写垃圾Python函数了,参数传递的坑你踩过几个?

39 阅读5分钟

你有没有遇到过这种情况:

def add_user(users, name, age):
    users[name] = age
    return users

user_list = {"张三": 25}
result = add_user(user_list, "李四", 30)
print(user_list)  # 你猜输出什么?

你兴高采烈地写下这段代码,以为结果就是 {"张三": 25},结果一运行...

{"张三": 25, "李四": 30}  # WTF?为什么我的user_list变了?

然后你百度了一圈,Stack Overflow翻了三页,最后发现... Python的参数传递根本不是你想的那样!

今天咱们就来彻底搞懂这个让人头大的玩意儿。

先给你上一课:Python参数传递的真相

很多教程都说:"Python参数传递是值传递",或者"Python参数传递是引用传递"。

都是错的!

Python的参数传递有个专门的术语,叫做:对象引用传递(Object Reference Passing)。

翻译成人话就是:你传给函数的不是值本身,也不是对象的引用,而是指向对象的一个引用

听起来像不像绕口令?别急,本小姐用海底捞的例子给你解释清楚:

# 想象你有一个海底捞等位牌(变量)
# 这个等位牌上有桌号(对象的内存地址)
def 修改等位信息(等位牌):
    # 你拿着等位牌,但你可以选择:
    # 1. 在原桌加菜(修改原对象)
    # 2. 换到新桌(让等位牌指向新对象)
    pass

两大阵营:可变对象 vs 不可变对象

Python的对象分两种,这也是Python函数参数传递绕不开的坑:

不可变对象(Immutable)

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

可变对象(Mutable)

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

记住这个铁律:

  • 传不可变对象:函数内部修改不会影响外部
  • 传可变对象:函数内部修改会影响外部(但在函数内部重新赋值不会)

来看具体例子:

# 不可变对象示例
def change_number(num):
    num = num + 1
    print(f"函数内部:{num}")

x = 10
change_number(x)
print(f"函数外部:{x}")
# 输出:
# 函数内部:11
# 函数外部:10  # 没变!
# 可变对象示例
def change_list(lst):
    lst.append(999)
    print(f"函数内部:{lst}")

my_list = [1, 2, 3]
change_list(my_list)
print(f"函数外部:{my_list}")
# 输出:
# 函数内部:[1, 2, 3, 999]
# 函数外部:[1, 2, 3, 999]  # 变了!

五种参数定义方式,你用对了吗?

Python函数参数有5种写法,每种都有它的使用场景:

1. 位置参数(Positional Arguments)

最基础的写法:

def send_email(to, subject, content):
    print(f"发给:{to}")
    print(f"主题:{subject}")
    print(f"内容:{content}")

# 调用时必须按顺序
send_email("老板@gmail.com", "加薪申请", "老板,我想涨工资...")

2. 关键字参数(Keyword Arguments)

调用时指定参数名,顺序可以乱:

send_email(
    content="老板,我想涨工资...",
    to="老板@gmail.com",
    subject="加薪申请"
)

3. 默认参数(Default Arguments)

给参数设置默认值,不传就用默认的:

def register_user(username, email, active=True, role="普通用户"):
    print(f"用户:{username}")
    print(f"邮箱:{email}")
    print(f"状态:{'活跃' if active else '禁用'}")
    print(f"角色:{role}")

register_user("张三", "zhangsan@qq.com")  # active和role用默认值
register_user("李四", "lisi@qq.com", active=False)  # 只改active

⚠️ 致命警告:默认参数的坑!

# ❌ 千万不要这样写!
def add_item(item, items=[]):  # 这里有个大坑!
    items.append(item)
    return items

add_item("苹果")  # 返回 ["苹果"]
add_item("香蕉")  # 返回 ["苹果", "香蕉"]  # WTF?为什么苹果还在?

# ✅ 正确写法
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

原因是:默认参数在函数定义时就创建了,不是在调用时!

4. 可变位置参数(*args)

接收任意数量的位置参数:

def calculate_sum(*numbers):
    print(f"收到 {len(numbers)} 个数字")
    total = sum(numbers)
    return total

print(calculate_sum(1, 2, 3))  # 6
print(calculate_sum(10, 20, 30, 40, 50))  # 150
print(calculate_sum())  # 0

5. 可变关键字参数(**kwargs)

接收任意数量的关键字参数:

def create_user_profile(**user_info):
    print("用户信息:")
    for key, value in user_info.items():
        print(f"  {key}: {value}")

create_user_profile(name="张三", age=25, city="北京", hobby="编程")

参数组合的正确姿势

当多种参数混用时,顺序很关键:

def 函数名(
    位置参数,
    *可变位置参数,
    关键字参数,
    **可变关键字参数
):
    pass
def complex_function(a, b, *args, c=10, d=20, **kwargs):
    print(f"a: {a}, b: {b}")
    print(f"args: {args}")
    print(f"c: {c}, d: {d}")
    print(f"kwargs: {kwargs}")

complex_function(1, 2, 3, 4, 5, c=100, extra_param="额外参数")

输出:

a: 1, b: 2
args: (3, 4, 5)
c: 100, d: 20
kwargs: {'extra_param': '额外参数'}

实战避坑指南

坑1:不要在函数内部修改可变参数

# ❌ 危险写法
def process_data(data):
    data.sort()  # 直接修改原数据!
    return data

# ✅ 安全写法
def process_data(data):
    new_data = data.copy()  # 先复制
    new_data.sort()
    return new_data

坑2:谨慎使用可变对象作为默认参数

(上面已经讲过了,再强调一遍)

坑3:注意参数类型检查

def calculate_discount(price, discount_rate):
    if not isinstance(price, (int, float)):
        raise TypeError("price必须是数字")
    if not isinstance(discount_rate, (int, float)):
        raise TypeError("discount_rate必须是数字")
    if not 0 <= discount_rate <= 1:
        raise ValueError("discount_rate必须在0-1之间")

    return price * (1 - discount_rate)

坑4:合理使用类型提示

from typing import List, Dict, Optional, Union

def user_profile(
    name: str,
    age: int,
    emails: List[str],
    metadata: Optional[Dict[str, Union[str, int]]] = None
) -> Dict[str, any]:
    """
    创建用户档案

    Args:
        name: 用户名
        age: 年龄
        emails: 邮箱列表
        metadata: 额外信息(可选)

    Returns:
        用户档案字典
    """
    if metadata is None:
        metadata = {}

    return {
        "name": name,
        "age": age,
        "emails": emails,
        "metadata": metadata
    }

最佳实践总结

  1. 优先使用不可变对象作为参数,避免副作用
  2. 默认参数用None,不要用可变对象
  3. 适当使用类型提示,提高代码可读性
  4. 函数只做一件事,参数控制在5个以内
  5. 合理使用返回值,不要试图在函数中"顺便"修改参数

记住一句话:函数参数就像给朋友的礼物,送出去了就不是你的了,别老想着去修改它。

下次再遇到Python函数参数的坑,你就知道该怎么避开了。


你在项目里遇到过哪些Python参数传递的坑?评论区聊聊,看看有没有更骚的操作!