Рубрики
Без рубрики

Сказка о двух сортировках: Сравнение наследства и состава

Поскольку советы идут, «одобрение состав по наследству» является содержательными и неоструктовыми. К сожалению, это также не имеет большого смысла, если вы уже не знаете, что это значит. Мы рассмотрим два разных подхода к решению одинаковой проблемы: наследование и композицию. На первый взгляд пример наследования выглядит более правильно и немного короче. Я хочу убедить вас, что подход композиции более правильный. Теги с программированием, DesignPatterns, Python, OOP.

Поскольку советы идут, «одобрение состав по наследству» является содержательными и неоструктовыми. К сожалению, это также не имеет большого смысла, если вы уже не знаете, что это значит. Мы рассмотрим два разных подхода к решению одинаковой проблемы: наследование и композицию. На первый взгляд пример наследования выглядит более правильно и немного короче. Я хочу убедить вас, что подход композиции более правильный.

Что такое состав? Что наследство?

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

  • Наследование, используемое для моделирования отношений «IS-A», например: «A Poissonrandomnumbergratererator IS-A (конкретный случай) A RandomNumberGranater »
  • Композиция используется для моделирования отношения «есть-A», например: «A Poissonrandomnumbergratererator есть-a (использует) А RandomNumberGenerator

Либо выбор оправдан для примера случайного числа. Тем не менее, пословица направляет нас к составу. У вас еще нет интуиции за то, почему это, но мы доберемся туда:)

Чтобы построить: сортирист

Мы хотели бы сделать структуру данных SOLTEDEDLIST. Поддерживая элементы в отсортированном порядке, мы можем сделать «ли этот список содержать X ?» Операция действительно быстро . Операции вставки и удаления будут немного медленнее, но мы готовы платить эту цену за некоторые случаи использования.

Мы будем использовать библиотеку Python Standard’s биссект Модуль, чтобы сделать большую часть тяжелой подъема. биссект Функция говорит вам, какой индекс найти элемент, при условии, что список отсортирован. Insort Функция (каламбур: «Сортировка вставки») выполняет вставку в сортированный список в правильном месте, чтобы он сортировал. Это именно то, что нам нужно! Это означает, что мы можем сосредоточиться почти полностью на различиях между нашими двумя подходами.

Вот как наши отсортированные списки будут вести себя:

sl = SortedList()
for elem in (12, 4, 15, 2):
    sl.insert(elem)
print(4 in sl) # True
print(18 in sl) # False
print(', '.join(elem for elem in sl)) # 2, 4, 12, 15

Вы можете сказать: «Зачем вообще писать SoteDeList . Не Insort и биссект Функции вместе делают то, что нам нужно? Они делают правильные действия, но мы вроде кодируйте без охранных рельсов, если мы используем их без класса обертки. Помните: эти функции предположить Список отсортирован, чтобы начать с – они не проверяют это! Мы должны были бы быть усердными о Никогда Случайно ломая отсортированный заказ в любом коде, используя список, который мы хотим использовать бислект и Insort на или мы могли ввести очень сложные ошибки в наш код. Пример того, как это мог укусить нас:

from bisect import bisect, insort
keep_me_sorted = []

insort(keep_me_sorted, 4)
insort(keep_me_sorted, 10)

# Danger! did the writers of include_fav_numbers know they have to
# keep the list in sorted order? Will they continue to remember in the future?
include_fav_numbers(keep_me_sorted)

# if they forgot, bisect and insort will no longer work properly on the list

Наш быстрый поиск работает только в том случае, если список отсортирован, что трудно гарантировать с дисциплиной в одиночку. Мы могли бы отсортировать список незадолго до того, как мы используем биссект или Insort Отказ Это будет работать, но это стирает все преимущества производительности, мы получаем от использования биссект на первом месте! Вместо этого давайте использовать Инкапсуляция Чтобы защитить список из модификаций, которые нарушит наш отсортированный заказ.

Реализация наследственности

Использование наследства, чтобы сделать SORTEDLIST Похоже, естественный выбор. А SORTEDLIST это – список! Сделано и сделано. Давайте начнем там.

from bisect import insort, bisect

class InheritanceSortedList(list):
    def insert(self, elem):
        insort(self, elem)

    def __contains__(self, elem):
        """
        __contains__ is the method that is called when
        code like `x in collection` runs. This is where
        we get our performance boost over a regular list.
        """
        # Find the index where elem *might* be found
        ix = bisect(self, elem)
        return ix != len(self) and self[ix] == elem

Так что … мы сделали? Похоже, это делать то, что мы хотим. Этот список будет работать для простых примеров, но мы увидим позже, что он не предоставляет инкапсуляции, которую мы ищем. Во-первых, давайте посмотрим на опцию композиции.

Создание композиционных устройств

Чтобы использовать композицию, мы сделаем объект, который включает в себя список в качестве скрытого свойства.

from bisect import insort, bisect

class CompositionSortedList:
    def __init__(self):
        self._lst = []

    def insert(self, elem):
        insort(self._lst, elem)

    def __contains__(self, elem):
        ix = bisect(self._lst, elem)
        return ix != len(self._lst) and self._lst[ix] == elem

Хорошо, немного дольше, чем пример наследования, но не так уж плохо. К сожалению, этот не совсем проходит наш тест. В частности, (Элем для Элема в СЛ) не удается с ошибкой:

TypeError: Итерация по непоследовательности

Похож на __sontains__ Magic Methods, объекты могут определить, как зацикливаться на них, используя __er____ метод. Когда мы сделали Наследовать inheritsorteDList У нас есть определение __er____ путем наследования от список . С композицией нам придется определить один:

class CompositionSortedList:
    # ...
    def __iter__(self):
        return iter(self._lst)

К счастью, это не слишком сложно. Мы в основном говорим: «Итерации за ComposionsIndeDList , просто повторяйте за скрытые _lst бит». Это достаточно распространено, чтобы у него есть особое имя. Мы можем сказать, что КомпозициониторТистлет делегаты к его _lst имущество. Позже позже, как сделать это даже лучше.

Наследование SOLTEDLISTLIST

Хотели ли мы или нет, Наследовать inheritsorteDList Унаследовал все Список поведение, включая вещи, которые мы не хотим. Например, у нас есть Добавить Метод:

lst = InheritanceSortedList()

lst.append(4)
lst.append(5)
print(5 in lst)  # prints 'False'

У нас также есть __init__ Что мы не торговались:

lst = InheritanceSortedList([5, 3, 2])
print(2 in lst)  # prints 'False'

__init__ и Добавить Методы пришли прямо из Список родительский класс. Они не знать что они должны поддерживать отсортированный заказ! Они делают правильные вещи для инициализации или присоединения к регулярному Список , независимо от того, что это не то, что делает Сортированный список Отказ

Мы можем исправить их без слишком много работы:

class InheritanceSortedList(list):
    def __init__(self, iterable=()):
        super().__init__(self)
        for elem in iterable:
            self.insert(elem)
    def append(self, elem):
        raise RuntimeError("Appending to a sorted list could break sorted-order. Use insert instead")
    def extend(self, iterable):
        raise RuntimeError("extending a sorted list could break sorted-order. Use insert instead")
    # ...are there others?

Но это чувствует себя хрупким для меня. Мы поймали и исправили все методы, которые могут сломать отсортированный заказ? Даже если у нас есть, может в будущей версию Список Добавить новый? Это иногда называют Хрупкая проблема базового класса , где изменения в родительском классе ( Список ) могут непреднамеренно сломать производный класс ( унаследовалСордлист ).

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

Композинзентиристы строятся

В пути, КомпозициониторТистлет Имеет противоположную проблему. В списке есть куча функциональности, которые мы хочу сделать доступным. Так же, как мы сделали с __er____ Мы можем сделать методы, которые делегат в частный список свойств:

class CompositionSortedList:
    # ...
    def __iter__(self):
        return self._lst.__iter__()
    def __len__(self):
        return self._lst.__len__()
    def __getitem__(self, ix):
        return self._lst.__getitem__(ix)
    def __reversed__(self):
        return self._lst.__reversed__()

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

Это может быть личным предпочтением моей, но я чувствую, что состав также яснее . Мне не нужно копать через иерархию наследования, чтобы выяснить, где был определен метод. Любая функциональность на классе с использованием состава – прямо там в определении класса.

Дополнительный кредит: еще лучший шаблон делегата

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

from superdelegate import SuperDelegate, delegate_to

class CompositionSortedList(SuperDelegate):
    # ...
    __iter__ = __len__ = __getitem__ = __reversed__ = delegate_to('_lst')

Довольно мило, я думаю!

Когда бы вы использовали наследство?

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

  • Когда вы действительно хотите принять все Поведение родительского класса (даже будущие изменения мы еще не знаем). Я сделал это в Bgschiller/Citrus Потому что я хотел, чтобы полученные предметы могли относиться, как будто они были родительским типом во всех ситуациях.
  • Когда это так, как каркас хочет, чтобы вы настраивали поведение. Классовые виды Django – это хороший пример этого.
  • Когда происходит какая-то странная метакласс. Подумайте о декларативных базах SQLALCHEMY, или классы WTForms.

использованная литература

Оригинал: “https://dev.to/bgschiller/a-tale-of-two-sorted-lists-comparing-inheritance-and-composition-5fm3”