如何基于给定函数高效地分组 Python 列表

PythonBeginner
立即练习

介绍

在 Python 编程中,组织和操作数据集合是一项基本任务。其中一个常见操作是根据特定标准对列表元素进行分组。这个过程将你的数据转换为有组织的类别,使其更易于分析和处理。

在本教程中,你将学习如何使用各种技术有效地对 Python 列表中的元素进行分组。我们将从基本方法开始,逐步引入更强大的内置函数来实现此目的。在本实验(Lab)结束时,你将对在 Python 中分组列表数据的不同方法有一个实际的理解。

使用字典进行基本列表分组

让我们首先了解一下列表分组的含义,以及如何使用 Python 字典实现基本的分组技术。

什么是列表分组?

列表分组是根据特定特征或函数将列表元素组织成类别的过程。例如,你可能希望根据数字是偶数还是奇数来对数字列表进行分组,或者根据单词的首字母对单词列表进行分组。

使用字典进行基本分组

在 Python 中对列表元素进行分组的最直接方法是使用字典:

  • 键(key)代表组
  • 值(value)是包含属于每个组的元素的列表

让我们创建一个简单的例子,根据数字是偶数还是奇数来对它们进行分组。

步骤 1:创建一个 Python 文件

首先,让我们创建一个新的 Python 文件来编写我们的代码:

  1. 打开 WebIDE 并在 /home/labex/project 目录下创建一个名为 group_numbers.py 的新文件。

  2. 将以下代码添加到文件中:

## 使用字典进行基本列表分组
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

## 初始化一个空字典来存储我们的组
even_odd_groups = {"even": [], "odd": []}

## 根据数字是偶数还是奇数进行分组
for num in numbers:
    if num % 2 == 0:
        even_odd_groups["even"].append(num)
    else:
        even_odd_groups["odd"].append(num)

## 打印结果组
print("按奇偶分组数字:")
print(f"偶数:{even_odd_groups['even']}")
print(f"奇数:{even_odd_groups['odd']}")
  1. 保存文件。

步骤 2:运行 Python 脚本

运行脚本以查看结果:

  1. 在 WebIDE 中打开一个终端。

  2. 执行脚本:

python3 /home/labex/project/group_numbers.py

你应该看到类似如下的输出:

按奇偶分组数字:
偶数: [2, 4, 6, 8, 10]
奇数: [1, 3, 5, 7, 9]

步骤 3:按更复杂的标准分组

现在,让我们修改我们的脚本,根据数字除以 3 后的余数进行分组:

  1. 将以下代码添加到你的 group_numbers.py 文件中:
## 按除以 3 后的余数分组数字
remainder_groups = {0: [], 1: [], 2: []}

for num in numbers:
    remainder = num % 3
    remainder_groups[remainder].append(num)

print("\n按除以 3 后的余数分组数字:")
for remainder, nums in remainder_groups.items():
    print(f"余数为 {remainder} 的数字:{nums}")
  1. 保存文件。

  2. 再次运行脚本:

python3 /home/labex/project/group_numbers.py

现在你应该看到额外的输出:

按除以 3 后的余数分组数字:
余数为 0 的数字: [3, 6, 9]
余数为 1 的数字: [1, 4, 7, 10]
余数为 2 的数字: [2, 5, 8]

这种使用字典的基本技术提供了一种直接的方法来对列表元素进行分组。但是,随着你的分组需求变得更加复杂,Python 提供了更强大和更有效的方法,我们将在接下来的步骤中进行探讨。

使用 itertools.groupby() 进行高效分组

现在你已经了解了分组的基本概念,让我们探索一种更强大的方法,使用内置的 itertools.groupby() 函数。当处理已排序的数据时,此函数特别有用。

理解 itertools.groupby()

itertools 模块中的 groupby() 函数根据一个键函数对可迭代对象中的连续元素进行分组。它返回一个迭代器,该迭代器生成以下配对:

  • 键函数返回的值
  • 一个迭代器,生成组中的项目

重要提示:groupby() 仅对连续的项目进行分组,因此输入数据通常需要先进行排序。

让我们实现一个例子,看看这在实践中是如何工作的。

步骤 1:创建一个新的 Python 文件

  1. /home/labex/project 目录下创建一个名为 groupby_example.py 的新文件。

  2. 添加以下代码以导入必要的模块:

import itertools

## 示例数据
words = ["apple", "banana", "avocado", "blueberry", "apricot", "blackberry"]

步骤 2:按首字母对单词进行分组

现在,让我们使用 itertools.groupby() 按首字母对单词进行分组:

  1. 将以下代码添加到你的 groupby_example.py 文件中:
## 首先,我们需要按将用于分组的键对列表进行排序
## 在这种情况下,每个单词的首字母
words.sort(key=lambda x: x[0])
print("排序后的单词:", words)

## 现在按首字母分组
grouped_words = {}
for first_letter, group in itertools.groupby(words, key=lambda x: x[0]):
    grouped_words[first_letter] = list(group)

## 打印结果组
print("\n按首字母分组单词:")
for letter, words_group in grouped_words.items():
    print(f"以 '{letter}' 开头的单词:{words_group}")
  1. 保存文件。

  2. 运行脚本:

python3 /home/labex/project/groupby_example.py

你应该看到类似如下的输出:

排序后的单词: ['apple', 'apricot', 'avocado', 'banana', 'blackberry', 'blueberry']

按首字母分组单词:
以 'a' 开头的单词: ['apple', 'apricot', 'avocado']
以 'b' 开头的单词: ['banana', 'blackberry', 'blueberry']

步骤 3:理解排序的重要性

为了演示在使用 groupby() 时排序的重要性,让我们添加另一个没有排序的例子:

  1. 将以下代码添加到你的 groupby_example.py 文件中:
## 示例数据(未排序)
unsorted_words = ["apple", "banana", "avocado", "blueberry", "apricot", "blackberry"]

print("\n--- 首先不排序 ---")
print("原始单词:", unsorted_words)

## 尝试在不排序的情况下分组
unsorted_grouped = {}
for first_letter, group in itertools.groupby(unsorted_words, key=lambda x: x[0]):
    unsorted_grouped[first_letter] = list(group)

print("\n在不排序的情况下分组:")
for letter, words_group in unsorted_grouped.items():
    print(f"以 '{letter}' 开头的单词:{words_group}")
  1. 保存文件。

  2. 再次运行脚本:

python3 /home/labex/project/groupby_example.py

在输出中,你会注意到在不排序的情况下分组会产生不同的结果:

--- 首先不排序 ---
原始单词: ['apple', 'banana', 'avocado', 'blueberry', 'apricot', 'blackberry']

在不排序的情况下分组:
以 'a' 开头的单词: ['apple']
以 'b' 开头的单词: ['banana']
以 'a' 开头的单词: ['avocado']
以 'b' 开头的单词: ['blueberry']
以 'a' 开头的单词: ['apricot']
以 'b' 开头的单词: ['blackberry']

注意我们如何有多个具有相同键的组。发生这种情况是因为 groupby() 仅对连续的项目进行分组。当数据未排序时,具有相同键但出现在列表中不同位置的项目将被放置在单独的组中。

itertools.groupby() 函数非常高效,并且是标准库的一部分,这使其成为许多分组任务的强大工具。但是,请记住,它最适合已排序的数据。

使用 collections.defaultdict 进行分组

在 Python 中,另一个用于分组的强大工具是 collections 模块中的 defaultdict 类。与使用普通字典相比,这种方法提供了更简洁、更有效的方式来对数据进行分组。

理解 defaultdict

defaultdict 是一个字典子类,它会自动初始化缺失键的第一个值。这消除了在将项目添加到字典之前检查键是否存在的需要。对于分组目的而言,这意味着我们可以避免编写条件代码来为新组初始化空列表。

让我们看看 defaultdict 如何简化分组过程。

步骤 1:创建一个新的 Python 文件

  1. /home/labex/project 目录下创建一个名为 defaultdict_grouping.py 的新文件。

  2. 添加以下代码以导入必要的模块并创建一些示例数据:

from collections import defaultdict

## 示例数据 - 包含人员及其年龄的列表
people = [
    {"name": "Alice", "age": 25, "city": "New York"},
    {"name": "Bob", "age": 30, "city": "Boston"},
    {"name": "Charlie", "age": 35, "city": "Chicago"},
    {"name": "David", "age": 25, "city": "Denver"},
    {"name": "Eve", "age": 30, "city": "Boston"},
    {"name": "Frank", "age": 35, "city": "Chicago"},
    {"name": "Grace", "age": 25, "city": "New York"}
]

步骤 2:按年龄对人员进行分组

现在,让我们使用 defaultdict 按年龄对人员进行分组:

  1. 将以下代码添加到你的 defaultdict_grouping.py 文件中:
## 使用 defaultdict 按年龄对人员进行分组
age_groups = defaultdict(list)

for person in people:
    age_groups[person["age"]].append(person["name"])

## 打印结果组
print("按年龄分组人员:")
for age, names in age_groups.items():
    print(f"年龄 {age}: {names}")
  1. 保存文件。

  2. 运行脚本:

python3 /home/labex/project/defaultdict_grouping.py

你应该看到类似如下的输出:

按年龄分组人员:
年龄 25: ['Alice', 'David', 'Grace']
年龄 30: ['Bob', 'Eve']
年龄 35: ['Charlie', 'Frank']

步骤 3:与普通字典方法进行比较

为了理解使用 defaultdict 的优势,让我们将其与普通字典方法进行比较:

  1. 将以下代码添加到你的 defaultdict_grouping.py 文件中:
print("\n--- 与普通字典的比较 ---")

## 使用普通字典(传统方法)
regular_dict_groups = {}

for person in people:
    age = person["age"]
    name = person["name"]

    ## 需要检查键是否存在
    if age not in regular_dict_groups:
        regular_dict_groups[age] = []

    regular_dict_groups[age].append(name)

print("\n普通字典方法:")
for age, names in regular_dict_groups.items():
    print(f"年龄 {age}: {names}")
  1. 保存文件。

  2. 再次运行脚本:

python3 /home/labex/project/defaultdict_grouping.py

你会注意到这两种方法都产生相同的结果,但是 defaultdict 方法更简洁,并且需要的代码更少。

步骤 4:按多个标准分组

现在,让我们扩展我们的示例,按城市和年龄对人员进行分组:

  1. 将以下代码添加到你的 defaultdict_grouping.py 文件中:
## 按城市然后按年龄分组
city_age_groups = defaultdict(lambda: defaultdict(list))

for person in people:
    city = person["city"]
    age = person["age"]
    name = person["name"]

    city_age_groups[city][age].append(name)

print("\n按城市然后按年龄分组人员:")
for city, age_groups in city_age_groups.items():
    print(f"\n城市:{city}")
    for age, names in age_groups.items():
        print(f"  年龄 {age}: {names}")
  1. 保存文件。

  2. 再次运行脚本:

python3 /home/labex/project/defaultdict_grouping.py

你应该看到额外的输出,类似于:

按城市然后按年龄分组人员:

城市: New York
  年龄 25: ['Alice', 'Grace']

城市: Boston
  年龄 30: ['Bob', 'Eve']

城市: Chicago
  年龄 35: ['Charlie', 'Frank']

城市: Denver
  年龄 25: ['David']

这种嵌套的 defaultdict 方法允许使用最少的代码进行更复杂的分组层次结构。当你事先不知道所有组键时,defaultdict 特别有用,因为它会在需要时自动创建新组。

实际应用:使用分组技术分析数据

现在你已经了解了几种分组数据的方法,让我们将这些技术应用于解决一个实际问题:分析学生记录数据集。我们将使用不同的分组方法从数据中提取有用的信息。

设置示例数据集

首先,让我们创建我们的学生记录数据集:

  1. /home/labex/project 目录下创建一个名为 student_analysis.py 的新文件。

  2. 添加以下代码以设置示例数据:

import itertools
from collections import defaultdict

## 示例学生数据
students = [
    {"id": 1, "name": "Emma", "grade": "A", "subject": "Math", "score": 95},
    {"id": 2, "name": "Noah", "grade": "B", "subject": "Math", "score": 82},
    {"id": 3, "name": "Olivia", "grade": "A", "subject": "Science", "score": 90},
    {"id": 4, "name": "Liam", "grade": "C", "subject": "Math", "score": 75},
    {"id": 5, "name": "Ava", "grade": "B", "subject": "Science", "score": 88},
    {"id": 6, "name": "William", "grade": "A", "subject": "History", "score": 96},
    {"id": 7, "name": "Sophia", "grade": "B", "subject": "History", "score": 85},
    {"id": 8, "name": "James", "grade": "C", "subject": "Science", "score": 72},
    {"id": 9, "name": "Isabella", "grade": "A", "subject": "Math", "score": 91},
    {"id": 10, "name": "Benjamin", "grade": "B", "subject": "History", "score": 84}
]

print("学生记录:")
for student in students:
    print(f"ID: {student['id']}, 姓名:{student['name']}, 科目:{student['subject']}, 等级:{student['grade']}, 分数:{student['score']}")
  1. 保存文件。

使用 defaultdict 按科目对学生进行分组

让我们分析一下哪些学生正在学习每个科目:

  1. 将以下代码添加到你的 student_analysis.py 文件中:
print("\n--- 按科目分组的学生 ---")

## 使用 defaultdict 按科目对学生进行分组
subject_groups = defaultdict(list)

for student in students:
    subject_groups[student["subject"]].append(student["name"])

## 按科目打印学生
for subject, names in subject_groups.items():
    print(f"{subject}: {names}")
  1. 保存文件。

计算按科目的平均分数

让我们计算每个科目的平均分数:

  1. 将以下代码添加到你的 student_analysis.py 文件中:
print("\n--- 按科目的平均分数 ---")

## 计算每个科目的平均分数
subject_scores = defaultdict(list)

for student in students:
    subject_scores[student["subject"]].append(student["score"])

## 计算并打印平均值
for subject, scores in subject_scores.items():
    average = sum(scores) / len(scores)
    print(f"{subject} 平均值:{average:.2f}")
  1. 保存文件。

使用 itertools.groupby() 分析成绩

现在让我们使用 itertools.groupby() 分析成绩分布:

  1. 将以下代码添加到你的 student_analysis.py 文件中:
print("\n--- 成绩分布 (使用 itertools.groupby) ---")

## 首先按成绩对学生进行排序
sorted_students = sorted(students, key=lambda x: x["grade"])

## 按成绩分组并计数学生
grade_counts = {}
for grade, group in itertools.groupby(sorted_students, key=lambda x: x["grade"]):
    grade_counts[grade] = len(list(group))

## 打印成绩分布
for grade, count in grade_counts.items():
    print(f"成绩 {grade}: {count} 个学生")
  1. 保存文件。

结合技术:高级分析

最后,让我们通过结合我们的分组技术来执行更复杂的分析:

  1. 将以下代码添加到你的 student_analysis.py 文件中:
print("\n--- 高级分析:按科目的成绩分布 ---")

## 按科目和成绩分组
subject_grade_counts = defaultdict(lambda: defaultdict(int))

for student in students:
    subject = student["subject"]
    grade = student["grade"]
    subject_grade_counts[subject][grade] += 1

## 打印按科目的详细成绩分布
for subject, grades in subject_grade_counts.items():
    print(f"\n{subject}:")
    for grade, count in grades.items():
        print(f"  成绩 {grade}: {count} 个学生")
  1. 保存文件。

  2. 运行完整的脚本:

python3 /home/labex/project/student_analysis.py

你应该看到对学生数据的全面分析,包括:

  • 学生记录
  • 按科目分组的学生
  • 按科目的平均分数
  • 总体成绩分布
  • 按科目的成绩分布

此示例演示了如何结合不同的分组技术,以相对简单的代码执行复杂的数据分析。每种方法都有其优点:

  • defaultdict 非常适合在无需检查键是否存在的情况下进行简单分组
  • itertools.groupby() 对于处理已排序的数据非常有效
  • 结合技术允许进行多级分组和复杂分析

选择正确的分组技术取决于你的特定需求和数据的结构。

总结

在本教程中,你学习了几种在 Python 中对列表进行分组的有效方法:

  1. 基本字典分组:你从使用普通字典的基本方法开始,根据特定标准创建组。

  2. itertools.groupby():你探索了这个内置函数,它有效地对已排序数据中的连续元素进行分组,了解了它的优点和局限性。

  3. collections.defaultdict:你使用了这个方便的字典子类,它会自动处理缺失的键,使你的分组代码更简洁明了。

  4. 实际数据分析:你将这些技术应用于分析数据集,了解了它们如何单独使用和组合使用以提取有意义的见解。

这些方法中的每一种都有其优点和理想的用例:

  • 当清晰度比简洁性更重要时,使用基本字典进行简单分组
  • 当你的数据已排序或可以按分组键排序时,使用 itertools.groupby()
  • 当你想要简洁的代码并且事先不知道所有组键时,使用 defaultdict
  • 结合技术进行复杂的多级分组和分析

通过掌握这些分组技术,你已经为你的 Python 编程工具包添加了强大的工具,这将帮助你更有效地组织、分析和操作数据。