Группировка данных с помощью кортежей Python
Группировка данных с кортежами Python: ключи словарей, groupby, namedtuple, Counter и zip — практические паттерны с примерами.
Кортежи идеально подходят для группировки данных в Python. Поскольку кортеж неизменяем и хешируем, он может служить ключом словаря — чего список сделать не может. Это делает кортежи естественным выбором, когда нужно группировать записи по комбинации полей, отслеживать многомерные координаты или подсчитывать составные события.
В этой главе рассматриваются четыре практических паттерна группировки:
- Кортеж как ключ словаря — группировка по одному и нескольким полям
itertools.groupbyс кортежами — потоковая группировка отсортированных последовательностейcollections.namedtuple— именование полей в сгруппированных записяхcollections.Counterс кортежами — подсчёт составных событий
Связанные главы: Python Tuples · Access Tuples · Loop Tuples · Python Dictionaries · Python Lists Group
Почему кортежи могут быть ключами словаря
Python требует, чтобы ключи словаря были хешируемыми — их значение не должно изменяться после того, как ключ сохранён. Кортежи удовлетворяют этому требованию, поскольку они неизменяемы. Списки не удовлетворяют ему и вызывают TypeError, если попытаться использовать их в качестве ключей.
# A tuple can be a dictionary key
coordinates = {}
coordinates[(10, 20)] = "warehouse A"
coordinates[(30, 40)] = "warehouse B"
print(coordinates[(10, 20)]) # warehouse A
# A list cannot be a dictionary key
try:
d = {[10, 20]: "warehouse A"}
except TypeError as e:
print(f"TypeError: {e}")
# TypeError: unhashable type: 'list'Важный нюанс: кортеж, содержащий изменяемый элемент (например, список), также не является хешируемым и не может использоваться в качестве ключа:
try:
d = {(1, [2, 3]): "value"}
except TypeError as e:
print(f"TypeError: {e}")
# TypeError: unhashable type: 'list'Убедитесь, что ключи-кортежи состоят исключительно из неизменяемых значений — строк, чисел, boolean-значений или других кортежей.
Группировка по одному полю кортежа
Простейший вариант использования — распаковка последовательности кортежей и группировка по одному элементу. Используйте collections.defaultdict(list), чтобы избежать шаблонного кода проверки существования ключа.
from collections import defaultdict
employees = [
("Alice", "Engineering"),
("Bob", "Marketing"),
("Carol", "Engineering"),
("Dave", "Marketing"),
("Eve", "Engineering"),
]
by_dept = defaultdict(list)
for name, dept in employees:
by_dept[dept].append(name)
for dept, members in sorted(by_dept.items()):
print(f"{dept}: {members}")
# Engineering: ['Alice', 'Carol', 'Eve']
# Marketing: ['Bob', 'Dave']defaultdict(list) автоматически создаёт пустой список при первом обращении к новому ключу dept, поэтому проверка if dept not in by_dept не нужна.
Группировка по нескольким ключам с кортежем-ключом
Настоящая мощь кортежей-ключей проявляется, когда нужно группировать сразу по нескольким полям. Объедините поля в кортеж и используйте его в качестве ключа словаря.
from collections import defaultdict
records = [
("Alice", "Engineering", "Senior"),
("Bob", "Marketing", "Junior"),
("Carol", "Engineering", "Junior"),
("Dave", "Marketing", "Senior"),
("Eve", "Engineering", "Senior"),
]
# Group by (department, level) — a two-field composite key
grouped = defaultdict(list)
for name, dept, level in records:
grouped[(dept, level)].append(name)
for (dept, level), names in sorted(grouped.items()):
print(f"{dept} / {level}: {names}")
# Engineering / Junior: ['Carol']
# Engineering / Senior: ['Alice', 'Eve']
# Marketing / Junior: ['Bob']
# Marketing / Senior: ['Dave']Поскольку (dept, level) сам является кортежем, он хешируем и может служить ключом словаря независимо от количества полей. Деструктуризация ключа через for (dept, level), names in ... делает код читаемым.
Группировка сетки и координат
Группировка по кортежу из нескольких ключей также естественным образом обрабатывает пространственные данные:
points = [(0, 0), (1, 2), (0, 1), (1, 3), (2, 4)]
from collections import defaultdict
by_x = defaultdict(list)
for x, y in points:
by_x[x].append(y)
for x, ys in sorted(by_x.items()):
print(f"x={x}: y-values={ys}")
# x=0: y-values=[0, 1]
# x=1: y-values=[2, 3]
# x=2: y-values=[4]Группировка с помощью itertools.groupby
itertools.groupby группирует последовательные элементы с одинаковым ключом. Функция эффективна по памяти, поскольку работает лениво — она не загружает все группы в память сразу. Оборотная сторона: перед передачей в groupby входные данные должны быть отсортированы по тому же ключу, иначе для одного ключа появятся несколько частичных групп вместо одной.
from itertools import groupby
sales = [
("East", "Q1", 1200),
("East", "Q2", 1500),
("West", "Q1", 900),
("West", "Q2", 1100),
("East", "Q3", 1800),
]
# Sort by region (index 0) before grouping
sales_sorted = sorted(sales, key=lambda t: t[0])
for region, group in groupby(sales_sorted, key=lambda t: t[0]):
items = list(group)
total = sum(q[2] for q in items)
print(f"{region}: total={total}, quarters={[q[1] for q in items]}")
# East: total=4500, quarters=['Q1', 'Q2', 'Q3']
# West: total=2000, quarters=['Q1', 'Q2']Два важных правила при использовании groupby с кортежами:
- Сначала сортируйте. Без сортировки каждое новое вхождение одного и того же значения ключа создаёт отдельную группу.
- Сразу потребляйте итератор группы. Внутренний итератор
groupисчерпывается, когда внешний цикл переходит к следующему ключу. Всегда вызывайтеlist(group)внутри тела цикла, прежде чем использовать его в другом месте.
Когда groupby предпочтительнее defaultdict
Используйте groupby при обработке большой, уже отсортированной последовательности, когда не нужно загружать весь результат группировки в память. Для общей группировки без гарантии сортировки defaultdict(list) проще и надёжнее.
Группировка с помощью collections.namedtuple
namedtuple позволяет давать имена полям кортежа, делая сгруппированные данные самодокументируемыми. После определения типа namedtuple экземпляры ведут себя точно так же, как обычные кортежи — они неизменяемы, хешируемы и итерируемы — но поля доступны как по имени, так и по индексу.
from collections import namedtuple, defaultdict
Employee = namedtuple("Employee", ["name", "department", "salary"])
employees = [
Employee("Alice", "Engineering", 95000),
Employee("Bob", "Marketing", 72000),
Employee("Carol", "Engineering", 88000),
Employee("Dave", "Marketing", 68000),
Employee("Eve", "Engineering", 102000),
]
by_dept = defaultdict(list)
for emp in employees:
by_dept[emp.department].append(emp)
for dept, members in sorted(by_dept.items()):
avg_salary = sum(e.salary for e in members) / len(members)
print(f"{dept}: {[e.name for e in members]}, avg salary={avg_salary:.0f}")
# Engineering: ['Alice', 'Carol', 'Eve'], avg salary=95000
# Marketing: ['Bob', 'Dave'], avg salary=70000Обратите внимание, что emp.department и emp.salary читаются значительно лучше, чем emp[1] и emp[2]. Подход с namedtuple особенно полезен, когда в кортеже много полей и позиционное индексирование становится трудночитаемым.
Подсчёт составных событий с помощью Counter
collections.Counter подсчитывает хешируемые объекты. Если «объект», который нужно посчитать, представляет собой комбинацию значений, оберните эти значения в кортеж и передайте последовательность кортежей в Counter.
from collections import Counter
log = [
("GET", 200),
("POST", 201),
("GET", 200),
("GET", 404),
("POST", 500),
("GET", 200),
("DELETE", 204),
]
counts = Counter(log)
for entry, n in counts.most_common():
method, status = entry
print(f"{method} {status}: {n} times")
# GET 200: 3 times
# POST 201: 1 times
# GET 404: 1 times
# POST 500: 1 times
# DELETE 204: 1 timesCounter внутренне использует кортеж как хеш-ключ, поэтому каждая уникальная комбинация (method, status) отслеживается отдельно без написания кода группировки вручную.
Построение групп кортежей с помощью zip
zip объединяет элементы из двух и более последовательностей в кортежи. Это удобный способ собрать сгруппированные записи из параллельных списков перед применением операции группировки.
from collections import defaultdict
names = ["Alice", "Bob", "Carol"]
scores = [95, 87, 92]
departments = ["Engineering", "Marketing", "Engineering"]
# Pair the three sequences into tuples
records = list(zip(names, scores, departments))
print(records)
# [('Alice', 95, 'Engineering'), ('Bob', 87, 'Marketing'), ('Carol', 92, 'Engineering')]
# Now group by department
by_dept = defaultdict(list)
for name, score, dept in records:
by_dept[dept].append((name, score))
for dept, members in sorted(by_dept.items()):
print(f"{dept}: {members}")
# Engineering: [('Alice', 95), ('Carol', 92)]
# Marketing: [('Bob', 87)]Выбор подходящего инструмента группировки
| Цель | Лучший инструмент |
|---|---|
| Группировка по одному полю из списка кортежей | defaultdict(list) |
| Группировка по двум и более полям одновременно | defaultdict(list) с кортежем-ключом |
| Потоковая группировка большой предварительно отсортированной последовательности | itertools.groupby |
| Добавление имён полей к сгруппированным записям | collections.namedtuple |
| Подсчёт вхождений составных событий | collections.Counter |
| Сборка параллельных списков в сгруппированные кортежи | zip |
Распространённые ошибки
Забыть отсортировать данные перед groupby. itertools.groupby объединяет только последовательные одинаковые ключи. Если один и тот же ключ встречается в нескольких непоследовательных позициях, каждое вхождение образует отдельную группу. Всегда сортируйте по той же функции ключа перед вызовом groupby.
Использование изменяемого значения внутри кортежа-ключа. Кортеж, содержащий список, не является хешируемым и вызывает TypeError при использовании в качестве ключа словаря. Используйте в качестве ключей кортежи, состоящие только из строк, чисел, boolean-значений или вложенных кортежей.
Потребление итератора groupby более одного раза. Суб-итератор группы из groupby исчерпывается, как только внешний цикл переходит к следующему элементу. Вызывайте list(group) внутри тела цикла, если нужно итерировать группу более одного раза.
Обращение с результатом defaultdict как с обычным dict. defaultdict автоматически создаёт новые ключи при обращении к отсутствующему ключу, что может незаметно заполнить словарь пустыми списками. Если нужно проверить наличие ключа без создания новых записей, сначала преобразуйте в обычный dict: dict(grouped).
Связанные темы
- Python Tuples — создание, индексирование и срезы кортежей
- Access Tuples — индексирование, срезы и проверка членства
- Loop Tuples — итерация по кортежу с
forиwhile - Unpack Tuples — присваивание элементов кортежа переменным в одну строку
- Update Tuples — способы изменения неизменяемых кортежей
- Join Tuples — конкатенация и конструктор
tuple() - Tuple Methods — методы
count()иindex()подробно - Python Dictionaries — хранилище ключ-значение, которое делает возможной группировку с кортежами-ключами
- Python Lists Group — группировка списков с помощью
defaultdict,groupbyи словарных включений - Python Collections Module —
defaultdict,Counter,namedtupleи другие