函数在脚本编程里首先是复用手段。原文从折扣计算切入:如果每次都把同一套折扣公式抄一遍,规则变更时容易漏改。把逻辑放入 calculate_discount,只保留一份计算规则,调用时传入不同价格即可。函数还承担命名作用,看到函数名就能判断用途。
下面是未封装写法与封装后的对比:
- price1 = 200
- final_price1 = price1 * 0.8
- print(final_price1)
- price2 = 350
- final_price2 = price2 * 0.8
- print(final_price2)
复制代码- def calculate_discount(price):
- final_price = price * 0.8
- return final_price
- print(calculate_discount(200))
- print(calculate_discount(350))
复制代码
一、定义和调用函数
Python 用 def 定义函数,函数名后要有圆括号,行尾冒号不能省略,函数体需要缩进。定义函数不会立即执行函数体,必须用函数名加圆括号调用:
- def say_hello():
- print('Hello')
- say_hello()
- say_hello()
复制代码
程序执行到函数定义时只是记住函数;执行到调用语句时才进入函数体。函数调用结束后,会回到调用位置继续执行后面的代码。函数通常先定义再调用。若先调用后定义,例如先写 say_hello(),再写 def say_hello():,会因为名字尚未建立而触发 NameError。
二、形参、实参与参数传递方式
定义函数时圆括号里的变量叫形参,调用时传入的数据叫实参。例如 def greet(name): 中 name 是形参,调用 greet('Ivan') 时 'Ivan' 是实参。
- def greet(name):
- print(f'Hello,{name}')
- greet('Ivan')
- greet('Tom')
复制代码
多个参数用逗号分隔。调用时实参数量要与形参对应,少传或多传会触发 TypeError:
- def add(a, b):
- result = a + b
- print(result)
- add(10, 20)
复制代码
按位置传值叫位置参数。位置写错时 Python 不一定报错,但数据含义可能已经改变,例如 introduce(20, '杭州', 'Ivan') 可以运行,却把 20 传给了 name。
调用时明确写参数名,就是关键字参数:
- def introduce(name, age, city):
- print(f'姓名:{name}')
- print(f'年龄:{age}')
- print(f'城市:{city}')
- introduce(name='Ivan', age=20, city='杭州')
- introduce(city='杭州', name='Ivan', age=20)
复制代码
关键字参数顺序可以调整,参数较多或布尔值、数字含义不直观时更易读。位置参数和关键字参数可以混用,但位置参数必须写在关键字参数前面;写成 introduce(name='Ivan', 20, '杭州') 会出现语法错误。
三、默认参数与可变对象陷阱
定义函数时可以给参数设置默认值。调用时没有传入该参数,就使用默认值;传入后默认值被覆盖:
- def greet(name, message='Hello'):
- print(f'{message},{name}')
- greet('Ivan')
- greet('Tom', '早上好')
复制代码
必填参数要放在默认参数前面。def greet(message='Hello', name): 的顺序不正确,因为只传一个位置参数时无法确定交给谁,Python 会提示语法错误。默认参数适合页码、排序方式、是否显示某个选项等真正有常用默认值的设置。
需要注意,不要直接用列表、字典、集合这类可变对象作为默认值。下面的 items=[] 只在函数定义时创建一次,后续调用会共用同一个列表:
- def add_item(item, items=[]):
- items.append(item)
- return items
- print(add_item('Python'))
- print(add_item('Java'))
复制代码
输出会是:
- ['Python']
- ['Python', 'Java']
复制代码
通常改成 None,再在函数内部创建新列表:
- def add_item(item, items=None):
- if items is None:
- items = []
- items.append(item)
- return items
复制代码
四、*args 与 **kwargs 的接收规则
参数数量不固定时,可以在参数名前加一个星号。*args 把多个位置参数收集成元组:
- def calculate_total(*prices):
- total = 0
- for price in prices:
- total += price
- return total
- print(calculate_total(20, 35, 18))
- print(calculate_total(10, 20, 30, 40, 50))
复制代码
prices 是元组,args 只是常用名称,真正起作用的是星号。普通参数可以和 *args 一起出现,第一个实参交给普通参数,剩余位置参数进入元组:
- def print_scores(title, *scores):
- print(title)
- for score in scores:
- print(score)
- print_scores('第一次考试', 86, 92, 78)
复制代码
两个星号 **kwargs 用来接收数量不固定的关键字参数,得到字典。调用时写下的参数名会成为键,传入的数据会成为值:
- def create_user(**info):
- print(info)
- create_user(name='Ivan', age=20, city='杭州')
复制代码
输出为 {'name': 'Ivan', 'age': 20, 'city': '杭州'}。普通参数、默认参数、*args、**kwargs 可以放在同一个函数中,常用的排列顺序是:普通参数、带默认值的参数、*args、**kwargs。参数过多会让调用和维护变复杂;如果一组数据本来属于同一对象,直接传字典往往更合适。
五、return 与 print 的区别
print 只是把内容输出到终端,调用者拿不到结果继续计算。return 会把结果交回函数调用位置,可以保存到变量,也可以参与后续运算:
- def add(a, b):
- return a + b
- result = add(10, 20)
- print(result)
- total = add(10, 20) * 2
- print(total)
复制代码
如果只在函数里写 print(a + b),函数虽然会显示 30,但没有返回结果,result 会是 None。
return 还会立即结束函数,后面的代码不会执行。这种特性常用于提前处理不符合要求的数据:
- def check_age(age):
- if age < 0:
- return '年龄不能小于 0'
- return '年龄有效'
- print(check_age(-1))
- print(check_age(20))
复制代码- def divide(a, b):
- if b == 0:
- return None
- return a / b
复制代码
如果函数没有写 return,或者只写了一个没有返回值的 return,Python 默认返回 None。只负责打印的函数,把调用结果保存到变量后通常得到 None。
一个函数可以返回多个值,写法上是 return total, difference,实际返回的是元组,调用时可以直接解包:
- def calculate(a, b):
- total = a + b
- difference = a - b
- return total, difference
- result = calculate(10, 3)
- print(result)
- total, difference = calculate(10, 3)
- print(total)
- print(difference)
复制代码
左侧变量数量要与返回值数量对应,否则会出现 ValueError。返回内容较多且每项含义明确时,返回字典更清楚。
六、参数传递的是对象
调用函数时,形参会引用传入的对象。数字、字符串、元组是不可变对象,函数内部重新赋值不会改变外部变量:
- def change_number(number):
- number = 100
- value = 10
- change_number(value)
- print(value)
复制代码
列表、字典、集合是可变对象。如果函数直接修改对象内容,外部也能看到变化:
- def add_skill(skills):
- skills.append('Python')
- user_skills = ['Java']
- add_skill(user_skills)
- print(user_skills)
复制代码
如果不想修改原列表,可以在函数内部复制后再返回:
- def add_skill(skills):
- new_skills = skills.copy()
- new_skills.append('Python')
- return new_skills
复制代码
七、文档字符串与函数命名
函数体第一行可以用三引号写文档字符串,说明函数做什么、接收什么参数、返回什么内容。可通过 help() 查看:
- def calculate_discount(price, rate=0.8):
- '''根据原价和折扣率计算折后价格。'''
- return price * rate
- help(calculate_discount)
复制代码
函数名通常使用小写字母和下划线,表达实际完成的操作,例如 calculate_total、get_user_info、save_file。f1、do_it 这类名字很难从调用位置判断用途。函数最好完成一件相对明确的事情;同时也没有必要把每一两行代码都拆成函数,关键看逻辑是否重复、命名后是否更清楚。
八、综合示例:计算订单价格
下面函数接收商品价格列表、折扣率和运费,返回订单各项金额。prices 是必填参数;discount 和 shipping_fee 有常用默认值。空列表会提前 return,不会继续执行后面的计算。返回字典而不是直接打印,可以让调用者决定如何展示或继续使用数据:
- def calculate_order(prices, discount=1.0, shipping_fee=0):
- '''计算订单原价、折扣金额和最终价格。'''
- if not prices:
- return {
- 'subtotal': 0,
- 'discount_amount': 0,
- 'shipping_fee': shipping_fee,
- 'final_price': shipping_fee,
- }
- subtotal = sum(prices)
- discounted_price = subtotal * discount
- discount_amount = subtotal - discounted_price
- final_price = discounted_price + shipping_fee
- return {
- 'subtotal': subtotal,
- 'discount_amount': discount_amount,
- 'shipping_fee': shipping_fee,
- 'final_price': final_price,
- }
- order = calculate_order(
- prices=[99, 128, 56],
- discount=0.9,
- shipping_fee=10,
- )
- print('商品原价:', order['subtotal'])
- print('优惠金额:', order['discount_amount'])
- print('运费:', order['shipping_fee'])
- 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、可变对象和返回值的差别,能减少大量脚本调试问题。 |