Позвольте классу вести себя как список в Python
У меня есть класс, который по сути является коллекцией/списком элементов. Но я хочу добавить некоторые дополнительные функции к этому списку. Мне нужно следующее:
- У меня есть экземпляр
li = MyFancyList()
. Переменнаяli
должна вести себя как список всякий раз, когда я использую её как список:[e for e in li]
,li.expand(...)
,for e in li
. - Кроме того, он должен иметь специальные функции, такие как
li.fancyPrint()
,li.getAMetric()
,li.getName()
.
В данный момент я использую следующий подход:
class MyFancyList:
def __iter__(self):
return self.li
def fancyFunc(self):
# выполняет что-то "крутое"
Этот способ подходит для использования в качестве итератора, например, [e for e in li]
, но у меня отсутствует полноценное поведение списка, такое как li.expand(...)
.
Первое предположение заключается в том, чтобы унаследовать от list
в классе MyFancyList
. Но является ли это рекомендованным "питоническим" способом? Если да, то на что стоит обратить внимание? Если нет, то какой был бы лучший подход?
4 ответ(ов)
Наиболее простой способ решить эту задачу — унаследоваться от класса list
:
class MyFancyList(list):
def fancyFunc(self):
# сделать что-то интересное
Теперь вы можете использовать тип MyFancyList
как обычный список и пользоваться его специфическими методами.
Однако стоит отметить, что наследование создает сильную связь между вашим объектом и list
. Реализованный вами подход по сути является прокси-объектом. Способ использования будет сильно зависеть от того, как именно вы планируете применять объект. Если он должен быть списком, то наследование, вероятно, является хорошим выбором.
ИЗМЕНЕНИЕ: как указал @acdr, некоторые методы, возвращающие копию списка, должны быть переопределены, чтобы они возвращали MyFancyList
, а не list
.
Вот простой способ это реализовать:
class MyFancyList(list):
def fancyFunc(self):
# сделать что-то интересное
def __add__(self, *args, **kwargs):
return MyFancyList(super().__add__(*args, **kwargs))
Если вы не хотите переопределять каждый метод класса list
, я предлагаю следующий подход:
class MyList:
def __init__(self, list_):
self.li = list_
def __getattr__(self, method):
return getattr(self.li, method)
С помощью этого кода методы, такие как append
, extend
и прочие, будут работать без дополнительных усилий. Однако учтите, что магические методы (например, __len__
, __getitem__
и т. д.) не будут функционировать в этом случае, поэтому вам следует как минимум переопределить их следующим образом:
class MyList:
def __init__(self, list_):
self.li = list_
def __getattr__(self, method):
return getattr(self.li, method)
def __len__(self):
return len(self.li)
def __getitem__(self, item):
return self.li[item]
def fancyPrint(self):
# делайте, что хотите...
Обратите внимание, что если вы хотите переопределить метод класса list
(например, extend
), вы можете просто объявить свой собственный метод, чтобы вызов не проходил через __getattr__
. Например:
class MyList:
def __init__(self, list_):
self.li = list_
def __getattr__(self, method):
return getattr(self.li, method)
def __len__(self):
return len(self.li)
def __getitem__(self, item):
return self.li[item]
def fancyPrint(self):
# делайте, что хотите...
def extend(self, list_):
# ваша версия метода extend
Основываясь на двух примерах методов, которые вы привели в своем посте (fancyPrint
, findAMetric
), не кажется, что вам нужно хранить какое-либо дополнительное состояние в ваших списках. Если это действительно так, наилучшим вариантом будет просто объявить эти методы как свободные функции и вовсе отказаться от иерархии классов; это полностью избавит вас от проблем типа list
против UserList
, хрупких краевых случаев, таких как возвращаемые типы для __add__
, неожиданных проблем Лискова и т. д. Вместо этого вы можете писать свои функции, писать для них модульные тесты и быть уверенными, что всё будет работать именно так, как задумано.
Кроме того, это имеет дополнительное преимущество: ваши функции будут работать с любыми итерабельными типами (такими как генераторные выражения) без каких-либо дополнительных усилий.
Вы можете создать собственный класс, наследующий от списка в Python, и переопределить методы append
и __setitem__
, чтобы ограничить элементы списка только целыми числами. Ваш код выглядит хорошо, но я бы предложил немного улучшить его для большей читаемости и следования стандартам кодирования PEP 8. Вот пример ваш код с некоторыми изменениями:
class ListInteger(list):
def __init__(self, *args):
if all(isinstance(i, int) for i in args[0]):
super().__init__(args[0])
else:
raise TypeError('Извините, допускаются только целые числа')
def __setitem__(self, idx, value):
if isinstance(value, int):
super().__setitem__(idx, value)
else:
raise TypeError('Извините, допускаются только целые числа')
def append(self, __object) -> None:
if isinstance(__object, int):
super().append(__object)
else:
raise TypeError('Извините, допускаются только целые числа')
Вот основные моменты, которые вы можете учесть:
- Я использовал
isinstance()
вместо проверки типа с помощьюtype()
. Это считается более "питоническим" стилем. - Сообщения об ошибках я слегка изменил, чтобы они были более информативными.
- Обратите внимание на корректное выравнивание. В Python важен отступ, поэтому убедитесь, что ваш код аккуратно отформатирован.
Теперь ваш класс будет успешно ограничивать список только целыми числами при использовании методов append
и __setitem__
.
Как вернуть ключи словаря в виде списка в Python?
Использование map() для возврата списка в Python 3.x
Фиксация количества знаков после запятой с помощью f-строк
Диапазон букв в Python
Доступ к атрибутам на литералах работает для всех типов, кроме `int`; почему?