一、回顾与本篇目标
前四篇我们学了变量、数据类型、条件判断、循环、四种容器。你已经能用 Python 写出有一定逻辑的小程序了——比如猜数字游戏、学生成绩管理系统。
但你可能注意到一个问题:随着程序越写越长,有些代码出现了重复。比如成绩管理系统里,打印学生信息的那段代码在多个地方重复出现。如果要改格式,就得改好几处。
函数就是解决这个问题的。它把一段代码打包,起个名字,以后需要的时候直接喊名字就行。函数是编程中最核心的抽象工具——它能让你把复杂的问题拆成小块,逐个击破。
本篇的目标:
- 学会用
def定义函数 - 理解位置参数、默认参数、关键字参数的区别
- 掌握函数的返回值
- 学会
lambda匿名函数 - 理解 Python 函数的参数传递机制(和 JS 的异同)
二、为什么需要函数
先看一个没有函数的反面例子:
# 没有函数:重复代码
student1 = '张三'
score1 = 85
print(f'学生:{student1},成绩:{score1}分,等级:{"优秀" if score1 >= 90 else "良好" if score1 >= 80 else "及格"}')
student2 = '李四'
score2 = 72
print(f'学生:{student2},成绩:{score2}分,等级:{"优秀" if score2 >= 90 else "良好" if score2 >= 80 else "及格"}')
student3 = '王五'
score3 = 95
print(f'学生:{student3},成绩:{score3}分,等级:{"优秀" if score3 >= 90 else "良好" if score3 >= 80 else "及格"}')
同样的逻辑写了三遍。如果要加一个“不及格”的判断,得改三个地方。如果有 100 个学生呢?
用函数重写:
# 有函数:一次定义,反复使用
def print_student_report(name, score):
if score >= 90:
grade = '优秀'
elif score >= 80:
grade = '良好'
elif score >= 60:
grade = '及格'
else:
grade = '不及格'
print(f'学生:{name},成绩:{score}分,等级:{grade}')
# 使用函数
print_student_report('张三', 85)
print_student_report('李四', 72)
print_student_report('王五', 95)
逻辑只写了一次,修 bug、改格式都只需要改一处。函数就是代码的“打包盒”,把一段逻辑封装起来,起个名字,随时调用。
三、定义和调用函数
基本语法
def 函数名(参数1, 参数2, ...):
"""文档字符串:说明这个函数是干什么的(可选)"""
# 函数体:要执行的代码
return 返回值 # 可选,没有 return 则返回 None
最简单的函数
def greet():
print('你好!')
print('欢迎学习 Python')
# 调用函数(写多少次就执行多少次)
greet()
greet()
greet()
和 JavaScript 的对比:
# Python
def greet():
print('你好')
// JavaScript
function greet() {
console.log('你好');
}
核心区别:
- Python 用
def,JS 用function。 - Python 函数体靠缩进,JS 靠花括号。
- Python 函数名后面的括号和冒号是必须的。
带参数的函数
参数是函数接收外部数据的入口。调用时把具体的值传进去:
def greet(name):
print(f'你好,{name}!')
greet('张三') # 你好,张三!
greet('李四') # 你好,李四!
多个参数用逗号分隔:
def introduce(name, age, city):
print(f'我叫{name},今年{age}岁,来自{city}')
introduce('张三', 28, '上海')
introduce('李四', 22, '北京')
形参和实参
这两个术语在面试和文档中经常出现,用一张表说清楚:
| 术语 | 是什么 | 在哪里出现 | 示例 |
|---|---|---|---|
| 形参 | 定义函数时括号里的变量名,用来占位 | 函数定义中 | def foo(name): 中的 name |
| 实参 | 调用函数时传入的具体值 | 函数调用中 | foo('张三') 中的 '张三' |
def add(a, b): # a 和 b 是形参(占位符)
return a + b
result = add(3, 5) # 3 和 5 是实参(实际传入的值)
print(result) # 8
四、参数的五种类型
Python 函数的参数比 JavaScript 更灵活,有五种不同的传参方式。
1. 位置参数:按顺序传值
这是最常见的传参方式——按参数定义的位置顺序传入值:
def describe_pet(animal_type, pet_name):
print(f'我有一只{animal_type},名叫{pet_name}')
describe_pet('狗', '旺财') # 我有一只狗,名叫旺财
describe_pet('猫', '咪咪') # 我有一只猫,名叫咪咪
顺序不能乱——describe_pet('旺财', '狗') 会输出“我有一只旺财,名叫狗”,完全不对。
2. 关键字参数:按名字传值
调用函数时,可以明确指定形参的名字来传值,顺序就可以任意了:
def describe_pet(animal_type, pet_name):
print(f'我有一只{animal_type},名叫{pet_name}')
# 用关键字参数,顺序任意
describe_pet(animal_type='狗', pet_name='旺财')
describe_pet(pet_name='咪咪', animal_type='猫') # 顺序反过来也行
位置参数和关键字参数可以混用,但位置参数必须在关键字参数前面:
describe_pet('狗', pet_name='旺财') # 正确
# describe_pet(animal_type='狗', '旺财') # 报错!位置参数不能跟在关键字参数后面
3. 默认参数:参数有备选值
给形参设置一个默认值,调用时如果不传这个参数,就使用默认值:
def describe_pet(pet_name, animal_type='狗'):
print(f'我有一只{animal_type},名叫{pet_name}')
describe_pet('旺财') # 不传 animal_type,默认是“狗”
describe_pet('咪咪', animal_type='猫') # 传了就用传的值
有默认值的参数必须放在没有默认值的参数后面:
# 正确:默认参数在后面
def func(a, b=10):
pass
# 错误:默认参数不能在前
# def func(a=10, b): # SyntaxError
# pass
默认参数的一个坑:可变默认值
这是一道经典的 Python 面试题,也是实际开发中的常见 bug:
# 有问题的写法:默认值是可变对象(列表)
def add_item(item, items=[]):
items.append(item)
return items
print(add_item('苹果')) # ['苹果']
print(add_item('香蕉')) # ['苹果', '香蕉'] —— 预期是 ['香蕉']!
print(add_item('橘子')) # ['苹果', '香蕉', '橘子'] —— 越来越多了!
原因:Python 的默认参数只在函数定义时计算一次,而不是每次调用时都重新创建。所以三次调用实际上操作的是同一个列表对象。
正确写法:
def add_item(item, items=None):
if items is None:
items = [] # 每次调用时新建一个空列表
items.append(item)
return items
print(add_item('苹果')) # ['苹果']
print(add_item('香蕉')) # ['香蕉'] —— 正确了!
4. 可变参数:接收任意数量的位置参数
在形参前面加一个星号 *args,表示这个参数可以接收任意数量的位置参数,它们会被打包成一个元组:
def make_pizza(*toppings):
print('你点的披萨配料有:')
for topping in toppings:
print(f' - {topping}')
make_pizza('蘑菇')
make_pizza('蘑菇', '香肠', '芝士', '青椒')
*args 这个名字是约定俗成的(arguments 的缩写),重点在那个星号。你可以起任何名字,但大家都用 *args。
5. 关键字可变参数:接收任意数量的关键字参数
在形参前面加两个星号 **kwargs,表示这个参数可以接收任意数量的关键字参数,它们会被打包成一个字典:
def build_profile(**user_info):
for key, value in user_info.items():
print(f'{key}: {value}')
build_profile(name='张三', age=28, city='上海', job='程序员')
**kwargs 同样是约定俗成的名字(keyword arguments 的缩写),重点是两个星号。
五种参数的组合顺序
如果把五种参数都写在一个函数里,它们的顺序必须是:
def func(位置参数, *args, 默认参数, **kwargs):
pass
实际上很少会五种全用到。最常见的是位置参数、默认参数,偶尔用到 *args 或 **kwargs。
五、返回值:函数交出的结果
函数不只是做事情,还能交回结果。这就是 return 的作用。
返回单个值
def add(a, b):
return a + b
result = add(3, 5)
print(result) # 8
print(add(10, 20)) # 30
返回多个值
Python 函数可以“返回多个值”——实际上返回的是一个元组:
def get_user_info():
name = '张三'
age = 28
city = '上海'
return name, age, city # 实际返回的是 (name, age, city) 元组
# 用多个变量接收
name, age, city = get_user_info()
print(name, age, city) # 张三 28 上海
# 用一个变量接收(会得到一个元组)
info = get_user_info()
print(info) # ('张三', 28, '上海')
print(type(info)) #
这和 JavaScript 的解构赋值非常像,但 Python 的写法更自然。
没有 return 语句
如果函数没有 return,或者 return 后面什么都没有,函数返回 None:
def say_hello():
print('你好')
result = say_hello()
print(result) # None
None 是 Python 的“空值”,相当于 JavaScript 的 null 和 undefined 的结合体。
return 会立即终止函数
函数体内 return 后面的代码不会被执行:
def check_age(age):
if age < 18:
return '未成年'
# 如果上面 return 了,下面的代码不会执行
return '已成年'
print(check_age(15)) # '未成年'
print(check_age(25)) # '已成年'
六、变量的作用域
函数内部定义的变量和函数外部定义的变量,它们的“可见范围”是不同的。
局部变量
函数内部定义的变量是局部变量,只能在函数内部使用,函数外部访问不到:
def my_function():
message = '我是局部变量'
print(message) # 函数内部可以访问
my_function()
# print(message) # 报错!NameError: name 'message' is not defined
全局变量
在所有函数外部定义的变量是全局变量,整个程序都可以访问:
global_message = '我是全局变量'
def read_global():
print(global_message) # 可以读取全局变量
read_global() # '我是全局变量'
修改全局变量需要用 global 关键字
如果要在函数内部修改全局变量的值,必须用 global 关键字声明:
counter = 0
def increment():
global counter # 声明要修改全局变量
counter += 1
increment()
increment()
print(counter) # 2
如果不写 global counter,counter += 1 会被 Python 认为是在创建一个新的局部变量 counter,从而导致错误(因为局部变量 counter 还没赋值就被读取了)。
实际开发中,尽量少用全局变量。通过参数把值传进函数,通过 return 把结果传出来,这样代码更好维护。
七、lambda 匿名函数
lambda 是一种快速定义单行函数的方式。它没有函数名,所以叫“匿名函数”。
基本语法
lambda 参数: 表达式
它等价于:
def 函数名(参数):
return 表达式
简单示例
# 普通函数
def add(a, b):
return a + b
# lambda 写法
add_lambda = lambda a, b: a + b
print(add(3, 5)) # 8
print(add_lambda(3, 5)) # 8
lambda 最常见的用途:作为排序的 key
# 按第二个元素排序
pairs = [(1, 'one'), (4, 'four'), (3, 'three'), (2, 'two')]
pairs.sort(key=lambda pair: pair[1]) # 按英文单词排序
print(pairs)
# [(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]
lambda 也可以和 map、filter 一起使用
nums = [1, 2, 3, 4, 5]
# map:对每个元素应用同一个函数
squared = list(map(lambda x: x ** 2, nums))
print(squared) # [1, 4, 9, 16, 25]
# filter:筛选出满足条件的元素
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens) # [2, 4]
不过,Python 社区更推荐用列表推导式代替 map/filter(后面会讲),所以 lambda 在实际代码中最常见的还是用作 sort 的 key 参数。
lambda 和 JavaScript 箭头函数的对比
# Python
square = lambda x: x ** 2
// JavaScript
const square = (x) => x ** 2;
两者功能相似,但 Python 的 lambda 只能写一个表达式,不能包含多行语句。如果需要复杂逻辑,就用 def 定义命名函数。
八、参数传递机制:可变对象与不可变对象
这是一个理解 Python 函数行为的关键概念,也是面试高频题。
Python 的参数传递是“传递对象引用”——不是传值,也不是传引用,而是把实参对象的引用(内存地址)传给了形参。具体表现取决于传入的对象是可变的还是不可变的:
# 不可变对象作为参数(数字、字符串、元组)
def change_value(x):
x = 100 # 这行让 x 指向了一个新的对象,不影响外部的变量
print(f'函数内部:{x}')
a = 10
change_value(a)
print(f'函数外部:{a}') # 10 —— 没变!
# 可变对象作为参数(列表、字典)
def change_list(lst):
lst.append(4) # 修改了 lst 指向的对象本身,外部也会受影响
print(f'函数内部:{lst}')
my_list = [1, 2, 3]
change_list(my_list)
print(f'函数外部:{my_list}') # [1, 2, 3, 4] —— 变了!
核心规律:
- 如果传入的是不可变对象(数字、字符串、元组),在函数内部重新赋值不会影响外部变量。
- 如果传入的是可变对象(列表、字典),在函数内部修改对象的内容会影响外部变量。
这和 JavaScript 的行为类似——基本类型传值,对象类型传引用。但 Python 的不可变类型更多(元组、字符串等),理解这个机制能避免很多奇怪的 bug。
九、综合演示:一个简单的工具库
下面这段代码把本篇学到的函数知识整合起来,写几个实用的小工具函数:
# ========== 工具函数库 ==========
import math
# 1. 计算圆的面积和周长
def circle_info(radius):
area = math.pi * radius ** 2
circumference = 2 * math.pi * radius
return area, circumference
# 2. 判断一个年份是不是闰年
def is_leap_year(year):
return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)
# 3. 计算列表的平均值
def average(*numbers):
if len(numbers) == 0:
return 0
return sum(numbers) / len(numbers)
# 4. 生成指定范围内的素数列表
def get_primes(start, end):
primes = []
for num in range(start, end + 1):
if num < 2:
continue
is_prime = True
for i in range(2, int(math.sqrt(num)) + 1):
if num % i == 0:
is_prime = False
break
if is_prime:
primes.append(num)
return primes
# ========== 测试 ==========
area, circumference = circle_info(5)
print(f'半径 5 的圆:面积={area:.2f},周长={circumference:.2f}')
print(f'2000 年是闰年吗?{is_leap_year(2000)}')
print(f'2023 年是闰年吗?{is_leap_year(2023)}')
print(f'1 到 10 的平均值:{average(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)}')
print(f'没有参数:{average()}')
print(f'1 到 30 之间的素数:{get_primes(1, 30)}')
十、本篇动手练习
练习 1:计算器函数
新建 practice5-1.py,写一个函数 calculator(a, b, operator),operator 可以是 '+'、'-'、'*'、'/',返回对应的计算结果。如果 operator 不在这四种里,返回 None。
练习 2:字符串处理函数
新建 practice5-2.py,写一个函数 process_text(text, operation),operation 可以是 'upper'(全大写)、'lower'(全小写)、'reverse'(反转)、'count_words'(统计单词数)。
练习 3:斐波那契数列
新建 practice5-3.py,写一个函数 fibonacci(n),返回包含前 n 个斐波那契数的列表。斐波那契数列:0, 1, 1, 2, 3, 5, 8, 13, …(每个数等于前两个数之和)。
练习 4:密码强度检测
新建 practice5-4.py,写一个函数 check_password_strength(password),根据密码长度、是否包含数字、是否包含大小写字母、是否包含特殊字符来打分。返回 ‘弱’、’中’、’强’ 三个等级。
十一、本篇小结
这一篇你系统学习了 Python 函数的核心知识:
- 函数的定义和调用:
def关键字、缩进表示函数体、调用用函数名()。 - 五种参数:位置参数(按顺序)、关键字参数(按名字)、默认参数(有备选值)、
*args(接收任意数量的位置参数,打包成元组)、**kwargs(接收任意数量的关键字参数,打包成字典)。 - 返回值:
return返回结果,可以返回多个值(实际返回元组)。没有return则返回None。return会立即终止函数。 - 变量作用域:局部变量只能在函数内使用,全局变量可以全局访问,修改全局变量需要用
global关键字。 - lambda 匿名函数:单行函数的快捷写法,常用于排序的
key参数。 - 参数传递机制:传递对象引用。不可变对象修改不影响外部,可变对象修改会影响外部。
- 默认参数的坑:默认值如果是可变对象(如列表),每次调用会共用同一个对象。用
None作为默认值并在函数内部初始化。
函数是编程中最核心的抽象工具。把常用的逻辑封装成函数,你的代码会越来越像搭积木——调用现成的函数,组合出更复杂的功能。
下一篇预告
下一篇——《模块与包——组织你的代码》:当你的函数越来越多,一个文件会变得很臃肿。我们需要学会把函数拆分到不同的文件里,然后用 import 来引用。还会介绍 pip 安装第三方库、创建虚拟环境管理项目依赖。
Python 零基础入门,每周更新。














暂无评论内容