Собственные последовательности в Python: от теории к практике

Раздел: Основы Python -> Структуры данных

Последовательности в Python и способы их реализации

Создание неизменяемой последовательности с помощью __getitem__ и __len__

Как реализовать собственную неизменяемую последовательность в Python?

Для создания класса, который ведет себя как последовательность, достаточно определить два метода: __len__ и __getitem__. Python автоматически получит поддержку итерации, оператора in, а также функций len() и индексации. Пример простой последовательности, хранящей данные во внутреннем списке:


class MySeq:
    def __init__(self, items):
        self._items = list(items)

    def __len__(self):
        return len(self._items)

    def __getitem__(self, index):
        return self._items[index]

значения списка числа python (итерация по значениям списка чисел в python)

Теперь объект MySeq можно использовать как обычную последовательность:


seq = MySeq([10, 20, 30])
print(len(seq))      # 3
print(seq[1])        # 20
for item in seq:
    print(item)

словарь set python (словарь и set в python)

3
20
10
20
30

Python dict set (словарь и множество в python)

Типичные проблемы и ошибки:

  • Если не обрабатывать отрицательные индексы в __getitem__, то seq[-1] вызовет IndexError. Решение: проверять отрицательные индексы и преобразовывать их в положительные.
  • Метод __getitem__ должен вызывать IndexError при недопустимом индексе, иначе поведение цикла for станет некорректным.
  • По умолчанию не будет работать срез (seq[1:3]) – нужно реализовать обработку объекта типа slice.

Как гарантировать полную реализацию методов последовательности?

Абстрактный базовый класс Sequence из модуля collections.abc предоставляет стандартную реализацию методов __contains__, __iter__, __reversed__, index и count на основе ваших __getitem__ и __len__. Достаточно унаследоваться от Sequence и реализовать эти два метода.


from collections.abc import Sequence

class MySeq(Sequence):
    def __init__(self, items):
        self._items = list(items)

    def __getitem__(self, index):
        return self._items[index]

    def __len__(self):
        return len(self._items)

типы структур python (типы структур данных в python)

Теперь доступны все методы последовательности:


seq = MySeq([1,2,3,2])
print(seq.count(2))   # 2
print(seq.index(3))   # 2
print(3 in seq)       # True

кортеж чисел python (кортеж чисел в python)

2
2
True

язык программирования python массивы (массивы (списки) в python)

Проблемы: Sequence является ABC, поэтому при наследовании необходимо правильно указать метакласс (обычно это автоматически). Если забыть реализовать __getitem__ или __len__, Python выдаст ошибку при создании экземпляра. Также Sequence не предоставляет реализацию среза – по-прежнему нужно обрабатывать slice в __getitem__.

Как создать изменяемую последовательность?

Для изменяемой последовательности используется MutableSequence. Помимо __getitem__ и __len__ нужно реализовать __setitem__, __delitem__ и метод insert. Пример простой изменяемой последовательности на основе списка:


from collections.abc import MutableSequence

class MyMutableSeq(MutableSequence):
    def __init__(self, items=None):
        self._items = list(items) if items else []

    def __getitem__(self, index):
        return self._items[index]

    def __setitem__(self, index, value):
        self._items[index] = value

    def __delitem__(self, index):
        del self._items[index]

    def __len__(self):
        return len(self._items)

    def insert(self, index, value):
        self._items.insert(index, value)

массивы данных python 3 (массивы данных в python)

Пример использования:


mseq = MyMutableSeq([1,2,3])
mseq.append(4)          # наследуется от MutableSequence
mseq[1] = 99
del mseq[0]
print(list(mseq))

одномерные массивы на языке программирования python (одномерные массивы в python)

[99, 3, 4]

последовательности в python и способы их реализации (последовательности в python и способы их реализации)

Типичные ошибки: Неправильная реализация insert – метод insert в MutableSequence должен вставлять элемент перед указанным индексом, сдвигая остальные. Если метод insert реализован неверно, то такие методы, как append, remove, pop, могут работать некорректно. Также необходимо следить за обработкой отрицательных индексов.

Как расширить функциональность существующей последовательности?

Простой способ – унаследоваться от list или tuple и добавить новые методы или переопределить существующие. Это сохраняет всю производительность встроенного типа. Пример подкласса списка с дополнительным методом среднего арифметического:


class AverageList(list):
    def average(self):
        if not self:
            return 0.0
        return sum(self) / len(self)

программы с массивами на python (программы с массивами на python)

Использование:


al = AverageList([10, 20, 30])
print(al.average())   # 20.0
al.append(40)
print(al.average())   # 25.0

Python пар (пары (ключ-значение) в python)

20.0
25.0

Python разница списков (разница между списками и кортежами в python)

Проблемы: При наследовании от list стоит помнить, что некоторые методы возвращают новый список (например, copy, slice). Эти методы вернут обычный list, а не подкласс, если не переопределить их. Можно переопределить __class__ или использовать декораторы. Также наследование от tuple ограничивает изменяемость.

Как создать последовательность для хранения однотипных данных с минимальным расходом памяти?

Модуль array предоставляет тип array.array, который является изменяемой последовательностью, хранящей элементы одного типа (числа, символы). Это эффективнее списка по памяти и скорости для числовых данных.


from array import array

arr = array('i', [1, 2, 3])  # 'i' – знаковое целое
arr.append(4)
print(arr[2])   # 3
for val in arr:
    print(val, end=' ')

как сделать массив python (создание массива (списка) в python)

3
1 2 3 4

Проблемы: array поддерживает только ограниченный набор типов (строковые коды). Нельзя хранить разнородные данные. При попытке вставить значение другого типа возникает TypeError. Также array не поддерживает изменение размера через срез (но можно использовать extend и pop).

- Python набор значений (множество (set) значений в python)

Расширенные примеры реализации последовательностей

Последовательность с поддержкой срезов и отрицательных индексов

В следующем примере класс SequenceWithSlice обрабатывает объект slice в методе __getitem__ и корректно работает с отрицательными индексами.

Пример

class SequenceWithSlice:
    def __init__(self, items):
        self._items = list(items)

    def __len__(self):
        return len(self._items)

    def __getitem__(self, index):
        if isinstance(index, slice):
            # обрабатываем срез
            start, stop, step = index.indices(len(self._items))
            return [self._items[i] for i in range(start, stop, step)]
        elif isinstance(index, int):
            # поддерживаем отрицательные индексы
            if index < 0:
                index += len(self._items)
            if index < 0 or index >= len(self._items):
                raise IndexError('list index out of range')
            return self._items[index]
        else:
            raise TypeError('indices must be integers or slices')
Пример

seq = SequenceWithSlice([10, 20, 30, 40, 50])
print(seq[1:4])        # срез
print(seq[-1])         # последний элемент
print(seq[::2])        # каждый второй
[20, 30, 40]
50
[10, 30, 50]

Ленивая последовательность с кэшированием

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

Пример

class LazySequence:
    def __init__(self, iterable):
        self._iter = iter(iterable)
        self._cache = []

    def __len__(self):
        # подгружаем все оставшиеся элементы для корректной длины
        try:
            while True:
                self._cache.append(next(self._iter))
        except StopIteration:
            pass
        return len(self._cache)

    def __getitem__(self, index):
        if isinstance(index, slice):
            # для среза сначала подгружаем нужные элементы
            start, stop, step = index.indices(10**9)  # условно большой лимит
            while len(self._cache) <= max(start, stop):
                try:
                    self._cache.append(next(self._iter))
                except StopIteration:
                    break
            return [self._cache[i] for i in range(start, stop, step) if i < len(self._cache)]
        if isinstance(index, int):
            if index < 0:
                # для отрицательных индексов загружаем все
                self.__len__()
                return self._cache[index]
            while len(self._cache) <= index:
                try:
                    self._cache.append(next(self._iter))
                except StopIteration:
                    raise IndexError('list index out of range')
            return self._cache[index]
        raise TypeError
Пример

def generate():
    for i in range(10):
        yield i

lazy = LazySequence(generate())
print(lazy[2])      # 2, загрузились элементы до индекса 2
print(lazy[5])      # 5, загрузились ещё
print(lazy[0])      # 0, уже в кэше
print(lazy[1:4])    # [1, 2, 3]
2
5
0
[1, 2, 3]

Последовательность на основе односвязного списка

Реализация неизменяемой последовательности, где данные хранятся в узлах односвязного списка. Метод __getitem__ проходит по цепочке до нужного индекса.

Пример

class Node:
    def __init__(self, value, next_node=None):
        self.value = value
        self.next = next_node

class LinkedListSeq:
    def __init__(self, head=None):
        self._head = head
        self._length = 0
        # подсчет длины при инициализации
        cur = head
        while cur:
            self._length += 1
            cur = cur.next

    def __len__(self):
        return self._length

    def __getitem__(self, index):
        if index < 0:
            index += self._length
        if index < 0 or index >= self._length:
            raise IndexError('list index out of range')
        cur = self._head
        for _ in range(index):
            cur = cur.next
        return cur.value
Пример

# создание списка: 10 -> 20 -> 30
tail = Node(30)
middle = Node(20, tail)
head = Node(10, middle)
lst = LinkedListSeq(head)
print(len(lst))   # 3
print(lst[1])     # 20
for val in lst:
    print(val, end=' ')
3
20
10 20 30

Последовательности в Python и способы их реализации - comments

En
последовательности в python и способы их реализации (python)