Кастомные коллекции

Опубликовано 24 August 2015 в Python

Хорошо написанный код на Python должен работать с последовательностями однообразно. При этом не важно встроенные это последовательности или нет. Тем более, что свою последовательность написать в питоне очень просто. Нужно всего лишь переопределить __len__, __getitem__ что бы работа с вашей последовательностью в большинстве случаев не отличалась от работы со встроенными последовательностями.

В качестве иллюстрации возьмем такой пример. У нас есть класс ShoppingBasket. В нем лежат какие-то товары. Пусть нам нужно пройтись по всем товарам, научиться получать товары по ключу и добавлять товары.

Первый вариант

Положимся на встроенные коллекции. Для хранения элементов используем список.

# -*- coding: utf-8 -*-
from collections import namedtuple


BasketItem = namedtuple('BasketItem', 'name, price, quantity')


class ShoppingBasket(object):
    def __init__(self):
        self.items = []


if __name__ == '__main__':
    basket = ShoppingBasket()
    basket.items = [
        BasketItem("Apple", 1.12, 2),
        BasketItem("Orange", 1.3, 4),
    ]

    # печатаем содержимое
    for item in basket.items:
        print(item)

    # только яблоки
    print("Apples:", basket.items[0])

    # всего элементов в корзине
    print("Items:", len(basket.items))

Код работает. Но у него большая проблема: класс ShoppingBasket открывает наружу то как он хранит товары в корзине. Этот код нас жестко привязывает к списку. Если по каким-то причинам мы захотим поменять модель хранения товаров в корзине (привязать к БД, на пример), то придется править код во множестве мест, где имеется работа с корзиной. Нам повезет, если код не является частью публичного API.

Второй вариант

Попробуем спрятать зависимость от дикта внутри корзины.

# -*- coding: utf-8 -*-
from collections import namedtuple


BasketItem = namedtuple('BasketItem', 'name, price, quantity')


class ShoppingBasket(object):
    def __init__(self):
        self.items = []

    def append(self, value):
        self.items.append(value)

    def __getitem__(self, key):
        return self.items[key]

    def __len__(self):
        return len(self.items)

if __name__ == '__main__':
    basket = ShoppingBasket()
    basket.append(BasketItem("Apple", 1.12, 2))
    basket.append(BasketItem("Orange", 1.3, 4))
    basket.append(BasketItem("Bananas", 0.8, 1))

    # печатаем содержимое
    for item in basket:
        print(item)

    # только яблоки
    print("Apples:", basket[0])

    # слайсы так же работают
    print(basket[:2])

    # всего элементов в корзине
    print("Items:", len(basket))

Код внутри __main__ стал более опрятным. Зависимость от способа хранения элементов убрана в класс корзины. Переделать этот вариант корзины на хранение элементов в БД с сохранением интерфейса не составит проблем.

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

---
Возник вопрос? Мне всегда можно написать в Twitter: avkorablev