Пусть класс ведет себя как список в 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):
# do something fancy
Это нормально для использования в качестве итератора типа [e for e in li]
, но у меня нет полного поведения в списке, например li.expand(...)
.
Первое предположение - наследовать list
в MyFancyList
. Но является ли это рекомендуемым питоническим способом? Если да, то что считать? Если нет, то какой будет лучший подход?
Ответы
Ответ 1
Если вы хотите только часть поведения списка, используйте композицию (т.е. ваши экземпляры содержат ссылку на фактический список) и реализуют только методы, необходимые для желаемого поведения. Эти методы должны делегировать работу в фактический список. Любой экземпляр вашего класса содержит ссылку, например:
def __getitem__(self, item):
return self.li[item] # delegate to li.__getitem__
Внедрение только __getitem__
даст вам удивительное количество функций, например, итерация и нарезка.
>>> class WrappedList:
... def __init__(self, lst):
... self._lst = lst
... def __getitem__(self, item):
... return self._lst[item]
...
>>> w = WrappedList([1, 2, 3])
>>> for x in w:
... x
...
1
2
3
>>> w[1:]
[2, 3]
Если вам нужно полное поведение списка, наследуйте от collections.UserList
. UserList
- полная реализация типа списка типов Python.
Итак, почему бы не наследовать от list
напрямую?
Одна серьезная проблема с наследованием непосредственно из list
(или любого другого встроенного языка, написанного на языке C) заключается в том, что код встроенных функций может включать или не вызывать специальные методы, переопределенные в классах, определенных пользователем. Вот соответствующий отрывок из pypy docs:
Официально, CPython не имеет никакого правила вообще, когда точно переопределенный метод подклассов встроенных типов получает неявно называемый или нет. В качестве приближения эти методы никогда не вызывают другие встроенные методы одного и того же объекта. Например, переопределенный __getitem__
в подклассе dict не будет вызываться, например. встроенный метод get
.
Еще одна цитата из Luciano Ramalho Свободный Python, стр. 351:
Подклассы встроенных типов, таких как dict или list или str, поскольку встроенные методы в основном игнорируют пользовательские переопределение. Вместо подкласса встроенных модулей выведите свои классы из UserDict, UserList и UserString из коллекций модуль, который предназначен для легкого расширения.
... и больше, страница 370 +:
Неверные встроенные функции: ошибка или функция? Встроенные типы dict, list и str являются важными строительными блоками самого Python, поэтому они должны быть быстрыми - любые проблемы с производительностью в них серьезно повлияли бы на все остальное. Вот почему CPython принял ярлыки, которые вызывают их встроенный методы плохого поведения, не взаимодействуя с методами, переопределяемыми подклассами.
После небольшой игры проблемы с встроенным встроенным list
кажутся менее критичными (я попытался сломать его в Python 3.4 некоторое время, но не нашел действительно очевидного неожиданного поведения), но я все еще хотел опубликуйте демонстрацию того, что может произойти в принципе, поэтому здесь один с dict
и a UserDict
:
>>> class MyDict(dict):
... def __setitem__(self, key, value):
... super().__setitem__(key, [value])
...
>>> d = MyDict(a=1)
>>> d
{'a': 1}
>>> class MyUserDict(UserDict):
... def __setitem__(self, key, value):
... super().__setitem__(key, [value])
...
>>> m = MyUserDict(a=1)
>>> m
{'a': [1]}
Как вы можете видеть, метод __init__
из dict
игнорировал переопределенный метод __setitem__
, тогда как метод __init__
из нашего UserDict
не выполнял.
Ответ 2
Простейшим решением здесь является наследование класса list
:
class MyFancyList(list):
def fancyFunc(self):
# do something fancy
Затем вы можете использовать тип MyFancyList
в качестве списка и использовать его специальные методы.
Наследование вводит сильную связь между вашим объектом и list
. Подход, который вы реализуете, в основном является прокси-объектом.
Способ использования зависит от того, как вы будете использовать объект. Если он должен быть список, то наследование, вероятно, является хорошим выбором.
EDIT: как указано в @acdr, некоторые методы, возвращающие копию списка, должны быть переопределены, чтобы вернуть MyFancyList
вместо a list
.
Простой способ реализовать это:
class MyFancyList(list):
def fancyFunc(self):
# do something fancy
def __add__(self, *args, **kwargs):
return MyFancyList(super().__add__(*args, **kwargs))
Ответ 3
Основываясь на двух примерах, которые вы включили в свой пост (fancyPrint
, findAMetric
), вам не нужно сохранять какое-либо дополнительное состояние в ваших списках. Если это так, вам лучше просто объявить их свободными функциями и вообще игнорировать подтипирование; это полностью устраняет такие проблемы, как list
vs UserList
, хрупкие краевые случаи, такие как типы возврата для __add__
, неожиданные проблемы Лискова и т.д. Вместо этого вы можете писать свои функции, записывать свои модульные тесты для их вывода и быть уверенными, что все будет работать точно так, как планировалось.
В качестве дополнительного преимущества это означает, что ваши функции будут работать с любыми итерабельными типами (такими как выражения генератора) без каких-либо дополнительных усилий.
Ответ 4
Если вы не хотите переопределять каждый метод 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):
# do whatever you want...
Обратите внимание, что в этом случае, если вы хотите переопределить метод 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):
# do whatever you want...
def extend(self, list_):
# your own version of extend