查看: 302|回复: 0

Python函数参数与返回值:*args与**kwargs避坑

[复制链接]
发表于 3 小时前 | 显示全部楼层 |阅读模式
函数在脚本编程里首先是复用手段。原文从折扣计算切入:如果每次都把同一套折扣公式抄一遍,规则变更时容易漏改。把逻辑放入 calculate_discount,只保留一份计算规则,调用时传入不同价格即可。函数还承担命名作用,看到函数名就能判断用途。

下面是未封装写法与封装后的对比:
  1. price1 = 200
  2. final_price1 = price1 * 0.8
  3. print(final_price1)
  4. price2 = 350
  5. final_price2 = price2 * 0.8
  6. print(final_price2)
复制代码
  1. def calculate_discount(price):
  2.     final_price = price * 0.8
  3.     return final_price
  4. print(calculate_discount(200))
  5. print(calculate_discount(350))
复制代码

一、定义和调用函数

Python 用 def 定义函数,函数名后要有圆括号,行尾冒号不能省略,函数体需要缩进。定义函数不会立即执行函数体,必须用函数名加圆括号调用:
  1. def say_hello():
  2.     print('Hello')
  3. say_hello()
  4. say_hello()
复制代码

程序执行到函数定义时只是记住函数;执行到调用语句时才进入函数体。函数调用结束后,会回到调用位置继续执行后面的代码。函数通常先定义再调用。若先调用后定义,例如先写 say_hello(),再写 def say_hello():,会因为名字尚未建立而触发 NameError。

二、形参、实参与参数传递方式

定义函数时圆括号里的变量叫形参,调用时传入的数据叫实参。例如 def greet(name): 中 name 是形参,调用 greet('Ivan') 时 'Ivan' 是实参。
  1. def greet(name):
  2.     print(f'Hello,{name}')
  3. greet('Ivan')
  4. greet('Tom')
复制代码

多个参数用逗号分隔。调用时实参数量要与形参对应,少传或多传会触发 TypeError:
  1. def add(a, b):
  2.     result = a + b
  3.     print(result)
  4. add(10, 20)
复制代码

按位置传值叫位置参数。位置写错时 Python 不一定报错,但数据含义可能已经改变,例如 introduce(20, '杭州', 'Ivan') 可以运行,却把 20 传给了 name。

调用时明确写参数名,就是关键字参数:
  1. def introduce(name, age, city):
  2.     print(f'姓名:{name}')
  3.     print(f'年龄:{age}')
  4.     print(f'城市:{city}')
  5. introduce(name='Ivan', age=20, city='杭州')
  6. introduce(city='杭州', name='Ivan', age=20)
复制代码

关键字参数顺序可以调整,参数较多或布尔值、数字含义不直观时更易读。位置参数和关键字参数可以混用,但位置参数必须写在关键字参数前面;写成 introduce(name='Ivan', 20, '杭州') 会出现语法错误。

三、默认参数与可变对象陷阱

定义函数时可以给参数设置默认值。调用时没有传入该参数,就使用默认值;传入后默认值被覆盖:
  1. def greet(name, message='Hello'):
  2.     print(f'{message},{name}')
  3. greet('Ivan')
  4. greet('Tom', '早上好')
复制代码

必填参数要放在默认参数前面。def greet(message='Hello', name): 的顺序不正确,因为只传一个位置参数时无法确定交给谁,Python 会提示语法错误。默认参数适合页码、排序方式、是否显示某个选项等真正有常用默认值的设置。

需要注意,不要直接用列表、字典、集合这类可变对象作为默认值。下面的 items=[] 只在函数定义时创建一次,后续调用会共用同一个列表:
  1. def add_item(item, items=[]):
  2.     items.append(item)
  3.     return items
  4. print(add_item('Python'))
  5. print(add_item('Java'))
复制代码

输出会是:
  1. ['Python']
  2. ['Python', 'Java']
复制代码

通常改成 None,再在函数内部创建新列表:
  1. def add_item(item, items=None):
  2.     if items is None:
  3.         items = []
  4.     items.append(item)
  5.     return items
复制代码

四、*args 与 **kwargs 的接收规则

参数数量不固定时,可以在参数名前加一个星号。*args 把多个位置参数收集成元组:
  1. def calculate_total(*prices):
  2.     total = 0
  3.     for price in prices:
  4.         total += price
  5.     return total
  6. print(calculate_total(20, 35, 18))
  7. print(calculate_total(10, 20, 30, 40, 50))
复制代码

prices 是元组,args 只是常用名称,真正起作用的是星号。普通参数可以和 *args 一起出现,第一个实参交给普通参数,剩余位置参数进入元组:
  1. def print_scores(title, *scores):
  2.     print(title)
  3.     for score in scores:
  4.         print(score)
  5. print_scores('第一次考试', 86, 92, 78)
复制代码

两个星号 **kwargs 用来接收数量不固定的关键字参数,得到字典。调用时写下的参数名会成为键,传入的数据会成为值:
  1. def create_user(**info):
  2.     print(info)
  3. create_user(name='Ivan', age=20, city='杭州')
复制代码

输出为 {'name': 'Ivan', 'age': 20, 'city': '杭州'}。普通参数、默认参数、*args、**kwargs 可以放在同一个函数中,常用的排列顺序是:普通参数、带默认值的参数、*args、**kwargs。参数过多会让调用和维护变复杂;如果一组数据本来属于同一对象,直接传字典往往更合适。

五、return 与 print 的区别

print 只是把内容输出到终端,调用者拿不到结果继续计算。return 会把结果交回函数调用位置,可以保存到变量,也可以参与后续运算:
  1. def add(a, b):
  2.     return a + b
  3. result = add(10, 20)
  4. print(result)
  5. total = add(10, 20) * 2
  6. print(total)
复制代码

如果只在函数里写 print(a + b),函数虽然会显示 30,但没有返回结果,result 会是 None。

return 还会立即结束函数,后面的代码不会执行。这种特性常用于提前处理不符合要求的数据:
  1. def check_age(age):
  2.     if age < 0:
  3.         return '年龄不能小于 0'
  4.     return '年龄有效'
  5. print(check_age(-1))
  6. print(check_age(20))
复制代码
  1. def divide(a, b):
  2.     if b == 0:
  3.         return None
  4.     return a / b
复制代码

如果函数没有写 return,或者只写了一个没有返回值的 return,Python 默认返回 None。只负责打印的函数,把调用结果保存到变量后通常得到 None。

一个函数可以返回多个值,写法上是 return total, difference,实际返回的是元组,调用时可以直接解包:
  1. def calculate(a, b):
  2.     total = a + b
  3.     difference = a - b
  4.     return total, difference
  5. result = calculate(10, 3)
  6. print(result)
  7. total, difference = calculate(10, 3)
  8. print(total)
  9. print(difference)
复制代码

左侧变量数量要与返回值数量对应,否则会出现 ValueError。返回内容较多且每项含义明确时,返回字典更清楚。

六、参数传递的是对象

调用函数时,形参会引用传入的对象。数字、字符串、元组是不可变对象,函数内部重新赋值不会改变外部变量:
  1. def change_number(number):
  2.     number = 100
  3. value = 10
  4. change_number(value)
  5. print(value)
复制代码

列表、字典、集合是可变对象。如果函数直接修改对象内容,外部也能看到变化:
  1. def add_skill(skills):
  2.     skills.append('Python')
  3. user_skills = ['Java']
  4. add_skill(user_skills)
  5. print(user_skills)
复制代码

如果不想修改原列表,可以在函数内部复制后再返回:
  1. def add_skill(skills):
  2.     new_skills = skills.copy()
  3.     new_skills.append('Python')
  4.     return new_skills
复制代码

七、文档字符串与函数命名

函数体第一行可以用三引号写文档字符串,说明函数做什么、接收什么参数、返回什么内容。可通过 help() 查看:
  1. def calculate_discount(price, rate=0.8):
  2.     '''根据原价和折扣率计算折后价格。'''
  3.     return price * rate
  4. help(calculate_discount)
复制代码

函数名通常使用小写字母和下划线,表达实际完成的操作,例如 calculate_total、get_user_info、save_file。f1、do_it 这类名字很难从调用位置判断用途。函数最好完成一件相对明确的事情;同时也没有必要把每一两行代码都拆成函数,关键看逻辑是否重复、命名后是否更清楚。

八、综合示例:计算订单价格

下面函数接收商品价格列表、折扣率和运费,返回订单各项金额。prices 是必填参数;discount 和 shipping_fee 有常用默认值。空列表会提前 return,不会继续执行后面的计算。返回字典而不是直接打印,可以让调用者决定如何展示或继续使用数据:
  1. def calculate_order(prices, discount=1.0, shipping_fee=0):
  2.     '''计算订单原价、折扣金额和最终价格。'''
  3.     if not prices:
  4.         return {
  5.             'subtotal': 0,
  6.             'discount_amount': 0,
  7.             'shipping_fee': shipping_fee,
  8.             'final_price': shipping_fee,
  9.         }
  10.     subtotal = sum(prices)
  11.     discounted_price = subtotal * discount
  12.     discount_amount = subtotal - discounted_price
  13.     final_price = discounted_price + shipping_fee
  14.     return {
  15.         'subtotal': subtotal,
  16.         'discount_amount': discount_amount,
  17.         'shipping_fee': shipping_fee,
  18.         'final_price': final_price,
  19.     }
  20. order = calculate_order(
  21.     prices=[99, 128, 56],
  22.     discount=0.9,
  23.     shipping_fee=10,
  24. )
  25. print('商品原价:', order['subtotal'])
  26. print('优惠金额:', order['discount_amount'])
  27. print('运费:', order['shipping_fee'])
  28. print('最终价格:', order['final_price'])
复制代码

九、常见错误排查

1. 定义函数后忘记调用:只写 def say_hello(): 不会执行,需要再写 say_hello()。
2. 调用时忘记圆括号:result = say_hello 取得的是函数对象,不是调用结果,应写成 say_hello()。
3. 参数数量不一致:def add(a, b) 需要两个参数,add(10) 会触发 TypeError;多传参数同样会报错,除非使用 *args 或 **kwargs。
4. 把 print 当返回值:函数打印了结果但没有 return,变量保存到的会是 None。
5. 在 return 后继续写要执行的代码:return 会立即结束函数,确实需要执行的逻辑要放在 return 前面。
6. 使用列表作为默认参数:可变默认值会被多次调用共同使用,需要空列表时通常写成 None,再在函数内部创建。

练习部分可以按原文思路做:写函数计算矩形面积;接收整数并返回偶数 True、奇数 False;用 *args 接收不固定成绩并返回平均分,没有成绩时返回 None;用 **kwargs 接收姓名、年龄、城市并返回用户信息字典;在订单函数基础上增加满减规则,例如原价达到 300 再减 20,并把满减金额放入返回结果。

整体来看,函数把重复逻辑单独保存,参数负责把数据传入函数,return 负责把结果交回调用位置。理解默认参数、*args、**kwargs、可变对象和返回值的差别,能减少大量脚本调试问题。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-17 13:17 , Processed in 0.024848 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部