第二阶段:数据结构与函数

模块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: (可选) 只有当 conditionTrue 时,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): 可以方便地将元组中的元素赋值给多个变量。

    Python

    coordinates = (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:简单的学生成绩管理

  1. 创建一个列表,名为 students。这个列表将用来存储多个学生的信息。
  2. 列表中的每个元素都是一个字典,代表一个学生。每个学生字典至少包含以下键:
    • "name": 学生姓名 (字符串)
    • "grades": 一个包含该学生各科成绩(数字)的列表
    • 例如:{"name": "Alice", "grades": [85, 90, 78]}
  3. students 列表中添加至少3个学生的信息。
  4. 编写代码,计算并打印出每个学生的平均成绩。你需要遍历 students 列表,对于每个学生字典,再遍历其 "grades" 列表来计算总分和平均分。
  5. (可选)添加一个新功能:允许用户输入学生姓名,然后查找并打印该学生的成绩列表和平均分。如果学生不存在,则打印提示信息。

练习2:词频统计

  1. 定义一个包含一段文本的字符串变量(比如一句或几句话)。
  2. 预处理文本:
    • 将文本全部转换为小写(使用字符串的 lower() 方法)。
    • (可选,简化处理)去除标点符号。你可以用 replace() 方法将常见的标点符号(如 ., ,, ?, !)替换为空格或空字符串。
  3. 分词: 使用字符串的 split() 方法将处理后的文本分割成一个单词列表。默认情况下,split() 会按空格分割。
  4. 统计词频:
    • 创建一个空字典,名为 word_counts,用来存储每个单词出现的次数。
    • 遍历你的单词列表。
    • 对于列表中的每个单词:
      • 检查这个单词是否已经是 word_counts 字典的
      • 如果是,将该键对应的值(计数)加 1。
      • 如果不是,将这个单词作为新键添加到字典中,并将值设为 1。
      • (提示:使用 get(word, 0) 可以很方便地获取当前计数,如果单词不存在则返回0,然后加1即可:word_counts[word] = word_counts.get(word, 0) + 1
  5. 遍历 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) - 初步认识

有时你希望函数能接受任意数量的参数。

  • *args (任意数量的位置参数): 在参数名前加一个星号 *。这会将调用时提供的多余的位置参数收集到一个元组 (tuple) 中。参数名通常约定俗成用 args,但也可以用别的名字(如 *numbers)。

    Python

    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 会是一个空元组
    
  • **kwargs (任意数量的关键字参数): 在参数名前加两个星号 **。这会将调用时提供的多余的关键字参数收集到一个字典 (dict) 中。参数名通常约定俗成用 kwargs,但也可以用别的名字(如 **user_info)。

    Python

    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): 在函数内部定义的变量(包括参数)是局部变量。它们只在函数被调用时创建,在函数执行结束时销毁。局部变量只能在函数内部访问,不能在函数外部访问。

    Python

    def 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): 在所有函数外部定义的变量是全局变量。它们在程序的整个生命周期内都存在。全局变量可以在程序的任何地方(包括函数内部)被读取。

    Python

    global_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 (全局变量未被修改)
    
  • 修改全局变量 (global 关键字): 如果确实需要在函数内部修改全局变量的值,需要使用 global 关键字明确声明。但通常不推荐这样做,因为它会破坏函数的封装性,使代码更难理解和调试。更好的做法是将需要修改的值作为参数传入,然后返回修改后的值。

    Python

    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:将之前的练习改写成函数形式

  1. 重构闰年判断器:
    • 定义一个函数 is_leap(year)
    • 它接收一个年份 year 作为参数。
    • 函数体包含之前判断闰年的逻辑。
    • 函数应该返回 True(如果 year 是闰年)或 False(如果不是)。
    • 在主程序部分,获取用户输入,调用 is_leap() 函数,并根据返回值打印结果。
  2. 重构简单计算器:
    • 定义一个函数 calculate(num1, num2, operator)
    • 接收两个数字 num1, num2 和一个代表运算符的字符串 operator ('+', '-', '*', /) 作为参数。
    • 根据 operator 执行相应的计算。
    • 返回计算结果。如果运算符无效或除数为零,可以考虑返回 None 或打印错误信息并返回 None
    • 在主程序部分,获取用户输入的两个数字和运算符,调用 calculate() 函数,并打印结果(如果结果不是 None)。

练习2:编写工具函数 - 计算平均值

  1. 定义一个函数 calculate_average(numbers)
  2. 它接收一个包含数字的列表 numbers 作为参数。
  3. 函数应该计算并返回列表中所有数字的平均值。
  4. 考虑边界情况: 如果输入的列表 numbers 是空的,直接计算平均值会报错(除以零)。在函数开始时检查列表是否为空 (if not numbers: 或者 if len(numbers) == 0:),如果为空,可以返回 0 或者 None(并在文档字符串中说明)。
  5. 测试你的函数:创建一个数字列表,调用 calculate_average() 并打印结果。也测试一下空列表的情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值