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?