Подтвердить что ты не робот

Пусть класс ведет себя как список в 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. Но является ли это рекомендуемым питоническим способом? Если да, то что считать? Если нет, то какой будет лучший подход?

4b9b3361

Ответ 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