mardi 20 mars 2018

Python: designing a Container class

I'm designing a Python library and need to define two classes: Container and Item.

Container contains multiple instances of Item.

Each Item instance inside a Container must be identified by name.

Item instances need not to be of the same type: Item is just a base class.

The same is true for Container: being a library, it is expected that other developers subclass both Container and Item so that SomeCustomContainer contains multiple instances of ManyCustomItem's.

As such, the main responsibility of Container and Item is to implement the basic logic to describe the "contains" relationship between some Container and its Item's.

Container has some properties that apply to all of its Item's: to avoid repeating stuff, an Item may define only Item-specific data and receive full information once it's added to a Container.

Moreover, an Item can exist of its own: it needs not to be associated with a Container. As such, an Item may define informations that would otherwise be provided by a Container.

The first draft is something like this:

class Container(object):

    def __init__(self, base, delegate):
        self._base = base
        self._delegate = delegate
        self.items = self._add_items()

    def _add_items(self):
        return []

    @property
    def items(self):
        return self._items.values()

    @items.setter
    def items(self, items):
        _items = {}
        for item in items:
            item.delegate = self.delegate
            item.info = self.base + item.info
            _items[item.name] = item
        self._items = _items

    def get_item(self, name):
        return self._items[name]

class Item(object):

    def __init__(self, info, delegate=None, name=None):
        self.info = info
        self.delegate = delegate
        self.name = name

Use Case 1:

class MyContainer(Container):

    def _add_items(self):
        return [
            SomeItem('info', name='some item'),
            SomeOtherItem('other info', name='another item')
        ]

c = MyContainer('some ', some_delegate)
assert c.get_item('some item').info == 'some info'
assert c.get_item('another item').delegate is c.delegate

Use Case 2:

c = Container('some ', some_delegate)
c.items = [
    SomeItem('info', name='some item'),
    SomeOtherItem('other info', name='another item')
]
assert c.get_item('some item').info == 'some info'
assert c.get_item('another item').delegate is c.delegate

Should "_add_items()" be public instead?

The second draft is something like this:

class Container(object):

    def __init__(self, base, delegate):
        self._base = base
        self._delegate = delegate
        self._add_items()

    def _add_items(self):
        pass

class Item(object):

    def __init__(self, info, delegate=None):
        self.info = info
        self.delegate = delegate

Use Case 1:

class MyContainer(Container):

    def _add_items(self):
        self.some_item = SomeItem(self.base + 'info', delegate=self.delegate)
        self.another_item = SomeOtherItem(self.base + 'other info', delegate=self.delegate)
        self.a_third_item = SomeOtherItem("I don't have common info", delegate=self.delegate)

This second implementation does not allow simply instantiating the base Container and adding items directly to the instance, but I don't expect that use case to be much useful. On the other hand, this second version greatly simplifies Container, but puts the responsibility of injecting common data into the items to the user of the library.

Third draft: declarative style using descriptors:

class Container(object):

    def __init__(self, base, delegate):
        self._base = base
        self._delegate = delegate

class Item(object):

    def __init__(self, info, delegate=None):
        self.info = info
        self.delegate = delegate

    def __get__(self, instance, owner):
        # I can think of two possible implementations
        if i_need_the_same_item_instance_for_a_given_container_instance:
            if self not in instance._items:
                copy = type(self)(instance.base + self.info, instance.delegate)
                instance._items[self] = copy
            return instance._items[self]
        else:
            copy = type(self)(instance.base + self.info, instance.delegate)
            return copy

Use Case 1:

class MyDeclarativeContainer(object):

    some_item = SomeItem('info')
    another_item = SomeOtherItem('other info')

c = MyDeclarativeContainer('some ', some_delegate)
assert c.some_item.info == 'some info'
assert c.another_item.delegate is c.delegate
if i_need_the_same_item_instance_for_a_given_container_instance:
    assert c.some_item is c.some_item
else:
    assert c.some_item is not c.some_item

This is my favourite implementation, because it minimizes boilerplate code and appears to be simple and readable, allowing library users to focus on adding items rather than complying to Container requirements for adding them. On the other hand, I'm pretty new to Python's descriptors and would like to know if this is a good use case for them.

  • Any drawbacks I am overlooking?
  • I am quite doubtful about the line "instance._items[self] = copy": is it sane? Is there a better alternative? What about Container having a protected "_items" attribute for the sole purpose of providing a cache to support Item's __get__ implementation?

Aucun commentaire:

Enregistrer un commentaire