W3docs

Группировка данных с помощью кортежей 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 с кортежами:

  1. Сначала сортируйте. Без сортировки каждое новое вхождение одного и того же значения ключа создаёт отдельную группу.
  2. Сразу потребляйте итератор группы. Внутренний итератор 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 times

Counter внутренне использует кортеж как хеш-ключ, поэтому каждая уникальная комбинация (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 Moduledefaultdict, Counter, namedtuple и другие

Практика

Практика
Which of the following can be used as a Python dictionary key?
Which of the following can be used as a Python dictionary key?
Was this page helpful?