Сообщить об ошибке.

Класс UserList модуля collections в Python

Создание пользовательского класса списка list в Python

Синтаксис:

import collections

collections.UserList([list])

Параметры:

  • list - список list используемый как источник данных для пользовательского класса.

Описание:

Класс UserList() модуля collections действует как обертка для объектов списка list. Это полезный базовый класс для собственных классов, подобных спискам, которые могут наследоваться от них и переопределять существующие методы или добавлять новые. Таким образом, в списки можно добавлять новые модели поведения.

Содержимое экземпляра хранится в обычном списке, который доступен через атрибут данных экземпляров UserList.data. Содержимое экземпляра изначально установлено как копия списка, по умолчанию пустой список []. Список может быть любым итерируемым объектом, например, реальным списком list или объектом UserList.

Требования к подклассам collections.UserList:

Ожидается, что подклассы UserList будут предлагать конструктор, который можно вызывать либо без аргументов, либо с одним аргументом. Операции списка, которые возвращают новую последовательность, пытаются создать экземпляр фактического класса реализации. Для этого предполагается, что конструктор может быть вызван с одним аргументом list, который является объектом последовательности, используемым в качестве источника данных.

Если производный класс не будет соответствовать этому требованию, все специальные методы, поддерживаемые этим классом, должны быть переопределены.

Потребность в этом классе была частично вытеснена возможностью создавать подклассы непосредственно из типа list, однако с этим классом может быть проще работать, поскольку базовый список доступен как атрибут UserList.data.

Создание пользовательского класса списка, наследуемого от UserList.

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

Так как пользовательский класс будет хранить элементы в виде строк, то необходимо переопределить все методы, которые добавляют или изменяют элементы в базовом списке list. Эти методы включают следующее:

  • __init__ - инициализирует все новые экземпляры класса.
  • __setitem__() - позволяет присвоить новое значение существующему элементу, используя индекс элемента, например, list[index] = item.
  • insert() - позволяет вам вставить новый элемент в заданную позицию в базовом списке, используя индекс элемента.
  • append() - добавляет один новый элемент в конец базового списка.
  • extend() - добавляет ряд элементов в конец списка.
from collections import UserList

class StringList(UserList):
    def __init__(self, iterable):
        super().__init__(str(item) for item in iterable)

    def __setitem__(self, index, item):
        self.data[index] = str(item)

    def insert(self, index, item):
        self.data.insert(index, str(item))

    def append(self, item):
        self.data.append(str(item))

    def extend(self, other):
        if isinstance(other, type(self)):
            self.data.extend(other)
        else:
            self.data.extend(str(item) for item in other)

В этом примере доступ к атрибуту UserList.data позволяет создать класс более простым способом, используя делегирование, т.е. список в UserList.data позаботится об обработке всех запросов.

Здесь почти не используется расширенные инструменты, такие как super(). Функция super() используется один раз в инициализаторе класса __init__, для предотвращения проблемы в дальнейших сценариях наследования. В остальных методах используется атрибут UserList.data, который содержит обычный список Python.

>>> lst = StringList([1, 2, 2, 4, 5])
>>> lst
# ['1', '2', '2', '4', '5']
>>> lst.append(6)
>>> lst
# ['1', '2', '2', '4', '5', '6']
>>> lst.insert(0, 0)
>>> lst
# ['0', '1', '2', '2', '4', '5', '6']
>>> lst.extend([7, 8, 9])
>>> lst
# ['0', '1', '2', '2', '4', '5', '6', '7', '8', '9']
>>> lst[3] = 3
>>> lst
# ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

Когда происходит добавление lst.append(), вставка lst.insert(), присваивание lst[index] = item или расширение lst.extend() экземпляра класса новыми значениями, то методы, поддерживающие каждую операцию, позаботятся о процессе преобразования передаваемых элементов в строку.

Другие методы, которые пользовательский класс StringList унаследовал от типа list, будут прекрасно работать, т.к. они не добавляют и не обновляют элементы.

Примечание. Если есть необходимость, чтобы класс StringList поддерживал конкатенацию с помощью оператора плюс +, то потребуется реализовать другие специальные методы, такие как __add__(), __radd__() и __iadd__().

Создание пользовательского класса списка, наследуемого от типа list.

При наследовании от встроенного типа list, необходимо менять внутреннюю реализацию всех методов родительского класса, которые добавляют или изменяют элементы, при помощи встроенной функции super().

class StringList(list):
    def __init__(self, iterable):
        super().__init__(str(item) for item in iterable)

    def __setitem__(self, index, item):
        super().__setitem__(index, str(item))

    def insert(self, index, item):
        super().insert(index, str(item))

    def append(self, item):
        super().append(str(item))

    def extend(self, other):
        if isinstance(other, type(self)):
            super().extend(other)
        else:
            super().extend(str(item) for item in other)

Практические примеры пользовательских классов списков.

Список, который принимает только числовые данные

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

Реализация класса NumberList с желаемой функциональностью:

class NumberList(list):
    def __init__(self, iterable):
        super().__init__(self._validate_number(item) for item in iterable)

    def __setitem__(self, index, item):
        super().__setitem__(index, self._validate_number(item))

    def insert(self, index, item):
        super().insert(index, self._validate_number(item))

    def append(self, item):
        super().append(self._validate_number(item))

    def extend(self, other):
        if isinstance(other, type(self)):
            super().extend(other)
        else:
            super().extend(self._validate_number(item) for item in other)

    def _validate_number(self, value):
        if isinstance(value, (int, float, complex)):
            return value
        raise TypeError(
            f"numeric value expected, got {type(value).__name__}"
        )

В этом примере класс NumberList наследуется непосредственно от встроенного типа list. Это означает, что класс разделяет все основные функции с типом list. Может перебирать экземпляры NumberList, получать доступ и обновлять его элементы, используя их индексы, вызывать общие методы списка и многое другое.

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

Для проверки входных данных используется вспомогательный метод NumberList._validate_number(). Этот метод использует встроенную функцию isinstance(), для проверки принадлежности текущего входного значения к экземплярам int, float или complex.

Альтернативная реализация NumberList с использованием collections.UserList может выглядеть примерно так:

from collections import UserList

class NumberList(UserList):
    def __init__(self, iterable):
        super().__init__(self._validate_number(item) for item in iterable)

    def __setitem__(self, index, item):
        self.data[index] = self._validate_number(item)

    def insert(self, index, item):
        self.data.insert(index, self._validate_number(item))

    def append(self, item):
        self.data.append(self._validate_number(item))

    def extend(self, other):
        if isinstance(other, type(self)):
            self.data.extend(other)
        else:
            self.data.extend(self._validate_number(item) for item in other)

    def _validate_number(self, value):
        if isinstance(value, (int, float, complex)):
            return value
        raise TypeError(
            f"numeric value expected, got {type(value).__name__}"
        )

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

Обратите внимание, что функция super() используется только в методе __init__(). Это лучшая практика при работе с наследованием в Python. Это позволяет правильно инициализировать атрибуты в родительском классе, не нарушая работу.

Пользовательский список с дополнительной функциональностью.

Допустим, что нужен список со всеми стандартными функциями, который также должен предоставлять некоторые дополнительные функции:

  • join(): объединяет все элементы списка в одну строку.
  • map(action): дает новые элементы, которые являются результатом применения внешней функции action() к каждому элементу в базовом списке.
  • filter(predicate): возвращает все элементы, которые возвращают True при вызове для них внешней функции predicate().
  • for_each(func): вызывает внешнюю функцию func() для каждого элемента в базовом списке, чтобы создать побочный эффект.

Реализация:

class CustomList(list):
    def join(self, separator=" "):
        return separator.join(str(item) for item in self)

    def map(self, action):
        return type(self)(action(item) for item in self)

    def filter(self, predicate):
        return type(self)(item for item in self if predicate(item))

    def for_each(self, func):
        for item in self:
            func(item)

>>> words = CustomList(
...     [
...         "Hello,",
...         "Pythonista!",
...         "Welcome",
...         "to",
...         "Python!"
...     ]
... )
>>> words.join()
# 'Hello, Pythonista! Welcome to Python!'
>>> words.map(str.upper)
# ['HELLO,', 'PYTHONISTA!', 'WELCOME', 'TO', 'PYTHON!']
>>> words.filter(lambda word: word.startswith("Py"))
# ['Pythonista!', 'Python!']
>>> words.for_each(print)
# Hello,
# Pythonista!
# Welcome
# to
# Python!

Можно реализовать CustomList, наследуя от UserList, а не от встроенного типа list. В этом случае не нужно менять внутреннюю реализацию, только базовый класс:

from collections import UserList

class CustomList(UserList):
    def join(self, separator=" "):
        return separator.join(str(item) for item in self.data)

    def map(self, action):
        return type(self)(action(item) for item in self.data)

    def filter(self, predicate):
        return type(self)(item for item in self.data if predicate(item))

    def for_each(self, func):
        for item in self.data:
            func(item)

Обратите внимание, что в этом примере нет необходимости использовать self.data напрямую (можно использовать просто self). Преимущество использования self.data в том, что становиться ясно, что работа ведется с подклассом UserList - это делает код более явным, а программисты читающие его видят больше контекста.