Коллекции в Python: от основ к тонкостям

Коллекции — это основа хранения и обработки данных в Python. Понимание их внутреннего устройства, сильных и слабых сторон — ключ к написанию эффективного и идиоматичного кода.

Что такое коллекция?

В Python коллекция — это объект, который одновременно является:

  • Контейнером (Container) — поддерживает оператор in для проверки вхождения.
  • Итерируемым объектом (Iterable) — его элементы можно перебирать в цикле.
  • Объектом ограниченной длины (Sized) — поддерживает функцию len().
from collections.abc import Collection

# Проверим, является ли список коллекцией
print(isinstance([1, 2, 3], Collection))  # True

Любопытные исключения

  • Интервал чисел может быть контейнером, но не быть итерируемым или иметь длину.
  • Генератор является итерируемым, но не контейнером и не имеет длины.
# Пример: Интервал как контейнер
from dataclasses import dataclass

@dataclass
class Interval:
    a: float
    b: float

    def __contains__(self, x):
        return self.a < x < self.b

interval = Interval(0, 1)
print(0.5 in interval)  # True
print(len(interval))    # TypeError: object of type 'Interval' has no len()

Иерархия коллекций

Стандартная библиотека Python определяет абстрактные базовые классы (ABC) для классификации коллекций:

Container -> Iterable -> Sized -> Collection
                                      |
                              --------+--------
                              |               |
                          Sequence        Mapping
  • Sequence (Последовательности): элементы упорядочены и доступны по индексу (списки, кортежи, строки).
  • Mapping (Отображения): хранят пары «ключ-значение» (словари).

Списки (list): универсальные и изменяемые

Списки — это, пожалуй, самая часто используемая коллекция.

Неочевидные особенности инициализации

# Кажется, что это создаст матрицу 2x1
chunks = [[0]] * 2
chunks[0][0] = 42
print(chunks)  # [[42], [42]] Оба элемента ссылаются на один и тот же список!

# Правильные способы инициализации матрицы:
correct_chunks = [[0] for _ in range(2)]
correct_chunks = [[0]] * 2  # Так делать нельзя, если планируется изменение вложенных списков!

Эффективные операции

  • append(item) и pop() — амортизированная сложность O(1).
  • insert(0, item) и pop(0) — сложность O(n), так как требуют сдвига всех элементов.
  • extend(iterable) эффективнее, чем многократный вызов append().

Совет: Используйте collections.deque, если вам нужны частые операции с обоих концов.

Кортежи (tuple): неизменяемые и хэшируемые

Кортежи не только защищают данные от изменений, но и могут быть использованы как ключи в словарях или элементы множеств, так как они хэшируемы.

Распаковка кортежей — мощный инструмент

# Распаковка с упаковкой «лишних» элементов в переменную
first, second, *rest = range(10)
print(first, second, rest)  # 0 1 [2, 3, 4, 5, 6, 7, 8, 9]

# Игнорирование ненужных значений
x, _, z = (1, 2, 3)

# Распаковка в аргументы функции
def greet(name, greeting):
    return f"{greeting}, {name}!"

person = ("Alice", "Hello")
print(greet(*person))  # "Hello, Alice!"

Именованные кортежи (namedtuple)

collections.namedtuple — это фабрика классов, создающая подтип кортежа с именованными полями. Это делает код самодокументируемым.

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])
p = Point(10, y=20)
print(p.x, p.y)  # 10 20
print(p._asdict()) # {'x': 10, 'y': 20}

Множества (set): уникальность и скорость

Множества реализованы как хэш-таблицы, поэтому проверка вхождения (in) имеет среднюю сложность O(1).

Неочевидные применения множеств

  1. Удаление дубликатов из списка — классика, но всегда актуально.

    unique_list = list(set(duplicated_list))
    
  2. Подсчет общих элементов между двумя коллекциями.

    common = set(list1) & set(list2)
    
  3. Фильтрация «мусора» при разборе данных.

    valid_tags = {'python', 'tutorial', 'advanced'}
    tags = ['python', 'beginner', 'advanced']
    filtered_tags = [tag for tag in tags if tag in valid_tags]
    # ['python', 'advanced']
    

frozenset: неизменяемое множество

Поскольку обычные множества изменяемы и, следовательно, нехэшируемы, их нельзя вложить в другие множества или использовать как ключи словаря. Для этого нужен frozenset.

# Множество множеств? Нет.
# {set([1,2]), set([3,4])}  # TypeError: unhashable type: 'set'

# А так — можно.
fs1 = frozenset([1, 2])
fs2 = frozenset([3, 4])
meta_set = {fs1, fs2}  # Valid

Словари (dict): сердце Python

Современные словари (Python 3.7+) сохраняют порядок добавления элементов.

Малоизвестные возможности словарей

  1. Метод setdefault() — проверяет наличие ключа и, если его нет, устанавливает значение за один проход по хэш-таблице.

    data = {}
    # Классический, но неэффективный способ
    if 'key' not in data:
        data['key'] = []
    data['key'].append(1)
    
    # Эффективный способ с setdefault
    data.setdefault('key', []).append(1)
    
  2. Метод popitem() — удаляет и возвращает пару (ключ, значение) в порядке LIFO (последним пришел — первым ушел). Полезно для обработки данных в обратном порядке.

  3. Словарные включения (Dict Comprehensions) — компактный и выразительный синтаксис.

    squares = {x: x*x for x in range(5)}
    # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
    

Коллекции из модуля collections

Модуль collections предоставляет специализированные типы данных, которые расширяют возможности стандартных коллекций.

  • defaultdict — словарь с заводской функцией для отсутствующих ключей.

    from collections import defaultdict
    graph = defaultdict(list)
    graph['a'].append('b')  # Не нужно проверять, есть ли ключ 'a'
    
  • Counter — подкласс словаря для подсчета хэшируемых объектов.

    from collections import Counter
    words = ['apple', 'banana', 'apple', 'orange']
    word_count = Counter(words)
    print(word_count.most_common(1))  # [('apple', 2)]
    
  • deque — двусторонняя очередь. Идеальна для очередей (FIFO) и стеков (LIFO).

    from collections import deque
    queue = deque([1, 2, 3])
    queue.append(4)    # O(1)
    queue.popleft()    # O(1) - в отличие от списка!
    
  • OrderedDict — словарь, который помнит порядок. В Python 3.7+ обычный dict тоже упорядочен, но OrderedDict имеет дополнительные методы (move_to_end, popitem(last=True/False)).

Заключение

Выбор правильной коллекции — это не просто вопрос синтаксиса. Это вопрос эффективности, читаемости и корректности вашего кода.

  • Используйте списки для упорядоченных коллекций, которые могут изменяться.
  • Используйте кортежи для фиксированных данных или когда нужны хэшируемые объекты.
  • Используйте множества для проверки уникальности и быстрого поиска.
  • Используйте словари для отображений ключей на значения.
  • Не забывайте о специализированных коллекциях из модуля collections для решения специфических задач.

Глубокое понимание коллекций позволит вам писать код, который не только работает, но и работает хорошо.