模块4:常用数据结构 (Organizing Lots of Data)
在前面的模块中,我们学习了如何使用变量来存储单个数据,比如一个数字、一个名字或一个布尔值。但很多时候,我们需要处理一组相关的数据,比如班级里所有学生的名字、一本书的所有章节标题、或者一个用户的各项配置信息。这时,就需要用到数据结构(Data Structures),它们是 Python 中用来组织和存储多个数据项的方式。
这个模块我们将学习 Python 中最常用的四种内置数据结构:列表(List)、元组(Tuple)、字典(Dictionary)和集合(Set)。
4.1 列表 (List): 最常用的“数据清单”
列表是 Python 中最常用、最灵活的数据结构。你可以把它想象成一个有序的、可以修改的清单。
- 有序 (Ordered): 列表中的每个元素都有一个固定的位置(索引),就像排队一样,顺序很重要。
- 可变 (Mutable): 你可以在创建列表后,随时添加、删除或修改里面的元素。
1. 创建列表
- 使用方括号
[]
,元素之间用逗号,
分隔。 - 列表可以包含任何类型的元素,甚至混合类型。
- 创建一个空列表:
[]
。
Python
# 一个包含数字的列表
numbers = [1, 2, 3, 4, 5]
print(numbers)
# 一个包含字符串的列表
fruits = ["apple", "banana", "cherry"]
print(fruits)
# 一个混合类型的列表
mixed_list = [10, "hello", 3.14, True, "banana"]
print(mixed_list)
# 一个空列表
empty_list = []
print(empty_list)
2. 访问列表元素 (Indexing)
- 和字符串一样,通过索引访问列表中的元素,索引从
0
开始。 - 也可以使用负数索引,
-1
表示最后一个元素,-2
表示倒数第二个,以此类推。
Python
colors = ["red", "green", "blue", "yellow"]
first_color = colors[0] # 'red'
second_color = colors[1] # 'green'
last_color = colors[-1] # 'yellow'
second_last = colors[-2] # 'blue'
print(f"第一个颜色是: {first_color}")
print(f"最后一个颜色是: {last_color}")
# 如果索引超出范围,会报错 IndexError
# print(colors[4]) # 会导致 IndexError
3. 列表切片 (Slicing)
- 和字符串一样,使用
[start:stop:step]
获取列表的一部分(子列表)。 - 包含
start
索引处的元素,但不包含stop
索引处的元素。 step
是可选的步长。
Python
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 获取索引 1 到 4 (不含 4) 的元素
sub_list1 = numbers[1:4] # [1, 2, 3]
print(sub_list1)
# 获取从索引 5 到末尾的元素
sub_list2 = numbers[5:] # [5, 6, 7, 8, 9]
print(sub_list2)
# 获取从开头到索引 3 (不含 3) 的元素
sub_list3 = numbers[:3] # [0, 1, 2]
print(sub_list3)
# 获取所有元素的一个副本
copy_list = numbers[:] # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(copy_list)
# 获取步长为 2 的元素 (隔一个取一个)
step_list = numbers[0:10:2] # [0, 2, 4, 6, 8]
print(step_list)
4. 修改列表元素
因为列表是可变的,你可以直接通过索引来修改元素的值。
Python
my_list = [10, 20, 30, 40]
print(f"原始列表: {my_list}")
# 修改索引为 1 的元素
my_list[1] = 25
print(f"修改后列表: {my_list}") # 输出: [10, 25, 30, 40]
5. 添加元素
append(item)
: 在列表末尾添加一个元素。insert(index, item)
: 在指定的索引位置插入一个元素,原来的元素及后面的元素会向后移动。
Python
hobbies = ["reading", "swimming"]
print(f"初始爱好: {hobbies}")
# 在末尾添加
hobbies.append("coding")
print(f"添加后: {hobbies}") # ['reading', 'swimming', 'coding']
# 在索引 1 的位置插入
hobbies.insert(1, "hiking")
print(f"插入后: {hobbies}") # ['reading', 'hiking', 'swimming', 'coding']
6. 删除元素
pop(index)
: 删除并返回指定索引位置的元素。如果省略index
,默认删除并返回最后一个元素。remove(value)
: 删除列表中第一个出现的指定值。如果值不存在,会报错ValueError
。del
语句:通过索引删除元素或切片。
Python
items = ["pen", "pencil", "eraser", "ruler", "pencil"]
print(f"原始物品: {items}")
# 删除并获取最后一个元素
last_item = items.pop()
print(f"被 pop 的元素: {last_item}") # 'pencil'
print(f"pop 后列表: {items}") # ['pen', 'pencil', 'eraser', 'ruler']
# 删除索引为 1 的元素
removed_item = items.pop(1)
print(f"被 pop(1) 的元素: {removed_item}") # 'pencil'
print(f"pop(1) 后列表: {items}") # ['pen', 'eraser', 'ruler']
# 删除第一个值为 "eraser" 的元素
items.remove("eraser")
print(f"remove 'eraser' 后列表: {items}") # ['pen', 'ruler']
# 如果 items.remove("notebook") 会报错 ValueError,因为 "notebook" 不存在
# 使用 del 删除索引为 0 的元素
del items[0]
print(f"del items[0] 后列表: {items}") # ['ruler']
# del 也可以删除切片
numbers = [1, 2, 3, 4, 5, 6]
del numbers[1:4] # 删除索引 1, 2, 3 的元素
print(f"del 切片后列表: {numbers}") # [1, 5, 6]
7. 常用列表方法
sort()
: 对列表进行原地排序(直接修改原列表)。默认升序。如果列表元素不能互相比较(如数字和字符串混合),会报错TypeError
。reverse()
: 将列表中的元素原地反转。len(list)
: (这是一个内置函数,不是方法) 返回列表中的元素个数。count(value)
: 返回列表中某个值出现的次数。index(value)
: 返回列表中某个值首次出现的索引。如果值不存在,会报错ValueError
。
Python
nums = [5, 1, 4, 2, 3, 1]
chars = ['c', 'a', 'b']
print(f"原始 nums: {nums}")
print(f"原始 chars: {chars}")
# 获取长度
print(f"nums 的长度: {len(nums)}") # 6
# 计数
print(f"nums 中 1 出现的次数: {nums.count(1)}") # 2
# 查找索引
print(f"nums 中 4 的索引: {nums.index(4)}") # 2
# 排序 (原地修改)
nums.sort()
chars.sort()
print(f"排序后 nums: {nums}") # [1, 1, 2, 3, 4, 5]
print(f"排序后 chars: {chars}") # ['a', 'b', 'c']
# 降序排序
nums.sort(reverse=True)
print(f"降序排序后 nums: {nums}") # [5, 4, 3, 2, 1, 1]
# 反转 (原地修改)
chars.reverse()
print(f"反转后 chars: {chars}") # ['c', 'b', 'a']
8. 列表推导式 (List Comprehensions) - 初步认识
列表推导式提供了一种更简洁、更高效的方式来创建列表,特别是基于现有列表或范围创建新列表时。
基本语法: [expression for item in iterable if condition]
expression
: 对item
进行处理的表达式,结果将是新列表的元素。for item in iterable
: 循环遍历一个可迭代对象(如列表、range()
等)。if condition
: (可选) 只有当condition
为True
时,item
才会被处理并添加到新列表中。
示例:
Python
# 1. 创建一个 0 到 9 的平方数的列表
squares = []
for x in range(10):
squares.append(x**2)
print(f"传统方法创建平方数列表: {squares}")
# 使用列表推导式
squares_comp = [x**2 for x in range(10)]
print(f"列表推导式创建平方数列表: {squares_comp}")
# 2. 创建一个 0 到 9 中偶数的列表
evens = []
for x in range(10):
if x % 2 == 0:
evens.append(x)
print(f"传统方法创建偶数列表: {evens}")
# 使用列表推导式
evens_comp = [x for x in range(10) if x % 2 == 0]
print(f"列表推导式创建偶数列表: {evens_comp}")
# 3. 将一个字符串列表中的所有单词转为大写
words = ["hello", "world", "python"]
upper_words = [word.upper() for word in words]
print(f"单词转大写: {upper_words}") # ['HELLO', 'WORLD', 'PYTHON']
列表推导式非常强大且常用,刚开始可能觉得有点抽象,但多练习几次就会发现它的便利。
4.2 元组 (Tuple): 不可变的“数据清单”
元组和列表非常相似,也是有序的序列。但它们之间有一个关键区别:元组是不可变的 (Immutable)。一旦创建,你就不能修改元组中的元素(不能添加、删除或更改)。
1. 创建元组
- 使用圆括号
()
,元素之间用逗号,
分隔。 - 注意: 创建只包含一个元素的元组时,必须在该元素后面加上逗号
,
,否则 Python 会把它当作普通的值。 - 创建空元组:
()
。 - 在某些情况下,括号可以省略(比如赋值时),Python 也能识别它是元组。
Python
# 一个包含数字的元组
numbers_tuple = (1, 2, 3)
print(numbers_tuple)
# 一个混合类型的元组
mixed_tuple = (10, "hello", 3.14)
print(mixed_tuple)
# 创建单元素元组 - 注意逗号!
single_tuple = (99,)
not_a_tuple = (99)
print(f"这是一个元组: {single_tuple}, 类型: {type(single_tuple)}")
print(f"这不是元组: {not_a_tuple}, 类型: {type(not_a_tuple)}") # 这是 int 类型
# 空元组
empty_tuple = ()
print(empty_tuple)
# 省略括号创建元组
point = 10, 20 # 这也是一个元组 (10, 20)
print(point)
print(type(point))
2. 访问元组元素 (Indexing & Slicing)
- 和列表、字符串完全一样,使用索引
[]
和切片[:]
。
Python
my_tuple = ('a', 'b', 'c', 'd', 'e')
print(my_tuple[0]) # 'a'
print(my_tuple[-1]) # 'e'
print(my_tuple[1:3]) # ('b', 'c')
3. 不可变性 (Immutability)
这是元组的核心特性。尝试修改元组会引发 TypeError
。
Python
immutable_tuple = (1, 2, 3)
# 下面的代码会报错 TypeError: 'tuple' object does not support item assignment
# immutable_tuple[0] = 100
# 下面的代码也会报错 AttributeError: 'tuple' object has no attribute 'append'
# immutable_tuple.append(4)
# 下面的代码也会报错 AttributeError: 'tuple' object has no attribute 'remove'
# immutable_tuple.remove(1)
4. 为什么使用元组?
既然列表那么灵活,为什么还需要不可变的元组呢?
-
性能: 元组通常比列表占用更少的内存,并且在某些操作上(如迭代访问)可能稍微快一点(尽管差异通常很小)。
-
安全性/数据保护: 不可变性确保了数据在创建后不会被意外修改,适用于存储不应改变的数据,如坐标点
(x, y)
、RGB颜色值(r, g, b)
等。 -
可以作为字典的键: 因为元组是不可变的,所以它可以作为字典的键(我们马上会学到字典),而列表不行。
-
元组解包 (Tuple Unpacking): 可以方便地将元组中的元素赋值给多个变量。
Pythoncoordinates = (10, 20, 30) x, y, z = coordinates # 解包 print(f"x={x}, y={y}, z={z}") # x=10, y=20, z=30
4.3 字典 (Dictionary / dict
): 键值对应的“查找表”
字典是另一种非常有用的数据结构,它存储的是键值对 (Key-Value Pairs)。你可以把它想象成一本真实的字典或电话簿:
- 键 (Key): 就像字典里的单词或电话簿里的名字,用来查找信息。键必须是唯一的、不可变的 (通常使用字符串、数字或元组)。
- 值 (Value): 就像单词的释义或人对应的电话号码,是与键相关联的数据。值可以是任何数据类型,也可以重复。
字典在 Python 3.7+ 版本中是按插入顺序存储的,但在更早的版本中是无序的。字典查找速度非常快,特别适合需要通过某个唯一标识来快速获取对应信息的场景。
1. 创建字典
- 使用花括号
{}
,键值对之间用逗号,
分隔,键和值之间用冒号:
分隔。 - 创建空字典:
{}
。
Python
# 一个存储学生信息的字典
student = {
"name": "Alice",
"age": 20,
"major": "Computer Science",
"is_graduated": False,
"courses": ["Math", "Physics", "Programming"] # 值可以是列表
}
print(student)
# 一个存储商品价格的字典
prices = {
"apple": 5.5,
"banana": 3.0,
"orange": 4.5
}
print(prices)
# 一个空字典
empty_dict = {}
print(empty_dict)
2. 访问字典的值
- 使用键放在方括号
[]
中来访问对应的值。如果键不存在,会引发KeyError
。 - 使用
get(key, default=None)
方法访问值。如果键不存在,它会返回None
(或者你指定的default
值),而不会报错。这通常是更安全的方式。
Python
student = {"name": "Bob", "age": 22, "major": "Physics"}
# 使用方括号访问
print(f"姓名: {student['name']}") # Bob
print(f"年龄: {student['age']}") # 22
# 尝试访问不存在的键会报错 KeyError
# print(student['city'])
# 使用 get() 方法访问
print(f"专业: {student.get('major')}") # Physics
print(f"城市: {student.get('city')}") # None (键不存在,返回 None)
print(f"城市 (带默认值): {student.get('city', 'Unknown')}") # Unknown (键不存在,返回指定的默认值 'Unknown')
3. 添加和修改键值对
- 直接给一个新键赋值,就可以添加新的键值对。
- 给一个已存在的键赋值,就会修改该键对应的值。
Python
student = {"name": "Charlie", "age": 19}
print(f"原始字典: {student}")
# 修改 age
student['age'] = 20
print(f"修改年龄后: {student}")
# 添加新的键值对 city
student['city'] = "London"
print(f"添加城市后: {student}") # {'name': 'Charlie', 'age': 20, 'city': 'London'}
4. 删除键值对
pop(key)
: 删除指定键的键值对,并返回对应的值。如果键不存在,会报错KeyError
。del
语句:通过键删除键值对。如果键不存在,也会报错KeyError
。
Python
contact = {"name": "David", "phone": "123-456", "email": "david@example.com"}
print(f"原始联系人: {contact}")
# 删除 email 并获取其值
removed_email = contact.pop("email")
print(f"被 pop 的 email: {removed_email}") # david@example.com
print(f"pop email 后: {contact}") # {'name': 'David', 'phone': '123-456'}
# 使用 del 删除 phone
del contact["phone"]
print(f"del phone 后: {contact}") # {'name': 'David'}
# 尝试删除不存在的键会报错
# del contact["address"] # KeyError
# contact.pop("city") # KeyError
5. 常用字典方法
keys()
: 返回一个包含所有键的“视图对象”(可以像列表一样遍历)。values()
: 返回一个包含所有值的“视图对象”。items()
: 返回一个包含所有**(键, 值)**元组对的“视图对象”。
Python
student = {"name": "Eve", "age": 21, "major": "Biology"}
# 获取所有键
keys = student.keys()
print(f"所有键: {keys}") # dict_keys(['name', 'age', 'major'])
print(list(keys)) # 可以转换为列表 ['name', 'age', 'major']
# 获取所有值
values = student.values()
print(f"所有值: {values}") # dict_values(['Eve', 21, 'Biology'])
print(list(values)) # 可以转换为列表 ['Eve', 21, 'Biology']
# 获取所有键值对
items = student.items()
print(f"所有项: {items}") # dict_items([('name', 'Eve'), ('age', 21), ('major', 'Biology')])
print(list(items)) # 可以转换为列表 [('name', 'Eve'), ('age', 21), ('major', 'Biology')]
6. 遍历字典
有几种常用的方式来遍历字典:
Python
student = {"name": "Frank", "age": 23, "major": "Chemistry"}
# 遍历键 (默认方式)
print("\n遍历键:")
for key in student:
print(f"键: {key}, 值: {student[key]}") # 通过键再次访问值
# 遍历值
print("\n遍历值:")
for value in student.values():
print(value)
# 遍历键值对 (推荐方式)
print("\n遍历键值对 (使用 .items()):")
for key, value in student.items(): # 使用元组解包
print(f"{key}: {value}")
4.4 集合 (Set): 无序且唯一的“元素包”
集合是一个无序的、包含不重复元素的集合。你可以把它想象成一个袋子,里面装的东西没有顺序,而且同样的东西只能装一个。
集合的主要用途:
- 去重 (Removing Duplicates): 快速去除列表或其他序列中的重复元素。
- 成员检测 (Membership Testing): 快速判断一个元素是否存在于集合中(比在列表中查找快得多)。
- 集合运算 (Set Operations): 进行数学上的集合运算,如交集、并集、差集等。
1. 创建集合
- 使用花括号
{}
,元素之间用逗号,
分隔。 - 注意: 创建空集合必须使用
set()
函数,因为{}
创建的是空字典! - 可以直接从列表或其他可迭代对象创建集合,它会自动去重。
Python
# 创建一个包含数字的集合 (重复的 3 会被忽略)
numbers_set = {1, 2, 3, 4, 3, 2}
print(numbers_set) # 输出可能是 {1, 2, 3, 4} (顺序不固定)
# 创建一个包含字符串的集合
fruits_set = {"apple", "banana", "cherry"}
print(fruits_set)
# 创建空集合 - 必须用 set()
empty_set = set()
print(empty_set)
print(type(empty_set)) # <class 'set'>
# 从列表创建集合 (自动去重)
my_list = [1, 1, 2, 3, 4, 4, 4, 5]
my_set_from_list = set(my_list)
print(my_set_from_list) # {1, 2, 3, 4, 5}
2. 无序性
集合不保证元素的顺序,所以你不能像列表或元组那样使用索引来访问元素。
Python
my_set = {10, 20, 30}
# 下面的代码会报错 TypeError: 'set' object is not subscriptable
# print(my_set[0])
3. 唯一性
集合中不允许有重复的元素。如果你尝试添加一个已存在的元素,集合不会发生任何变化。
4. 添加和删除元素
add(element)
: 添加一个元素到集合中。如果元素已存在,则什么也不做。remove(element)
: 从集合中删除一个元素。如果元素不存在,会引发KeyError
。discard(element)
: 从集合中删除一个元素。如果元素不存在,它不会报错,而是什么也不做。通常discard
更安全。
Python
colors = {"red", "green"}
print(f"原始集合: {colors}")
# 添加元素
colors.add("blue")
print(f"添加 blue 后: {colors}")
colors.add("red") # 尝试添加已存在的元素
print(f"再次添加 red 后: {colors}") # 集合不变
# 删除元素
colors.remove("green")
print(f"删除 green 后: {colors}")
# 尝试 remove 不存在的元素会报错
# colors.remove("yellow") # KeyError
# 使用 discard 删除存在的元素
colors.discard("blue")
print(f"discard blue 后: {colors}")
# 使用 discard 删除不存在的元素 (不会报错)
colors.discard("yellow")
print(f"discard yellow 后: {colors}") # 集合不变
5. 集合运算
集合支持标准的数学集合运算:
- 成员检测 (
in
): 判断元素是否在集合中(非常高效)。 - 并集 (
|
或union()
): 返回包含两个集合中所有元素的新集合。 - 交集 (
&
或intersection()
): 返回两个集合中共同存在的元素组成的新集合。 - 差集 (
-
或difference()
): 返回存在于第一个集合但不在第二个集合中的元素组成的新集合。 - 对称差集 (
^
或symmetric_difference()
): 返回存在于两个集合中,但不同时存在于两个集合中的元素(即并集减去交集)。
Python
set_a = {1, 2, 3, 4}
set_b = {3, 4, 5, 6}
print(f"集合 A: {set_a}")
print(f"集合 B: {set_b}")
# 成员检测
print(f"2 在 A 中吗? {2 in set_a}") # True
print(f"5 在 A 中吗? {5 in set_a}") # False
# 并集
union_set = set_a | set_b
# union_set = set_a.union(set_b)
print(f"并集 A | B: {union_set}") # {1, 2, 3, 4, 5, 6}
# 交集
intersection_set = set_a & set_b
# intersection_set = set_a.intersection(set_b)
print(f"交集 A & B: {intersection_set}") # {3, 4}
# 差集 (A 中有,B 中没有)
difference_set = set_a - set_b
# difference_set = set_a.difference(set_b)
print(f"差集 A - B: {difference_set}") # {1, 2}
# 差集 (B 中有,A 中没有)
print(f"差集 B - A: {set_b - set_a}") # {5, 6}
# 对称差集 (只在 A 或只在 B 中的元素)
sym_diff_set = set_a ^ set_b
# sym_diff_set = set_a.symmetric_difference(set_b)
print(f"对称差集 A ^ B: {sym_diff_set}") # {1, 2, 5, 6}
4.5 实践时间!
练习1:简单的学生成绩管理
- 创建一个列表,名为
students
。这个列表将用来存储多个学生的信息。 - 列表中的每个元素都是一个字典,代表一个学生。每个学生字典至少包含以下键:
"name"
: 学生姓名 (字符串)"grades"
: 一个包含该学生各科成绩(数字)的列表。- 例如:
{"name": "Alice", "grades": [85, 90, 78]}
- 向
students
列表中添加至少3个学生的信息。 - 编写代码,计算并打印出每个学生的平均成绩。你需要遍历
students
列表,对于每个学生字典,再遍历其"grades"
列表来计算总分和平均分。 - (可选)添加一个新功能:允许用户输入学生姓名,然后查找并打印该学生的成绩列表和平均分。如果学生不存在,则打印提示信息。
练习2:词频统计
- 定义一个包含一段文本的字符串变量(比如一句或几句话)。
- 预处理文本:
- 将文本全部转换为小写(使用字符串的
lower()
方法)。 - (可选,简化处理)去除标点符号。你可以用
replace()
方法将常见的标点符号(如.
,,
,?
,!
)替换为空格或空字符串。
- 将文本全部转换为小写(使用字符串的
- 分词: 使用字符串的
split()
方法将处理后的文本分割成一个单词列表。默认情况下,split()
会按空格分割。 - 统计词频:
- 创建一个空字典,名为
word_counts
,用来存储每个单词出现的次数。 - 遍历你的单词列表。
- 对于列表中的每个单词:
- 检查这个单词是否已经是
word_counts
字典的键。 - 如果是,将该键对应的值(计数)加 1。
- 如果不是,将这个单词作为新键添加到字典中,并将值设为 1。
- (提示:使用
get(word, 0)
可以很方便地获取当前计数,如果单词不存在则返回0,然后加1即可:word_counts[word] = word_counts.get(word, 0) + 1
)
- 检查这个单词是否已经是
- 创建一个空字典,名为
- 遍历
word_counts
字典,打印出每个单词及其出现的次数。
模块5:函数——代码的复用
在我们之前的编程练习中,你可能已经注意到,有时我们会重复写几乎完全一样的代码块。比如,计算平均分的逻辑、打印特定格式信息的代码等。如果这样的代码很多,程序会变得冗长、难以阅读,而且一旦需要修改这部分逻辑,你就得找到所有重复的地方一一修改,非常麻烦且容易出错。
函数(Function)就是解决这个问题的利器!它可以让你把一段具有特定功能的代码打包起来,给它起一个名字,然后在需要执行这段代码的任何地方,简单地“调用”这个名字即可。
5.1 为什么需要函数?(DRY 原则)
使用函数的主要原因是为了遵循 DRY 原则:Don't Repeat Yourself(不要重复你自己)。
- 提高代码复用性: 定义一次,多次调用。
- 提高代码可读性: 将复杂的任务分解成小的、有明确功能的函数,让主程序逻辑更清晰。给函数起一个有意义的名字本身就是一种注释。
- 提高代码可维护性: 如果需要修改某项功能的逻辑,只需要修改对应的函数定义即可,所有调用该函数的地方都会自动生效。
- 方便协作: 不同的开发者可以分工编写不同的函数模块。
5.2 定义函数
使用 def
关键字来定义一个函数。
基本语法:
Python
def function_name(parameters):
"""
(可选) 函数文档字符串 (Docstring) - 解释函数的作用、参数和返回值。
通常用三引号包裹。
"""
# 函数体 (缩进的代码块)
# 这里是函数的具体逻辑
statement1
statement2
# ...
return value # (可选) 返回一个结果
def
: 定义函数的关键字。function_name
: 你给函数起的名字,遵循变量命名规则(snake_case)。()
: 函数名后面的圆括号,必须有。parameters
: (可选) 括号里的变量名,是函数接收的输入值(参数),多个参数用逗号分隔。如果没有参数,括号也是空的()
。:
: 圆括号后面必须有冒号。- Docstring: (可选但强烈推荐) 一个用三引号
"""Docstring goes here"""
包裹的字符串,用于解释函数的功能。好的文档字符串对于理解和使用函数至关重要。 - 函数体: 缩进的代码块,包含函数的实际逻辑。
return value
: (可选) 使用return
关键字将函数的结果(值)发送回调用它的地方。如果函数没有return
语句,或者return
后面没有值,它默认返回None
。
示例:一个简单的问候函数
Python
def greet():
"""打印一句简单的问候语。"""
print("Hello there! Welcome.")
def greet_user(name):
"""根据提供的名字打印个性化问候语。
Args:
name: 要问候的人的名字 (字符串)。
"""
print(f"Hello, {name}! Nice to meet you.")
5.3 调用函数
定义好函数后,你需要调用(Call 或 Invoke)它来执行函数体内的代码。调用函数的方法是写出函数名,后面跟上圆括号 ()
,并在括号内提供必要的参数(Arguments)。
Python
# 调用没有参数的函数
greet() # 输出: Hello there! Welcome.
# 调用有参数的函数,需要提供参数值
greet_user("Alice") # 输出: Hello, Alice! Nice to meet you.
greet_user("Bob") # 输出: Hello, Bob! Nice to meet you.
当程序执行到函数调用时,它会“跳转”到函数的定义处,执行函数体内的代码,然后再“跳回”到函数被调用的地方继续执行。
5.4 参数 (Parameters) 与 实参 (Arguments)
这两个词经常互换使用,但严格来说:
- 参数 (Parameters): 定义函数时,写在圆括号里的变量名(如
greet_user
函数中的name
)。它们是函数内部使用的占位符。 - 实参 (Arguments): 调用函数时,传递给函数的实际值(如
greet_user("Alice")
中的"Alice"
)。
Python 支持多种传递参数的方式:
1. 位置参数 (Positional Arguments)
最常见的方式。实参会按照它们在调用时出现的顺序,依次传递给函数定义中的参数。
Python
def describe_pet(animal_type, pet_name):
"""显示宠物的信息。"""
print(f"I have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet("hamster", "harry") # "hamster" 传给 animal_type, "harry" 传给 pet_name
describe_pet("dog", "willie")
注意:位置参数的顺序很重要,传错了会导致逻辑错误。 describe_pet("willie", "dog")
就会输出错误的信息。
2. 关键字参数 (Keyword Arguments)
调用函数时,你可以明确指定哪个实参传递给哪个参数,使用 参数名=值
的形式。这时,实参的顺序就不重要了。
Python
describe_pet(animal_type="cat", pet_name="whiskers")
describe_pet(pet_name="goldie", animal_type="fish") # 顺序不同,但结果正确
注意:在一个函数调用中,关键字参数必须跟在所有位置参数之后。
3. 默认参数值 (Default Parameter Values)
在定义函数时,可以为参数指定一个默认值。如果在调用函数时没有为该参数提供实参,那么就会使用这个默认值。
Python
def power(base, exponent=2): # exponent 参数默认值为 2
"""计算 base 的 exponent 次方。"""
result = base ** exponent
print(f"{base} 的 {exponent} 次方是 {result}")
power(5) # 没有提供 exponent,使用默认值 2。输出: 5 的 2 次方是 25
power(3, 3) # 提供了 exponent,使用提供的值 3。输出: 3 的 3 次方是 27
power(base=4) # 也可以用关键字参数调用
power(base=2, exponent=5)
注意:在函数定义中,所有带默认值的参数必须放在所有不带默认值的参数之后。
4. 可变参数 (Variable-Length Arguments) - 初步认识
有时你希望函数能接受任意数量的参数。
-
Python*args
(任意数量的位置参数): 在参数名前加一个星号*
。这会将调用时提供的多余的位置参数收集到一个元组 (tuple) 中。参数名通常约定俗成用args
,但也可以用别的名字(如*numbers
)。def make_pizza(size, *toppings): """概述要制作的比萨。 Args: size: 比萨的尺寸。 *toppings: 一个包含所有配料的元组。 """ print(f"\nMaking a {size}-inch pizza with the following toppings:") if toppings: # 检查 toppings 元组是否为空 for topping in toppings: print(f"- {topping}") else: print("- Plain cheese") make_pizza(12, "mushrooms") make_pizza(16, "mushrooms", "green peppers", "extra cheese") make_pizza(10) # toppings 会是一个空元组
-
Python**kwargs
(任意数量的关键字参数): 在参数名前加两个星号**
。这会将调用时提供的多余的关键字参数收集到一个字典 (dict) 中。参数名通常约定俗成用kwargs
,但也可以用别的名字(如**user_info
)。def build_profile(first, last, **user_info): """创建一个字典,包含我们知道的有关用户的一切。 Args: first: 名。 last: 姓。 **user_info: 包含其他信息的字典。 """ profile = {} profile['first_name'] = first profile['last_name'] = last for key, value in user_info.items(): # 遍历 kwargs 字典 profile[key] = value return profile # 返回构建好的字典 user_profile = build_profile('albert', 'einstein', location='princeton', field='physics') print(user_profile) # 输出: {'first_name': 'albert', 'last_name': 'einstein', 'location': 'princeton', 'field': 'physics'} user_profile2 = build_profile('marie', 'curie', field='physics', award='Nobel Prize') print(user_profile2)
注意:在函数定义中,参数的顺序通常是:普通位置参数 -> 默认参数 -> *args
-> **kwargs
。对于初学者,理解 *args
和 **kwargs
的基本概念(收集多余参数)即可。
5.5 返回值 (Return Values)
函数不仅可以执行操作(比如打印),还可以计算一个结果并将其“返回”给调用它的代码。这通过 return
语句实现。
- 当执行到
return
语句时,函数立即停止执行,并将return
后面的值作为结果返回。 - 调用函数的地方可以接收这个返回值,通常是赋给一个变量。
- 如果函数没有
return
语句,或者return
后面没有值,它默认返回特殊值None
。 - 一个函数可以有多个
return
语句(例如在不同的if
分支里),但只要执行到任何一个return
,函数就会结束。 - 可以返回任何类型的数据:数字、字符串、列表、字典,甚至另一个函数!
- 可以一次返回多个值,它们会被自动打包成一个元组。
Python
def add(x, y):
"""计算两个数的和并返回结果。"""
result = x + y
return result
sum_result = add(5, 3)
print(f"5 + 3 = {sum_result}") # 输出: 5 + 3 = 8
def get_name_parts(full_name):
"""将全名分割成名和姓。"""
parts = full_name.split() # 按空格分割
if len(parts) >= 2:
first_name = parts[0]
last_name = " ".join(parts[1:]) # 处理可能有中间名的情况
return first_name, last_name # 返回两个值 (打包成元组)
elif len(parts) == 1:
return parts[0], None # 如果只有一个部分,姓氏返回 None
else:
return None, None # 如果输入为空,都返回 None
first, last = get_name_parts("Albert Einstein")
print(f"First: {first}, Last: {last}") # First: Albert, Last: Einstein
first, last = get_name_parts("Marie Curie")
print(f"First: {first}, Last: {last}") # First: Marie, Last: Curie
first, last = get_name_parts("Madonna")
print(f"First: {first}, Last: {last}") # First: Madonna, Last: None
def check_even(number):
"""检查数字是否为偶数。"""
if number % 2 == 0:
return True # 如果是偶数,返回 True 并结束函数
# 如果上面的 if 不满足,会执行到这里
return False
print(f"4 是偶数吗? {check_even(4)}") # True
print(f"7 是偶数吗? {check_even(7)}") # False
def print_and_return_nothing(message):
"""打印消息,但不显式返回值。"""
print(message)
result_none = print_and_return_nothing("Hello") # 会打印 Hello
print(f"函数返回值: {result_none}") # 输出: 函数返回值: None
5.6 函数的作用域 (Scope)
作用域指的是变量能够被访问的区域。
-
局部变量 (Local Variables): 在函数内部定义的变量(包括参数)是局部变量。它们只在函数被调用时创建,在函数执行结束时销毁。局部变量只能在函数内部访问,不能在函数外部访问。
Pythondef my_function(): local_var = 10 # 局部变量 print(f"函数内部: {local_var}") my_function() # 输出: 函数内部: 10 # 下面这行会报错 NameError: name 'local_var' is not defined # print(f"函数外部: {local_var}")
-
全局变量 (Global Variables): 在所有函数外部定义的变量是全局变量。它们在程序的整个生命周期内都存在。全局变量可以在程序的任何地方(包括函数内部)被读取。
Pythonglobal_var = 100 # 全局变量 def read_global(): print(f"函数内部读取全局变量: {global_var}") def try_modify_global(): # 试图直接修改全局变量 (不推荐,且可能产生 UnboundLocalError) # global_var = global_var + 1 # 这会报错,Python 认为你想创建一个新的局部变量 print(f"函数内部尝试修改前读取: {global_var}") # 可以读取 read_global() # 输出: 函数内部读取全局变量: 100 try_modify_global() print(f"函数外部: {global_var}") # 输出: 函数外部: 100 (全局变量未被修改)
-
修改全局变量 (
Pythonglobal
关键字): 如果确实需要在函数内部修改全局变量的值,需要使用global
关键字明确声明。但通常不推荐这样做,因为它会破坏函数的封装性,使代码更难理解和调试。更好的做法是将需要修改的值作为参数传入,然后返回修改后的值。count = 0 # 全局变量 def increment_global(): global count # 声明要修改的是全局变量 count count += 1 print(f"函数内 count: {count}") increment_global() # 输出: 函数内 count: 1 increment_global() # 输出: 函数内 count: 2 print(f"函数外 count: {count}") # 输出: 函数外 count: 2
对于初学者,主要理解函数内部定义的变量是局部的,外部无法访问即可。尽量避免使用 global
关键字。
5.7 匿名函数 (lambda
) - (可选/简要了解)
有时你需要一个非常简单的、只用一次的小函数,lambda
表达式提供了一种快速定义这种匿名函数(没有名字的函数)的方式。
语法: lambda arguments: expression
lambda
: 关键字。arguments
: 和普通函数的参数一样。:
: 分隔符。expression
: 一个表达式,它的计算结果就是函数的返回值(不需要return
关键字)。Lambda 函数体只能包含一个表达式。
Python
# 普通函数定义
def add_regular(x, y):
return x + y
# 等效的 lambda 函数
add_lambda = lambda x, y: x + y
print(add_regular(5, 3)) # 8
print(add_lambda(5, 3)) # 8
# 直接使用 lambda
result = (lambda a, b: a * b)(6, 7) # 定义并立即调用
print(result) # 42
# lambda 常用于需要传入简单函数的地方,比如排序的 key
points = [(1, 2), (3, 1), (5, -4), (0, 8)]
# 按每个元组的第二个元素排序
points.sort(key=lambda point: point[1])
print(points) # [(5, -4), (3, 1), (1, 2), (0, 8)]
Lambda 对于初学者来说不是必需掌握的,了解有这样一种简洁写法即可,当你看到别人代码里用 lambda
时能大概理解它的意思就行。
5.8 实践时间!
练习1:将之前的练习改写成函数形式
- 重构闰年判断器:
- 定义一个函数
is_leap(year)
。 - 它接收一个年份
year
作为参数。 - 函数体包含之前判断闰年的逻辑。
- 函数应该返回
True
(如果year
是闰年)或False
(如果不是)。 - 在主程序部分,获取用户输入,调用
is_leap()
函数,并根据返回值打印结果。
- 定义一个函数
- 重构简单计算器:
- 定义一个函数
calculate(num1, num2, operator)
。 - 接收两个数字
num1
,num2
和一个代表运算符的字符串operator
('+'
,'-'
,'*'
,/
) 作为参数。 - 根据
operator
执行相应的计算。 - 返回计算结果。如果运算符无效或除数为零,可以考虑返回
None
或打印错误信息并返回None
。 - 在主程序部分,获取用户输入的两个数字和运算符,调用
calculate()
函数,并打印结果(如果结果不是None
)。
- 定义一个函数
练习2:编写工具函数 - 计算平均值
- 定义一个函数
calculate_average(numbers)
。 - 它接收一个包含数字的列表
numbers
作为参数。 - 函数应该计算并返回列表中所有数字的平均值。
- 考虑边界情况: 如果输入的列表
numbers
是空的,直接计算平均值会报错(除以零)。在函数开始时检查列表是否为空 (if not numbers:
或者if len(numbers) == 0:
),如果为空,可以返回0
或者None
(并在文档字符串中说明)。 - 测试你的函数:创建一个数字列表,调用
calculate_average()
并打印结果。也测试一下空列表的情况。